Compare commits

...

63 Commits

Author SHA1 Message Date
syuilo 2ec445f83e 10.66.0 2018-12-21 16:26:46 +09:00
syuilo 51b915428e [Client] Fix #3693 2018-12-21 16:22:34 +09:00
Acid Chicken (硫酸鶏) 1395cf89ce Feed (#3698)
* wip

* Implement feed

* Update feed.ts

* Update index.ts

* Update feed.ts
2018-12-21 11:54:39 +09:00
Aya Morisawa 2a8f984db7 Fix comment 2018-12-21 11:28:30 +09:00
MeiMei decf2d396f Fix processing icon (#3705) 2018-12-21 02:30:49 +09:00
MeiMei f7964da899 Fix: ap/show does not return on error (#3704) 2018-12-21 00:09:02 +09:00
MeiMei c8607ff7b6 Tune polls/recommendation (#3703) 2018-12-20 22:56:12 +09:00
Aya Morisawa e9f8897fe2 Refactor MFM
Co-authored-by: syuilo syuilotan@yahoo.co.jp
2018-12-20 19:42:10 +09:00
ibrokemypie e0b107a3a0 Fix overlap of birthday label on datepicker (#3697) 2018-12-20 17:01:29 +09:00
syuilo 1d3e6a7197 10.65.0 2018-12-20 04:37:47 +09:00
syuilo 1c93fcb1c4 Fix #3683 2018-12-20 04:11:10 +09:00
syuilo e3389e7899 Merge branch 'develop' of https://github.com/syuilo/misskey into develop 2018-12-20 04:08:22 +09:00
syuilo 454632d785 Resolve #3687 2018-12-20 04:08:13 +09:00
syuilo c9bca7dc85 Update CONTRIBUTING.md 2018-12-20 03:44:19 +09:00
syuilo 710ba526fa Better cw detection 2018-12-20 03:22:27 +09:00
syuilo aa47b6732d [Doc] Clean up 2018-12-20 03:19:44 +09:00
syuilo 20f83420ca Update CONTRIBUTING.md 2018-12-20 03:02:19 +09:00
syuilo d09a68ef11 Update CONTRIBUTING.md 2018-12-20 03:01:02 +09:00
syuilo b545be5799 Fix wrong comment 2018-12-20 02:47:24 +09:00
MeiMei 4fc377584f Fix tag length limit from AP (#3688) 2018-12-20 02:20:56 +09:00
syuilo a5f09c90dd [Client] Resolve #3686 2018-12-20 01:09:35 +09:00
tamaina d059d7f972 open処理中はopenの処理をしないように (#3661)
* autocomplettimeout

* fix

* fix

* Update autocomplete.ts

* Update autocomplete.ts
2018-12-20 00:02:28 +09:00
MeiMei c03e2dfbc0 Change naming (#3678)
* Change naming

* x to a
2018-12-19 22:38:27 +09:00
MeiMei 45c5e7b967 Hide hidden contents in welcome timeline (#3682) 2018-12-19 22:18:58 +09:00
Acid Chicken (硫酸鶏) c81a94ff75 Resolve #3676 (#3677) 2018-12-19 21:20:25 +09:00
MeiMei acc6f54557 Update remote Emoji (#3680) 2018-12-19 21:19:43 +09:00
Aya Morisawa 8025b121af Add Predicate type 2018-12-19 17:08:09 +09:00
Aya Morisawa 78ec06bda3 Add relation types 2018-12-19 17:00:07 +09:00
syuilo 6ef83d9c59 Update deck.notes.vue 2018-12-19 11:23:46 +09:00
syuilo fca4ceef21 [Client] デッキのTLにUIの動きを減らすオプションが適用されていなかったのを修正 2018-12-19 11:22:27 +09:00
syuilo 00f979f0e6 Fix bug 2018-12-19 11:16:29 +09:00
Aya Morisawa 556677be7a Refactor 2018-12-19 10:23:57 +09:00
Aya Morisawa 624fd093f2 Fix comment 2018-12-19 10:02:58 +09:00
Aya Morisawa 2ee438dece Add comments for prelude/array.ts 2018-12-19 09:54:45 +09:00
Aya Morisawa 534de24406 Use consistent naming convention 2018-12-19 09:14:05 +09:00
syuilo e88ce1746d リスト関連の操作を強化
Resolve #2069
Resolve #2051
Resolve #2807
Resolve #3647
2018-12-19 07:22:01 +09:00
syuilo b8aad35009 Fix error 2018-12-19 06:47:47 +09:00
syuilo 47bd485a39 Clean up 2018-12-19 06:09:31 +09:00
syuilo ad869d7469 Merge branch 'develop' of https://github.com/syuilo/misskey into develop 2018-12-19 06:05:47 +09:00
syuilo d15cce5337 [Client] Show more images 2018-12-19 06:05:44 +09:00
syuilo 37daff6d61 [Client] Fix #2764 2018-12-19 06:04:59 +09:00
MeiMei 5417e40f59 Send original URL for quote (#3668) 2018-12-19 05:07:54 +09:00
Acid Chicken (硫酸鶏) 0fed33bfdb Create PULL_REQUEST_TEMPLATE.md (#3552)
* Create PULL_REQUEST_TEMPLATE.md

* Update PULL_REQUEST_TEMPLATE.md
2018-12-19 04:48:49 +09:00
MeiMei 5dddc75d09 Add AP emojis endpoint (#3667) 2018-12-19 04:23:08 +09:00
syuilo 081578c604 Merge branch 'develop' of https://github.com/syuilo/misskey into develop 2018-12-19 01:11:08 +09:00
syuilo 6c47bf5b76 [Client] Resolve #3662 2018-12-19 01:10:53 +09:00
MeiMei 9e85291cd3 Add example nginx configuration (#3659)
* Sample Nginx configuration

* nginxによせる

* 非Debian系ではsites-enabledがない
2018-12-19 01:00:57 +09:00
syuilo 7f77517fc8 [Client] Resolve #3658 2018-12-19 00:57:28 +09:00
syuilo b2f288dcac [Client] Fix #3657 2018-12-19 00:45:00 +09:00
syuilo 52b59e9d7b [Client] Fix #3655 2018-12-19 00:41:53 +09:00
syuilo 80c74b1fa7 Improve readability 2018-12-19 00:40:29 +09:00
syuilo 91811ea500 Clean up 2018-12-19 00:40:13 +09:00
syuilo 57150fd910 Improve readability 2018-12-19 00:39:28 +09:00
syuilo cddbbdf5d0 clean up 2018-12-19 00:39:04 +09:00
syuilo 423dc2349b [Client] Improve performance 2018-12-19 00:25:35 +09:00
syuilo 0556a2a2da Merge branch 'develop' of https://github.com/syuilo/misskey into develop 2018-12-17 20:18:09 +09:00
syuilo 65d943e42a Fix #3646 2018-12-17 20:17:21 +09:00
Acid Chicken (硫酸鶏) 3bcb344ecb Re: #3457 (#3614)
* Update parser.ts

* Update user.ts

* Update search.ts

* Update parser.ts

* Update parser.ts

* Update parser.ts

* Update parser.ts

* Update parser.ts

* Update parser.ts

* Update mfm.ts

* Update parser.ts

* Merge branch 'develop' into 3440-mk2

* Fix typo

* Update parser.ts

* Update mfm.ts

* Update mfm.ts
2018-12-17 19:11:38 +09:00
Acid Chicken (硫酸鶏) 82d721d60b Refactor Reversi (#3584)
* Update core.ts

* Update core.ts

* Create functional-syntax.ts

* Update core.ts

* Update functional-syntax.ts

* Update core.ts

* Delete functional-syntax.ts
2018-12-17 19:10:38 +09:00
syuilo 48dc56e834 10.64.2 2018-12-17 17:27:07 +09:00
Acid Chicken (硫酸鶏) 2c33bd6e31 Update README.md [AUTOGEN] (#3641) 2018-12-17 17:26:02 +09:00
syuilo b6524616bc Clean up 2018-12-17 17:21:45 +09:00
syuilo 7e2b70f912 [Client] UIの動きを減らすオプションが一部のアニメーションに適用されなかったのを修正
Resolve #3632
2018-12-17 17:21:36 +09:00
89 changed files with 1490 additions and 738 deletions
-8
View File
@@ -108,13 +108,5 @@ autoAdmin: true
# port: 9200
# pass: null
# ServiceWorker
#sw:
# # Public key of VAPID
# public_key: example-sw-public-key
#
# # Private key of VAPID
# private_key: example-sw-private-key
# Clustering
#clusterLimit: 1
+34
View File
@@ -1,6 +1,40 @@
ChangeLog
=========
10.66.0
-------
* ユーザーごとのRSSフィードを提供するように
* リストのユーザーがすべて表示できない問題を修正
* デザインの調整
* パフォーマンスの改善
10.65.0
-------
* 検索で投稿やユーザーのURLを入力した際にそれをフェッチして表示するように
* リストのリネームと削除をできるように
* リストからユーザーを削除できるように
* リモートの絵文字を更新するように
* ActivityPubのための絵文字エンドポイントを実装
* 管理者がドライブのファイルのNSFWを設定できるように
* ServiceWorkerの設定を管理者ページで行えるように
* メンションの判定を改善
* リモートの投稿を引用した際にオリジナルのURLを挿入するように
* クライアントのパフォーマンス改善
* CWの内容がタブタイトルに表示されるのを修正
* アカウントを作成したときにログイン状態にならない問題を修正
* 時計の針にテーマカラーが適用されていなかったのを修正
* 一部の日時の表示が日本語で表示されていたのを修正
* プロフィールの写真欄に画像以外のファイルが含まれる問題を修正
* メンションが含まれる投稿に返信する際、フォームに予めそれらのメンションがセットされた状態にならない問題を修正
* デッキのTLにUIの動きを減らすオプションが適用されていなかったのを修正
* ログイン画面のタイムラインに隠した投稿が表示される問題を修正
* サジェストが複数開いてしまう問題を修正
* APから来たタグに登録時の長さ制限が適用されていなかったのを修正
10.64.2
-------
* UIの動きを減らすオプションが一部のアニメーションに適用されなかったのを修正
10.64.1
-------
* レートリミットの調整
+13
View File
@@ -25,3 +25,16 @@ Misskey uses [vue-i18n](https://github.com/kazupon/vue-i18n).
## Continuous integration
Misskey uses CircleCI for automated test.
Configuration files are located in `/.circleci`.
## Glossary
### AP
Stands for _**A**ctivity**P**ub_.
### MFM
Stands for _**M**isskey **F**lavored **M**arkdown_.
### Mk
Stands for _**M**iss**k**ey_.
### SW
Stands for _**S**ervice**W**orker_.
+13
View File
@@ -0,0 +1,13 @@
# Summary
<!--
-
- * Please describe your changes here *
-
- If you are going to resolve some issue, please add this context.
- Resolve #ISSUE_NUMBER
-
- If you are going to fix some bug issue, please add this context.
- Fix #ISSUE_NUMBER
-
-->
+2 -2
View File
@@ -86,7 +86,7 @@ Please see [Contribution guide](./CONTRIBUTING.md).
<td><a href="https://www.patreon.com/user?u=13376668">Arctic</a></td>
<td><a href="https://www.patreon.com/negao">negao</a></td>
<td><a href="https://www.patreon.com/user?u=12913507">Melilot</a></td>
<td><a href="https://www.patreon.com/AxellaMC">Xeltica</a></td>
<td><a href="https://www.patreon.com/Xeltica">Xeltica</a></td>
<td><a href="https://www.patreon.com/user?u=3384329">べすれい</a></td>
<td><a href="https://www.patreon.com/gutfuckllc">gutfuckllc</a></td>
<td><a href="https://www.patreon.com/mydarkstar">mydarkstar</a></td>
@@ -118,7 +118,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:** Sun, 16 Dec 2018 13:46:05 UTC
**Last updated:** Sun, 16 Dec 2018 18:32:06 UTC
<!-- PATREON_END -->
:four_leaf_clover: Copyright
+70
View File
@@ -0,0 +1,70 @@
# Sample nginx configuration for Misskey
#
# 1. Replace example.tld to your domain
# 2. Copy to /etc/nginx/sites-available/ and then symlink from /etc/nginx/sites-ebabled/
# or copy to /etc/nginx/conf.d/
# For WebSocket
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
proxy_cache_path /tmp/nginx_cache levels=1:2 keys_zone=cache1:16m max_size=1g inactive=720m use_temp_path=off;
server {
listen 80;
listen [::]:80;
server_name example.tld;
# For SSL domain validation
root /var/www/html;
location /.well-known/acme-challenge/ { allow all; }
location /.well-known/pki-validation/ { allow all; }
location / { return 301 https://$server_name$request_uri; }
}
server {
listen 443 http2;
listen [::]:443 http2;
server_name example.tld;
ssl on;
ssl_session_cache shared:ssl_session_cache:10m;
# To use Let's Encrypt certificate
ssl_certificate /etc/letsencrypt/live/example.tld/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.tld/privkey.pem;
# To use Debian/Ubuntu's self-signed certificate (For testing or before issuing a certificate)
#ssl_certificate /etc/ssl/certs/ssl-cert-snakeoil.pem;
#ssl_certificate_key /etc/ssl/private/ssl-cert-snakeoil.key;
# SSL protocol settings
ssl_protocols TLSv1 TLSv1.2;
ssl_ciphers ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA:ECDHE-RSA-AES128-SHA:AES128-SHA;
ssl_prefer_server_ciphers on;
# Change to your upload limit
client_max_body_size 80m;
# Proxy to Node
location / {
proxy_pass http://127.0.0.1:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto https;
proxy_http_version 1.1;
proxy_redirect off;
# For WebSocket
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
# Cache settings
proxy_cache cache1;
proxy_cache_lock on;
proxy_cache_use_stale updating;
add_header X-Cache $upstream_cache_status;
}
}
-10
View File
@@ -47,16 +47,6 @@ As root:
4. `git checkout $(git tag -l | grep -v 'rc[0-9]*$' | sort -V | tail -n 1)` Checkout to the [latest release](https://github.com/syuilo/misskey/releases/latest)
5. `npm install` Install misskey dependencies.
*(optional)* Generate VAPID keys
----------------------------------------------------------------
If you want to enable ServiceWorker, you need to generate VAPID keys:
Unless you have set your global node_modules location elsewhere, you need to run this as root.
``` shell
npm install web-push -g
web-push generate-vapid-keys
```
*5.* Configure Misskey
----------------------------------------------------------------
1. `cp .config/example.yml .config/default.yml` Copy the `.config/example.yml` and rename it to `default.yml`.
-10
View File
@@ -47,16 +47,6 @@ En mode root :
4. `git checkout $(git tag -l | grep -v 'rc[0-9]*$' | sort -V | tail -n 1)` Télécharge la [version la plus récente](https://github.com/syuilo/misskey/releases/latest)
5. `npm install` Installez les dépendances de misskey.
*(optionnel)* Génération des clés VAPID
----------------------------------------------------------------
Si vous désirez activer ServiceWorker, vous devez générer les clés VAPID :
Unless you have set your global node_modules location elsewhere, vous devez lancer ceci en mode root.
``` shell
npm install web-push -g
web-push generate-vapid-keys
```
*5.* Création du fichier de configuration
----------------------------------------------------------------
1. `cp .config/example.yml .config/default.yml` Copiez le fichier `.config/example.yml` et renommez-le `default.yml`.
-9
View File
@@ -53,15 +53,6 @@ adduser --disabled-password --disabled-login misskey
4. `git checkout $(git tag -l | grep -v 'rc[0-9]*$' | sort -V | tail -n 1)` [最新のリリース](https://github.com/syuilo/misskey/releases/latest)を確認
5. `npm install` Misskeyの依存パッケージをインストール
*(オプション)* VAPIDキーペアの生成
----------------------------------------------------------------
ServiceWorkerを有効にする場合、VAPIDキーペアを生成する必要があります:
``` shell
npm install web-push -g
web-push generate-vapid-keys
```
*5.* 設定ファイルを作成する
----------------------------------------------------------------
1. `cp .config/example.yml .config/default.yml` `.config/example.yml`をコピーし名前を`default.yml`にする。
+16
View File
@@ -519,6 +519,14 @@ common/views/components/profile-editor.vue:
email-verified: "メールアドレスが確認されました"
email-not-verified: "メールアドレスが確認されていません。メールボックスをご確認ください。"
common/views/components/user-list-editor.vue:
users: "ユーザー"
rename: "リスト名を変更"
delete: "リストを削除"
remove-user: "このリストから削除"
delete-are-you-sure: "リスト「$1」を削除しますか?"
deleted: "削除しました"
common/views/widgets/broadcast.vue:
fetching: "確認中"
no-broadcasts: "お知らせはありません"
@@ -1156,6 +1164,12 @@ admin/views/instance.vue:
smtp-port: "SMTPポート"
smtp-user: "SMTPユーザー"
smtp-pass: "SMTPパスワード"
serviceworker-config: "ServiceWorker"
enable-serviceworker: "ServiceWorkerを有効にする"
serviceworker-info: "プッシュ通知を行うには有効する必要があります。"
vapid-publickey: "VAPID公開鍵"
vapid-privatekey: "VAPID秘密鍵"
vapid-info: "ServiceWorkerを有効にする場合、VAPIDキーペアを生成する必要があります。シェルで次のようにします:"
admin/views/charts.vue:
title: "チャート"
@@ -1197,6 +1211,8 @@ admin/views/drive.vue:
remote: "リモート"
delete: "削除"
deleted: "削除しました"
mark-as-sensitive: "閲覧注意に設定"
unmark-as-sensitive: "閲覧注意を解除"
admin/views/users.vue:
operation: "操作"
+3 -2
View File
@@ -1,8 +1,8 @@
{
"name": "misskey",
"author": "syuilo <i@syuilo.com>",
"version": "10.64.1",
"clientVersion": "2.0.12792",
"version": "10.66.0",
"clientVersion": "2.0.12855",
"codename": "nighthike",
"main": "./built/index.js",
"private": true,
@@ -114,6 +114,7 @@
"eslint": "5.8.0",
"eslint-plugin-vue": "4.7.1",
"eventemitter3": "3.1.0",
"feed": "2.0.2",
"file-loader": "2.0.0",
"file-type": "10.6.0",
"fuckadblock": "3.2.1",
+17 -4
View File
@@ -39,7 +39,11 @@
</div>
</div>
<div v-show="file._open">
<ui-button @click="del(file)"><fa :icon="faTrashAlt"/> {{ $t('delete') }}</ui-button>
<ui-horizon-group>
<ui-button @click="toggleSensitive(file)" v-if="file.isSensitive"><fa :icon="faEye"/> {{ $t('unmark-as-sensitive') }}</ui-button>
<ui-button @click="toggleSensitive(file)" v-else><fa :icon="faEyeSlash"/> {{ $t('mark-as-sensitive') }}</ui-button>
<ui-button @click="del(file)"><fa :icon="faTrashAlt"/> {{ $t('delete') }}</ui-button>
</ui-horizon-group>
</div>
</div>
</sequential-entrance>
@@ -53,7 +57,7 @@
import Vue from 'vue';
import i18n from '../../i18n';
import { faCloud } from '@fortawesome/free-solid-svg-icons';
import { faTrashAlt } from '@fortawesome/free-regular-svg-icons';
import { faTrashAlt, faEye, faEyeSlash } from '@fortawesome/free-regular-svg-icons';
export default Vue.extend({
i18n: i18n('admin/views/drive.vue'),
@@ -66,7 +70,7 @@ export default Vue.extend({
offset: 0,
files: [],
existMore: false,
faCloud, faTrashAlt
faCloud, faTrashAlt, faEye, faEyeSlash
};
},
@@ -132,7 +136,16 @@ export default Vue.extend({
text: e.toString()
});
});
}
},
toggleSensitive(file: any) {
this.$root.api('drive/files/update', {
fileId: file.id,
isSensitive: !file.isSensitive
});
file.isSensitive = !file.isSensitive;
},
}
});
</script>
+21 -3
View File
@@ -57,6 +57,15 @@
</ui-horizon-group>
<ui-switch v-model="smtpSecure" :disabled="!enableEmail">{{ $t('smtp-secure') }}<span slot="desc">{{ $t('smtp-secure-info') }}</span></ui-switch>
</section>
<section>
<header><fa :icon="faBolt"/> {{ $t('serviceworker-config') }}</header>
<ui-switch v-model="enableServiceWorker">{{ $t('enable-serviceworker') }}<span slot="desc">{{ $t('serviceworker-info') }}</span></ui-switch>
<ui-info>{{ $t('vapid-info') }}<br><code>npm i web-push -g<br>web-push generate-vapid-keys</code></ui-info>
<ui-horizon-group inputs class="fit-bottom">
<ui-input v-model="swPublicKey" :disabled="!enableServiceWorker"><i slot="icon"><fa icon="key"/></i>{{ $t('vapid-publickey') }}</ui-input>
<ui-input v-model="swPrivateKey" :disabled="!enableServiceWorker"><i slot="icon"><fa icon="key"/></i>{{ $t('vapid-privatekey') }}</ui-input>
</ui-horizon-group>
</section>
<section>
<header>summaly Proxy</header>
<ui-input v-model="summalyProxy">URL</ui-input>
@@ -126,7 +135,7 @@ import Vue from 'vue';
import i18n from '../../i18n';
import { url, host } from '../../config';
import { toUnicode } from 'punycode';
import { faHeadset, faShieldAlt, faGhost, faUserPlus } from '@fortawesome/free-solid-svg-icons';
import { faHeadset, faShieldAlt, faGhost, faUserPlus, faBolt } from '@fortawesome/free-solid-svg-icons';
import { faEnvelope as farEnvelope } from '@fortawesome/free-regular-svg-icons';
export default Vue.extend({
@@ -174,7 +183,10 @@ export default Vue.extend({
smtpPort: null,
smtpUser: null,
smtpPass: null,
faHeadset, faShieldAlt, faGhost, faUserPlus, farEnvelope
enableServiceWorker: false,
swPublicKey: null,
swPrivateKey: null,
faHeadset, faShieldAlt, faGhost, faUserPlus, farEnvelope, faBolt
};
},
@@ -217,6 +229,9 @@ export default Vue.extend({
this.smtpPort = meta.smtpPort;
this.smtpUser = meta.smtpUser;
this.smtpPass = meta.smtpPass;
this.enableServiceWorker = meta.enableServiceWorker;
this.swPublicKey = meta.swPublickey;
this.swPrivateKey = meta.swPrivateKey;
});
},
@@ -270,7 +285,10 @@ export default Vue.extend({
smtpHost: this.smtpHost,
smtpPort: parseInt(this.smtpPort, 10),
smtpUser: this.smtpUser,
smtpPass: this.smtpPass
smtpPass: this.smtpPass,
enableServiceWorker: this.enableServiceWorker,
swPublicKey: this.swPublicKey,
swPrivateKey: this.swPrivateKey
}).then(() => {
this.$root.dialog({
type: 'success',
@@ -22,7 +22,7 @@ export default function(type, data): Notification {
case 'unreadMessagingMessage':
return {
title: '%i18n:common.notification.message-from%'.split("{}")[0] + `${getUserName(data.user)}` + '%i18n:common.notification.message-from%'.split("{}")[1] ,
title: '%i18n:common.notification.message-from%'.split('{}')[0] + `${getUserName(data.user)}` + '%i18n:common.notification.message-from%'.split('{}')[1] ,
body: data.text, // TODO: getMessagingMessageSummary(data),
icon: data.user.avatarUrl
};
@@ -30,7 +30,7 @@ export default function(type, data): Notification {
case 'reversiInvited':
return {
title: '%i18n:common.notification.reversi-invited%',
body: '%i18n:common.notification.reversi-invited-by%'.split("{}")[0] + `${getUserName(data.parent)}` + '%i18n:common.notification.reversi-invited-by%'.split("{}")[1],
body: '%i18n:common.notification.reversi-invited-by%'.split('{}')[0] + `${getUserName(data.parent)}` + '%i18n:common.notification.reversi-invited-by%'.split('{}')[1],
icon: data.parent.avatarUrl
};
@@ -38,21 +38,21 @@ export default function(type, data): Notification {
switch (data.type) {
case 'mention':
return {
title: '%i18n:common.notification.notified-by%'.split("{}")[0] + `${getUserName(data.user)}:` + '%i18n:common.notification.notified-by%'.split("{}")[1],
title: '%i18n:common.notification.notified-by%'.split('{}')[0] + `${getUserName(data.user)}:` + '%i18n:common.notification.notified-by%'.split('{}')[1],
body: getNoteSummary(data),
icon: data.user.avatarUrl
};
case 'reply':
return {
title: '%i18n:common.notification.reply-from%'.split("{}")[0] + `${getUserName(data.user)}` + '%i18n:common.notification.reply-from%'.split("{}")[1],
title: '%i18n:common.notification.reply-from%'.split('{}')[0] + `${getUserName(data.user)}` + '%i18n:common.notification.reply-from%'.split('{}')[1],
body: getNoteSummary(data),
icon: data.user.avatarUrl
};
case 'quote':
return {
title: '%i18n:common.notification.quoted-by%'.split("{}")[0] + `${getUserName(data.user)}` + '%i18n:common.notification.quoted-by%'.split("{}")[1],
title: '%i18n:common.notification.quoted-by%'.split('{}')[0] + `${getUserName(data.user)}` + '%i18n:common.notification.quoted-by%'.split('{}')[1],
body: getNoteSummary(data),
icon: data.user.avatarUrl
};
@@ -15,7 +15,7 @@ export default function(sec) {
const t
= tod < 60 ? `${Math.floor(tod)} sec`
: tod < 3600 ? `${Math.floor(tod / 60)} min`
: `${Math.floor(tod / 60 / 60)}:${Math.floor((tod / 60) % 60).toString().padStart(2, "0")}`;
: `${Math.floor(tod / 60 / 60)}:${Math.floor((tod / 60) % 60).toString().padStart(2, '0')}`;
let str = '';
if (d) str += `${d}, `;
+2 -2
View File
@@ -3,8 +3,8 @@
export default (data: ArrayBuffer) => {
//const buf = new Buffer(data);
//const hash = crypto.createHash("md5");
//const hash = crypto.createHash('md5');
//hash.update(buf);
//return hash.digest("hex");
//return hash.digest('hex');
return '';
};
+2 -2
View File
@@ -80,8 +80,8 @@ export default (opts: Opts = {}) => ({
const ast = parse(this.appearNote.text);
// TODO: 再帰的にURL要素がないか調べる
return unique(ast
.filter(t => ((t.name == 'url' || t.name == 'link') && t.props.url && !t.props.silent))
.map(t => t.props.url));
.filter(t => ((t.node.type == 'url' || t.node.type == 'link') && t.node.props.url && !t.node.props.silent))
.map(t => t.node.props.url));
} else {
return null;
}
@@ -75,7 +75,7 @@ export default Vue.extend({
return this.dark ? '#fff' : '#777';
},
hHandColor(): string {
return tinycolor(getComputedStyle(document.documentElement).getPropertyValue('--themeColor')).toHexString();
return tinycolor(getComputedStyle(document.documentElement).getPropertyValue('--primary')).toHexString();
},
ms(): number {
@@ -48,7 +48,7 @@ export default Vue.extend({
iconAndText(): any[] {
return (
(this.hasPendingFollowRequestFromYou && this.user.isLocked) ? ['hourglass-half', this.$t('request-pending')] :
(this.hasPendingFollowRequestFromYou && !this.user.isLocked) ? ['hourglass-start', this.$t('follow-processing')] :
(this.hasPendingFollowRequestFromYou && !this.user.isLocked) ? ['spinner', this.$t('follow-processing')] :
(this.isFollowing) ? ['minus', this.$t('following')] :
(!this.isFollowing && this.user.isLocked) ? ['plus', this.$t('follow-request')] :
(!this.isFollowing && !this.user.isLocked) ? ['plus', this.$t('follow')] :
@@ -52,8 +52,8 @@ export default Vue.extend({
if (this.message.text) {
const ast = parse(this.message.text);
return unique(ast
.filter(t => ((t.name == 'url' || t.name == 'link') && t.props.url && !t.silent))
.map(t => t.props.url));
.filter(t => ((t.node.type == 'url' || t.node.type == 'link') && t.node.props.url && !t.node.props.silent))
.map(t => t.node.props.url));
} else {
return null;
}
+25 -36
View File
@@ -1,6 +1,6 @@
import Vue, { VNode } from 'vue';
import { length } from 'stringz';
import { Node } from '../../../../../mfm/parser';
import { MfmForest } from '../../../../../mfm/parser';
import parse from '../../../../../mfm/parse';
import MkUrl from './url.vue';
import MkMention from './mention.vue';
@@ -9,16 +9,11 @@ import MkFormula from './formula.vue';
import MkGoogle from './google.vue';
import syntaxHighlight from '../../../../../mfm/syntax-highlight';
import { host } from '../../../config';
import { preorderF, countNodesF } from '../../../../../prelude/tree';
function getTextCount(tokens: Node[]): number {
const rootCount = sum(tokens.filter(x => x.name === 'text').map(x => length(x.props.text)));
const childrenCount = sum(tokens.filter(x => x.children).map(x => getTextCount(x.children)));
return rootCount + childrenCount;
}
function getChildrenCount(tokens: Node[]): number {
const countTree = tokens.filter(x => x.children).map(x => getChildrenCount(x.children));
return countTree.length + sum(countTree);
function sumTextsLength(ts: MfmForest): number {
const textNodes = preorderF(ts).filter(n => n.type === 'text');
return sum(textNodes.map(x => length(x.props.text)));
}
export default Vue.component('misskey-flavored-markdown', {
@@ -27,10 +22,6 @@ export default Vue.component('misskey-flavored-markdown', {
type: String,
required: true
},
ast: {
type: [],
required: false
},
shouldBreak: {
type: Boolean,
default: true
@@ -55,17 +46,15 @@ export default Vue.component('misskey-flavored-markdown', {
render(createElement) {
if (this.text == null || this.text == '') return;
const ast = this.ast == null ?
parse(this.text, this.plainText) : // Parse text to ast
this.ast as Node[];
const ast = parse(this.text, this.plainText);
let bigCount = 0;
let motionCount = 0;
const genEl = (ast: Node[]) => concat(ast.map((token): VNode[] => {
switch (token.name) {
const genEl = (ast: MfmForest) => concat(ast.map((token): VNode[] => {
switch (token.node.type) {
case 'text': {
const text = token.props.text.replace(/(\r\n|\n|\r)/g, '\n');
const text = token.node.props.text.replace(/(\r\n|\n|\r)/g, '\n');
if (this.shouldBreak) {
const x = text.split('\n')
@@ -95,7 +84,7 @@ export default Vue.component('misskey-flavored-markdown', {
case 'big': {
bigCount++;
const isLong = getTextCount(token.children) > 10 || getChildrenCount(token.children) > 5;
const isLong = sumTextsLength(token.children) > 10 || countNodesF(token.children) > 5;
const isMany = bigCount > 3;
return (createElement as any)('strong', {
attrs: {
@@ -122,7 +111,7 @@ export default Vue.component('misskey-flavored-markdown', {
case 'motion': {
motionCount++;
const isLong = getTextCount(token.children) > 10 || getChildrenCount(token.children) > 5;
const isLong = sumTextsLength(token.children) > 10 || countNodesF(token.children) > 5;
const isMany = motionCount > 3;
return (createElement as any)('span', {
attrs: {
@@ -139,7 +128,7 @@ export default Vue.component('misskey-flavored-markdown', {
return [createElement(MkUrl, {
key: Math.random(),
props: {
url: token.props.url,
url: token.node.props.url,
target: '_blank',
style: 'color:var(--mfmLink);'
}
@@ -150,9 +139,9 @@ export default Vue.component('misskey-flavored-markdown', {
return [createElement('a', {
attrs: {
class: 'link',
href: token.props.url,
href: token.node.props.url,
target: '_blank',
title: token.props.url,
title: token.node.props.url,
style: 'color:var(--mfmLink);'
}
}, genEl(token.children))];
@@ -162,8 +151,8 @@ export default Vue.component('misskey-flavored-markdown', {
return [createElement(MkMention, {
key: Math.random(),
props: {
host: (token.props.host == null && this.author && this.author.host != null ? this.author.host : token.props.host) || host,
username: token.props.username
host: (token.node.props.host == null && this.author && this.author.host != null ? this.author.host : token.node.props.host) || host,
username: token.node.props.username
}
})];
}
@@ -172,10 +161,10 @@ export default Vue.component('misskey-flavored-markdown', {
return [createElement('router-link', {
key: Math.random(),
attrs: {
to: `/tags/${encodeURIComponent(token.props.hashtag)}`,
to: `/tags/${encodeURIComponent(token.node.props.hashtag)}`,
style: 'color:var(--mfmHashtag);'
}
}, `#${token.props.hashtag}`)];
}, `#${token.node.props.hashtag}`)];
}
case 'blockCode': {
@@ -184,7 +173,7 @@ export default Vue.component('misskey-flavored-markdown', {
}, [
createElement('code', {
domProps: {
innerHTML: syntaxHighlight(token.props.code)
innerHTML: syntaxHighlight(token.node.props.code)
}
})
])];
@@ -193,7 +182,7 @@ export default Vue.component('misskey-flavored-markdown', {
case 'inlineCode': {
return [createElement('code', {
domProps: {
innerHTML: syntaxHighlight(token.props.code)
innerHTML: syntaxHighlight(token.node.props.code)
}
})];
}
@@ -227,8 +216,8 @@ export default Vue.component('misskey-flavored-markdown', {
return [createElement('mk-emoji', {
key: Math.random(),
attrs: {
emoji: token.props.emoji,
name: token.props.name
emoji: token.node.props.emoji,
name: token.node.props.name
},
props: {
customEmojis: this.customEmojis || customEmojis,
@@ -242,7 +231,7 @@ export default Vue.component('misskey-flavored-markdown', {
return [createElement(MkFormula, {
key: Math.random(),
props: {
formula: token.props.formula
formula: token.node.props.formula
}
})];
}
@@ -252,13 +241,13 @@ export default Vue.component('misskey-flavored-markdown', {
return [createElement(MkGoogle, {
key: Math.random(),
props: {
q: token.props.query
q: token.node.props.query
}
})];
}
default: {
console.log('unknown ast type:', token.name);
console.log('unknown ast type:', token.node.type);
return [];
}
@@ -24,7 +24,7 @@
</ui-input>
<ui-input v-model="birthday" type="date">
<span>{{ $t('birthday') }}</span>
<span slot="title">{{ $t('birthday') }}</span>
<span slot="prefix"><fa icon="birthday-cake"/></span>
</ui-input>
@@ -67,6 +67,9 @@ export default Vue.extend({
});
},
anime(reaction: string) {
if (this.$store.state.device.reduceMotion) return;
if (document.hidden) return;
this.$nextTick(() => {
const rect = this.$refs[reaction].$el.getBoundingClientRect();
@@ -26,6 +26,7 @@ import { toUnicode } from 'punycode';
export default Vue.extend({
i18n: i18n('common/views/components/signin.vue'),
props: {
withAvatar: {
type: Boolean,
@@ -33,6 +34,7 @@ export default Vue.extend({
default: true
}
},
data() {
return {
signing: false,
@@ -45,11 +47,13 @@ export default Vue.extend({
meta: null
};
},
created() {
this.$root.getMeta().then(meta => {
this.meta = meta;
});
},
methods: {
onUsernameChange() {
this.$root.api('users/show', {
@@ -60,6 +64,7 @@ export default Vue.extend({
this.user = null;
});
},
onSubmit() {
this.signing = true;
@@ -80,8 +85,6 @@ export default Vue.extend({
</script>
<style lang="stylus" scoped>
.mk-signin
color #555
@@ -50,6 +50,7 @@ import { toUnicode } from 'punycode';
export default Vue.extend({
i18n: i18n('common/views/components/signup.vue'),
data() {
return {
host: toUnicode(host),
@@ -64,6 +65,7 @@ export default Vue.extend({
meta: null
}
},
computed: {
shouldShowProfileUrl(): boolean {
return (this.username != '' &&
@@ -72,17 +74,20 @@ export default Vue.extend({
this.usernameState != 'max-range');
}
},
created() {
this.$root.getMeta().then(meta => {
this.meta = meta;
});
},
mounted() {
const head = document.getElementsByTagName('head')[0];
const script = document.createElement('script');
script.setAttribute('src', 'https://www.google.com/recaptcha/api.js');
head.appendChild(script);
},
methods: {
onChangeUsername() {
if (this.username == '') {
@@ -111,6 +116,7 @@ export default Vue.extend({
this.usernameState = 'error';
});
},
onChangePassword() {
if (this.password == '') {
this.passwordStrength = '';
@@ -120,6 +126,7 @@ export default Vue.extend({
const strength = getPasswordStrength(this.password);
this.passwordStrength = strength > 0.7 ? 'high' : strength > 0.3 ? 'medium' : 'low';
},
onChangePasswordRetype() {
if (this.retypedPassword == '') {
this.passwordRetypeState = null;
@@ -128,6 +135,7 @@ export default Vue.extend({
this.passwordRetypeState = this.password == this.retypedPassword ? 'match' : 'not-match';
},
onSubmit() {
this.$root.api('signup', {
username: this.username,
@@ -138,8 +146,9 @@ export default Vue.extend({
this.$root.api('signin', {
username: this.username,
password: this.password
}, true).then(() => {
location.href = '/';
}, true).then(res => {
localStorage.setItem('i', res.i);
location.reload();
});
}).catch(() => {
alert(this.$t('some-error'));
@@ -154,8 +163,6 @@ export default Vue.extend({
</script>
<style lang="stylus" scoped>
.mk-signup
min-width 302px
</style>
@@ -33,14 +33,7 @@ export default Vue.extend({
return typeof this.time == 'string' ? new Date(this.time) : this.time;
},
absolute(): string {
const time = this._time;
return (
time.getFullYear() + '年' +
(time.getMonth() + 1) + '月' +
time.getDate() + '日' +
' ' +
time.getHours() + '時' +
time.getMinutes() + '分');
return this._time.toLocaleString();
},
relative(): string {
const time = this._time;
@@ -6,6 +6,7 @@
<div class="value" ref="passwordMetar"></div>
</div>
<span class="label" ref="label"><slot></slot></span>
<span class="title" ref="title"><slot name="title"></slot></span>
<div class="prefix" ref="prefix"><slot name="prefix"></slot></div>
<template v-if="type != 'file'">
<input ref="input"
@@ -281,6 +282,20 @@ root(fill)
transform-origin top left
transform scale(1)
> .title
position absolute
z-index 1
top fill ? -24px : -17px
left 0 !important
pointer-events none
font-size 16px
line-height 32px
color var(--inputLabel)
pointer-events none
//will-change transform
transform-origin top left
transform scale(.75)
> input
display block
width 100%
@@ -0,0 +1,150 @@
<template>
<div class="cudqjmnl">
<ui-card>
<div slot="title"><fa :icon="faList"/> {{ list.title }}</div>
<section>
<ui-button @click="rename"><fa :icon="faICursor"/> {{ $t('rename') }}</ui-button>
<ui-button @click="del"><fa :icon="faTrashAlt"/> {{ $t('delete') }}</ui-button>
</section>
</ui-card>
<ui-card>
<div slot="title"><fa :icon="faUsers"/> {{ $t('users') }}</div>
<section>
<sequential-entrance animation="entranceFromTop" delay="25">
<div class="phcqulfl" v-for="user in users">
<div>
<a :href="user | userPage">
<mk-avatar class="avatar" :user="user" :disable-link="true"/>
</a>
</div>
<div>
<header>
<b><mk-user-name :user="user"/></b>
<span class="username">@{{ user | acct }}</span>
</header>
<div>
<a @click="remove(user)">{{ $t('remove-user') }}</a>
</div>
</div>
</div>
</sequential-entrance>
</section>
</ui-card>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
import i18n from '../../../i18n';
import { faList, faICursor, faUsers } from '@fortawesome/free-solid-svg-icons';
import { faTrashAlt } from '@fortawesome/free-regular-svg-icons';
export default Vue.extend({
i18n: i18n('common/views/components/user-list-editor.vue'),
props: {
list: {
required: true
}
},
data() {
return {
users: [],
faList, faICursor, faTrashAlt, faUsers
};
},
mounted() {
this.fetchUsers();
},
methods: {
fetchUsers() {
this.$root.api('users/show', {
userIds: this.list.userIds
}).then(users => {
this.users = users;
});
},
rename() {
this.$root.dialog({
title: this.$t('rename'),
input: {
default: this.list.title
}
}).then(({ canceled, result: title }) => {
if (canceled) return;
this.$root.api('users/lists/update', {
listId: this.list.id,
title: title
});
});
},
del() {
this.$root.dialog({
type: 'warning',
text: this.$t('delete-are-you-sure').replace('$1', this.list.title),
showCancelButton: true
}).then(({ canceled }) => {
if (canceled) return;
this.$root.api('users/lists/delete', {
listId: this.list.id
}).then(() => {
this.$root.dialog({
type: 'success',
text: this.$t('deleted')
});
}).catch(e => {
this.$root.dialog({
type: 'error',
text: e
});
});
});
},
remove(user: any) {
this.$root.api('users/lists/pull', {
listId: this.list.id,
userId: user.id
}).then(() => {
this.fetchUsers();
});
}
}
});
</script>
<style lang="stylus" scoped>
.cudqjmnl
.phcqulfl
display flex
padding 16px 0
border-top solid 1px var(--faceDivider)
> div:first-child
> a
> .avatar
width 64px
height 64px
> div:last-child
flex 1
padding-left 16px
@media (max-width 500px)
font-size 14px
> header
> .username
margin-left 8px
opacity 0.7
</style>
@@ -16,7 +16,7 @@
</div>
</header>
<div class="text">
<misskey-flavored-markdown v-if="note.text" :text="note.text" :author="note.user" :custom-emojis="note.emojis"/>
<misskey-flavored-markdown v-if="note.text" :text="note.cw != null ? note.cw : note.text" :author="note.user" :custom-emojis="note.emojis"/>
</div>
</div>
</div>
@@ -25,6 +25,7 @@ class Autocomplete {
private opts: {
model: string;
};
private opening: boolean;
private get text(): string {
return this.vm[this.opts.model];
@@ -48,6 +49,7 @@ class Autocomplete {
this.textarea = textarea;
this.vm = vm;
this.opts = opts;
this.opening = false;
}
/**
@@ -128,6 +130,8 @@ class Autocomplete {
if (type != this.currentType) {
this.close();
}
if (this.opening) return;
this.opening = true;
this.currentType = type;
//#region サジェストを表示すべき位置を計算
@@ -143,6 +147,8 @@ class Autocomplete {
this.suggestion.x = x;
this.suggestion.y = y;
this.suggestion.q = q;
this.opening = false;
} else {
const MkAutocomplete = await import('../components/autocomplete.vue').then(m => m.default);
@@ -162,6 +168,8 @@ class Autocomplete {
// 要素追加
document.body.appendChild(this.suggestion.$el);
this.opening = false;
}
}
@@ -2,6 +2,8 @@ import Particle from '../components/particle.vue';
export default {
bind(el, binding, vn) {
if (vn.context.$store.state.device.reduceMotion) return;
el.addEventListener('click', () => {
const rect = el.getBoundingClientRect();
@@ -18,9 +20,5 @@ export default {
document.body.appendChild(particle.$el);
});
},
unbind(el, binding, vn) {
}
};
+1 -1
View File
@@ -22,7 +22,7 @@
:disabled="followWait">
<template v-if="!followWait">
<template v-if="user.hasPendingFollowRequestFromYou && user.isLocked"><fa icon="hourglass-half"/> {{ $t('request-pending') }}</template>
<template v-else-if="user.hasPendingFollowRequestFromYou && !user.isLocked"><fa icon="hourglass-start"/> {{ $t('follow-processing') }}</template>
<template v-else-if="user.hasPendingFollowRequestFromYou && !user.isLocked"><fa icon="spinner"/> {{ $t('follow-processing') }}</template>
<template v-else-if="user.isFollowing"><fa icon="minus"/> {{ $t('following') }}</template>
<template v-else-if="!user.isFollowing && user.isLocked"><fa icon="plus"/> {{ $t('follow-request') }}</template>
<template v-else-if="!user.isFollowing && !user.isLocked"><fa icon="plus"/> {{ $t('follow') }}</template>
@@ -74,6 +74,7 @@ import { host } from '../../../config';
import { erase, unique } from '../../../../../prelude/array';
import { length } from 'stringz';
import { toASCII } from 'punycode';
import extractMentions from '../../../../../misc/extract-mentions';
export default Vue.extend({
i18n: i18n('desktop/views/components/post-form.vue'),
@@ -184,8 +185,7 @@ export default Vue.extend({
if (this.reply && this.reply.text != null) {
const ast = parse(this.reply.text);
// TODO: MFM
for (const x of ast.filter(t => t.type == 'mention')) {
for (const x of extractMentions(ast)) {
const mention = x.host ? `@${x.username}@${toASCII(x.host)}` : `@${x.username}`;
//
@@ -92,6 +92,7 @@
import Vue from 'vue';
import i18n from '../../../i18n';
import MkUserListsWindow from './user-lists-window.vue';
import MkUserListWindow from './user-list-window.vue';
import MkFollowRequestsWindow from './received-follow-requests-window.vue';
import MkSettingsWindow from './settings-window.vue';
import MkDriveWindow from './drive-window.vue';
@@ -143,7 +144,9 @@ export default Vue.extend({
this.close();
const w = this.$root.new(MkUserListsWindow);
w.$once('choosen', list => {
this.$router.push(`i/lists/${ list.id }`);
this.$root.new(MkUserListWindow, {
list
});
});
},
followRequests() {
@@ -14,16 +14,34 @@ export default Vue.extend({
i18n: i18n('desktop/views/components/ui.header.search.vue'),
data() {
return {
q: ''
q: '',
wait: false
};
},
methods: {
onSubmit() {
async onSubmit() {
if (this.wait) return;
const q = this.q.trim();
if (q.startsWith('@')) {
this.$router.push(`/${q}`);
} else if (q.startsWith('#')) {
this.$router.push(`/tags/${encodeURIComponent(q.substr(1))}`);
} else if (q.startsWith('https://')) {
this.wait = true;
try {
const res = await this.$root.api('ap/show', {
uri: q
});
if (res.type == 'User') {
this.$router.push(`/@${res.object.username}@${res.object.host}`);
} else if (res.type == 'Note') {
this.$router.push(`/notes/${res.object.id}`);
}
} catch (e) {
// TODO
}
this.wait = false;
} else {
this.$router.push(`/search?q=${encodeURIComponent(q)}`);
}
@@ -0,0 +1,24 @@
<template>
<mk-window ref="window" width="450px" height="500px" @closed="destroyDom">
<span slot="header"><fa icon="list"/> {{ list.title }}</span>
<x-editor :list="list"/>
</mk-window>
</template>
<script lang="ts">
import Vue from 'vue';
import XEditor from '../../../common/views/components/user-list-editor.vue';
export default Vue.extend({
components: {
XEditor
},
props: {
list: {
required: true
}
}
});
</script>
@@ -1,5 +1,5 @@
<template>
<mk-window ref="window" is-modal width="450px" height="500px" @closed="destroyDom">
<mk-window ref="window" width="450px" height="500px" @closed="destroyDom">
<span slot="header"><fa icon="list"/> {{ $t('title') }}</span>
<div class="xkxvokkjlptzyewouewmceqcxhpgzprp">
@@ -627,6 +627,7 @@ export default Vue.extend({
> .content
height 100%
overflow auto
&:not([flexible])
> .main > .body > .content
@@ -11,7 +11,7 @@
<mk-error v-if="!fetching && requestInitPromise != null" @retry="resolveInitPromise"/>
<!-- トランジションを有効にするとなぜかメモリリークする -->
<transition-group name="mk-notes" class="transition notes" ref="notes" tag="div">
<component :is="!$store.state.device.reduceMotion ? 'transition-group' : 'div'" name="mk-notes" class="transition notes" ref="notes" tag="div">
<template v-for="(note, i) in _notes">
<x-note
:note="note"
@@ -24,7 +24,7 @@
<span><fa icon="angle-down"/>{{ _notes[i + 1]._datetext }}</span>
</p>
</template>
</transition-group>
</component>
<footer v-if="more">
<button @click="loadMore" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }">
@@ -24,9 +24,14 @@ export default Vue.extend({
};
},
mounted() {
const image = [
'image/jpeg',
'image/png',
'image/gif'
];
this.$root.api('users/notes', {
userId: this.user.id,
withFiles: true,
fileType: image,
limit: 9,
untilDate: new Date().getTime() + 1000 * 86400 * 365
}).then(notes => {
+2
View File
@@ -123,6 +123,7 @@ import {
faArrowLeft,
faMapMarker,
faRobot,
faHourglassHalf,
} from '@fortawesome/free-solid-svg-icons';
import {
@@ -253,6 +254,7 @@ library.add(
faArrowLeft,
faMapMarker,
faRobot,
faHourglassHalf,
farBell,
farEnvelope,
@@ -66,6 +66,7 @@ import { host } from '../../../config';
import { erase, unique } from '../../../../../prelude/array';
import { length } from 'stringz';
import { toASCII } from 'punycode';
import extractMentions from '../../../../../misc/extract-mentions';
export default Vue.extend({
i18n: i18n('mobile/views/components/post-form.vue'),
@@ -174,7 +175,7 @@ export default Vue.extend({
if (this.reply && this.reply.text != null) {
const ast = parse(this.reply.text);
for (const x of ast.filter(t => t.type == 'mention')) {
for (const x of extractMentions(ast)) {
const mention = x.host ? `@${x.username}@${toASCII(x.host)}` : `@${x.username}`;
//
@@ -60,7 +60,8 @@ export default Vue.extend({
hasGameInvitation: false,
connection: null,
aboutUrl: `/docs/${lang}/about`,
announcements: []
announcements: [],
searching: false,
};
},
@@ -95,17 +96,34 @@ export default Vue.extend({
methods: {
search() {
if (this.searching) return;
this.$root.dialog({
title: this.$t('search'),
input: true
}).then(({ canceled, result: query }) => {
}).then(async ({ canceled, result: query }) => {
if (canceled) return;
const q = query.trim();
const q = this.q.trim();
if (q.startsWith('@')) {
this.$router.push(`/${q}`);
} else if (q.startsWith('#')) {
this.$router.push(`/tags/${encodeURIComponent(q.substr(1))}`);
} else if (q.startsWith('https://')) {
this.searching = true;
try {
const res = await this.$root.api('ap/show', {
uri: q
});
if (res.type == 'User') {
this.$router.push(`/@${res.object.username}@${res.object.host}`);
} else if (res.type == 'Note') {
this.$router.push(`/notes/${res.object.id}`);
}
} catch (e) {
// TODO
}
this.searching = false;
} else {
this.$router.push(`/search?q=${encodeURIComponent(q)}`);
}
@@ -3,11 +3,7 @@
<span slot="header" v-if="!fetching"><fa icon="list"/>{{ list.title }}</span>
<main v-if="!fetching">
<ul>
<li v-for="user in users" :key="user.id"><router-link :to="user | userPage">
<mk-user-name :user="user"/>
</router-link></li>
</ul>
<x-editor :list="list"/>
</main>
</mk-ui>
</template>
@@ -15,13 +11,16 @@
<script lang="ts">
import Vue from 'vue';
import Progress from '../../../common/scripts/loading';
import XEditor from '../../../common/views/components/user-list-editor.vue';
export default Vue.extend({
components: {
XEditor
},
data() {
return {
fetching: true,
list: null,
users: null
list: null
};
},
watch: {
@@ -42,12 +41,6 @@ export default Vue.extend({
this.fetching = false;
Progress.done();
this.$root.api('users/show', {
userIds: this.list.userIds
}).then(users => {
this.users = users;
});
});
}
}
@@ -55,8 +48,6 @@ export default Vue.extend({
</script>
<style lang="stylus" scoped>
main
width 100%
max-width 680px
@@ -26,10 +26,15 @@ export default Vue.extend({
};
},
mounted() {
const image = [
'image/jpeg',
'image/png',
'image/gif'
];
this.$root.api('users/notes', {
userId: this.user.id,
withFiles: true,
limit: 6,
fileType: image,
limit: 9,
untilDate: new Date().getTime() + 1000 * 86400 * 365
}).then(notes => {
for (const note of notes) {
+1 -1
View File
@@ -1,4 +1,4 @@
declare module "*.vue" {
declare module '*.vue' {
import Vue from 'vue';
export default Vue;
}
-14
View File
@@ -39,21 +39,7 @@ export type Source = {
accesslog?: string;
/**
* Service Worker
*/
sw?: {
public_key: string;
private_key: string;
};
clusterLimit?: number;
user_recommendation?: {
external: boolean;
engine: string;
timeout: number;
};
};
/**
+26 -62
View File
@@ -1,4 +1,4 @@
import { count, concat } from "../../prelude/array";
import { count, concat } from '../../prelude/array';
// MISSKEY REVERSI ENGINE
@@ -76,27 +76,14 @@ export default class Reversi {
this.mapHeight = map.length;
const mapData = map.join('');
this.board = mapData.split('').map(d => {
if (d == '-') return null;
if (d == 'b') return BLACK;
if (d == 'w') return WHITE;
return undefined;
});
this.board = mapData.split('').map(d => d === '-' ? null : d === 'b' ? BLACK : d === 'w' ? WHITE : undefined);
this.map = mapData.split('').map(d => {
if (d == '-' || d == 'b' || d == 'w') return 'empty';
return 'null';
});
this.map = mapData.split('').map(d => d === '-' || d === 'b' || d === 'w' ? 'empty' : 'null');
//#endregion
// ゲームが始まった時点で片方の色の石しかないか、始まった時点で勝敗が決定するようなマップの場合がある
if (!this.canPutSomewhere(BLACK)) {
if (!this.canPutSomewhere(WHITE)) {
this.turn = null;
} else {
this.turn = WHITE;
}
}
if (!this.canPutSomewhere(BLACK))
this.turn = this.canPutSomewhere(WHITE) ? WHITE : null;
}
/**
@@ -117,16 +104,14 @@ export default class Reversi {
*
*/
public get blackP() {
if (this.blackCount == 0 && this.whiteCount == 0) return 0;
return this.blackCount / (this.blackCount + this.whiteCount);
return this.blackCount == 0 && this.whiteCount == 0 ? 0 : this.blackCount / (this.blackCount + this.whiteCount);
}
/**
*
*/
public get whiteP() {
if (this.blackCount == 0 && this.whiteCount == 0) return 0;
return this.whiteCount / (this.blackCount + this.whiteCount);
return this.blackCount == 0 && this.whiteCount == 0 ? 0 : this.whiteCount / (this.blackCount + this.whiteCount);
}
public transformPosToXy(pos: number): number[] {
@@ -172,13 +157,10 @@ export default class Reversi {
private calcTurn() {
// ターン計算
if (this.canPutSomewhere(!this.prevColor)) {
this.turn = !this.prevColor;
} else if (this.canPutSomewhere(this.prevColor)) {
this.turn = this.prevColor;
} else {
this.turn = null;
}
this.turn =
this.canPutSomewhere(!this.prevColor) ? !this.prevColor :
this.canPutSomewhere(this.prevColor) ? this.prevColor :
null;
}
public undo() {
@@ -199,8 +181,7 @@ export default class Reversi {
*/
public mapDataGet(pos: number): MapPixel {
const [x, y] = this.transformPosToXy(pos);
if (x < 0 || y < 0 || x >= this.mapWidth || y >= this.mapHeight) return 'null';
return this.map[pos];
return x < 0 || y < 0 || x >= this.mapWidth || y >= this.mapHeight ? 'null' : this.map[pos];
}
/**
@@ -223,16 +204,10 @@ export default class Reversi {
* @param pos
*/
public canPut(color: Color, pos: number): boolean {
// 既に石が置いてある場所には打てない
if (this.board[pos] !== null) return false;
if (this.opts.canPutEverywhere) {
// 挟んでなくても置けるモード
return this.mapDataGet(pos) == 'empty';
} else {
// 相手の石を1つでも反転させられるか
return this.effects(color, pos).length !== 0;
}
return (
this.board[pos] !== null ? false : // 既に石が置いてある場所には打てない
this.opts.canPutEverywhere ? this.mapDataGet(pos) == 'empty' : // 挟んでなくても置けるモード
this.effects(color, pos).length !== 0); // 相手の石を1つでも反転させられるか
}
/**
@@ -263,19 +238,13 @@ export default class Reversi {
[x, y] = nextPos(x, y);
// 座標が指し示す位置がボード外に出たとき
if (this.opts.loopedBoard) {
x = ((x % this.mapWidth) + this.mapWidth) % this.mapWidth;
y = ((y % this.mapHeight) + this.mapHeight) % this.mapHeight;
if (this.transformXyToPos(x, y) == initPos) {
if (this.opts.loopedBoard && this.transformXyToPos(
(x = ((x % this.mapWidth) + this.mapWidth) % this.mapWidth),
(y = ((y % this.mapHeight) + this.mapHeight) % this.mapHeight)) == initPos)
// 盤面の境界でループし、自分が石を置く位置に戻ってきたとき、挟めるようにしている (ref: Test4のマップ)
return found;
}
} else {
if (x == -1 || y == -1 || x == this.mapWidth || y == this.mapHeight) {
return []; // 挟めないことが確定 (盤面外に到達)
}
}
return found;
else if (x == -1 || y == -1 || x == this.mapWidth || y == this.mapHeight)
return []; // 挟めないことが確定 (盤面外に到達)
const pos = this.transformXyToPos(x, y);
if (this.mapDataGet(pos) === 'null') return []; // 挟めないことが確定 (配置不可能なマスに到達)
@@ -300,14 +269,9 @@ export default class Reversi {
* (null = )
*/
public get winner(): Color {
if (!this.isEnded) return undefined;
if (this.blackCount == this.whiteCount) return null;
if (this.opts.isLlotheo) {
return this.blackCount > this.whiteCount ? WHITE : BLACK;
} else {
return this.blackCount > this.whiteCount ? BLACK : WHITE;
}
return this.isEnded ?
this.blackCount == this.whiteCount ? null :
this.opts.isLlotheo === this.blackCount > this.whiteCount ? WHITE : BLACK :
undefined;
}
}
+18 -18
View File
@@ -2,10 +2,10 @@ const jsdom = require('jsdom');
const { JSDOM } = jsdom;
import config from '../config';
import { INote } from '../models/note';
import { Node } from './parser';
import { intersperse } from '../prelude/array';
import { MfmForest, MfmTree } from './parser';
export default (tokens: Node[], mentionedRemoteUsers: INote['mentionedRemoteUsers'] = []) => {
export default (tokens: MfmForest, mentionedRemoteUsers: INote['mentionedRemoteUsers'] = []) => {
if (tokens == null) {
return null;
}
@@ -14,11 +14,11 @@ export default (tokens: Node[], mentionedRemoteUsers: INote['mentionedRemoteUser
const doc = window.document;
function appendChildren(children: Node[], targetElement: any): void {
for (const child of children.map(n => handlers[n.name](n))) targetElement.appendChild(child);
function appendChildren(children: MfmForest, targetElement: any): void {
for (const child of children.map(t => handlers[t.node.type](t))) targetElement.appendChild(child);
}
const handlers: { [key: string]: (token: Node) => any } = {
const handlers: { [key: string]: (token: MfmTree) => any } = {
bold(token) {
const el = doc.createElement('b');
appendChildren(token.children, el);
@@ -58,7 +58,7 @@ export default (tokens: Node[], mentionedRemoteUsers: INote['mentionedRemoteUser
blockCode(token) {
const pre = doc.createElement('pre');
const inner = doc.createElement('code');
inner.innerHTML = token.props.code;
inner.innerHTML = token.node.props.code;
pre.appendChild(inner);
return pre;
},
@@ -70,39 +70,39 @@ export default (tokens: Node[], mentionedRemoteUsers: INote['mentionedRemoteUser
},
emoji(token) {
return doc.createTextNode(token.props.emoji ? token.props.emoji : `:${token.props.name}:`);
return doc.createTextNode(token.node.props.emoji ? token.node.props.emoji : `:${token.node.props.name}:`);
},
hashtag(token) {
const a = doc.createElement('a');
a.href = `${config.url}/tags/${token.props.hashtag}`;
a.textContent = `#${token.props.hashtag}`;
a.href = `${config.url}/tags/${token.node.props.hashtag}`;
a.textContent = `#${token.node.props.hashtag}`;
a.setAttribute('rel', 'tag');
return a;
},
inlineCode(token) {
const el = doc.createElement('code');
el.textContent = token.props.code;
el.textContent = token.node.props.code;
return el;
},
math(token) {
const el = doc.createElement('code');
el.textContent = token.props.formula;
el.textContent = token.node.props.formula;
return el;
},
link(token) {
const a = doc.createElement('a');
a.href = token.props.url;
a.href = token.node.props.url;
appendChildren(token.children, a);
return a;
},
mention(token) {
const a = doc.createElement('a');
const { username, host, acct } = token.props;
const { username, host, acct } = token.node.props;
switch (host) {
case 'github.com':
a.href = `https://github.com/${username}`;
@@ -133,7 +133,7 @@ export default (tokens: Node[], mentionedRemoteUsers: INote['mentionedRemoteUser
text(token) {
const el = doc.createElement('span');
const nodes = (token.props.text as string).split('\n').map(x => doc.createTextNode(x));
const nodes = (token.node.props.text as string).split('\n').map(x => doc.createTextNode(x));
for (const x of intersperse('br', nodes)) {
el.appendChild(x === 'br' ? doc.createElement('br') : x);
@@ -144,15 +144,15 @@ export default (tokens: Node[], mentionedRemoteUsers: INote['mentionedRemoteUser
url(token) {
const a = doc.createElement('a');
a.href = token.props.url;
a.textContent = token.props.url;
a.href = token.node.props.url;
a.textContent = token.node.props.url;
return a;
},
search(token) {
const a = doc.createElement('a');
a.href = `https://www.google.com/?#q=${token.props.query}`;
a.textContent = token.props.content;
a.href = `https://www.google.com/?#q=${token.node.props.query}`;
a.textContent = token.node.props.content;
return a;
}
};
+28 -32
View File
@@ -1,40 +1,36 @@
import parser, { Node, plainParser } from './parser';
import parser, { plainParser, MfmForest, MfmTree } from './parser';
import * as A from '../prelude/array';
import * as S from '../prelude/string';
import { createTree, createLeaf } from '../prelude/tree';
export default (source: string, plainText = false): Node[] => {
function concatTextTrees(ts: MfmForest): MfmTree {
return createLeaf({ type: 'text', props: { text: S.concat(ts.map(x => x.node.props.text)) } });
}
function concatIfTextTrees(ts: MfmForest): MfmForest {
return ts[0].node.type === 'text' ? [concatTextTrees(ts)] : ts;
}
function concatConsecutiveTextTrees(ts: MfmForest): MfmForest {
const us = A.concat(A.groupOn(t => t.node.type, ts).map(concatIfTextTrees));
return us.map(t => createTree(t.node, concatConsecutiveTextTrees(t.children)));
}
function isEmptyTextTree(t: MfmTree): boolean {
return t.node.type == 'text' && t.node.props.text === '';
}
function removeEmptyTextNodes(ts: MfmForest): MfmForest {
return ts
.filter(t => !isEmptyTextTree(t))
.map(t => createTree(t.node, removeEmptyTextNodes(t.children)));
}
export default (source: string, plainText = false): MfmForest => {
if (source == null || source == '') {
return null;
}
let nodes: Node[] = plainText ? plainParser.root.tryParse(source) : parser.root.tryParse(source);
const combineText = (es: Node[]): Node =>
({ name: 'text', props: { text: S.concat(es.map(e => e.props.text)) } });
const concatText = (nodes: Node[]): Node[] =>
A.concat(A.groupOn(x => x.name, nodes).map(es =>
es[0].name === 'text' ? [combineText(es)] : es
));
const concatTextRecursive = (es: Node[]): void => {
for (const x of es.filter(x => x.children)) {
x.children = concatText(x.children);
concatTextRecursive(x.children);
}
};
nodes = concatText(nodes);
concatTextRecursive(nodes);
const removeEmptyTextNodes = (nodes: Node[]) => {
for (const n of nodes.filter(n => n.children)) {
n.children = removeEmptyTextNodes(n.children);
}
return nodes.filter(n => !(n.name == 'text' && n.props.text == ''));
};
nodes = removeEmptyTextNodes(nodes);
return nodes;
const raw = plainText ? plainParser.root.tryParse(source) : parser.root.tryParse(source) as MfmForest;
return removeEmptyTextNodes(concatConsecutiveTextTrees(raw));
};
+64 -52
View File
File diff suppressed because one or more lines are too long
+2 -2
View File
@@ -1,4 +1,4 @@
import { capitalize, toUpperCase } from "../prelude/string";
import { capitalize, toUpperCase } from '../prelude/string';
function escape(text: string) {
return text
@@ -307,7 +307,7 @@ const elements: Element[] = [
}
];
// specify lang is todo
// TODO: specify lang
export default (source: string, lang?: string): string => {
let code = source;
let html = '';
+9
View File
@@ -0,0 +1,9 @@
import { EmojiNode, MfmForest } from '../mfm/parser';
import { preorderF } from '../prelude/tree';
import { unique } from '../prelude/array';
export default function(mfmForest: MfmForest): string[] {
const emojiNodes = preorderF(mfmForest).filter(x => x.type === 'emoji') as EmojiNode[];
const emojis = emojiNodes.filter(x => x.props.name && x.props.name.length <= 100).map(x => x.props.name);
return unique(emojis);
}
+9
View File
@@ -0,0 +1,9 @@
import { HashtagNode, MfmForest } from '../mfm/parser';
import { preorderF } from '../prelude/tree';
import { unique } from '../prelude/array';
export default function(mfmForest: MfmForest): string[] {
const hashtagNodes = preorderF(mfmForest).filter(x => x.type === 'hashtag') as HashtagNode[];
const hashtags = hashtagNodes.map(x => x.props.hashtag);
return unique(hashtags);
}
+10
View File
@@ -0,0 +1,10 @@
// test is located in test/extract-mentions
import { MentionNode, MfmForest } from '../mfm/parser';
import { preorderF } from '../prelude/tree';
export default function(mfmForest: MfmForest): MentionNode['props'][] {
// TODO: 重複を削除
const mentionNodes = preorderF(mfmForest).filter(x => x.type === 'mention') as MentionNode[];
return mentionNodes.map(x => x.props);
}
+3 -2
View File
@@ -17,9 +17,10 @@ const defaultMeta: any = {
enableGithubIntegration: false,
enableDiscordIntegration: false,
enableExternalUserRecommendation: false,
externalUserRecommendationEngine: "https://vinayaka.distsn.org/cgi-bin/vinayaka-user-match-misskey-api.cgi?{{host}}+{{user}}+{{limit}}+{{offset}}",
externalUserRecommendationEngine: 'https://vinayaka.distsn.org/cgi-bin/vinayaka-user-match-misskey-api.cgi?{{host}}+{{user}}+{{limit}}+{{offset}}',
externalUserRecommendationTimeout: 300000,
errorImageUrl: 'https://ai.misskey.xyz/aiart/yubitun.png'
errorImageUrl: 'https://ai.misskey.xyz/aiart/yubitun.png',
enableServiceWorker: false
};
export default async function(): Promise<IMeta> {
+5 -1
View File
@@ -14,7 +14,11 @@ const summarize = (note: any): string => {
let summary = '';
// 本文
summary += note.text ? note.text : '';
if (note.cw != null) {
summary += note.cw;
} else {
summary += note.text ? note.text : '';
}
// ファイルが添付されているとき
if ((note.files || []).length != 0) {
+2
View File
@@ -15,4 +15,6 @@ export type IEmoji = {
url: string;
aliases?: string[];
updatedAt?: Date;
/** AP object id */
uri?: string;
};
+17
View File
@@ -138,6 +138,19 @@ if ((config as any).user_recommendation) {
}
});
}
if ((config as any).sw) {
Meta.findOne({}).then(m => {
if (m != null && m.enableServiceWorker == null) {
Meta.update({}, {
$set: {
enableServiceWorker: true,
swPublicKey: (config as any).sw.public_key,
swPrivateKey: (config as any).sw.private_key
}
});
}
});
}
export type IMeta = {
name?: string;
@@ -223,4 +236,8 @@ export type IMeta = {
smtpPort?: number;
smtpUser?: string;
smtpPass?: string;
enableServiceWorker?: boolean;
swPublicKey?: string;
swPrivateKey?: string;
};
+1 -1
View File
@@ -155,7 +155,7 @@ export const isRemoteUser = (user: any): user is IRemoteUser =>
//#region Validators
export function validateUsername(username: string, remote?: boolean): boolean {
return typeof username == 'string' && (remote ? /^\w+([\w\.-]+\w+)?$/ : /^[a-zA-Z0-9_]{1,20}$/).test(username);
return typeof username == 'string' && (remote ? /^\w([\w-]*\w)?$/ : /^\w{1,20}$/).test(username);
}
export function validatePassword(password: string): boolean {
+45 -9
View File
@@ -1,31 +1,53 @@
export function countIf<T>(f: (x: T) => boolean, xs: T[]): number {
import { EndoRelation, Predicate } from './relation';
/**
* Count the number of elements that satisfy the predicate
*/
export function countIf<T>(f: Predicate<T>, xs: T[]): number {
return xs.filter(f).length;
}
export function count<T>(x: T, xs: T[]): number {
return countIf(y => x === y, xs);
/**
* Count the number of elements that is equal to the element
*/
export function count<T>(a: T, xs: T[]): number {
return countIf(x => x === a, xs);
}
/**
* Concatenate an array of arrays
*/
export function concat<T>(xss: T[][]): T[] {
return ([] as T[]).concat(...xss);
}
/**
* Intersperse the element between the elements of the array
* @param sep The element to be interspersed
*/
export function intersperse<T>(sep: T, xs: T[]): T[] {
return concat(xs.map(x => [sep, x])).slice(1);
}
export function erase<T>(x: T, xs: T[]): T[] {
return xs.filter(y => x !== y);
/**
* Returns the array of elements that is not equal to the element
*/
export function erase<T>(a: T, xs: T[]): T[] {
return xs.filter(x => x !== a);
}
/**
* Finds the array of all elements in the first array not contained in the second array.
* The order of result values are determined by the first array.
*/
export function difference<T>(includes: T[], excludes: T[]): T[] {
return includes.filter(x => !excludes.includes(x));
export function difference<T>(xs: T[], ys: T[]): T[] {
return xs.filter(x => !ys.includes(x));
}
/**
* Remove all but the first element from every group of equivalent elements
*/
export function unique<T>(xs: T[]): T[] {
return [...new Set(xs)];
}
@@ -38,7 +60,11 @@ export function maximum(xs: number[]): number {
return Math.max(...xs);
}
export function groupBy<T>(f: (x: T, y: T) => boolean, xs: T[]): T[][] {
/**
* Splits an array based on the equivalence relation.
* The concatenation of the result is equal to the argument.
*/
export function groupBy<T>(f: EndoRelation<T>, xs: T[]): T[][] {
const groups = [] as T[][];
for (const x of xs) {
if (groups.length !== 0 && f(groups[groups.length - 1][0], x)) {
@@ -50,10 +76,17 @@ export function groupBy<T>(f: (x: T, y: T) => boolean, xs: T[]): T[][] {
return groups;
}
/**
* Splits an array based on the equivalence relation induced by the function.
* The concatenation of the result is equal to the argument.
*/
export function groupOn<T, S>(f: (x: T) => S, xs: T[]): T[][] {
return groupBy((a, b) => f(a) === f(b), xs);
}
/**
* Compare two arrays by lexicographical order
*/
export function lessThan(xs: number[], ys: number[]): boolean {
for (let i = 0; i < Math.min(xs.length, ys.length); i++) {
if (xs[i] < ys[i]) return true;
@@ -62,7 +95,10 @@ export function lessThan(xs: number[], ys: number[]): boolean {
return xs.length < ys.length;
}
export function takeWhile<T>(f: (x: T) => boolean, xs: T[]): T[] {
/**
* Returns the longest prefix of elements that satisfy the predicate
*/
export function takeWhile<T>(f: Predicate<T>, xs: T[]): T[] {
const ys = [];
for (const x of xs) {
if (f(x)) {
+5
View File
@@ -0,0 +1,5 @@
export type Predicate<T> = (a: T) => boolean;
export type Relation<T, U> = (a: T, b: U) => boolean;
export type EndoRelation<T> = Relation<T, T>;
+1 -1
View File
@@ -1,5 +1,5 @@
export function concat(xs: string[]): string {
return xs.reduce((a, b) => a + b, "");
return xs.reduce((a, b) => a + b, '');
}
export function capitalize(s: string): string {
+36
View File
@@ -0,0 +1,36 @@
import { concat, sum } from './array';
export type Tree<T> = {
node: T,
children: Forest<T>;
};
export type Forest<T> = Tree<T>[];
export function createLeaf<T>(node: T): Tree<T> {
return { node, children: [] };
}
export function createTree<T>(node: T, children: Forest<T>): Tree<T> {
return { node, children };
}
export function hasChildren<T>(t: Tree<T>): boolean {
return t.children.length !== 0;
}
export function preorder<T>(t: Tree<T>): T[] {
return [t.node, ...preorderF(t.children)];
}
export function preorderF<T>(ts: Forest<T>): T[] {
return concat(ts.map(preorder));
}
export function countNodes<T>(t: Tree<T>): number {
return preorder(t).length;
}
export function countNodesF<T>(ts: Forest<T>): number {
return sum(ts.map(countNodes));
}
+17 -8
View File
@@ -2,17 +2,26 @@ const push = require('web-push');
import * as mongo from 'mongodb';
import Subscription from './models/sw-subscription';
import config from './config';
import fetchMeta from './misc/fetch-meta';
import { IMeta } from './models/meta';
if (config.sw) {
// アプリケーションの連絡先と、サーバーサイドの鍵ペアの情報を登録
push.setVapidDetails(
config.url,
config.sw.public_key,
config.sw.private_key);
}
let meta: IMeta = null;
setInterval(() => {
fetchMeta().then(m => {
meta = m;
if (meta.enableServiceWorker) {
// アプリケーションの連絡先と、サーバーサイドの鍵ペアの情報を登録
push.setVapidDetails(config.url,
meta.swPublicKey,
meta.swPrivateKey);
}
});
}, 3000);
export default async function(userId: mongo.ObjectID | string, type: string, body?: any) {
if (!config.sw) return;
if (!meta.enableServiceWorker) return;
if (typeof userId === 'string') {
userId = new mongo.ObjectID(userId);
+17 -1
View File
@@ -181,6 +181,20 @@ export async function extractEmojis(tags: ITag[], host_: string) {
});
if (exists) {
if ((tag.updated != null && exists.updatedAt == null)
|| (tag.id != null && exists.uri == null)
|| (tag.updated != null && exists.updatedAt != null && new Date(tag.updated) > exists.updatedAt)) {
return await Emoji.findOneAndUpdate({
host,
name,
}, {
$set: {
uri: tag.id,
url: tag.icon.url,
updatedAt: new Date(tag.updated),
}
});
}
return exists;
}
@@ -189,8 +203,10 @@ export async function extractEmojis(tags: ITag[], host_: string) {
return await Emoji.insert({
host,
name,
uri: tag.id,
url: tag.icon.url,
aliases: [],
updatedAt: tag.updated ? new Date(tag.updated) : undefined,
aliases: []
});
})
);
+1 -1
View File
@@ -1,4 +1,4 @@
import { IIcon } from "./icon";
import { IIcon } from './icon';
/***
* tag (ActivityPub)
+1 -1
View File
@@ -1,5 +1,5 @@
import config from '../../../config';
import { ILocalUser, IRemoteUser } from "../../../models/user";
import { ILocalUser, IRemoteUser } from '../../../models/user';
export default (blocker?: ILocalUser, blockee?: IRemoteUser) => ({
type: 'Block',
+1 -1
View File
@@ -1,5 +1,5 @@
import config from '../../../config';
import { ILocalUser } from "../../../models/user";
import { ILocalUser } from '../../../models/user';
export default (object: any, user: ILocalUser) => ({
type: 'Delete',
+2 -3
View File
@@ -102,10 +102,9 @@ export default async function renderNote(note: INote, dive = true): Promise<any>
let apText = text;
if (note.renoteId != null) {
if (quote) {
if (apText == null) apText = '';
const url = `${config.url}/notes/${note.renoteId}`;
apText += `\n\nRE: ${url}`;
apText += `\n\nRE: ${quote}`;
}
const summary = note.cw === '' ? String.fromCharCode(0x200B) : note.cw;
+1 -1
View File
@@ -1,5 +1,5 @@
import config from '../../../config';
import { ILocalUser, IUser } from "../../../models/user";
import { ILocalUser, IUser } from '../../../models/user';
export default (object: any, user: ILocalUser | IUser) => ({
type: 'Undo',
+19
View File
@@ -7,9 +7,11 @@ import { createHttpJob } from '../queue';
import pack from '../remote/activitypub/renderer';
import Note from '../models/note';
import User, { isLocalUser, ILocalUser, IUser } from '../models/user';
import Emoji from '../models/emoji';
import renderNote from '../remote/activitypub/renderer/note';
import renderKey from '../remote/activitypub/renderer/key';
import renderPerson from '../remote/activitypub/renderer/person';
import renderEmoji from '../remote/activitypub/renderer/emoji';
import Outbox, { packActivity } from './activitypub/outbox';
import Followers from './activitypub/followers';
import Following from './activitypub/following';
@@ -188,4 +190,21 @@ router.get('/@:user', async (ctx, next) => {
});
//#endregion
// emoji
router.get('/emojis/:emoji', async ctx => {
const emoji = await Emoji.findOne({
host: null,
name: ctx.params.emoji
});
if (emoji === null) {
ctx.status = 404;
return;
}
ctx.body = pack(await renderEmoji(emoji));
ctx.set('Cache-Control', 'public, max-age=180');
setResponseType(ctx);
});
export default router;
@@ -285,6 +285,27 @@ export const meta = {
'ja-JP': 'SMTPサーバのパスワード'
}
},
enableServiceWorker: {
validator: $.bool.optional,
desc: {
'ja-JP': 'ServiceWorkerを有効にするか否か'
}
},
swPublicKey: {
validator: $.str.optional.nullable,
desc: {
'ja-JP': 'ServiceWorkerのVAPIDキーペアの公開鍵'
}
},
swPrivateKey: {
validator: $.str.optional.nullable,
desc: {
'ja-JP': 'ServiceWorkerのVAPIDキーペアの秘密鍵'
}
},
}
};
@@ -447,6 +468,18 @@ export default define(meta, (ps) => new Promise(async (res, rej) => {
set.errorImageUrl = ps.errorImageUrl;
}
if (ps.enableServiceWorker !== undefined) {
set.enableServiceWorker = ps.enableServiceWorker;
}
if (ps.swPublicKey !== undefined) {
set.swPublicKey = ps.swPublicKey;
}
if (ps.swPrivateKey !== undefined) {
set.swPrivateKey = ps.swPrivateKey;
}
await Meta.update({}, {
$set: set
}, { upsert: true });
+4 -5
View File
@@ -25,11 +25,10 @@ export const meta = {
},
};
export default define(meta, (ps) => new Promise(async (res, rej) => {
const object = await fetchAny(ps.uri);
if (object == null) return rej('object not found');
res(object);
export default define(meta, (ps) => new Promise((res, rej) => {
fetchAny(ps.uri)
.then(object => object != null ? res(object) : rej('object not found'))
.catch(e => rej(e));
}));
/***
@@ -57,14 +57,17 @@ export default define(meta, (ps, user) => new Promise(async (res, rej) => {
// Fetch file
const file = await DriveFile
.findOne({
_id: ps.fileId,
'metadata.userId': user._id
_id: ps.fileId
});
if (file === null) {
return rej('file-not-found');
}
if (!user.isAdmin && !user.isModerator && !file.metadata.userId.equals(user._id)) {
return rej('access denied');
}
if (ps.name) file.filename = ps.name;
if (ps.isSensitive !== undefined) file.metadata.isSensitive = ps.isSensitive;
+1 -1
View File
@@ -7,7 +7,7 @@ import { publishToFollowers } from '../../../../services/i/update';
import define from '../../define';
import getDriveFileUrl from '../../../../misc/get-drive-file-url';
import parse from '../../../../mfm/parse';
import { extractEmojis } from '../../../../services/note/create';
import extractEmojis from '../../../../misc/extract-emojis';
const langmap = require('langmap');
export const meta = {
+3 -2
View File
@@ -63,7 +63,7 @@ export default define(meta, (ps, me) => new Promise(async (res, rej) => {
cacheRemoteFiles: instance.cacheRemoteFiles,
enableRecaptcha: instance.enableRecaptcha,
recaptchaSiteKey: instance.recaptchaSiteKey,
swPublickey: config.sw ? config.sw.public_key : null,
swPublickey: instance.swPublicKey,
bannerUrl: instance.bannerUrl,
errorImageUrl: instance.errorImageUrl,
maxNoteTextLength: instance.maxNoteTextLength,
@@ -85,7 +85,7 @@ export default define(meta, (ps, me) => new Promise(async (res, rej) => {
twitter: instance.enableTwitterIntegration,
github: instance.enableGithubIntegration,
discord: instance.enableDiscordIntegration,
serviceWorker: config.sw ? true : false,
serviceWorker: instance.enableServiceWorker,
userRecommendation: {
external: instance.enableExternalUserRecommendation,
engine: instance.externalUserRecommendationEngine,
@@ -114,6 +114,7 @@ export default define(meta, (ps, me) => new Promise(async (res, rej) => {
response.smtpPort = instance.smtpPort;
response.smtpUser = instance.smtpUser;
response.smtpPass = instance.smtpPass;
response.swPrivateKey = instance.swPrivateKey;
}
res(response);
@@ -39,6 +39,7 @@ export default define(meta, (ps, user) => new Promise(async (res, rej) => {
const notes = await Note
.find({
'_user.host': null,
_id: {
$nin: nin
},
+5 -3
View File
@@ -1,7 +1,7 @@
import $ from 'cafy';
import Subscription from '../../../../models/sw-subscription';
import config from '../../../../config';
import define from '../../define';
import fetchMeta from '../../../../misc/fetch-meta';
export const meta = {
requireCredential: true,
@@ -31,10 +31,12 @@ export default define(meta, (ps, user) => new Promise(async (res, rej) => {
deletedAt: { $exists: false }
});
const instance = await fetchMeta();
if (exist != null) {
return res({
state: 'already-subscribed',
key: config.sw.public_key
key: instance.swPublicKey
});
}
@@ -47,6 +49,6 @@ export default define(meta, (ps, user) => new Promise(async (res, rej) => {
res({
state: 'subscribed',
key: config.sw.public_key
key: instance.swPublicKey
});
}));
@@ -0,0 +1,64 @@
import $ from 'cafy'; import ID, { transform } from '../../../../../misc/cafy-id';
import UserList from '../../../../../models/user-list';
import User, { pack as packUser } from '../../../../../models/user';
import { publishUserListStream } from '../../../../../stream';
import define from '../../../define';
export const meta = {
desc: {
'ja-JP': '指定したユーザーリストから指定したユーザーを削除します。',
'en-US': 'Remove a user to a user list.'
},
requireCredential: true,
kind: 'account-write',
params: {
listId: {
validator: $.type(ID),
transform: transform,
},
userId: {
validator: $.type(ID),
transform: transform,
desc: {
'ja-JP': '対象のユーザーのID',
'en-US': 'Target user ID'
}
},
}
};
export default define(meta, (ps, me) => new Promise(async (res, rej) => {
// Fetch the list
const userList = await UserList.findOne({
_id: ps.listId,
userId: me._id,
});
if (userList == null) {
return rej('list not found');
}
// Fetch the user
const user = await User.findOne({
_id: ps.userId
});
if (user == null) {
return rej('user not found');
}
// Pull the user
await UserList.update({ _id: userList._id }, {
$pull: {
userIds: user._id
}
});
res();
publishUserListStream(userList._id, 'userRemoved', await packUser(user));
}));
+1 -1
View File
@@ -53,7 +53,7 @@ export const meta = {
};
export default define(meta, (ps, me) => new Promise(async (res, rej) => {
const isUsername = validateUsername(ps.query.replace('@', ''), true);
const isUsername = validateUsername(ps.query.replace('@', ''), !ps.localOnly);
let users: IUser[] = [];
+2 -1
View File
@@ -20,7 +20,8 @@ export default class extends Channel {
// Subscribe stream
this.subscriber.on('hashtag', async note => {
const matched = q.some(tags => tags.every(tag => note.tags.map((t: string) => t.toLowerCase()).includes(tag.toLowerCase())));
const noteTags = note.tags.map((t: string) => t.toLowerCase());
const matched = q.some(tags => tags.every(tag => noteTags.includes(tag.toLowerCase())));
if (!matched) return;
// Renoteなら再pack
+54
View File
@@ -0,0 +1,54 @@
import { Feed } from 'feed';
import config from '../../config';
import Note from '../../models/note';
import { IUser } from '../../models/user';
import { getOriginalUrl } from '../../misc/get-drive-file-url';
export default async function(user: IUser) {
const author: Author = {
link: `${config.url}/@${user.username}`,
name: user.name || user.username
};
const notes = await Note.find({
userId: user._id,
renoteId: null,
$or: [
{ visibility: 'public' },
{ visibility: 'home' }
]
}, {
sort: { createdAt: -1 },
limit: 20
});
const feed = new Feed({
id: author.link,
title: `${author.name} (@${user.username}@${config.host})`,
updated: notes[0].createdAt,
generator: 'Misskey',
description: `${user.notesCount} Notes, ${user.followingCount} Following, ${user.followersCount} Followers${user.description ? ` · ${user.description}` : ''}`,
link: author.link,
image: user.avatarUrl,
feedLinks: {
json: `${author.link}.json`,
atom: `${author.link}.atom`,
},
author
} as FeedOptions);
for (const note of notes) {
const file = note._files && note._files.find(file => file.contentType.startsWith('image/'));
feed.addItem({
title: `New note by ${author.name}`,
link: `${config.url}/notes/${note._id}`,
date: note.createdAt,
description: note.cw,
content: note.text,
image: file && getOriginalUrl(file)
});
}
return feed;
}
+47
View File
@@ -10,6 +10,7 @@ import * as favicon from 'koa-favicon';
import * as views from 'koa-views';
import docs from './docs';
import packFeed from './feed';
import User from '../../models/user';
import parseAcct from '../../misc/acct/parse';
import config from '../../config';
@@ -82,6 +83,52 @@ router.use('/docs', docs.routes());
// URL preview endpoint
router.get('/url', require('./url-preview'));
const getFeed = async (acct: string) => {
const { username, host } = parseAcct(acct);
const user = await User.findOne({
usernameLower: username.toLowerCase(),
host
});
return user && await packFeed(user);
};
// Atom
router.get('/@:user.atom', async ctx => {
const feed = await getFeed(ctx.params.user);
if (feed) {
ctx.set('Content-Type', 'application/atom+xml; charset=utf-8');
ctx.body = feed.atom1();
} else {
ctx.status = 404;
}
});
// RSS
router.get('/@:user.rss', async ctx => {
const feed = await getFeed(ctx.params.user);
if (feed) {
ctx.set('Content-Type', 'application/rss+xml; charset=utf-8');
ctx.body = feed.rss2();
} else {
ctx.status = 404;
}
});
// JSON
router.get('/@:user.json', async ctx => {
const feed = await getFeed(ctx.params.user);
if (feed) {
ctx.set('Content-Type', 'application/json; charset=utf-8');
ctx.body = feed.json1();
} else {
ctx.status = 404;
}
});
//#region for crawlers
// User
router.get('/@:user', async (ctx, next) => {
+8 -56
View File
@@ -24,11 +24,13 @@ import isQuote from '../../misc/is-quote';
import notesChart from '../../chart/notes';
import perUserNotesChart from '../../chart/per-user-notes';
import { erase, unique } from '../../prelude/array';
import { erase } from '../../prelude/array';
import insertNoteUnread from './unread';
import registerInstance from '../register-instance';
import Instance from '../../models/instance';
import { Node } from '../../mfm/parser';
import extractMentions from '../../misc/extract-mentions';
import extractEmojis from '../../misc/extract-emojis';
import extractHashtags from '../../misc/extract-hashtags';
type NotificationType = 'reply' | 'renote' | 'quote' | 'mention';
@@ -162,14 +164,14 @@ export default async (user: IUser, data: Option, silent = false) => new Promise<
tags = data.apHashtags || extractHashtags(combinedTokens);
// MongoDBのインデックス対象は128文字以上にできない
tags = tags.filter(tag => tag.length <= 100);
emojis = data.apEmojis || extractEmojis(combinedTokens);
mentionedUsers = data.apMentions || await extractMentionedUsers(user, combinedTokens);
}
// MongoDBのインデックス対象は128文字以上にできない
tags = tags.filter(tag => tag.length <= 100);
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 }));
}
@@ -465,44 +467,6 @@ async function insertNote(user: IUser, data: Option, tags: string[], emojis: str
}
}
function extractHashtags(tokens: ReturnType<typeof parse>): string[] {
const hashtags: string[] = [];
const extract = (tokens: Node[]) => {
for (const x of tokens.filter(x => x.name === 'hashtag')) {
hashtags.push(x.props.hashtag);
}
for (const x of tokens.filter(x => x.children)) {
extract(x.children);
}
};
// Extract hashtags
extract(tokens);
return unique(hashtags);
}
export function extractEmojis(tokens: ReturnType<typeof parse>): string[] {
const emojis: string[] = [];
const extract = (tokens: Node[]) => {
for (const x of tokens.filter(x => x.name === 'emoji')) {
if (x.props.name && x.props.name.length <= 100) {
emojis.push(x.props.name);
}
}
for (const x of tokens.filter(x => x.children)) {
extract(x.children);
}
};
// Extract emojis
extract(tokens);
return unique(emojis);
}
function index(note: INote) {
if (note.text == null || config.elasticsearch == null) return;
@@ -665,19 +629,7 @@ function incNotesCount(user: IUser) {
async function extractMentionedUsers(user: IUser, tokens: ReturnType<typeof parse>): Promise<IUser[]> {
if (tokens == null) return [];
const mentions: any[] = [];
const extract = (tokens: Node[]) => {
for (const x of tokens.filter(x => x.name === 'mention')) {
mentions.push(x.props);
}
for (const x of tokens.filter(x => x.children)) {
extract(x.children);
}
};
// Extract hashtags
extract(tokens);
const mentions = extractMentions(tokens);
let mentionedUsers =
erase(null, await Promise.all(mentions.map(async m => {
+1 -1
View File
@@ -1,5 +1,5 @@
import * as debug from 'debug';
import Emoji from "../models/emoji";
import Emoji from '../models/emoji';
debug.enable('*');
+1 -1
View File
@@ -1,4 +1,4 @@
import parseAcct from "../misc/acct/parse";
import parseAcct from '../misc/acct/parse';
import resolveUser from '../remote/resolve-user';
import * as debug from 'debug';
+48
View File
@@ -0,0 +1,48 @@
import * as assert from 'assert';
import extractMentions from '../src/misc/extract-mentions';
import parse from '../src/mfm/parse';
describe('Extract mentions', () => {
it('simple', () => {
const ast = parse('@foo @bar @baz');
const mentions = extractMentions(ast);
assert.deepStrictEqual(mentions, [{
username: 'foo',
acct: '@foo',
canonical: '@foo',
host: null
}, {
username: 'bar',
acct: '@bar',
canonical: '@bar',
host: null
}, {
username: 'baz',
acct: '@baz',
canonical: '@baz',
host: null
}]);
});
it('nested', () => {
const ast = parse('@foo **@bar** @baz');
const mentions = extractMentions(ast);
assert.deepStrictEqual(mentions, [{
username: 'foo',
acct: '@foo',
canonical: '@foo',
host: null
}, {
username: 'bar',
acct: '@bar',
canonical: '@bar',
host: null
}, {
username: 'baz',
acct: '@baz',
canonical: '@baz',
host: null
}]);
});
});
+347 -305
View File
File diff suppressed because it is too large Load Diff