Compare commits

..

50 Commits

Author SHA1 Message Date
2e83440e70 10.60.2 2018-12-02 21:20:36 +09:00
2633873fcc Fix syntax error (#3477)
src/remote/activitypub/models/person.ts(326,4): error TS1005: ',' expected.
2018-12-02 21:19:28 +09:00
5f33713f53 10.60.1 2018-12-02 21:08:20 +09:00
fe84c5010c Merge pull request #3458 from syuilo/l10n_develop
New Crowdin translations
2018-12-02 20:43:52 +09:00
7f39df0713 New translations ja-JP.yml (English) 2018-12-02 20:43:12 +09:00
928d359dd2 Use takeWhile instead of some (#3475) 2018-12-02 20:28:22 +09:00
184eb00133 Remove self-assignment (#3476) 2018-12-02 20:26:15 +09:00
e264a49b08 [Client] Resolve #2596 2018-12-02 20:24:38 +09:00
8caf853c80 Merge branches 'develop', 'develop' and 'develop' of https://github.com/syuilo/misskey into develop 2018-12-02 20:11:03 +09:00
b451c04787 Use primitive type boolean (#3474) 2018-12-02 20:10:56 +09:00
1653977392 Improve input dialog 2018-12-02 20:10:53 +09:00
a0d9def98a Simplify expressions (#3473) 2018-12-02 19:35:41 +09:00
92701e5cec Fix #3445 (#3471) 2018-12-02 19:24:57 +09:00
0b6b6a4f2f Remove unneccesary casts 2018-12-02 19:05:08 +09:00
3a2dc95850 No MFM parsing when remote note (#3470)
* Use tag for hashtag detection of remote note

* No MFM parsing when remote note
2018-12-02 18:05:33 +09:00
163cf49f16 10.60.0 2018-12-02 15:39:46 +09:00
37fc3103f6 New translations ja-JP.yml (Norwegian) 2018-12-02 15:33:32 +09:00
b7bd1ff69f New translations ja-JP.yml (Dutch) 2018-12-02 15:33:28 +09:00
dc36134f10 New translations ja-JP.yml (Japanese, Kansai) 2018-12-02 15:33:22 +09:00
fea8821091 New translations ja-JP.yml (Spanish) 2018-12-02 15:33:17 +09:00
33faf40aca New translations ja-JP.yml (Russian) 2018-12-02 15:33:12 +09:00
4410f8d7f7 New translations ja-JP.yml (Portuguese) 2018-12-02 15:33:08 +09:00
3e6029e69d New translations ja-JP.yml (Polish) 2018-12-02 15:33:04 +09:00
96c7707e6c New translations ja-JP.yml (Korean) 2018-12-02 15:32:59 +09:00
d8e545db3c New translations ja-JP.yml (Italian) 2018-12-02 15:32:54 +09:00
a9a6ba0aed New translations ja-JP.yml (German) 2018-12-02 15:32:50 +09:00
a898c6ceab New translations ja-JP.yml (French) 2018-12-02 15:32:44 +09:00
0205c5c2d7 New translations ja-JP.yml (English) 2018-12-02 15:32:38 +09:00
f8074ab74b New translations ja-JP.yml (Chinese Simplified) 2018-12-02 15:32:34 +09:00
f15c491d5f New translations ja-JP.yml (Catalan) 2018-12-02 15:32:29 +09:00
c38a32dee9 🎨 2018-12-02 15:31:24 +09:00
4f5abed70d Fix 2018-12-02 15:30:48 +09:00
c9ac9923df Refactor: rename alert to dialog 2018-12-02 15:28:52 +09:00
bb14895fd8 [Client] Resolve #2225 2018-12-02 15:26:56 +09:00
6f92d601ec 🎨 2018-12-02 14:26:30 +09:00
345143b0c1 Suggest in CW (#3467) 2018-12-02 07:33:03 +09:00
dc80d5d376 10.59.4 2018-12-02 07:08:26 +09:00
d3544f9637 Update README.md [AUTOGEN] (#3466) 2018-12-02 07:05:22 +09:00
864b6ad1bd Resolve #1826 2018-12-02 07:02:08 +09:00
c58027e521 [MFM] Better hashtag detection 2018-12-02 06:53:57 +09:00
10fb399588 Merge branch 'develop' of https://github.com/syuilo/misskey into develop 2018-12-02 06:44:25 +09:00
10f466c895 Improve performance 2018-12-02 06:44:18 +09:00
32068b4bcc Update README.md [AUTOGEN] (#3465) 2018-12-02 06:16:24 +09:00
381f2b7fdf New translations ja-JP.yml (French) 2018-12-02 05:12:02 +09:00
1c3c733c6b New translations ja-JP.yml (French) 2018-12-02 05:02:46 +09:00
8bc5febe66 [Client] Add missing icon (#3464) 2018-12-02 03:43:05 +09:00
20335e23f9 Resolve external recommended users (#3462)
* Resolve external recommended users

* Skip unresolvable users

* Fix indent

* Use original for unresolvable users
2018-12-02 03:42:45 +09:00
9dbc9115c9 New translations ja-JP.yml (English) 2018-12-01 12:51:44 +09:00
fe707f88a4 [MFM] Better MFM parsing 2018-12-01 10:40:09 +09:00
9b23ebd4a3 🎨 2018-12-01 09:45:48 +09:00
83 changed files with 550 additions and 505 deletions

View File

@ -116,7 +116,7 @@ Please see [Contribution guide](./CONTRIBUTING.md).
<td><a href="https://www.patreon.com/user?u=12531784">Takashi Shibuya</a></td>
</tr></table>
**Last updated:** Tue, 27 Nov 2018 06:24:05 UTC
**Last updated:** Sat, 01 Dec 2018 22:05:05 UTC
<!-- PATREON_END -->
:four_leaf_clover: Copyright

View File

@ -1434,6 +1434,9 @@ mobile/views/pages/user.vue:
block: "ブロック"
unblock: "ブロック解除"
years-old: "{age}歳"
push-to-list: "リストに追加"
select-list: "リストを選択してください"
list-pushed: "{user}を{list}に追加しました"
mobile/views/pages/user/home.vue:
recent-notes: "最近の投稿"
images: "画像"

View File

@ -1434,6 +1434,9 @@ mobile/views/pages/user.vue:
block: "ブロック"
unblock: "ブロック解除"
years-old: "{age}歳"
push-to-list: "リストに追加"
select-list: "リストを選択してください"
list-pushed: "{user}を{list}に追加しました"
mobile/views/pages/user/home.vue:
recent-notes: "最近の投稿"
images: "画像"

View File

@ -26,7 +26,7 @@ common:
close: "Close"
do-not-copy-paste: "Please do not enter or paste the code here. Account may be compromised."
load-more: "Load more"
enter-password: "パスワードを入力してください"
enter-password: "Please enter the Password"
got-it: "Got it!"
customization-tips:
title: "Customization tips"
@ -1008,8 +1008,8 @@ admin/views/instance.vue:
email-config-info: "Used to confirm email and password reset etc."
enable-email: "Enable email delivery"
email: "Email Address"
smtp-secure: "SMTP接続に暗黙的なSSL/TLSを使用する"
smtp-secure-info: "STARTTLS使用時はオフにします。"
smtp-secure: "Use implicit SSL/TLS in the SMTP connection"
smtp-secure-info: "Turn off STARTTLS when used that."
smtp-host: "SMTP Host"
smtp-port: "SMTP Port"
smtp-user: "SMTP User"
@ -1434,6 +1434,9 @@ mobile/views/pages/user.vue:
block: "Block"
unblock: "Unblock"
years-old: "{age} years old"
push-to-list: "Add to list"
select-list: "Select a list"
list-pushed: "Successfully added {user} to {list}."
mobile/views/pages/user/home.vue:
recent-notes: "Recent notes"
images: "Images"

View File

@ -1434,6 +1434,9 @@ mobile/views/pages/user.vue:
block: "ブロック"
unblock: "ブロック解除"
years-old: "{age}歳"
push-to-list: "リストに追加"
select-list: "リストを選択してください"
list-pushed: "{user}を{list}に追加しました"
mobile/views/pages/user/home.vue:
recent-notes: "最近の投稿"
images: "画像"

View File

@ -26,7 +26,7 @@ common:
close: "Fermer"
do-not-copy-paste: "Veuillez ne pas entrer ou coller le code ici. Le compte peut être compromis."
load-more: "Charger plus"
enter-password: "パスワードを入力してください"
enter-password: "Veuillez entrer le mot de passe"
got-it: "Jai compris !"
customization-tips:
title: "Conseils de personnalisation"
@ -458,10 +458,10 @@ common/views/components/profile-editor.vue:
saved: "Profil mis à jour avec succès"
uploading: "En cours d'envoi …"
upload-failed: "Échec de l'envoi"
email: "メール設定"
email-address: "メールアドレス"
email-verified: "メールアドレスが確認されました"
email-not-verified: "メールアドレスが確認されていません。メールボックスをご確認ください。"
email: "Paramètres de messagerie"
email-address: "Adresse de courrier électronique"
email-verified: "Ladresse du courrier électronique a été vérifiée."
email-not-verified: "Adresse de courriel nest pas confirmée. Veuillez vérifier votre boite de réception."
common/views/widgets/broadcast.vue:
fetching: "Récupération"
no-broadcasts: "Aucune annonce"
@ -736,8 +736,8 @@ desktop/views/components/settings.vue:
note-visibility: "Visibilité de la publication"
default-note-visibility: "Visibilité par défaut"
remember-note-visibility: "Se souvenir du mode de visibilité de la publication"
web-search-engine: "ウェブ検索エンジン"
web-search-engine-desc: ": https://www.google.com/?#q={{query}}"
web-search-engine: "Moteur de recherche Web"
web-search-engine-desc: "Exemple: https://www.google.com/?#q={{query}}"
auto-popout: "Fenêtre contextuelle automatique"
auto-popout-desc: "ウィンドウが開かれるとき、ポップアウト(ブラウザ外に切り離す)可能なら自動でポップアウトします。この設定はブラウザに記憶されます。"
advanced: "Paramètres avancés"
@ -1004,16 +1004,16 @@ admin/views/instance.vue:
external-user-recommendation-engine-desc: "Exemple: https://vinayaka.distsn.org/cgi-bin/vinayaka-user-match-misskey-api.cgi?{{host}}+{{user}}+{{limit}}+{{offset}}"
external-user-recommendation-timeout: "Délai dexpiration"
external-user-recommendation-timeout-desc: "En millisecondes (par exemple : 300000)"
email-config: "メールサーバーの設定"
email-config-info: "メールアドレス確認やパスワードリセットの際に使われます。"
enable-email: "メール配信を有効にする"
email: "メールアドレス"
smtp-secure: "SMTP接続に暗黙的なSSL/TLSを使用する"
smtp-secure-info: "STARTTLS使用時はオフにします。"
smtp-host: "SMTPホスト"
smtp-port: "SMTPポート"
smtp-user: "SMTPユーザー"
smtp-pass: "SMTPパスワード"
email-config: "Paramètres du serveur de messagerie"
email-config-info: "Utilisé pour confirmer votre adresse de courrier électronique et la réinitialisation de votre mot de passe."
enable-email: "Activation de la distribution du courrier"
email: "Adresse de courrier électronique"
smtp-secure: "Utiliser SSL/TLS implicitement dans la connexion SMTP"
smtp-secure-info: "Désactiver STARTTLS lorsque celui-ci est utilisé."
smtp-host: "Hôte SMTP"
smtp-port: "Port SMTP"
smtp-user: "Utilisateur SMTP"
smtp-pass: "Mot de passe SMTP"
admin/views/charts.vue:
title: "Graph"
per-day: "par jour"
@ -1388,8 +1388,8 @@ mobile/views/pages/settings.vue:
note-visibility: "Visibilité de la publication"
default-note-visibility: "Visibilité par défaut"
remember-note-visibility: "Se souvenir du mode de visibilité de la publication"
web-search-engine: "ウェブ検索エンジン"
web-search-engine-desc: ": https://www.google.com/?#q={{query}}"
web-search-engine: "Moteur de recherche Web"
web-search-engine-desc: "Exemple: https://www.google.com/?#q={{query}}"
disable-via-mobile: "Ne pas mentionner que ma publication provient d'un 'périphérique mobile'"
load-raw-images: "Afficher les photos jointes en haute qualité"
load-remote-media: "Afficher les médias sur le serveur distant"
@ -1434,6 +1434,9 @@ mobile/views/pages/user.vue:
block: "Bloquer"
unblock: "Débloquer"
years-old: "{age} ans"
push-to-list: "リストに追加"
select-list: "リストを選択してください"
list-pushed: "{user}を{list}に追加しました"
mobile/views/pages/user/home.vue:
recent-notes: "Notes récentes"
images: "Images"

View File

@ -1434,6 +1434,9 @@ mobile/views/pages/user.vue:
block: "ブロック"
unblock: "ブロック解除"
years-old: "{age}歳"
push-to-list: "リストに追加"
select-list: "リストを選択してください"
list-pushed: "{user}を{list}に追加しました"
mobile/views/pages/user/home.vue:
recent-notes: "最近の投稿"
images: "画像"

View File

@ -1628,6 +1628,9 @@ mobile/views/pages/user.vue:
block: "ブロック"
unblock: "ブロック解除"
years-old: "{age}歳"
push-to-list: "リストに追加"
select-list: "リストを選択してください"
list-pushed: "{user}を{list}に追加しました"
mobile/views/pages/user/home.vue:
recent-notes: "最近の投稿"

View File

@ -1434,6 +1434,9 @@ mobile/views/pages/user.vue:
block: "ブロック"
unblock: "ブロックやめたる"
years-old: "{age}歳"
push-to-list: "リストに追加"
select-list: "リストを選択してください"
list-pushed: "{user}を{list}に追加しました"
mobile/views/pages/user/home.vue:
recent-notes: "最近儲かりまっか?"
images: "画像"

View File

@ -1434,6 +1434,9 @@ mobile/views/pages/user.vue:
block: "차단"
unblock: "차단 해제"
years-old: "{age}세"
push-to-list: "リストに追加"
select-list: "リストを選択してください"
list-pushed: "{user}を{list}に追加しました"
mobile/views/pages/user/home.vue:
recent-notes: "최근 글"
images: "이미지"

View File

@ -1434,6 +1434,9 @@ mobile/views/pages/user.vue:
block: "ブロック"
unblock: "ブロック解除"
years-old: "{age}歳"
push-to-list: "リストに追加"
select-list: "リストを選択してください"
list-pushed: "{user}を{list}に追加しました"
mobile/views/pages/user/home.vue:
recent-notes: "Recente notities"
images: "Afbeeldingen"

View File

@ -1434,6 +1434,9 @@ mobile/views/pages/user.vue:
block: "ブロック"
unblock: "ブロック解除"
years-old: "{age}歳"
push-to-list: "リストに追加"
select-list: "リストを選択してください"
list-pushed: "{user}を{list}に追加しました"
mobile/views/pages/user/home.vue:
recent-notes: "Nylige innlegg"
images: "Bilder"

View File

@ -1434,6 +1434,9 @@ mobile/views/pages/user.vue:
block: "ブロック"
unblock: "ブロック解除"
years-old: "{age} lat"
push-to-list: "リストに追加"
select-list: "リストを選択してください"
list-pushed: "{user}を{list}に追加しました"
mobile/views/pages/user/home.vue:
recent-notes: "Ostatnie wpisy"
images: "Zdjęcia"

View File

@ -1434,6 +1434,9 @@ mobile/views/pages/user.vue:
block: "ブロック"
unblock: "ブロック解除"
years-old: "{age}歳"
push-to-list: "リストに追加"
select-list: "リストを選択してください"
list-pushed: "{user}を{list}に追加しました"
mobile/views/pages/user/home.vue:
recent-notes: "Notas recentes"
images: "Imagens"

View File

@ -1434,6 +1434,9 @@ mobile/views/pages/user.vue:
block: "ブロック"
unblock: "ブロック解除"
years-old: "{age}歳"
push-to-list: "リストに追加"
select-list: "リストを選択してください"
list-pushed: "{user}を{list}に追加しました"
mobile/views/pages/user/home.vue:
recent-notes: "最近の投稿"
images: "画像"

View File

@ -1434,6 +1434,9 @@ mobile/views/pages/user.vue:
block: "屏蔽"
unblock: "取消屏蔽"
years-old: "{age}岁"
push-to-list: "リストに追加"
select-list: "リストを選択してください"
list-pushed: "{user}を{list}に追加しました"
mobile/views/pages/user/home.vue:
recent-notes: "最近的帖子"
images: "图片"

View File

@ -1,8 +1,8 @@
{
"name": "misskey",
"author": "syuilo <i@syuilo.com>",
"version": "10.59.3",
"clientVersion": "2.0.12313",
"version": "10.60.2",
"clientVersion": "2.0.12361",
"codename": "nighthike",
"main": "./built/index.js",
"private": true,

View File

@ -48,15 +48,15 @@ export default Vue.extend({
},
remove(i) {
this.$root.alert({
this.$root.dialog({
type: 'warning',
text: this.$t('_remove.are-you-sure').replace('$1', this.announcements.find((_, j) => j == i).title),
showCancelButton: true
}).then(res => {
if (!res) return;
}).then(({ canceled }) => {
if (canceled) return;
this.announcements = this.announcements.filter((_, j) => j !== i);
this.save(true);
this.$root.alert({
this.$root.dialog({
type: 'success',
text: this.$t('_remove.removed')
});
@ -68,13 +68,13 @@ export default Vue.extend({
broadcasts: this.announcements
}).then(() => {
if (!silent) {
this.$root.alert({
this.$root.dialog({
type: 'success',
text: this.$t('saved')
});
}
}).catch(e => {
this.$root.alert({
this.$root.dialog({
type: 'error',
text: e
});

View File

@ -75,13 +75,13 @@ export default Vue.extend({
url: this.url,
aliases: this.aliases.split(' ').filter(x => x.length > 0)
}).then(() => {
this.$root.alert({
this.$root.dialog({
type: 'success',
text: this.$t('add-emoji.added')
});
this.fetchEmojis();
}).catch(e => {
this.$root.alert({
this.$root.dialog({
type: 'error',
text: e
});
@ -103,12 +103,12 @@ export default Vue.extend({
url: emoji.url,
aliases: emoji.aliases.split(' ').filter(x => x.length > 0)
}).then(() => {
this.$root.alert({
this.$root.dialog({
type: 'success',
text: this.$t('updated')
});
}).catch(e => {
this.$root.alert({
this.$root.dialog({
type: 'error',
text: e
});
@ -116,23 +116,23 @@ export default Vue.extend({
},
removeEmoji(emoji) {
this.$root.alert({
this.$root.dialog({
type: 'warning',
text: this.$t('remove-emoji.are-you-sure').replace('$1', emoji.name),
showCancelButton: true
}).then(res => {
if (!res) return;
}).then(({ canceled }) => {
if (canceled) return;
this.$root.api('admin/emoji/remove', {
id: emoji.id
}).then(() => {
this.$root.alert({
this.$root.dialog({
type: 'success',
text: this.$t('remove-emoji.removed')
});
this.fetchEmojis();
}).catch(e => {
this.$root.alert({
this.$root.dialog({
type: 'error',
text: e
});

View File

@ -212,7 +212,7 @@ export default Vue.extend({
this.$root.api('admin/invite').then(x => {
this.inviteCode = x.code;
}).catch(e => {
this.$root.alert({
this.$root.dialog({
type: 'error',
text: e
});
@ -258,12 +258,12 @@ export default Vue.extend({
smtpUser: this.smtpUser,
smtpPass: this.smtpPass
}).then(() => {
this.$root.alert({
this.$root.dialog({
type: 'success',
text: this.$t('saved')
});
}).catch(e => {
this.$root.alert({
this.$root.dialog({
type: 'error',
text: e
});

View File

@ -34,14 +34,14 @@ export default Vue.extend({
const process = async () => {
const user = await this.$root.api('users/show', parseAcct(this.username));
await this.$root.api('admin/moderators/add', { userId: user.id });
this.$root.alert({
this.$root.dialog({
type: 'success',
text: this.$t('add-moderator.added')
});
};
await process().catch(e => {
this.$root.alert({
this.$root.dialog({
type: 'error',
text: e.toString()
});

View File

@ -115,12 +115,12 @@ export default Vue.extend({
return await this.$root.api('users/show', this.target.startsWith('@') ? parseAcct(this.target) : { userId: this.target });
} catch (e) {
if (e == 'user not found') {
this.$root.alert({
this.$root.dialog({
type: 'error',
text: this.$t('user-not-found')
});
} else {
this.$root.alert({
this.$root.dialog({
type: 'error',
text: e.toString()
});
@ -138,7 +138,7 @@ export default Vue.extend({
async resetPassword() {
const user = await this.fetchUser();
this.$root.api('admin/reset-password', { userId: user.id }).then(res => {
this.$root.alert({
this.$root.dialog({
type: 'success',
text: this.$t('password-updated', { password: res.password })
});
@ -151,14 +151,14 @@ export default Vue.extend({
const process = async () => {
const user = await this.fetchUser();
await this.$root.api('admin/verify-user', { userId: user.id });
this.$root.alert({
this.$root.dialog({
type: 'success',
text: this.$t('verified')
});
};
await process().catch(e => {
this.$root.alert({
this.$root.dialog({
type: 'error',
text: e.toString()
});
@ -173,14 +173,14 @@ export default Vue.extend({
const process = async () => {
const user = await this.fetchUser();
await this.$root.api('admin/unverify-user', { userId: user.id });
this.$root.alert({
this.$root.dialog({
type: 'success',
text: this.$t('unverified')
});
};
await process().catch(e => {
this.$root.alert({
this.$root.dialog({
type: 'error',
text: e.toString()
});
@ -195,14 +195,14 @@ export default Vue.extend({
const process = async () => {
const user = await this.fetchUser();
await this.$root.api('admin/suspend-user', { userId: user.id });
this.$root.alert({
this.$root.dialog({
type: 'success',
text: this.$t('suspended')
});
};
await process().catch(e => {
this.$root.alert({
this.$root.dialog({
type: 'error',
text: e.toString()
});
@ -217,14 +217,14 @@ export default Vue.extend({
const process = async () => {
const user = await this.fetchUser();
await this.$root.api('admin/unsuspend-user', { userId: user.id });
this.$root.alert({
this.$root.dialog({
type: 'success',
text: this.$t('unsuspended')
});
};
await process().catch(e => {
this.$root.alert({
this.$root.dialog({
type: 'error',
text: e.toString()
});

View File

@ -22,7 +22,7 @@ export default async function($root: any, force = false, silent = false) {
}
/*if (!silent) {
$root.alert({
$root.dialog({
title: $root.$t('@.update-available-title'),
text: $root.$t('@.update-available', { newer, current })
});

View File

@ -4,7 +4,7 @@ export default ($root: any) => {
require('fuckadblock');
function adBlockDetected() {
$root.alert({
$root.dialog({
title: $root.$t('@.adblock.detected'),
text: $root.$t('@.adblock.warning')
});

View File

@ -142,7 +142,7 @@ export default (opts: Opts = {}) => ({
this.$root.api('notes/favorites/create', {
noteId: this.appearNote.id
}).then(() => {
this.$root.alert({
this.$root.dialog({
type: 'success',
splash: true
});

View File

@ -50,10 +50,13 @@ export default Vue.extend({
methods: {
regenerateToken() {
this.$input({
this.$root.dialog({
title: this.$t('enter-password'),
type: 'password'
}).then(password => {
input: {
type: 'password'
}
}).then(({ canceled, result: password }) => {
if (canceled) return;
this.$root.api('i/regenerate_token', {
password: password
});

View File

@ -187,7 +187,8 @@ export default Vue.extend({
} else {
this.$root.api('users/search', {
query: this.q,
limit: 10
limit: 10,
detail: false
}).then(users => {
this.users = users;
this.fetching = false;

View File

@ -2,12 +2,17 @@
<div class="felqjxyj" :class="{ splash }">
<div class="bg" ref="bg" @click="onBgClick"></div>
<div class="main" ref="main">
<div class="icon" :class="type"><fa :icon="icon"/></div>
<div class="icon" v-if="!input && !select && !user" :class="type"><fa :icon="icon"/></div>
<header v-if="title" v-html="title"></header>
<div class="body" v-if="text" v-html="text"></div>
<ui-input v-if="input" v-model="inputValue" autofocus :type="input.type || 'text'" :placeholder="input.placeholder" @keydown="onInputKeydown"></ui-input>
<ui-input v-if="user" v-model="userInputValue" autofocus @keydown="onInputKeydown"><span slot="prefix">@</span></ui-input>
<ui-select v-if="select" v-model="selectedValue">
<option v-for="item in select.items" :value="item.value">{{ item.text }}</option>
</ui-select>
<ui-horizon-group no-grow class="buttons fit-bottom" v-if="!splash">
<ui-button @click="ok" primary autofocus>OK</ui-button>
<ui-button @click="cancel" v-if="showCancelButton">Cancel</ui-button>
<ui-button @click="ok" primary :autofocus="!input && !select && !user">OK</ui-button>
<ui-button @click="cancel" v-if="showCancelButton || input || select || user">Cancel</ui-button>
</ui-horizon-group>
</div>
</div>
@ -17,6 +22,7 @@
import Vue from 'vue';
import * as anime from 'animejs';
import { faTimesCircle, faQuestionCircle } from '@fortawesome/free-regular-svg-icons';
import parseAcct from "../../../../../misc/acct/parse";
export default Vue.extend({
props: {
@ -33,6 +39,15 @@ export default Vue.extend({
type: String,
required: false
},
input: {
required: false
},
select: {
required: false
},
user: {
required: false
},
showCancelButton: {
type: Boolean,
default: false
@ -43,6 +58,14 @@ export default Vue.extend({
}
},
data() {
return {
inputValue: this.input && this.input.default ? this.input.default : null,
userInputValue: null,
selectedValue: null
};
},
computed: {
icon(): any {
switch (this.type) {
@ -82,9 +105,21 @@ export default Vue.extend({
},
methods: {
ok() {
this.$emit('ok');
this.close();
async ok() {
if (this.user) {
const user = await this.$root.api('users/show', parseAcct(this.userInputValue));
if (user) {
this.$emit('ok', user);
this.close();
}
} else {
const result =
this.input ? this.inputValue :
this.select ? this.selectedValue :
true;
this.$emit('ok', result);
this.close();
}
},
cancel() {
@ -114,6 +149,14 @@ export default Vue.extend({
onBgClick() {
this.cancel();
},
onInputKeydown(e) {
if (e.which == 13) { // Enter
e.preventDefault();
e.stopPropagation();
this.ok();
}
}
}
});
@ -180,8 +223,11 @@ export default Vue.extend({
display block
margin 0 auto
& + header
margin-top 16px
> header
margin 16px 0 8px 0
margin 0 0 8px 0
font-weight bold
font-size 20px

View File

@ -99,23 +99,22 @@ export default Vue.extend({
this.$emit('go', game);
},
match() {
this.$input({
title: this.$t('enter-username')
}).then(username => {
this.$root.api('users/show', {
username
}).then(user => {
this.$root.api('games/reversi/match', {
userId: user.id
}).then(res => {
if (res == null) {
this.$emit('matching', user);
} else {
this.$emit('go', res);
}
});
});
async match() {
const { result: user } = await this.$root.dialog({
title: this.$t('enter-username'),
user: {
local: true
}
});
if (user == null) return;
this.$root.api('games/reversi/match', {
userId: user.id
}).then(res => {
if (res == null) {
this.$emit('matching', user);
} else {
this.$emit('go', res);
}
});
},

View File

@ -1,5 +1,5 @@
<template>
<div class="onchrpzrvnoruiaenfcqvccjfuupzzwv">
<div class="onchrpzrvnoruiaenfcqvccjfuupzzwv" :class="{ big: $root.isMobile }">
<div class="backdrop" ref="backdrop" @click="close"></div>
<div class="popover" :class="{ hukidasi }" ref="popover">
<template v-for="item, i in items">
@ -125,6 +125,11 @@ export default Vue.extend({
position initial
&.big
> .popover
> button
font-size 15px
> .backdrop
position fixed
top 0

View File

@ -116,7 +116,8 @@ export default Vue.extend({
this.$root.api('users/search', {
query: this.q,
localOnly: true,
limit: 10
limit: 10,
detail: false
}).then(users => {
this.result = users.filter(user => user.id != this.$store.state.i.id);
});

View File

@ -78,7 +78,7 @@ export default Vue.extend({
this.$root.api('i/pin', {
noteId: this.note.id
}).then(() => {
this.$root.alert({
this.$root.dialog({
type: 'success',
splash: true
});
@ -95,12 +95,12 @@ export default Vue.extend({
},
del() {
this.$root.alert({
this.$root.dialog({
type: 'warning',
text: this.$t('delete-confirm'),
showCancelButton: true
}).then(res => {
if (!res) return;
}).then(({ canceled }) => {
if (canceled) return;
this.$root.api('notes/delete', {
noteId: this.note.id
@ -114,7 +114,7 @@ export default Vue.extend({
this.$root.api('notes/favorites/create', {
noteId: this.note.id
}).then(() => {
this.$root.alert({
this.$root.dialog({
type: 'success',
splash: true
});
@ -126,7 +126,7 @@ export default Vue.extend({
this.$root.api('notes/favorites/delete', {
noteId: this.note.id
}).then(() => {
this.$root.alert({
this.$root.dialog({
type: 'success',
splash: true
});

View File

@ -11,34 +11,43 @@ import i18n from '../../../i18n';
export default Vue.extend({
i18n: i18n('common/views/components/password-settings.vue'),
methods: {
reset() {
this.$input({
async reset() {
const { canceled: canceled1, result: currentPassword } = await this.$root.dialog({
title: this.$t('enter-current-password'),
type: 'password'
}).then(currentPassword => {
this.$input({
title: this.$t('enter-new-password'),
input: {
type: 'password'
}).then(newPassword => {
this.$input({
title: this.$t('enter-new-password-again'),
type: 'password'
}).then(newPassword2 => {
if (newPassword !== newPassword2) {
this.$root.alert({
title: null,
text: this.$t('not-match')
});
return;
}
this.$root.api('i/change_password', {
currentPasword: currentPassword,
newPassword: newPassword
}).then(() => {
this.$notify(this.$t('changed'));
});
});
}
});
if (canceled1) return;
const { canceled: canceled2, result: newPassword } = await this.$root.dialog({
title: this.$t('enter-new-password'),
input: {
type: 'password'
}
});
if (canceled2) return;
const { canceled: canceled3, result: newPassword2 } = await this.$root.dialog({
title: this.$t('enter-new-password-again'),
input: {
type: 'password'
}
});
if (canceled3) return;
if (newPassword !== newPassword2) {
this.$root.dialog({
title: null,
text: this.$t('not-match')
});
return;
}
this.$root.api('i/change_password', {
currentPasword: currentPassword,
newPassword: newPassword
}).then(() => {
this.$notify(this.$t('changed'));
});
}
}

View File

@ -213,7 +213,7 @@ export default Vue.extend({
this.$store.state.i.bannerUrl = i.bannerUrl;
if (notify) {
this.$root.alert({
this.$root.dialog({
type: 'success',
text: this.$t('saved')
});
@ -222,10 +222,13 @@ export default Vue.extend({
},
updateEmail() {
this.$input({
this.$root.dialog({
title: this.$t('@.enter-password'),
type: 'password'
}).then(password => {
input: {
type: 'password'
}
}).then(({ canceled, result: password }) => {
if (canceled) return;
this.$root.api('i/update_email', {
password: password,
email: this.email == '' ? null : this.email

View File

@ -223,7 +223,7 @@ export default Vue.extend({
try {
theme = JSON5.parse(code);
} catch (e) {
this.$root.alert({
this.$root.dialog({
type: 'error',
text: this.$t('invalid-theme')
});
@ -236,7 +236,7 @@ export default Vue.extend({
}
if (theme.id == null) {
this.$root.alert({
this.$root.dialog({
type: 'error',
text: this.$t('invalid-theme')
});
@ -244,7 +244,7 @@ export default Vue.extend({
}
if (this.$store.state.device.themes.some(t => t.id == theme.id)) {
this.$root.alert({
this.$root.dialog({
type: 'info',
text: this.$t('already-installed')
});
@ -256,7 +256,7 @@ export default Vue.extend({
key: 'themes', value: themes
});
this.$root.alert({
this.$root.dialog({
type: 'success',
text: this.$t('installed').replace('{}', theme.name)
});
@ -269,7 +269,7 @@ export default Vue.extend({
key: 'themes', value: themes
});
this.$root.alert({
this.$root.dialog({
type: 'info',
text: this.$t('uninstalled').replace('{}', theme.name)
});
@ -306,7 +306,7 @@ export default Vue.extend({
const theme = this.myTheme;
if (theme.name == null || theme.name.trim() == '') {
this.$root.alert({
this.$root.dialog({
type: 'warning',
text: this.$t('theme-name-required')
});
@ -320,7 +320,7 @@ export default Vue.extend({
key: 'themes', value: themes
});
this.$root.alert({
this.$root.dialog({
type: 'success',
text: this.$t('saved')
});

View File

@ -14,17 +14,19 @@
:disabled="disabled"
:required="required"
:readonly="readonly"
:placeholder="placeholder"
:pattern="pattern"
:autocomplete="autocomplete"
:spellcheck="spellcheck"
@focus="focused = true"
@blur="focused = false"
@keydown="$emit('keydown', $event)"
>
</template>
<template v-else>
<input ref="input"
type="text"
:value="placeholder"
:value="filePlaceholder"
readonly
@click="chooseFile"
>
@ -74,6 +76,15 @@ export default Vue.extend({
type: String,
required: false
},
placeholder: {
type: String,
required: false
},
autofocus: {
type: Boolean,
required: false,
default: false
},
autocomplete: {
required: false
},
@ -109,7 +120,7 @@ export default Vue.extend({
filled(): boolean {
return this.v != '' && this.v != null;
},
placeholder(): string {
filePlaceholder(): string {
if (this.type != 'file') return null;
if (this.v == null) return null;
@ -142,6 +153,12 @@ export default Vue.extend({
}
},
mounted() {
if (this.autofocus) {
this.$nextTick(() => {
this.$refs.input.focus();
});
}
this.$nextTick(() => {
if (this.$refs.prefix) {
this.$refs.label.style.left = (this.$refs.prefix.offsetLeft + this.$refs.prefix.offsetWidth) + 'px';

View File

@ -8,7 +8,7 @@ export default ($root: any) => {
const regex = RegExp('\.(jpg|jpeg|png|gif|webp|bmp|tiff)$');
if (!regex.test(file.name) ) {
$root.alert({
$root.dialog({
title: '%fa:info-circle% %i18n:desktop.invalid-filetype%',
text: null
});
@ -87,7 +87,7 @@ export default ($root: any) => {
value: i.avatarUrl
});
$root.alert({
$root.dialog({
title: '%fa:info-circle% %i18n:desktop.avatar-updated%',
text: null
});

View File

@ -87,7 +87,7 @@ export default ($root: any) => {
value: i.bannerUrl
});
$root.alert({
$root.dialog({
title: '%fa:info-circle% %i18n:desktop.banner-updated%',
text: null
});

View File

@ -34,7 +34,6 @@ import PostFormWindow from './views/components/post-form-window.vue';
import RenoteFormWindow from './views/components/renote-form-window.vue';
import MkChooseFileFromDriveWindow from './views/components/choose-file-from-drive-window.vue';
import MkChooseFolderFromDriveWindow from './views/components/choose-folder-from-drive-window.vue';
import InputDialog from './views/components/input-dialog.vue';
import Notification from './views/components/ui-notification.vue';
import { url } from '../config';
@ -113,22 +112,6 @@ init(async (launch) => {
});
},
$input(opts) {
return new Promise<string>((res, rej) => {
const o = opts || {};
const d = this.$root.new(InputDialog, {
title: o.title,
placeholder: o.placeholder,
default: o.default,
type: o.type || 'text',
allowEmpty: o.allowEmpty
});
d.$once('done', text => {
res(text);
});
});
},
$notify(message) {
this.$root.new(Notification, {
message

View File

@ -148,12 +148,15 @@ export default Vue.extend({
},
rename() {
this.$input({
this.$root.dialog({
title: this.$t('contextmenu.rename-file'),
placeholder: this.$t('contextmenu.input-new-file-name'),
default: this.file.name,
allowEmpty: false
}).then(name => {
input: {
placeholder: this.$t('contextmenu.input-new-file-name'),
default: this.file.name,
allowEmpty: false
}
}).then(({ canceled, result: name }) => {
if (canceled) return;
this.$root.api('drive/files/update', {
fileId: this.file.id,
name: name
@ -170,7 +173,7 @@ export default Vue.extend({
copyUrl() {
copyToClipboard(this.file.url);
this.$root.alert({
this.$root.dialog({
title: this.$t('contextmenu.copied'),
text: this.$t('contextmenu.copied-url-to-clipboard')
});

View File

@ -155,7 +155,7 @@ export default Vue.extend({
}).catch(err => {
switch (err) {
case 'detected-circular-definition':
this.$root.alert({
this.$root.dialog({
title: this.$t('unable-to-process'),
text: this.$t('circular-reference-detected')
});
@ -192,11 +192,14 @@ export default Vue.extend({
},
rename() {
this.$input({
this.$root.dialog({
title: this.$t('contextmenu.rename-folder'),
placeholder: this.$t('contextmenu.input-new-folder-name'),
default: this.folder.name
}).then(name => {
input: {
placeholder: this.$t('contextmenu.input-new-folder-name'),
default: this.folder.name
}
}).then(({ canceled, result: name }) => {
if (canceled) return;
this.$root.api('drive/folders/update', {
folderId: this.folder.id,
name: name

View File

@ -313,7 +313,7 @@ export default Vue.extend({
}).catch(err => {
switch (err) {
case 'detected-circular-definition':
this.$root.alert({
this.$root.dialog({
title: this.$t('unable-to-process'),
text: this.$t('circular-reference-detected')
});
@ -331,16 +331,19 @@ export default Vue.extend({
},
urlUpload() {
this.$input({
this.$root.dialog({
title: this.$t('url-upload'),
placeholder: this.$t('url-of-file')
}).then(url => {
input: {
placeholder: this.$t('url-of-file')
}
}).then(({ canceled, result: url }) => {
if (canceled) return;
this.$root.api('drive/files/upload_from_url', {
url: url,
folderId: this.folder ? this.folder.id : undefined
});
this.$root.alert({
this.$root.dialog({
title: this.$t('url-upload-requested'),
text: this.$t('may-take-time')
});
@ -348,10 +351,13 @@ export default Vue.extend({
},
createFolder() {
this.$input({
this.$root.dialog({
title: this.$t('create-folder'),
placeholder: this.$t('folder-name')
}).then(name => {
input: {
placeholder: this.$t('folder-name')
}
}).then(({ canceled, result: name }) => {
if (canceled) return;
this.$root.api('drive/folders/create', {
name: name,
parentId: this.folder ? this.folder.id : undefined

View File

@ -186,7 +186,7 @@ export default Vue.extend({
methods: {
hint() {
this.$root.alert({
this.$root.dialog({
title: this.$t('@.customization-tips.title'),
text: this.$t('@.customization-tips.paragraph')
});

View File

@ -1,180 +0,0 @@
<template>
<mk-window ref="window" is-modal width="500px" @before-close="beforeClose" @closed="destroyDom">
<span slot="header" :class="$style.header">
<fa icon="i-cursor"/>{{ title }}
</span>
<div :class="$style.body">
<input ref="text" v-model="text" :type="type" @keydown="onKeydown" :placeholder="placeholder"/>
</div>
<div :class="$style.actions">
<button :class="$style.cancel" @click="cancel">{{ $t('cancel') }}</button>
<button :class="$style.ok" :disabled="!allowEmpty && text.length == 0" @click="ok">{{ $t('ok') }}</button>
</div>
</mk-window>
</template>
<script lang="ts">
import Vue from 'vue';
import i18n from '../../../i18n';
export default Vue.extend({
i18n: i18n('desktop/views/input-dialog.vue'),
props: {
title: {
type: String
},
placeholder: {
type: String
},
default: {
type: String
},
allowEmpty: {
default: true
},
type: {
default: 'text'
}
},
data() {
return {
done: false,
text: ''
};
},
mounted() {
if (this.default) this.text = this.default;
this.$nextTick(() => {
(this.$refs.text as any).focus();
});
},
methods: {
ok() {
if (!this.allowEmpty && this.text == '') return;
this.done = true;
(this.$refs.window as any).close();
},
cancel() {
this.done = false;
(this.$refs.window as any).close();
},
beforeClose() {
if (this.done) {
this.$emit('done', this.text);
} else {
this.$emit('canceled');
}
},
onKeydown(e) {
if (e.which == 13) { // Enter
e.preventDefault();
e.stopPropagation();
this.ok();
}
}
}
});
</script>
<style lang="stylus" module>
.header
> [data-icon]
margin-right 4px
.body
padding 16px
> input
display block
padding 8px
margin 0
width 100%
max-width 100%
min-width 100%
font-size 1em
color #333
background #fff
outline none
border solid 1px var(--primaryAlpha01)
border-radius 4px
transition border-color .3s ease
&:hover
border-color var(--primaryAlpha02)
transition border-color .1s ease
&:focus
color var(--primary)
border-color var(--primaryAlpha05)
transition border-color 0s ease
&::-webkit-input-placeholder
color var(--primaryAlpha03)
.actions
height 72px
background var(--primaryLighten95)
.ok
.cancel
display block
position absolute
bottom 16px
cursor pointer
padding 0
margin 0
width 120px
height 40px
font-size 1em
outline none
border-radius 4px
&:focus
&:after
content ""
pointer-events none
position absolute
top -5px
right -5px
bottom -5px
left -5px
border 2px solid var(--primaryAlpha03)
border-radius 8px
&:disabled
opacity 0.7
cursor default
.ok
right 16px
color var(--primaryForeground)
background linear-gradient(to bottom, var(--primaryLighten25) 0%, var(--primaryLighten10) 100%)
border solid 1px var(--primaryLighten15)
&:not(:disabled)
font-weight bold
&:hover:not(:disabled)
background linear-gradient(to bottom, var(--primaryLighten8) 0%, var(--primaryDarken8) 100%)
border-color var(--primary)
&:active:not(:disabled)
background var(--primary)
border-color var(--primary)
.cancel
right 148px
color #888
background linear-gradient(to bottom, #ffffff 0%, #f5f5f5 100%)
border solid 1px #e2e2e2
&:hover
background linear-gradient(to bottom, #f9f9f9 0%, #ececec 100%)
border-color #dcdcdc
&:active
background #ececec
border-color #dcdcdc
</style>

View File

@ -39,7 +39,7 @@
</header>
<div class="body">
<p v-if="appearNote.cw != null" class="cw">
<span class="text" v-if="appearNote.cw != ''">{{ appearNote.cw }}</span>
<misskey-flavored-markdown v-if="appearNote.cw != ''" class="text" :text="appearNote.cw" :author="appearNote.user" :i="$store.state.i" :custom-emojis="appearNote.emojis" />
<mk-cw-button v-model="showContent"/>
</p>
<div class="content" v-show="appearNote.cw == null || showContent">

View File

@ -5,7 +5,7 @@
<mk-note-header class="header" :note="note" :mini="true"/>
<div class="body">
<p v-if="note.cw != null" class="cw">
<span class="text" v-if="note.cw != ''">{{ note.cw }}</span>
<misskey-flavored-markdown v-if="note.cw != ''" class="text" :text="note.cw" :author="note.user" :i="$store.state.i" :custom-emojis="note.emojis" />
<mk-cw-button v-model="showContent"/>
</p>
<div class="content" v-show="note.cw == null || showContent">

View File

@ -5,7 +5,7 @@
<mk-note-header class="header" :note="note"/>
<div class="body">
<p v-if="note.cw != null" class="cw">
<span class="text" v-if="note.cw != ''">{{ note.cw }}</span>
<misskey-flavored-markdown v-if="note.cw != ''" class="text" :text="note.cw" :author="note.user" :i="$store.state.i" :custom-emojis="note.emojis" />
<mk-cw-button v-model="showContent"/>
</p>
<div class="content" v-show="note.cw == null || showContent">

View File

@ -20,7 +20,7 @@
<mk-note-header class="header" :note="appearNote" :mini="mini"/>
<div class="body" v-if="appearNote.deletedAt == null">
<p v-if="appearNote.cw != null" class="cw">
<span class="text" v-if="appearNote.cw != ''">{{ appearNote.cw }}</span>
<misskey-flavored-markdown v-if="appearNote.cw != ''" class="text" :text="appearNote.cw" :author="appearNote.user" :i="$store.state.i" :custom-emojis="appearNote.emojis" />
<mk-cw-button v-model="showContent"/>
</p>
<div class="content" v-show="appearNote.cw == null || showContent">

View File

@ -265,6 +265,7 @@ export default Vue.extend({
display inline-block
overflow hidden
max-height 48px
word-break break-all
.note-ref
color var(--noteText)

View File

@ -15,7 +15,7 @@
<a v-for="tag in recentHashtags.slice(0, 5)" @click="addTag(tag)" :title="$t('click-to-tagging')">#{{ tag }}</a>
</div>
<div class="local-only" v-if="this.localOnly == true">{{ $t('local-only-message') }}</div>
<input v-show="useCw" v-model="cw" :placeholder="$t('annotations')">
<input v-show="useCw" ref="cw" v-model="cw" :placeholder="$t('annotations')" v-autocomplete="'cw'">
<div class="textarea">
<textarea :class="{ with: (files.length != 0 || poll) }"
ref="text" v-model="text" :disabled="posting"
@ -384,13 +384,12 @@ export default Vue.extend({
},
addVisibleUser() {
this.$input({
title: this.$t('enter-username')
}).then(acct => {
if (acct.startsWith('@')) acct = acct.substr(1);
this.$root.api('users/show', parseAcct(acct)).then(user => {
this.visibleUsers.push(user);
});
this.$root.dialog({
title: this.$t('enter-username'),
user: true
}).then(({ canceled, result: user }) => {
if (canceled) return;
this.visibleUsers.push(user);
});
},

View File

@ -35,10 +35,13 @@ export default Vue.extend({
},
methods: {
register() {
this.$input({
this.$root.dialog({
title: this.$t('enter-password'),
type: 'password'
}).then(password => {
input: {
type: 'password'
}
}).then(({ canceled, result: password }) => {
if (canceled) return;
this.$root.api('i/2fa/register', {
password: password
}).then(data => {
@ -48,10 +51,13 @@ export default Vue.extend({
},
unregister() {
this.$input({
this.$root.dialog({
title: this.$t('enter-password'),
type: 'password'
}).then(password => {
input: {
type: 'password'
}
}).then(({ canceled, result: password }) => {
if (canceled) return;
this.$root.api('i/2fa/unregister', {
password: password
}).then(() => {

View File

@ -596,12 +596,12 @@ export default Vue.extend({
this.checkingForUpdate = false;
this.latestVersion = newer;
if (newer == null) {
this.$root.alert({
this.$root.dialog({
title: this.$t('no-updates'),
text: this.$t('no-updates-desc')
});
} else {
this.$root.alert({
this.$root.dialog({
title: this.$t('update-available'),
text: this.$t('update-available-desc')
});
@ -610,7 +610,7 @@ export default Vue.extend({
},
clean() {
localStorage.clear();
this.$root.alert({
this.$root.dialog({
title: this.$t('cache-cleared'),
text: this.$t('cache-cleared-desc')
});

View File

@ -109,9 +109,11 @@ export default Vue.extend({
icon: 'plus',
text: this.$t('add-list'),
action: () => {
this.$input({
this.$root.dialog({
title: this.$t('list-name'),
}).then(async title => {
input: true
}).then(async ({ canceled, result: title }) => {
if (canceled) return;
const list = await this.$root.api('users/lists/create', {
title
});

View File

@ -29,9 +29,11 @@ export default Vue.extend({
},
methods: {
add() {
this.$input({
this.$root.dialog({
title: this.$t('list-name'),
}).then(async title => {
input: true
}).then(async ({ canceled, result: title }) => {
if (canceled) return;
const list = await this.$root.api('users/lists/create', {
title
});

View File

@ -156,7 +156,7 @@ export default Vue.extend({
> .follow-button
position absolute
top 92px
top 8px
right 8px
</style>

View File

@ -167,11 +167,14 @@ export default Vue.extend({
icon: 'pencil-alt',
text: this.$t('rename'),
action: () => {
this.$input({
this.$root.dialog({
title: this.$t('rename'),
default: this.name,
allowEmpty: false
}).then(name => {
input: {
default: this.name,
allowEmpty: false
}
}).then(({ canceled, result: name }) => {
if (canceled) return;
this.$store.dispatch('settings/renameDeckColumn', { id: this.column.id, name });
});
}

View File

@ -307,7 +307,7 @@ export default Vue.extend({
listId: list.id,
userId: this.user.id
});
this.$root.alert({
this.$root.dialog({
type: 'success',
splash: true
});

View File

@ -252,9 +252,11 @@ export default Vue.extend({
icon: 'hashtag',
text: this.$t('@deck.hashtag'),
action: () => {
this.$input({
title: this.$t('enter-hashtag-tl-title')
}).then(title => {
this.$root.dialog({
title: this.$t('enter-hashtag-tl-title'),
input: true
}).then(({ canceled, result: title }) => {
if (canceled) return;
this.$store.dispatch('settings/addDeckColumn', {
id: uuid(),
type: 'hashtag',

View File

@ -73,12 +73,12 @@ export default Vue.extend({
},
block() {
this.$root.alert({
this.$root.dialog({
type: 'warning',
text: this.$t('block-confirm'),
showCancelButton: true
}).then(res => {
if (!res) return;
}).then(({ canceled }) => {
if (canceled) return;
this.$root.api('blocking/create', {
userId: this.user.id
@ -108,9 +108,13 @@ export default Vue.extend({
listId: list.id,
userId: this.user.id
});
this.$root.alert({
this.$root.dialog({
type: 'success',
title: 'Done!',
text: this.$t('list-pushed').replace('{user}', this.user.name).replace('{list}', list.title)
text: this.$t('list-pushed', {
user: this.user.name,
list: list.title
})
});
});
}

View File

@ -15,7 +15,7 @@ import checkForUpdate from './common/scripts/check-for-update';
import MiOS from './mios';
import { clientVersion as version, codename, lang } from './config';
import { builtinThemes, lightTheme, applyTheme } from './theme';
import Alert from './common/views/components/alert.vue';
import Dialog from './common/views/components/dialog.vue';
if (localStorage.getItem('theme') == null) {
applyTheme(lightTheme);
@ -144,6 +144,8 @@ import {
faHdd as farHdd,
faMoon as farMoon,
faPlayCircle as farPlayCircle,
faLightbulb as farLightbulb,
faStickyNote as farStickyNote,
} from '@fortawesome/free-regular-svg-icons';
import {
@ -249,7 +251,7 @@ library.add(
faSync,
faArrowLeft,
faMapMarker,
faRobot,
faRobot,
farBell,
farEnvelope,
@ -270,6 +272,8 @@ library.add(
farHdd,
farMoon,
farPlayCircle,
farLightbulb,
farStickyNote,
fabTwitter,
fabGithub,
@ -453,11 +457,11 @@ export default (callback: (launch: (router: VueRouter) => [Vue, MiOS]) => void,
document.body.appendChild(x.$el);
return x;
},
alert(opts) {
dialog(opts) {
return new Promise((res) => {
const vm = this.new(Alert, opts);
vm.$once('ok', () => res(true));
vm.$once('cancel', () => res(false));
const vm = this.new(Dialog, opts);
vm.$once('ok', result => res({ canceled: false, result }));
vm.$once('cancel', () => res({ canceled: true }));
});
}
},

View File

@ -41,6 +41,12 @@ import FolderChooser from './views/components/drive-folder-chooser.vue';
*/
init((launch) => {
Vue.mixin({
data() {
return {
isMobile: true
};
},
methods: {
$post(opts) {
const o = opts || {};
@ -89,15 +95,6 @@ init((launch) => {
});
},
$input(opts) {
return new Promise<string>((res, rej) => {
const x = window.prompt(opts.title);
if (x) {
res(x);
}
});
},
$notify(message) {
alert(message);
}

View File

@ -26,7 +26,7 @@
</header>
<div class="body">
<p v-if="appearNote.cw != null" class="cw">
<span class="text" v-if="appearNote.cw != ''">{{ appearNote.cw }}</span>
<misskey-flavored-markdown v-if="appearNote.cw != ''" class="text" :text="appearNote.cw" :author="appearNote.user" :i="$store.state.i" :custom-emojis="appearNote.emojis" />
<mk-cw-button v-model="showContent"/>
</p>
<div class="content" v-show="appearNote.cw == null || showContent">

View File

@ -5,7 +5,7 @@
<mk-note-header class="header" :note="note" :mini="true"/>
<div class="body">
<p v-if="note.cw != null" class="cw">
<span class="text" v-if="note.cw != ''">{{ note.cw }}</span>
<misskey-flavored-markdown v-if="note.cw != ''" class="text" :text="note.cw" :author="note.user" :i="$store.state.i" :custom-emojis="note.emojis" />
<mk-cw-button v-model="showContent"/>
</p>
<div class="content" v-show="note.cw == null || showContent">

View File

@ -16,7 +16,7 @@
<mk-note-header class="header" :note="appearNote" :mini="true"/>
<div class="body" v-if="appearNote.deletedAt == null">
<p v-if="appearNote.cw != null" class="cw">
<span class="text" v-if="appearNote.cw != ''">{{ appearNote.cw }}</span>
<misskey-flavored-markdown v-if="appearNote.cw != ''" class="text" :text="appearNote.cw" :author="appearNote.user" :i="$store.state.i" :custom-emojis="appearNote.emojis" />
<mk-cw-button v-model="showContent"/>
</p>
<div class="content" v-show="appearNote.cw == null || showContent">

View File

@ -16,7 +16,7 @@
<span v-for="u in visibleUsers">{{ u | userName }}<a @click="removeVisibleUser(u)">[x]</a></span>
<a @click="addVisibleUser">+{{ $t('add-visible-user') }}</a>
</div>
<input v-show="useCw" v-model="cw" :placeholder="$t('annotations')">
<input v-show="useCw" ref="cw" v-model="cw" :placeholder="$t('annotations')" v-autocomplete="'cw'">
<textarea v-model="text" ref="text" :disabled="posting" :placeholder="placeholder" v-autocomplete="'text'"></textarea>
<div class="attaches" v-show="files.length != 0">
<x-draggable class="files" :list="files" :options="{ animation: 150 }">
@ -219,6 +219,16 @@ export default Vue.extend({
(this.$refs.text as any).focus();
},
addVisibleUser() {
this.$root.dialog({
title: this.$t('enter-username'),
user: true
}).then(({ canceled, result: user }) => {
if (canceled) return;
this.visibleUsers.push(user);
});
},
chooseFile() {
(this.$refs.file as any).click();
},

View File

@ -23,12 +23,12 @@ export default Vue.extend({
},
methods: {
fn() {
this.$root.alert({
this.$root.dialog({
type: 'warning',
text: this.$t('read-all'),
showCancelButton: true
}).then(res => {
if (!res) return;
}).then(({ canceled }) => {
if (canceled) return;
this.$root.api('notifications/mark_all_as_read');
});

View File

@ -402,12 +402,12 @@ export default Vue.extend({
this.checkingForUpdate = false;
this.latestVersion = newer;
if (newer == null) {
this.$root.alert({
this.$root.dialog({
title: this.$t('no-updates'),
text: this.$t('no-updates-desc')
});
} else {
this.$root.alert({
this.$root.dialog({
title: this.$t('update-available'),
text: this.$t('update-available-desc')
});

View File

@ -38,9 +38,11 @@ export default Vue.extend({
},
methods: {
fn() {
this.$input({
this.$root.dialog({
title: this.$t('enter-list-name'),
}).then(async title => {
input: true
}).then(async ({ canceled, result: title }) => {
if (canceled) return;
const list = await this.$root.api('users/lists/create', {
title
});

View File

@ -116,6 +116,34 @@ export default Vue.extend({
menu() {
let menu = [{
icon: ['fas', 'list'],
text: this.$t('push-to-list'),
action: async () => {
const lists = await this.$root.api('users/lists/list');
const { canceled, result: listId } = await this.$root.dialog({
type: null,
title: this.$t('select-list'),
select: {
items: lists.map(list => ({
value: list.id, text: list.title
}))
},
showCancelButton: true
});
if (canceled) return;
await this.$root.api('users/lists/push', {
listId: listId,
userId: this.user.id
});
this.$root.dialog({
type: 'success',
text: this.$t('list-pushed', {
user: this.user.name,
list: lists.find(l => l.id === listId).title
})
});
}
}, null, {
icon: this.user.isMuted ? ['fas', 'eye'] : ['far', 'eye-slash'],
text: this.user.isMuted ? this.$t('unmute') : this.$t('mute'),
action: () => {

View File

@ -63,7 +63,7 @@ export default Vue.extend({
methods: {
showDialog() {
this.$root.alert({
this.$root.dialog({
type: this.dialogType,
title: this.dialogTitle,
text: this.dialogText,

View File

@ -26,45 +26,6 @@ export default (source: string): Node[] => {
nodes = concatText(nodes);
concatTextRecursive(nodes);
function getBeforeTextNode(node: Node): Node {
if (node == null) return null;
if (node.name == 'text') return node;
if (node.children) return getBeforeTextNode(node.children[node.children.length - 1]);
return null;
}
function getAfterTextNode(node: Node): Node {
if (node == null) return null;
if (node.name == 'text') return node;
if (node.children) return getBeforeTextNode(node.children[0]);
return null;
}
function isBlockNode(node: Node): boolean {
return ['blockCode', 'center', 'quote', 'title'].includes(node.name);
}
/**
* ブロック要素の前後にある改行を削除します
* (ブロック要素自体が改行の役割を果たすため、余計に改行されてしまう)
* @param nodes
*/
const removeNeedlessLineBreaks = (nodes: Node[]) => {
nodes.forEach((node, i) => {
if (node.children) removeNeedlessLineBreaks(node.children);
if (isBlockNode(node)) {
const before = getBeforeTextNode(nodes[i - 1]);
const after = getAfterTextNode(nodes[i + 1]);
if (before && before.props.text.endsWith('\n')) {
before.props.text = before.props.text.substring(0, before.props.text.length - 1);
}
if (after && after.props.text.startsWith('\n')) {
after.props.text = after.props.text.substring(1);
}
}
});
};
const removeEmptyTextNodes = (nodes: Node[]) => {
nodes.forEach(n => {
if (n.children) {
@ -74,8 +35,6 @@ export default (source: string): Node[] => {
return nodes.filter(n => !(n.name == 'text' && n.props.text == ''));
};
removeNeedlessLineBreaks(nodes);
nodes = removeEmptyTextNodes(nodes);
return nodes;

File diff suppressed because one or more lines are too long

View File

@ -48,7 +48,7 @@ export interface INotification {
/**
* 通知が読まれたかどうか
*/
isRead: Boolean;
isRead: boolean;
}
export const packMany = (

View File

@ -57,3 +57,15 @@ export function lessThan(xs: number[], ys: number[]): boolean {
}
return xs.length < ys.length;
}
export function takeWhile<T>(f: (x: T) => boolean, xs: T[]): T[] {
const ys = [];
for (const x of xs) {
if (f(x)) {
ys.push(x);
} else {
break;
}
}
return ys;
}

View File

@ -10,7 +10,7 @@ import { resolvePerson, updatePerson } from './person';
import { resolveImage } from './image';
import { IRemoteUser, IUser } from '../../../models/user';
import htmlToMFM from '../../../mfm/html-to-mfm';
import Emoji from '../../../models/emoji';
import Emoji, { IEmoji } from '../../../models/emoji';
import { ITag } from './tag';
import { toUnicode } from 'punycode';
import { unique, concat, difference } from '../../../prelude/array';
@ -84,6 +84,8 @@ export async function createNote(value: any, resolver?: Resolver, silent = false
const apMentions = await extractMentionedUsers(actor, note.to, note.cc, resolver);
const apHashtags = await extractHashtags(note.tag);
// 添付ファイル
// TODO: attachmentは必ずしもImageではない
// TODO: attachmentは必ずしも配列ではない
@ -108,10 +110,13 @@ export async function createNote(value: any, resolver?: Resolver, silent = false
// テキストのパース
const text = note._misskey_content ? note._misskey_content : htmlToMFM(note.content);
await extractEmojis(note.tag, actor.host).catch(e => {
const emojis = await extractEmojis(note.tag, actor.host).catch(e => {
console.log(`extractEmojis: ${e}`);
return [] as IEmoji[];
});
const apEmojis = emojis.map(emoji => emoji.name);
// ユーザーの情報が古かったらついでに更新しておく
if (actor.lastFetchedAt == null || Date.now() - actor.lastFetchedAt.getTime() > 1000 * 60 * 60 * 24) {
updatePerson(note.attributedTo);
@ -130,6 +135,8 @@ export async function createNote(value: any, resolver?: Resolver, silent = false
visibility,
visibleUsers,
apMentions,
apHashtags,
apEmojis,
uri: note.id
}, silent);
}
@ -199,3 +206,14 @@ async function extractMentionedUsers(actor: IRemoteUser, to: string[], cc: strin
return users.filter(x => x != null);
}
function extractHashtags(tags: ITag[]) {
if (!tags) return [];
const hashtags = tags.filter(tag => tag.type === 'Hashtag' && typeof tag.name == 'string');
return hashtags.map(tag => {
const m = tag.name.match(/^#(.+)/);
return m ? m[1] : null;
}).filter(x => x != null);
}

View File

@ -164,7 +164,7 @@ export async function createPerson(uri: string, resolver?: Resolver): Promise<IU
uri: person.id,
url: person.url,
isBot: isBot,
isCat: (person as any).isCat === true ? true : false
isCat: (person as any).isCat === true
}) as IRemoteUser;
} catch (e) {
// duplicate key error
@ -322,7 +322,7 @@ export async function updatePerson(uri: string, resolver?: Resolver, hint?: obje
url: person.url,
endpoints: person.endpoints,
isBot: object.type == 'Service',
isCat: (person as any).isCat === true ? true : false,
isCat: (person as any).isCat === true,
isLocked: person.manuallyApprovesFollowers,
createdAt: Date.parse(person.published) || null,
publicKey: {

View File

@ -63,7 +63,7 @@ export default define(meta, (ps) => new Promise(async (res, rej) => {
});
// タグを人気順に並べ替え
tags = tags.sort((a, b) => b.count - a.count);
tags.sort((a, b) => b.count - a.count);
tags = tags.slice(0, 30);

View File

@ -106,9 +106,7 @@ export default define(meta, (ps) => new Promise(async (res, rej) => {
const withFiles = ps.withFiles != undefined ? ps.withFiles : ps.media;
if (withFiles) {
query.fileIds = withFiles ? { $exists: true, $ne: null } : [];
}
if (withFiles) query.fileIds = { $exists: true, $ne: null };
if (ps.poll != undefined) {
query.poll = ps.poll ? { $exists: true, $ne: null } : null;

View File

@ -1,12 +1,13 @@
const ms = require('ms');
import $ from 'cafy';
import User, { pack } from '../../../../models/user';
import User, { pack, ILocalUser } from '../../../../models/user';
import { getFriendIds } from '../../common/get-friends';
import Mute from '../../../../models/mute';
import * as request from 'request';
import * as request from 'request-promise-native';
import config from '../../../../config';
import define from '../../define';
import fetchMeta from '../../../../misc/fetch-meta';
import resolveUser from '../../../../remote/resolve-user';
export const meta = {
desc: {
@ -53,13 +54,10 @@ export default define(meta, (ps, me) => new Promise(async (res, rej) => {
json: true,
followRedirect: true,
followAllRedirects: true
}, (error: any, response: any, body: any) => {
if (!error && response.statusCode == 200) {
res(body);
} else {
res([]);
}
});
})
.then(body => convertUsers(body, me))
.then(packed => res(packed))
.catch(e => rej(e));
} else {
// ID list of the user itself and other users who the user follows
const followingIds = await getFriendIds(me._id);
@ -90,3 +88,30 @@ export default define(meta, (ps, me) => new Promise(async (res, rej) => {
res(await Promise.all(users.map(user => pack(user, me, { detail: true }))));
}
}));
type IRecommendUser = {
name: string;
username: string;
host: string;
description: string;
avatarUrl: string;
};
/**
* Resolve/Pack dummy users
*/
async function convertUsers(src: IRecommendUser[], me: ILocalUser) {
const packed = await Promise.all(src.map(async x => {
const user = await resolveUser(x.username, x.host)
.catch(() => {
console.warn(`Can't resolve ${x.username}@${x.host}`);
return null;
});
if (user == null) return x;
return await pack(user, me, { detail: true });
}));
return packed;
}

View File

@ -41,6 +41,14 @@ export const meta = {
'ja-JP': 'ローカルユーザーのみ検索対象にするか否か'
}
},
detail: {
validator: $.bool.optional,
default: true,
desc: {
'ja-JP': '詳細なユーザー情報を含めるか否か'
}
},
},
};
@ -72,6 +80,5 @@ export default define(meta, (ps, me) => new Promise(async (res, rej) => {
}
}
// Serialize
res(await Promise.all(users.map(user => pack(user, me, { detail: true }))));
res(await Promise.all(users.map(user => pack(user, me, { detail: ps.detail }))));
}));

View File

@ -30,7 +30,7 @@ async function genVars(lang: string): Promise<{ [key: string]: any }> {
const entities = glob.sync('src/docs/api/entities/**/*.yaml', { cwd });
vars['entities'] = entities.map(x => {
const _x = yaml.safeLoad(fs.readFileSync(cwd + x, 'utf-8')) as any;
const _x = yaml.safeLoad(fs.readFileSync(cwd + x, 'utf-8'));
return _x.name;
});
@ -197,7 +197,7 @@ router.get('/*/api/entities/*', async ctx => {
const lang = ctx.params[0];
const entity = ctx.params[1];
const x = yaml.safeLoad(fs.readFileSync(path.resolve(`${__dirname}/../../../src/docs/api/entities/${entity}.yaml`), 'utf-8')) as any;
const x = yaml.safeLoad(fs.readFileSync(path.resolve(`${__dirname}/../../../src/docs/api/entities/${entity}.yaml`), 'utf-8'));
await ctx.render('../../../../src/docs/api/entities/view', Object.assign(await genVars(lang), {
id: `api/entities/${entity}`,

View File

@ -98,6 +98,8 @@ type Option = {
visibility?: string;
visibleUsers?: IUser[];
apMentions?: IUser[];
apHashtags?: string[];
apEmojis?: string[];
uri?: string;
app?: IApp;
};
@ -153,14 +155,22 @@ export default async (user: IUser, data: Option, silent = false) => new Promise<
data.text = data.text.trim();
}
// Parse MFM
const tokens = data.text ? parse(data.text) : [];
let tags = data.apHashtags;
let emojis = data.apEmojis;
let mentionedUsers = data.apMentions;
const tags = extractHashtags(tokens);
// Parse MFM if needed
if (!tags || !emojis || !mentionedUsers) {
const tokens = data.text ? parse(data.text) : [];
const cwTokens = data.cw ? parse(data.cw) : [];
const combinedTokens = tokens.concat(cwTokens);
const emojis = extractEmojis(tokens);
tags = data.apHashtags || extractHashtags(combinedTokens);
const mentionedUsers = data.apMentions || await extractMentionedUsers(user, tokens);
emojis = data.apEmojis || extractEmojis(combinedTokens);
mentionedUsers = data.apMentions || await extractMentionedUsers(user, combinedTokens);
}
if (data.reply && !user._id.equals(data.reply.userId) && !mentionedUsers.some(u => u._id.equals(data.reply.userId))) {
mentionedUsers.push(await User.findOne({ _id: data.reply.userId }));

View File

@ -187,9 +187,9 @@ describe('Text', () => {
});
it('with text (zenkaku)', () => {
const tokens = analyze('こんにちは #世界');
const tokens = analyze('こんにちは#世界');
assert.deepEqual([
text('こんにちは '),
text('こんにちは'),
node('hashtag', { hashtag: '世界' })
], tokens);
});
@ -299,6 +299,7 @@ describe('Text', () => {
nodeWithChildren('quote', [
text('foo')
]),
text('\n'),
nodeWithChildren('quote', [
text('bar')
]),
@ -358,7 +359,7 @@ describe('Text', () => {
it('with before and after texts', () => {
const tokens = analyze('before\n> foo\nafter');
assert.deepEqual([
text('before'),
text('before\n'),
nodeWithChildren('quote', [
text('foo')
]),
@ -366,6 +367,24 @@ describe('Text', () => {
], tokens);
});
it('multiple quotes', () => {
const tokens = analyze('> foo\nbar\n\n> foo\nbar\n\n> foo\nbar');
assert.deepEqual([
nodeWithChildren('quote', [
text('foo')
]),
text('bar\n\n'),
nodeWithChildren('quote', [
text('foo')
]),
text('bar\n\n'),
nodeWithChildren('quote', [
text('foo')
]),
text('bar'),
], tokens);
});
it('require line break before ">"', () => {
const tokens = analyze('foo>bar');
assert.deepEqual([
@ -388,11 +407,11 @@ describe('Text', () => {
it('trim line breaks', () => {
const tokens = analyze('foo\n\n>a\n>>b\n>>\n>>>\n>>>c\n>>>\n>d\n\n');
assert.deepEqual([
text('foo\n'),
text('foo\n\n'),
nodeWithChildren('quote', [
text('a'),
text('a\n'),
nodeWithChildren('quote', [
text('b\n'),
text('b\n\n'),
nodeWithChildren('quote', [
text('\nc\n')
])
@ -664,7 +683,7 @@ describe('Text', () => {
it('with before and after texts', () => {
const tokens = analyze('before\n【foo】\nafter');
assert.deepEqual([
text('before'),
text('before\n'),
nodeWithChildren('title', [
text('foo')
]),