Compare commits
54 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
1d3e6a7197 | ||
![]() |
1c93fcb1c4 | ||
![]() |
e3389e7899 | ||
![]() |
454632d785 | ||
![]() |
c9bca7dc85 | ||
![]() |
710ba526fa | ||
![]() |
aa47b6732d | ||
![]() |
20f83420ca | ||
![]() |
d09a68ef11 | ||
![]() |
b545be5799 | ||
![]() |
4fc377584f | ||
![]() |
a5f09c90dd | ||
![]() |
d059d7f972 | ||
![]() |
c03e2dfbc0 | ||
![]() |
45c5e7b967 | ||
![]() |
c81a94ff75 | ||
![]() |
acc6f54557 | ||
![]() |
8025b121af | ||
![]() |
78ec06bda3 | ||
![]() |
6ef83d9c59 | ||
![]() |
fca4ceef21 | ||
![]() |
00f979f0e6 | ||
![]() |
556677be7a | ||
![]() |
624fd093f2 | ||
![]() |
2ee438dece | ||
![]() |
534de24406 | ||
![]() |
e88ce1746d | ||
![]() |
b8aad35009 | ||
![]() |
47bd485a39 | ||
![]() |
ad869d7469 | ||
![]() |
d15cce5337 | ||
![]() |
37daff6d61 | ||
![]() |
5417e40f59 | ||
![]() |
0fed33bfdb | ||
![]() |
5dddc75d09 | ||
![]() |
081578c604 | ||
![]() |
6c47bf5b76 | ||
![]() |
9e85291cd3 | ||
![]() |
7f77517fc8 | ||
![]() |
b2f288dcac | ||
![]() |
52b59e9d7b | ||
![]() |
80c74b1fa7 | ||
![]() |
91811ea500 | ||
![]() |
57150fd910 | ||
![]() |
cddbbdf5d0 | ||
![]() |
423dc2349b | ||
![]() |
0556a2a2da | ||
![]() |
65d943e42a | ||
![]() |
3bcb344ecb | ||
![]() |
82d721d60b | ||
![]() |
48dc56e834 | ||
![]() |
2c33bd6e31 | ||
![]() |
b6524616bc | ||
![]() |
7e2b70f912 |
@@ -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
|
||||
|
27
CHANGELOG.md
27
CHANGELOG.md
@@ -1,6 +1,33 @@
|
||||
ChangeLog
|
||||
=========
|
||||
|
||||
10.65.0
|
||||
-------
|
||||
* 検索で投稿やユーザーのURLを入力した際にそれをフェッチして表示するように
|
||||
* リストのリネームと削除をできるように
|
||||
* リストからユーザーを削除できるように
|
||||
* リモートの絵文字を更新するように
|
||||
* ActivityPubのための絵文字エンドポイントを実装
|
||||
* 管理者がドライブのファイルのNSFWを設定できるように
|
||||
* ServiceWorkerの設定を管理者ページで行えるように
|
||||
* メンションの判定を改善
|
||||
* リモートの投稿を引用した際にオリジナルのURLを挿入するように
|
||||
* クライアントのパフォーマンス改善
|
||||
* CWの内容がタブタイトルに表示されるのを修正
|
||||
* アカウントを作成したときにログイン状態にならない問題を修正
|
||||
* 時計の針にテーマカラーが適用されていなかったのを修正
|
||||
* 一部の日時の表示が日本語で表示されていたのを修正
|
||||
* プロフィールの写真欄に画像以外のファイルが含まれる問題を修正
|
||||
* メンションが含まれる投稿に返信する際、フォームに予めそれらのメンションがセットされた状態にならない問題を修正
|
||||
* デッキのTLにUIの動きを減らすオプションが適用されていなかったのを修正
|
||||
* ログイン画面のタイムラインに隠した投稿が表示される問題を修正
|
||||
* サジェストが複数開いてしまう問題を修正
|
||||
* APから来たタグに登録時の長さ制限が適用されていなかったのを修正
|
||||
|
||||
10.64.2
|
||||
-------
|
||||
* UIの動きを減らすオプションが一部のアニメーションに適用されなかったのを修正
|
||||
|
||||
10.64.1
|
||||
-------
|
||||
* レートリミットの調整
|
||||
|
@@ -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
PULL_REQUEST_TEMPLATE.md
Normal file
13
PULL_REQUEST_TEMPLATE.md
Normal 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
|
||||
-
|
||||
-->
|
@@ -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
docs/examples/misskey.nginx
Normal file
70
docs/examples/misskey.nginx
Normal 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;
|
||||
}
|
||||
}
|
@@ -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`.
|
||||
|
@@ -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`.
|
||||
|
@@ -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`にする。
|
||||
|
@@ -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: "操作"
|
||||
|
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"name": "misskey",
|
||||
"author": "syuilo <i@syuilo.com>",
|
||||
"version": "10.64.1",
|
||||
"clientVersion": "2.0.12792",
|
||||
"version": "10.65.0",
|
||||
"clientVersion": "2.0.12846",
|
||||
"codename": "nighthike",
|
||||
"main": "./built/index.js",
|
||||
"private": true,
|
||||
|
@@ -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>
|
||||
|
@@ -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}, `;
|
||||
|
@@ -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 '';
|
||||
};
|
||||
|
@@ -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 {
|
||||
|
@@ -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;
|
||||
|
150
src/client/app/common/views/components/user-list-editor.vue
Normal file
150
src/client/app/common/views/components/user-list-editor.vue
Normal file
@@ -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) {
|
||||
|
||||
}
|
||||
};
|
||||
|
@@ -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)}`);
|
||||
}
|
||||
|
24
src/client/app/desktop/views/components/user-list-window.vue
Normal file
24
src/client/app/desktop/views/components/user-list-window.vue
Normal file
@@ -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">
|
||||
|
@@ -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 => {
|
||||
|
@@ -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) {
|
||||
|
2
src/client/app/v.d.ts
vendored
2
src/client/app/v.d.ts
vendored
@@ -1,4 +1,4 @@
|
||||
declare module "*.vue" {
|
||||
declare module '*.vue' {
|
||||
import Vue from 'vue';
|
||||
export default Vue;
|
||||
}
|
||||
|
@@ -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;
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
|
@@ -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;
|
||||
}
|
||||
}
|
||||
|
@@ -11,6 +11,15 @@ export type Node = {
|
||||
props?: any;
|
||||
};
|
||||
|
||||
export interface IMentionNode extends Node {
|
||||
props: {
|
||||
canonical: string;
|
||||
username: string;
|
||||
host: string;
|
||||
acct: string;
|
||||
};
|
||||
}
|
||||
|
||||
function _makeNode(name: string, children?: Node[], props?: any): Node {
|
||||
return children ? {
|
||||
name,
|
||||
@@ -275,7 +284,7 @@ const mfm = P.createLanguage({
|
||||
mention: r =>
|
||||
P((input, i) => {
|
||||
const text = input.substr(i);
|
||||
const match = text.match(/^@[a-z0-9_]+(?:@[a-z0-9\.\-]+[a-z0-9])?/i);
|
||||
const match = text.match(/^@\w([\w-]*\w)?(?:@[\w\.\-]+\w)?/);
|
||||
if (!match) return P.makeFailure(i, 'not a mention');
|
||||
if (input[i - 1] != null && input[i - 1].match(/[a-z0-9]/i)) return P.makeFailure(i, 'not a mention');
|
||||
return P.makeSuccess(i + match[0].length, match[0]);
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import { capitalize, toUpperCase } from "../prelude/string";
|
||||
import { capitalize, toUpperCase } from '../prelude/string';
|
||||
|
||||
function escape(text: string) {
|
||||
return text
|
||||
|
19
src/misc/extract-mentions.ts
Normal file
19
src/misc/extract-mentions.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import parse from '../mfm/parse';
|
||||
import { Node, IMentionNode } from '../mfm/parser';
|
||||
|
||||
export default function(tokens: ReturnType<typeof parse>): IMentionNode['props'][] {
|
||||
const mentions: IMentionNode['props'][] = [];
|
||||
|
||||
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(tokens);
|
||||
|
||||
return mentions;
|
||||
}
|
@@ -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> {
|
||||
|
@@ -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) {
|
||||
|
@@ -15,4 +15,6 @@ export type IEmoji = {
|
||||
url: string;
|
||||
aliases?: string[];
|
||||
updatedAt?: Date;
|
||||
/** AP object id */
|
||||
uri?: string;
|
||||
};
|
||||
|
@@ -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;
|
||||
};
|
||||
|
@@ -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 {
|
||||
|
@@ -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
src/prelude/relation.ts
Normal file
5
src/prelude/relation.ts
Normal 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,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 {
|
||||
|
@@ -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);
|
||||
|
@@ -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,4 +1,4 @@
|
||||
import { IIcon } from "./icon";
|
||||
import { IIcon } from './icon';
|
||||
|
||||
/***
|
||||
* tag (ActivityPub)
|
||||
|
@@ -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,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',
|
||||
|
@@ -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,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',
|
||||
|
@@ -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 });
|
||||
|
@@ -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;
|
||||
|
@@ -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);
|
||||
|
@@ -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
|
||||
});
|
||||
}));
|
||||
|
64
src/server/api/endpoints/users/lists/pull.ts
Normal file
64
src/server/api/endpoints/users/lists/pull.ts
Normal file
@@ -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));
|
||||
}));
|
@@ -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[] = [];
|
||||
|
||||
|
@@ -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
|
||||
|
@@ -29,6 +29,7 @@ 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';
|
||||
|
||||
type NotificationType = 'reply' | 'renote' | 'quote' | 'mention';
|
||||
|
||||
@@ -162,14 +163,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 }));
|
||||
}
|
||||
@@ -665,19 +666,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,5 +1,5 @@
|
||||
import * as debug from 'debug';
|
||||
import Emoji from "../models/emoji";
|
||||
import Emoji from '../models/emoji';
|
||||
|
||||
debug.enable('*');
|
||||
|
||||
|
@@ -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';
|
||||
|
||||
|
16
test/mfm.ts
16
test/mfm.ts
@@ -168,6 +168,22 @@ describe('Text', () => {
|
||||
]),
|
||||
node('mention', { acct: '@a', canonical: '@a', username: 'a', host: null })
|
||||
], tokens3);
|
||||
|
||||
const tokens4 = analyze('@\n@v\n@veryverylongusername' /* \n@toolongtobeasamention */ );
|
||||
assert.deepEqual([
|
||||
text('@\n'),
|
||||
node('mention', { acct: '@v', canonical: '@v', username: 'v', host: null }),
|
||||
text('\n'),
|
||||
node('mention', { acct: '@veryverylongusername', canonical: '@veryverylongusername', username: 'veryverylongusername', host: null }),
|
||||
// text('\n@toolongtobeasamention')
|
||||
], tokens4);
|
||||
/*
|
||||
const tokens5 = analyze('@domain_is@valid.example.com\n@domain_is@.invalid\n@domain_is@invali.d\n@domain_is@invali.d\n@domain_is@-invalid.com\n@domain_is@invalid-.com');
|
||||
assert.deepEqual([
|
||||
node('mention', { acct: '@domain_is@valid.example.com', canonical: '@domain_is@valid.example.com', username: 'domain_is', host: 'valid.example.com' }),
|
||||
text('\n@domain_is@.invalid\n@domain_is@invali.d\n@domain_is@invali.d\n@domain_is@-invalid.com\n@domain_is@invalid-.com')
|
||||
], tokens5);
|
||||
*/
|
||||
});
|
||||
});
|
||||
|
||||
|
Reference in New Issue
Block a user