Compare commits

...

51 Commits

Author SHA1 Message Date
ed5cb991e3 10.97.0 2019-03-18 01:11:18 +09:00
bea84ec2bf New Crowdin translations (#4509)
* New translations ja-JP.yml (Polish)

* New translations ja-JP.yml (Chinese Simplified)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (French)

* New translations ja-JP.yml (Korean)

* New translations ja-JP.yml (Polish)

* New translations ja-JP.yml (Spanish)

* New translations ja-JP.yml (Japanese, Kansai)

* New translations ja-JP.yml (Czech)

* New translations ja-JP.yml (Polish)

* New translations ja-JP.yml (Czech)

* New translations ja-JP.yml (English)
2019-03-18 01:10:03 +09:00
08c176e549 不明なリアクションのフォールバックに star を使うようにするオプション 2019-03-18 01:03:35 +09:00
810ed50976 Update black.json5 2019-03-18 00:41:30 +09:00
2684541693 Custom reaction (#4517)
* Custom reaction

* increase limit of reactions/delete

* リアクションの場合は OS標準の絵文字を使用 を迂回する

* カスタムリアクションを無効にする設定

* fix

* disableCustomReaction --> enableEmojiReaction

* Avoid MFM rendering

* 🎨

* 🎨

* Auto accept

* custom emoji reaction

* Improve usability

* Extract emojiRegex

* Fix

* Clean up

* 🎨

* 🎨

* toDbReaction で reaction は必須に

あとフォールバックは like に

* Clean up

* Make required

* 3eb08748fe (r266241728)

* Refactor

* Allow null
2019-03-18 00:03:57 +09:00
a5b12bac54 Change default dark theme 2019-03-17 20:48:55 +09:00
fea1b06e43 Update black.json5 2019-03-17 20:46:14 +09:00
182ca5d434 10.96.0 2019-03-16 21:30:31 +09:00
facde9a75d Increase chart limit 2019-03-16 21:02:17 +09:00
41385640b9 Merge branch 'develop' of https://github.com/syuilo/misskey into develop 2019-03-16 18:17:15 +09:00
7bad9db32e Update interval 2019-03-16 18:16:28 +09:00
af66f0a497 Fix #3504 (#4513) 2019-03-16 18:00:23 +09:00
95e1b80f41 updatePersonを試行した時点でもlastFetchedAtを更新する (#4510) 2019-03-16 09:55:19 +09:00
556e2eba95 Resolve #1727 (#4512) 2019-03-16 09:54:40 +09:00
efe530cb17 Update CONTRIBUTING.md 2019-03-16 02:53:35 +09:00
34e7c99283 Increase display instances limit 2019-03-16 01:36:10 +09:00
4157ea8bc3 Add icons 🎨 2019-03-16 01:34:21 +09:00
550517bbf3 Resolve #4506 2019-03-16 01:20:13 +09:00
eb910cd8a1 Merge branch 'develop' of https://github.com/syuilo/misskey into develop 2019-03-16 01:08:34 +09:00
75131c4e8a Fix bug 2019-03-16 01:08:26 +09:00
ee29ab95be Fix users/search (#4505) 2019-03-15 22:22:59 +09:00
e97951fc51 10.95.0 2019-03-15 13:54:08 +09:00
dfabdef60f Resolve #4501 2019-03-15 13:51:23 +09:00
5a87763193 New Crowdin translations (#4481)
* New translations ja-JP.yml (Chinese Simplified)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (French)

* New translations ja-JP.yml (Korean)

* New translations ja-JP.yml (Japanese, Kansai)

* New translations ja-JP.yml (Czech)

* New translations ja-JP.yml (Korean)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (Czech)

* New translations ja-JP.yml (Chinese Simplified)

* New translations ja-JP.yml (Polish)

* New translations ja-JP.yml (English)
2019-03-15 13:48:46 +09:00
6bb90f56fa ジョブを一覧できるように 2019-03-15 13:48:17 +09:00
c883ae1350 🎨 2019-03-15 13:14:50 +09:00
09e25e6a02 Better queue chart 2019-03-15 13:09:19 +09:00
bf5d43054b Fix bug 2019-03-15 13:09:05 +09:00
63b3c65691 ファビコンが保存されないのを修正 (#4500)
* Fix ファビコン保存されない

* Fix meta
2019-03-15 12:40:10 +09:00
f193da7f67 🎨 2019-03-15 02:17:10 +09:00
40f38c2c0a Improve queue page 2019-03-15 01:56:40 +09:00
db439ef804 🎨 2019-03-15 01:43:11 +09:00
56eb896a03 Accept Article object (#4499) 2019-03-15 00:23:24 +09:00
68d43e43b6 Fix hashtag style 2019-03-15 00:03:24 +09:00
c60517e49a Follow #3676 2019-03-14 22:18:31 +09:00
3f59d261f2 Follow #3676 2019-03-14 22:18:10 +09:00
4068d220e5 Follow #3676 2019-03-14 22:17:26 +09:00
18968e7208 Fix bug 2019-03-14 21:51:33 +09:00
38656103c0 Add angle bracket covered url syntax to mfm (#4483)
* Add angle bracket covered url syntax to mfm

* Fix path

* Fix match

* Fix index
2019-03-14 21:23:15 +09:00
0f65b1bcc5 10.94.0 2019-03-14 16:40:06 +09:00
a628821834 Improve readability 2019-03-14 16:35:07 +09:00
6ceff60c1e Faviconを可変にするなど 2019-03-14 16:30:51 +09:00
d762a6ce58 Merge branch 'develop' of https://github.com/syuilo/misskey into develop 2019-03-14 15:19:09 +09:00
75a8037a46 Fix #4489 2019-03-14 15:19:02 +09:00
1179920790 unFollowAll on suspend (#4490)
* unFollowAll on suspend

* use services

* silent
2019-03-14 15:16:07 +09:00
b323a160e3 Follow #3676 2019-03-14 01:20:25 +09:00
b157e9535e Merge branch 'develop' of https://github.com/syuilo/misskey into develop 2019-03-13 23:27:21 +09:00
7668475bd6 🎨 2019-03-13 23:27:11 +09:00
8bda2a1fb7 Refactor 2019-03-13 23:04:17 +09:00
b092086b5b Add languages (#4452) 2019-03-13 14:22:55 +09:00
69a0d9034f Fix #4486 (#4487) 2019-03-13 11:21:16 +09:00
85 changed files with 793 additions and 187 deletions

View File

@ -5,6 +5,40 @@ If you encounter any problems with updating, please try the following:
1. `npm run clean` or `npm run cleanall`
2. Retry update (Don't forget `npm i`)
10.97.0
----------
* リアクションに絵文字やカスタム絵文字を使えるように
* 不明なリアクションのフォールバックに star を使えるように
* デザインの調整
10.96.0
----------
* 連合ユーザーの投稿に対してActivityPubオブジェクトを要求されたら元のインスタンスにリダイレクトするように
* updatePersonを試行した時点でもlastFetchedAtを更新するように
* 管理画面でリモートインスタンスの登録日時を表示
* ユーザーサジェストが機能しなくなっていた問題を修正
* 最近使ったハッシュタグ表示が機能していない問題を修正
* バグ修正
* デザインの調整
10.95.0
----------
* ジョブを一覧できるように
* MFMでURLを明示する構文の追加
* Articleタイプのアクティビティを受け入れるように
* 凍結されたユーザーをサジェストしないように
* ファビコンが保存されないのを修正
* キューのジョブクリアの動作を修正
* デザインの調整
10.94.0
----------
* Faviconを設定できるように
* アカウントを凍結したときすべてのフォローを解除するように
* シェアページが機能していない問題を修正
* インスタンスブロックをしていてもRenote等すると取得されてしまう問題を修正
* デザインの調整
10.93.1
----------
* データのエクスポートとインポートの動作を修正

View File

@ -46,6 +46,9 @@ Convert な(na) to にゃ(nya)
Revert Nyaize
## Code style
### Use semicolon
To avoid ASI Hazard
### Don't use `export default`
Bad:
``` ts

View File

@ -2,12 +2,12 @@
meta:
lang: "Čeština"
common:
misskey: "⭐ ve fediverse"
about-title: "⭐ ve fediverse."
about: "Děkujeme, že jste našli Misskey. Misskey je <b>decentralizovaná mikroblogovací platforma</b> zrozená na Zemi. Neboť existuje ve Fediverse (vesmíru, kde jsou organizovány různé sociální sítě), je vzájemně propojena s jinými sociálními sítěmi. Co takhle si chvilku odpočinout od ruchu a shonu města a ponořit se do nového internetu?"
misskey: "⭐ ve fedivesmíru"
about-title: "⭐ ve fedivesmíru."
about: "Děkujeme, že jste našli Misskey. Misskey je <b>decentralizovaná mikroblogovací platforma</b> zrozená na Zemi. Neboť existuje ve fedivesmíru (vesmíru, kde jsou organizovány různé sociální sítě), je vzájemně propojena s jinými sociálními sítěmi. Co takhle si chvilku odpočinout od ruchu a shonu města a ponořit se do nového internetu?"
intro:
title: "Co je Misskey?"
about: "Misskey je open-source <b>decentralizovaný mikroblogovací software</b>. Má sofistikované, zcela přizpůsobitelné uživatelské rozhraní, různé způsoby reagování na příspěvky, bezplatné úložiště souborů nabízející integrovaný management system, a další pokročilé vlastnosti. Misskey je navíc připojeno k systému sítí zvanému „fediverse“, který nám dovoluje komunikovat s uživateli na jiných sociálních sítí. Pokud například něco napíšete, nebude to posláno pouze uživatelů Misskey, ale také lidem na sítích Mastodon a Pleroma. Jen si představte, že planeta posílá jiné planetě rádiový signál, aby s ní komunikovala."
about: "Misskey je open-source <b>decentralizovaný mikroblogovací software</b>. Má sofistikované, zcela přizpůsobitelné uživatelské rozhraní, různé způsoby reagování na příspěvky, bezplatné úložiště souborů nabízející integrovaný management system, a další pokročilé vlastnosti. Misskey je navíc připojeno k systému sítí zvanému „fedivesmír“ nebo „fediverse“, který nám dovoluje komunikovat s uživateli na jiných sociálních sítí. Pokud například něco napíšete, nebude to posláno pouze uživatelů Misskey, ale také lidem na sítích Mastodon a Pleroma. Jen si představte, že planeta posílá jiné planetě rádiový signál, aby s ní komunikovala."
features: "Vlastnosti"
rich-contents: "Příspěvky"
rich-contents-desc: "Pouze napište svoje nápady, žhavá témata a cokoliv, co chcete sdílet. Můžete ozdobit svá slova, připojit vaše oblíbené obrázky, posílat soubory včetně videí či vytvořit hlasování to je jen několik věcí, co můžete dělat s Misskey!"
@ -300,10 +300,11 @@ common/views/pages/explore.vue:
recently-updated-users: "Nedávno aktívni uživatelé"
recently-registered-users: "Nedávno registrovaní uživatelé"
popular-tags: "Populární tagy"
federated: "Z fediverse"
federated: "Z fedivesmíru"
explore: "Prozkoumat {host}"
common/views/components/url-preview.vue:
enable-player: "Otevřít v přehrávači"
disable-player: "Zavřít přehrávač"
common/views/components/user-list.vue:
no-users: "Žádní uživatelé"
common/views/components/games/reversi/reversi.vue:
@ -835,7 +836,7 @@ admin/views/index.vue:
emoji: "Emoji"
moderators: "Moderátoři"
users: "Uživatelé"
federation: "Z fediversu"
federation: "Federovaná"
announcements: "Oznámení"
hashtags: "Hashtagy"
queue: "Fronta úloh"
@ -847,9 +848,7 @@ admin/views/dashboard.vue:
drive: "Disk"
instances: "Instance"
this-instance: "Tato instance"
federated: "Z fediversu"
admin/views/queue.vue:
operation: "Akce"
federated: "Federovaná"
admin/views/abuse.vue:
details: "Popis"
remove-report: "Odstranit"
@ -996,14 +995,15 @@ admin/views/announcements.vue:
admin/views/hashtags.vue:
hided-tags: "Skryté tagy"
admin/views/federation.vue:
federation: "Z fediversu"
instance: "Instance"
host: "Hostitel"
notes: "Poznámky"
users: "Uživatelé"
caught-at: "Vytvořeno"
status: "Status"
latest-request-received-at: "Poslední požadavek přijat"
block: "Blokován"
instances: "Instance"
instances: "Federovaná"
states:
all: "Všechny"
blocked: "Blokován"

View File

@ -169,9 +169,9 @@ common:
deck-column-align-flexible: "Flexible"
deck-column-width: "Deck column width"
deck-column-width-narrow: "Narrow"
deck-column-width-narrower: "Somewhat narrow"
deck-column-width-narrower: "Narrower"
deck-column-width-normal: "Regular"
deck-column-width-wider: "Somewhat wide"
deck-column-width-wider: "Slightly wide"
deck-column-width-wide: "Wide"
use-shadow: "Use shadows in the UI"
rounded-corners: "Round the corners of the UI"
@ -1057,7 +1057,7 @@ admin/views/dashboard.vue:
this-instance: "This instance"
federated: "Federated"
admin/views/queue.vue:
operation: "Action(s)"
title: "Queue"
remove-all-jobs: "Clear all queued jobs"
admin/views/abuse.vue:
title: "Abuse"
@ -1113,6 +1113,7 @@ admin/views/instance.vue:
disable-local-timeline: "Disable the Local Timeline"
disable-global-timeline: "Disable global timeline"
disabling-timelines-info: "Even if you disable these timelines, the administrator as well as moderators can use them continually."
enable-emoji-reaction: "Enable pictograms for reactions"
invite: "Invite"
save: "Save"
saved: "Saved"
@ -1275,12 +1276,13 @@ admin/views/announcements.vue:
admin/views/hashtags.vue:
hided-tags: "Hidden Tags"
admin/views/federation.vue:
federation: "Federation"
instance: "Instance"
host: "Host"
notes: "Notes"
users: "Users"
following: "Following"
followers: "Followers"
caught-at: "Created at"
status: "Statuses"
latest-request-sent-at: "Time of last request sent"
latest-request-received-at: "Last request received at"
@ -1289,7 +1291,7 @@ admin/views/federation.vue:
block: "Block"
marked-as-closed: "Marked as closed"
lookup: "Look up"
instances: "Instances"
instances: "Federated"
instance-not-registered: "The instance has not been discovered"
sort: "Sort by"
sorts:

View File

@ -815,11 +815,11 @@ admin/views/announcements.vue:
remove: "eliminar"
add: "Agregar"
admin/views/federation.vue:
instance: "Instancia"
host: "Host"
following: "Siguiendo"
status: "Estado"
block: "Bloquear"
instances: "Instancia"
states:
all: "Todo"
blocked: "Bloquear"

View File

@ -927,7 +927,6 @@ admin/views/dashboard.vue:
this-instance: "Cette instance"
federated: "Fédérées"
admin/views/queue.vue:
operation: "Action(s)"
remove-all-jobs: "Enlever toutes les tâches en attente"
admin/views/abuse.vue:
title: "Abus"
@ -1143,12 +1142,13 @@ admin/views/announcements.vue:
admin/views/hashtags.vue:
hided-tags: "Tags cachés"
admin/views/federation.vue:
federation: "Fédération"
instance: "Instance"
host: "Hôte"
notes: "Notes"
users: "Utilisateur·rice·s"
following: "Abonnements"
followers: "Abonné·e·s"
caught-at: "Créé le"
status: "Statuts"
latest-request-sent-at: "Dernière requête envoyée"
latest-request-received-at: "Dernière requête reçue"
@ -1156,7 +1156,7 @@ admin/views/federation.vue:
block: "Bloquer"
marked-as-closed: "Marquées comme fermées"
lookup: "Recherche"
instances: "Instances"
instances: "Fédérées"
sort: "Trier par"
sorts:
caughtAtAsc: "Date dinscription (Ascendant)"

View File

@ -14,6 +14,7 @@ const merge = (...args) => args.reduce((a, c) => ({
}), {});
const languages = [
'cs-CZ',
'de-DE',
'en-US',
'es-ES',
@ -24,9 +25,11 @@ const languages = [
'nl-NL',
'pl-PL',
'zh-CN',
'zh-TW',
];
const primaries = {
'en': 'US',
'ja': 'JP',
'zh': 'CN',
};

View File

@ -1238,6 +1238,8 @@ admin/views/instance.vue:
disable-local-timeline: "ローカルタイムラインを無効にする"
disable-global-timeline: "グローバルタイムラインを無効にする"
disabling-timelines-info: "これらのタイムラインを無効にしても、管理者およびモデレーターは引き続き利用できます。"
enable-emoji-reaction: "リアクションに絵文字を使えるようにする"
use-star-for-reaction-fallback: "不明なリアクションのフォールバックに star を使う"
invite: "招待"
save: "保存"
saved: "保存しました"
@ -1408,12 +1410,13 @@ admin/views/hashtags.vue:
hided-tags: "Hidden Tags"
admin/views/federation.vue:
federation: "連合"
instance: "インスタンス"
host: "ホスト"
notes: "投稿"
users: "ユーザー"
following: "フォロー中"
followers: "フォロワー"
caught-at: "登録日時"
status: "ステータス"
latest-request-sent-at: "直近のリクエスト送信"
latest-request-received-at: "直近のリクエスト受信"
@ -1422,7 +1425,7 @@ admin/views/federation.vue:
block: "ブロック"
marked-as-closed: "閉鎖されているとマーク"
lookup: "照会"
instances: "インスタンス"
instances: "連合"
instance-not-registered: "そのインスタンスは登録されていません"
sort: "ソート"
sorts:

View File

@ -860,8 +860,6 @@ admin/views/dashboard.vue:
instances: "インスタンス"
this-instance: "ワイのインスタンス"
federated: "連合"
admin/views/queue.vue:
operation: "操作"
admin/views/abuse.vue:
details: "もっと"
remove-report: "削除"
@ -990,7 +988,7 @@ admin/views/announcements.vue:
add: "増やす"
saved: "保存したで!"
admin/views/federation.vue:
federation: "連合"
instance: "インスタンス"
host: "ホスト"
notes: "投稿"
users: "ユーザー"
@ -999,7 +997,7 @@ admin/views/federation.vue:
status: "ステータス"
block: "ブロック"
lookup: "照会"
instances: "インスタンス"
instances: "連合"
states:
all: "すべて"
blocked: "ブロック"

View File

@ -314,6 +314,7 @@ common/views/pages/explore.vue:
users-info: "현재 {users} 사용자가 등록되어 있습니다"
common/views/components/url-preview.vue:
enable-player: "플레이어 열기"
disable-player: "플레이어 닫기"
common/views/components/user-list.vue:
no-users: "사용자가 없습니다"
common/views/components/games/reversi/reversi.vue:
@ -1056,7 +1057,7 @@ admin/views/dashboard.vue:
this-instance: "이 인스턴스"
federated: "연합"
admin/views/queue.vue:
operation: "동작"
title: ""
remove-all-jobs: "모든 작업 제거"
admin/views/abuse.vue:
title: "스팸 신고"
@ -1274,12 +1275,13 @@ admin/views/announcements.vue:
admin/views/hashtags.vue:
hided-tags: "Hidden Tags"
admin/views/federation.vue:
federation: "연합"
instance: "인스턴스"
host: "호스트"
notes: "글"
users: "사용자"
following: "팔로우 중"
followers: "팔로워"
caught-at: "등록 날짜"
status: "상태"
latest-request-sent-at: "마지막으로 요청을 전송한 시간"
latest-request-received-at: "마지막으로 요청을 받은 시간"
@ -1288,7 +1290,7 @@ admin/views/federation.vue:
block: "차단"
marked-as-closed: "폐쇄된 것으로 표시"
lookup: "조회"
instances: "인스턴스"
instances: "연합"
instance-not-registered: "해당 인스턴스가 등록되어 있지 않습니다"
sort: "정렬"
sorts:

View File

@ -121,12 +121,17 @@ common:
other: "Inne"
appearance: "Wygląd"
behavior: "Zachowanie"
fetch-on-scroll: "Automatycznie ładuj po przeciągnięciu w dół"
note-visibility: "Widoczność wpisów"
web-search-engine: "Wyszukiwarka internetowa"
line-width: "Szerokości linii"
line-width-thin: "Cienka"
line-width-normal: "Normalna"
line-width-thick: "Gruba"
font-size: "Rozmiar tekstu"
font-size-x-small: "Małe"
font-size-medium: "Normalna"
font-size-large: "Trochę duży"
font-size-x-large: "Duży"
deck-column-align-center: "Po środku"
deck-column-align-left: "Z lewej"
@ -137,7 +142,13 @@ common:
deck-column-width-normal: "Normalna"
deck-column-width-wider: "Trochę szerokie"
deck-column-width-wide: "Szeroka"
wallpaper: "Tapeta"
choose-wallpaper: "Wybierz tapetę"
timeline: "Oś czasu"
sound: "Dźwięk"
test: "Test"
update: "Aktualizacja Misskey"
version: "Wersja:"
navbar-position-left: "Z lewej"
search: "Szukaj"
delete: "Usuń"
@ -936,13 +947,14 @@ admin/views/announcements.vue:
are-you-sure: "Usunąć \"$1\"?"
removed: "Usunięto"
admin/views/federation.vue:
instance: "Instancja"
notes: "Wpis"
users: "Użytkownicy"
following: "Śledzisz"
followers: "Śledzący"
caught-at: "Utworzono"
status: "Stan"
block: "Zablokuj"
instances: "Instancja"
sort: "Sortuj"
states:
all: "Wszyscy"

View File

@ -314,6 +314,7 @@ common/views/pages/explore.vue:
users-info: "当前有{users}个注册用户"
common/views/components/url-preview.vue:
enable-player: "打开播放器"
disable-player: "关闭播放器"
common/views/components/user-list.vue:
no-users: "无用户"
common/views/components/games/reversi/reversi.vue:
@ -1056,7 +1057,7 @@ admin/views/dashboard.vue:
this-instance: "此实例"
federated: "联合"
admin/views/queue.vue:
operation: "操作"
title: "队列"
remove-all-jobs: "清除所有作业"
admin/views/abuse.vue:
title: "举报垃圾信息"
@ -1274,12 +1275,13 @@ admin/views/announcements.vue:
admin/views/hashtags.vue:
hided-tags: "隐藏标签"
admin/views/federation.vue:
federation: "联合"
instance: ""
host: "主机名"
notes: "帖子"
users: "用户"
following: "正在关注"
followers: "关注者"
caught-at: "注册日期"
status: "状态"
latest-request-sent-at: "上次发送的请求"
latest-request-received-at: "上次收到的请求"
@ -1288,7 +1290,7 @@ admin/views/federation.vue:
block: "拉黑"
marked-as-closed: "标记为已关闭"
lookup: "查询"
instances: "实例"
instances: "联合"
instance-not-registered: "实例未注册"
sort: "排序"
sorts:

View File

@ -1,7 +1,7 @@
{
"name": "misskey",
"author": "syuilo <i@syuilo.com>",
"version": "10.93.1",
"version": "10.97.0",
"codename": "nighthike",
"repository": {
"type": "git",

View File

@ -181,7 +181,12 @@ export default Vue.extend({
},
grid: {
clipMarkers: false,
borderColor: 'rgba(0, 0, 0, 0.1)'
borderColor: 'rgba(0, 0, 0, 0.1)',
xaxis: {
lines: {
show: true,
}
},
},
stroke: {
curve: 'straight',

View File

@ -23,6 +23,8 @@ import { faInbox } from '@fortawesome/free-solid-svg-icons';
import { faPaperPlane } from '@fortawesome/free-regular-svg-icons';
import ApexCharts from 'apexcharts';
const limit = 150;
export default Vue.extend({
data() {
return {
@ -124,7 +126,7 @@ export default Vue.extend({
connection.on('statsLog', this.onStatsLog);
connection.send('requestLog', {
id: Math.random().toString().substr(2, 8),
length: 100
length: limit
});
this.$once('hook:beforeDestroy', () => {
@ -137,7 +139,7 @@ export default Vue.extend({
methods: {
onStats(stats) {
this.stats.push(stats);
if (this.stats.length > 100) this.stats.shift();
if (this.stats.length > limit) this.stats.shift();
},
onStatsLog(statsLog) {

View File

@ -92,8 +92,8 @@ import Vue from 'vue';
import i18n from '../../i18n';
import XCpuMemory from "./dashboard.cpu-memory.vue";
import XQueue from "./dashboard.queue-charts.vue";
import XCharts from "./charts.vue";
import XApLog from "./ap-log.vue";
import XCharts from "./dashboard.charts.vue";
import XApLog from "./dashboard.ap-log.vue";
import { faDatabase } from '@fortawesome/free-solid-svg-icons';
import MarqueeText from 'vue-marquee-text-component';
import randomColor from 'randomcolor';

View File

@ -1,43 +1,58 @@
<template>
<div>
<ui-card>
<template #title><fa :icon="faTerminal"/> {{ $t('federation') }}</template>
<template #title><fa :icon="faTerminal"/> {{ $t('instance') }}</template>
<section class="fit-top">
<ui-input class="target" v-model="target" type="text" @enter="showInstance()">
<span>{{ $t('host') }}</span>
<template #prefix><fa :icon="faServer"/></template>
</ui-input>
<ui-button @click="showInstance()"><fa :icon="faSearch"/> {{ $t('lookup') }}</ui-button>
<div class="instance" v-if="instance">
<ui-input :value="instance.host" type="text" readonly>
<span>{{ $t('host') }}</span>
</ui-input>
<ui-horizon-group inputs>
<ui-input :value="instance.host" type="text" readonly>
<span>{{ $t('host') }}</span>
<template #prefix><fa :icon="faServer"/></template>
</ui-input>
<ui-input :value="instance.caughtAt | date" type="text" readonly>
<span>{{ $t('caught-at') }}</span>
<template #prefix><fa :icon="faCrosshairs"/></template>
</ui-input>
</ui-horizon-group>
<ui-horizon-group inputs>
<ui-input :value="instance.notesCount | number" type="text" readonly>
<span>{{ $t('notes') }}</span>
<template #prefix><fa :icon="faEnvelopeOpenText"/></template>
</ui-input>
<ui-input :value="instance.usersCount | number" type="text" readonly>
<span>{{ $t('users') }}</span>
<template #prefix><fa :icon="faUsers"/></template>
</ui-input>
</ui-horizon-group>
<ui-horizon-group inputs>
<ui-input :value="instance.followingCount | number" type="text" readonly>
<span>{{ $t('following') }}</span>
<template #prefix><fa :icon="faCaretDown"/></template>
</ui-input>
<ui-input :value="instance.followersCount | number" type="text" readonly>
<span>{{ $t('followers') }}</span>
<template #prefix><fa :icon="faCaretUp"/></template>
</ui-input>
</ui-horizon-group>
<ui-horizon-group inputs>
<ui-input :value="instance.latestRequestSentAt" type="text" readonly>
<ui-input :value="instance.latestRequestSentAt | date" type="text" readonly>
<span>{{ $t('latest-request-sent-at') }}</span>
<template #prefix><fa :icon="faPaperPlane"/></template>
</ui-input>
<ui-input :value="instance.latestStatus" type="text" readonly>
<span>{{ $t('status') }}</span>
<template #prefix><fa :icon="faTrafficLight"/></template>
</ui-input>
</ui-horizon-group>
<ui-input :value="instance.latestRequestReceivedAt" type="text" readonly>
<ui-input :value="instance.latestRequestReceivedAt | date" type="text" readonly>
<span>{{ $t('latest-request-received-at') }}</span>
<template #prefix><fa :icon="faInbox"/></template>
</ui-input>
<ui-switch v-model="instance.isBlocked" @change="updateInstance()">{{ $t('block') }}</ui-switch>
<ui-switch v-model="instance.isMarkedAsClosed" @change="updateInstance()">{{ $t('marked-as-closed') }}</ui-switch>
@ -133,7 +148,8 @@
<script lang="ts">
import Vue from 'vue';
import i18n from '../../i18n';
import { faGlobe, faTerminal, faSearch, faMinusCircle, faServer } from '@fortawesome/free-solid-svg-icons';
import { faPaperPlane } from '@fortawesome/free-regular-svg-icons';
import { faGlobe, faTerminal, faSearch, faMinusCircle, faServer, faCrosshairs, faEnvelopeOpenText, faUsers, faCaretDown, faCaretUp, faTrafficLight, faInbox } from '@fortawesome/free-solid-svg-icons';
import ApexCharts from 'apexcharts';
import * as tinycolor from 'tinycolor2';
@ -144,19 +160,23 @@ const negate = arr => arr.map(x => -x);
export default Vue.extend({
i18n: i18n('admin/views/federation.vue'),
filters: {
date: v => v ? new Date(v).toLocaleString() : 'N/A'
},
data() {
return {
instance: null,
target: null,
sort: '+lastCommunicatedAt',
state: 'all',
limit: 50,
limit: 100,
instances: [],
chart: null,
chartSrc: 'requests',
chartSpan: 'hour',
chartInstance: null,
faGlobe, faTerminal, faSearch, faMinusCircle, faServer
faGlobe, faTerminal, faSearch, faMinusCircle, faServer, faCrosshairs, faEnvelopeOpenText, faUsers, faCaretDown, faCaretUp, faPaperPlane, faTrafficLight, faInbox
};
},

View File

@ -6,6 +6,7 @@
<ui-input :value="host" readonly>{{ $t('host') }}</ui-input>
<ui-input v-model="name">{{ $t('instance-name') }}</ui-input>
<ui-textarea v-model="description">{{ $t('instance-description') }}</ui-textarea>
<ui-input v-model="iconUrl"><template #icon><fa icon="link"/></template>{{ $t('icon-url') }}</ui-input>
<ui-input v-model="mascotImageUrl"><template #icon><fa icon="link"/></template>{{ $t('logo-url') }}</ui-input>
<ui-input v-model="bannerUrl"><template #icon><fa icon="link"/></template>{{ $t('banner-url') }}</ui-input>
<ui-input v-model="errorImageUrl"><template #icon><fa icon="link"/></template>{{ $t('error-image-url') }}</ui-input>
@ -24,6 +25,8 @@
<ui-switch v-model="disableLocalTimeline">{{ $t('disable-local-timeline') }}</ui-switch>
<ui-switch v-model="disableGlobalTimeline">{{ $t('disable-global-timeline') }}</ui-switch>
<ui-info>{{ $t('disabling-timelines-info') }}</ui-info>
<ui-switch v-model="enableEmojiReaction">{{ $t('enable-emoji-reaction') }}</ui-switch>
<ui-switch v-model="useStarForReactionFallback">{{ $t('use-star-for-reaction-fallback') }}</ui-switch>
</section>
<section class="fit-bottom">
<header><fa icon="cloud"/> {{ $t('drive-config') }}</header>
@ -154,9 +157,12 @@ export default Vue.extend({
disableRegistration: false,
disableLocalTimeline: false,
disableGlobalTimeline: false,
enableEmojiReaction: true,
useStarForReactionFallback: false,
mascotImageUrl: null,
bannerUrl: null,
errorImageUrl: null,
iconUrl: null,
name: null,
description: null,
languages: null,
@ -204,9 +210,12 @@ export default Vue.extend({
this.disableRegistration = meta.disableRegistration;
this.disableLocalTimeline = meta.disableLocalTimeline;
this.disableGlobalTimeline = meta.disableGlobalTimeline;
this.enableEmojiReaction = meta.enableEmojiReaction;
this.useStarForReactionFallback = meta.useStarForReactionFallback;
this.mascotImageUrl = meta.mascotImageUrl;
this.bannerUrl = meta.bannerUrl;
this.errorImageUrl = meta.errorImageUrl;
this.iconUrl = meta.iconUrl;
this.name = meta.name;
this.description = meta.description;
this.languages = meta.langs.join(' ');
@ -264,9 +273,12 @@ export default Vue.extend({
disableRegistration: this.disableRegistration,
disableLocalTimeline: this.disableLocalTimeline,
disableGlobalTimeline: this.disableGlobalTimeline,
enableEmojiReaction: this.enableEmojiReaction,
useStarForReactionFallback: this.useStarForReactionFallback,
mascotImageUrl: this.mascotImageUrl,
bannerUrl: this.bannerUrl,
errorImageUrl: this.errorImageUrl,
iconUrl: this.iconUrl,
name: this.name,
description: this.description,
langs: this.languages.split(' '),

View File

@ -4,7 +4,7 @@
<template #title><fa :icon="faStream"/> {{ $t('logs') }}</template>
<section class="fit-top">
<ui-horizon-group inputs>
<ui-input v-model="domain" debounce>
<ui-input v-model="domain" :debounce="true">
<span>{{ $t('domain') }}</span>
</ui-input>
<ui-select v-model="level">

View File

@ -1,14 +1,15 @@
<template>
<div>
<ui-card>
<template #title><fa :icon="faTasks"/> {{ $t('title') }}</template>
<template #title><fa :icon="faChartBar"/> {{ $t('title') }}</template>
<section class="wptihjuy">
<header><fa :icon="faPaperPlane"/> Deliver</header>
<ui-info warn v-if="latestStats && latestStats.deliver.waiting > 0">The queue is jammed.</ui-info>
<ui-horizon-group inputs v-if="latestStats" class="fit-bottom">
<ui-input :value="latestStats.deliver.activeSincePrevTick | number" type="text" readonly>
<span>Process</span>
<template #prefix><fa :icon="fasPlayCircle"/></template>
<template #suffix>jobs/s</template>
<template #suffix>jobs/tick</template>
</ui-input>
<ui-input :value="latestStats.deliver.active | number" type="text" readonly>
<span>Active</span>
@ -30,11 +31,12 @@
</section>
<section class="wptihjuy">
<header><fa :icon="faInbox"/> Inbox</header>
<ui-info warn v-if="latestStats && latestStats.inbox.waiting > 0">The queue is jammed.</ui-info>
<ui-horizon-group inputs v-if="latestStats" class="fit-bottom">
<ui-input :value="latestStats.inbox.activeSincePrevTick | number" type="text" readonly>
<span>Process</span>
<template #prefix><fa :icon="fasPlayCircle"/></template>
<template #suffix>jobs/s</template>
<template #suffix>jobs/tick</template>
</ui-input>
<ui-input :value="latestStats.inbox.active | number" type="text" readonly>
<span>Active</span>
@ -58,6 +60,35 @@
<ui-button @click="removeAllJobs">{{ $t('remove-all-jobs') }}</ui-button>
</section>
</ui-card>
<ui-card>
<template #title><fa :icon="faTasks"/> {{ $t('jobs') }}</template>
<section class="fit-top">
<ui-horizon-group inputs>
<ui-select v-model="domain">
<template #label>{{ $t('queue') }}</template>
<option value="deliver">{{ $t('domains.deliver') }}</option>
<option value="inbox">{{ $t('domains.inbox') }}</option>
</ui-select>
<ui-select v-model="state">
<template #label>{{ $t('state') }}</template>
<option value="delayed">{{ $t('states.delayed') }}</option>
</ui-select>
</ui-horizon-group>
<sequential-entrance animation="entranceFromTop" delay="25">
<div class="xvvuvgsv" v-for="job in jobs">
<b>{{ job.id }}</b>
<template v-if="domain === 'deliver'">
<span>{{ job.data.to }}</span>
</template>
<template v-if="domain === 'inbox'">
<span>{{ job.activity.id }}</span>
</template>
</div>
</sequential-entrance>
<ui-info v-if="jobs.length == jobsLimit">{{ $t('result-is-truncated', { n: jobsLimit }) }}</ui-info>
</section>
</ui-card>
</div>
</template>
@ -67,7 +98,9 @@ import i18n from '../../i18n';
import ApexCharts from 'apexcharts';
import * as tinycolor from 'tinycolor2';
import { faTasks, faInbox, faStopwatch, faPlayCircle as fasPlayCircle } from '@fortawesome/free-solid-svg-icons';
import { faPaperPlane, faStopCircle, faPlayCircle as farPlayCircle } from '@fortawesome/free-regular-svg-icons';
import { faPaperPlane, faStopCircle, faPlayCircle as farPlayCircle, faChartBar } from '@fortawesome/free-regular-svg-icons';
const limit = 200;
export default Vue.extend({
i18n: i18n('admin/views/queue.vue'),
@ -77,7 +110,11 @@ export default Vue.extend({
stats: [],
deliverChart: null,
inboxChart: null,
faTasks, faPaperPlane, faInbox, faStopwatch, faStopCircle, farPlayCircle, fasPlayCircle
jobs: [],
jobsLimit: 50,
domain: 'deliver',
state: 'delayed',
faTasks, faPaperPlane, faInbox, faStopwatch, faStopCircle, farPlayCircle, fasPlayCircle, faChartBar
};
},
@ -91,36 +128,58 @@ export default Vue.extend({
stats(stats) {
this.inboxChart.updateSeries([{
name: 'Process',
type: 'area',
data: stats.map((x, i) => ({ x: i, y: x.inbox.activeSincePrevTick }))
}, {
name: 'Active',
type: 'area',
data: stats.map((x, i) => ({ x: i, y: x.inbox.active }))
}, {
name: 'Waiting',
type: 'line',
data: stats.map((x, i) => ({ x: i, y: x.inbox.waiting }))
}, {
name: 'Delayed',
type: 'line',
data: stats.map((x, i) => ({ x: i, y: x.inbox.delayed }))
}]);
this.deliverChart.updateSeries([{
name: 'Process',
type: 'area',
data: stats.map((x, i) => ({ x: i, y: x.deliver.activeSincePrevTick }))
}, {
name: 'Active',
type: 'area',
data: stats.map((x, i) => ({ x: i, y: x.deliver.active }))
}, {
name: 'Waiting',
type: 'line',
data: stats.map((x, i) => ({ x: i, y: x.deliver.waiting }))
}, {
name: 'Delayed',
type: 'line',
data: stats.map((x, i) => ({ x: i, y: x.deliver.delayed }))
}]);
}
},
domain() {
this.jobs = [];
this.fetchJobs();
},
state() {
this.jobs = [];
this.fetchJobs();
},
},
mounted() {
const chartOpts = {
this.fetchJobs();
const chartOpts = id => ({
chart: {
id,
group: 'queue',
type: 'area',
height: 200,
animations: {
@ -140,7 +199,12 @@ export default Vue.extend({
},
grid: {
clipMarkers: false,
borderColor: 'rgba(0, 0, 0, 0.1)'
borderColor: 'rgba(0, 0, 0, 0.1)',
xaxis: {
lines: {
show: true,
}
},
},
stroke: {
curve: 'straight',
@ -169,10 +233,10 @@ export default Vue.extend({
show: false,
min: 0,
}
};
});
this.inboxChart = new ApexCharts(this.$refs.inboxChart, chartOpts);
this.deliverChart = new ApexCharts(this.$refs.deliverChart, chartOpts);
this.inboxChart = new ApexCharts(this.$refs.inboxChart, chartOpts('a'));
this.deliverChart = new ApexCharts(this.$refs.deliverChart, chartOpts('b'));
this.inboxChart.render();
this.deliverChart.render();
@ -182,7 +246,7 @@ export default Vue.extend({
connection.on('statsLog', this.onStatsLog);
connection.send('requestLog', {
id: Math.random().toString().substr(2, 8),
length: 100
length: limit
});
this.$once('hook:beforeDestroy', () => {
@ -212,14 +276,24 @@ export default Vue.extend({
onStats(stats) {
this.stats.push(stats);
if (this.stats.length > 100) this.stats.shift();
if (this.stats.length > limit) this.stats.shift();
},
onStatsLog(statsLog) {
for (const stats of statsLog.reverse()) {
this.onStats(stats);
}
}
},
fetchJobs() {
this.$root.api('admin/queue/jobs', {
domain: this.domain,
state: this.state,
limit: this.jobsLimit
}).then(jobs => {
this.jobs = jobs;
});
},
}
});
</script>
@ -228,5 +302,10 @@ export default Vue.extend({
.wptihjuy
> .chart
min-height 200px !important
margin 0 -8px
.xvvuvgsv
> b
margin-right 16px
</style>

View File

@ -69,7 +69,7 @@ export default Vue.extend({
},
plotOptions: {
bar: {
columnWidth: '90%'
columnWidth: '80%'
}
},
grid: {

View File

@ -29,7 +29,11 @@ export default Vue.extend({
customEmojis: {
required: false,
default: () => []
}
},
isReaction: {
type: Boolean,
default: false
},
},
data() {
@ -46,7 +50,7 @@ export default Vue.extend({
},
useOsDefaultEmojis(): boolean {
return this.$store.state.device.useOsDefaultEmojis;
return this.$store.state.device.useOsDefaultEmojis && !this.isReaction;
}
},

View File

@ -1,19 +1,5 @@
<template>
<span class="mk-reaction-icon">
<img v-if="reaction == 'like'" src="https://twemoji.maxcdn.com/2/svg/1f44d.svg" :alt="$t('@.reactions.like')">
<img v-if="reaction == 'love'" src="https://twemoji.maxcdn.com/2/svg/2764.svg" :alt="$t('@.reactions.love')">
<img v-if="reaction == 'laugh'" src="https://twemoji.maxcdn.com/2/svg/1f606.svg" :alt="$t('@.reactions.laugh')">
<img v-if="reaction == 'hmm'" src="https://twemoji.maxcdn.com/2/svg/1f914.svg" :alt="$t('@.reactions.hmm')">
<img v-if="reaction == 'surprise'" src="https://twemoji.maxcdn.com/2/svg/1f62e.svg" :alt="$t('@.reactions.surprise')">
<img v-if="reaction == 'congrats'" src="https://twemoji.maxcdn.com/2/svg/1f389.svg" :alt="$t('@.reactions.congrats')">
<img v-if="reaction == 'angry'" src="https://twemoji.maxcdn.com/2/svg/1f4a2.svg" :alt="$t('@.reactions.angry')">
<img v-if="reaction == 'confused'" src="https://twemoji.maxcdn.com/2/svg/1f625.svg" :alt="$t('@.reactions.confused')">
<img v-if="reaction == 'rip'" src="https://twemoji.maxcdn.com/2/svg/1f607.svg" :alt="$t('@.reactions.rip')">
<template v-if="reaction == 'pudding'">
<img v-if="$store.getters.isSignedIn && $store.state.settings.iLikeSushi" src="https://twemoji.maxcdn.com/2/svg/1f363.svg" :alt="$t('@.reactions.pudding')">
<img v-else src="https://twemoji.maxcdn.com/2/svg/1f36e.svg" :alt="$t('@.reactions.pudding')">
</template>
</span>
<mk-emoji :emoji="str.startsWith(':') ? null : str" :name="str.startsWith(':') ? str.substr(1, str.length - 2) : null" :is-reaction="true" :custom-emojis="customEmojis" :normal="true"/>
</template>
<script lang="ts">
@ -21,7 +7,35 @@ import Vue from 'vue';
import i18n from '../../../i18n';
export default Vue.extend({
i18n: i18n(),
props: ['reaction']
props: {
reaction: {
type: String,
required: true
},
},
data() {
return {
customEmojis: (this.$root.getMetaSync() || { emojis: [] }).emojis || []
};
},
computed: {
str(): any {
switch (this.reaction) {
case 'like': return '👍';
case 'love': return '❤';
case 'laugh': return '😆';
case 'hmm': return '🤔';
case 'surprise': return '😮';
case 'congrats': return '🎉';
case 'angry': return '💢';
case 'confused': return '😥';
case 'rip': return '😇';
case 'pudding': return (this.$store.getters.isSignedIn && this.$store.state.settings.iLikeSushi) ? '🍣' : '🍮';
case 'star': return '⭐';
default: return this.reaction;
}
},
},
});
</script>

View File

@ -3,7 +3,7 @@
<div class="backdrop" ref="backdrop" @click="close"></div>
<div class="popover" :class="{ isMobile: $root.isMobile }" ref="popover">
<p v-if="!$root.isMobile">{{ title }}</p>
<div ref="buttons" :class="{ showFocus }">
<div class="buttons" ref="buttons" :class="{ showFocus }">
<button @click="react('like')" @mouseover="onMouseover" @mouseout="onMouseout" tabindex="1" :title="$t('@.reactions.like')" v-particle><mk-reaction-icon reaction="like"/></button>
<button @click="react('love')" @mouseover="onMouseover" @mouseout="onMouseout" tabindex="2" :title="$t('@.reactions.love')" v-particle><mk-reaction-icon reaction="love"/></button>
<button @click="react('laugh')" @mouseover="onMouseover" @mouseout="onMouseout" tabindex="3" :title="$t('@.reactions.laugh')" v-particle><mk-reaction-icon reaction="laugh"/></button>
@ -15,6 +15,9 @@
<button @click="react('rip')" @mouseover="onMouseover" @mouseout="onMouseout" tabindex="9" :title="$t('@.reactions.rip')" v-particle><mk-reaction-icon reaction="rip"/></button>
<button @click="react('pudding')" @mouseover="onMouseover" @mouseout="onMouseout" tabindex="10" :title="$t('@.reactions.pudding')" v-particle><mk-reaction-icon reaction="pudding"/></button>
</div>
<div v-if="enableEmojiReaction" class="text">
<input v-model="text" placeholder="または絵文字を入力" @keyup.enter="reactText" @input="tryReactText" v-autocomplete="{ model: 'text' }">
</div>
</div>
</div>
</template>
@ -23,6 +26,7 @@
import Vue from 'vue';
import i18n from '../../../i18n';
import anime from 'animejs';
import { emojiRegex } from '../../../../../misc/emoji-regex';
export default Vue.extend({
i18n: i18n('common/views/components/reaction-picker.vue'),
@ -56,6 +60,8 @@ export default Vue.extend({
data() {
return {
title: this.$t('choose-reaction'),
text: null,
enableEmojiReaction: false,
focus: null
};
},
@ -94,6 +100,10 @@ export default Vue.extend({
},
mounted() {
this.$root.getMeta().then(meta => {
this.enableEmojiReaction = meta.enableEmojiReaction;
});
this.$nextTick(() => {
this.focus = 0;
@ -143,6 +153,17 @@ export default Vue.extend({
});
},
reactText() {
if (!this.text) return;
this.react(this.text);
},
tryReactText() {
if (!this.text) return;
if (!this.text.match(emojiRegex)) return;
this.reactText();
},
onMouseover(e) {
this.title = e.target.title;
},
@ -256,9 +277,9 @@ export default Vue.extend({
color var(--popupFg)
border-bottom solid var(--lineWidth) var(--faceDivider)
> div
> .buttons
padding 4px
width 240px
width 216px
text-align center
&.showFocus
@ -283,6 +304,9 @@ export default Vue.extend({
font-size 24px
border-radius 2px
> *
height 1em
&:hover
background var(--reactionPickerButtonHoverBg)
@ -290,4 +314,29 @@ export default Vue.extend({
background var(--primary)
box-shadow inset 0 0.15em 0.3em rgba(27, 31, 35, 0.15)
> .text
width 216px
padding 4px 8px 8px 8px
> input
width 100%
padding 10px
margin 0
text-align center
font-size 16px
color var(--desktopPostFormTextareaFg)
background var(--desktopPostFormTextareaBg)
outline none
border solid 1px var(--primaryAlpha01)
border-radius 4px
transition border-color .2s ease
&:hover
border-color var(--primaryAlpha02)
transition border-color .1s ease
&:focus
border-color var(--primaryAlpha05)
transition border-color 0s ease
</style>

View File

@ -136,12 +136,8 @@ export default Vue.extend({
&:hover
background var(--reactionViewerButtonHoverBg)
> .mk-reaction-icon
font-size 1.4em
> span
font-size 1.1em
line-height 32px
vertical-align middle
color var(--text)
</style>

View File

@ -93,12 +93,17 @@ export default Vue.extend({
},
plotOptions: {
bar: {
columnWidth: '90%'
columnWidth: '80%'
}
},
grid: {
clipMarkers: false,
borderColor: 'rgba(0, 0, 0, 0.1)'
borderColor: 'rgba(0, 0, 0, 0.1)',
xaxis: {
lines: {
show: true,
}
},
},
tooltip: {
shared: true,

View File

@ -366,9 +366,6 @@ root(fill)
&[type='file']
display none
&[type='number']
text-align right
> .prefix
> .suffix
display block

View File

@ -172,7 +172,7 @@ export default Vue.extend({
},
plotOptions: {
bar: {
columnWidth: '90%'
columnWidth: '80%'
}
},
grid: {

View File

@ -28,10 +28,10 @@ export default Vue.extend({
computed: {
template(): string {
let t = '';
if (this.title && this.url) t += `【[${title}](${url})】\n`;
if (this.title && !this.url) t += `${title}\n`;
if (this.text) t += `${text}\n`;
if (!this.title && this.url) t += `${url}`;
if (this.title && this.url) t += `【[${this.title}](${this.url})】\n`;
if (this.title && !this.url) t += `${this.title}\n`;
if (this.text) t += `${this.text}\n`;
if (!this.title && this.url) t += `${this.url}`;
return t.trim();
}
},

View File

@ -42,21 +42,29 @@ export default define({
watch: {
stats(stats) {
this.inChart.updateSeries([{
type: 'area',
data: stats.map((x, i) => ({ x: i, y: x.inbox.activeSincePrevTick }))
}, {
type: 'area',
data: stats.map((x, i) => ({ x: i, y: x.inbox.active }))
}, {
type: 'line',
data: stats.map((x, i) => ({ x: i, y: x.inbox.waiting }))
}, {
type: 'line',
data: stats.map((x, i) => ({ x: i, y: x.inbox.delayed }))
}]);
this.outChart.updateSeries([{
type: 'area',
data: stats.map((x, i) => ({ x: i, y: x.deliver.activeSincePrevTick }))
}, {
type: 'area',
data: stats.map((x, i) => ({ x: i, y: x.deliver.active }))
}, {
type: 'line',
data: stats.map((x, i) => ({ x: i, y: x.deliver.waiting }))
}, {
type: 'line',
data: stats.map((x, i) => ({ x: i, y: x.deliver.delayed }))
}]);
}

View File

@ -480,7 +480,7 @@ export default Vue.extend({
});
if (this.text && this.text != '') {
const hashtags = parse(this.text).filter(x => x.type == 'hashtag').map(x => x.hashtag);
const hashtags = parse(this.text).filter(x => x.node.type === 'hashtag').map(x => x.node.props.hashtag);
const history = JSON.parse(localStorage.getItem('hashtags') || '[]') as string[];
localStorage.setItem('hashtags', JSON.stringify(unique(hashtags.concat(history))));
}

View File

@ -16,11 +16,11 @@ import App from './app.vue';
import checkForUpdate from './common/scripts/check-for-update';
import MiOS from './mios';
import { version, codename, lang, locale } from './config';
import { builtinThemes, applyTheme, darkTheme } from './theme';
import { builtinThemes, applyTheme, blackTheme } from './theme';
import Dialog from './common/views/components/dialog.vue';
if (localStorage.getItem('theme') == null) {
applyTheme(darkTheme);
applyTheme(blackTheme);
}
//#region FontAwesome

View File

@ -367,7 +367,7 @@ export default Vue.extend({
});
if (this.text && this.text != '') {
const hashtags = parse(this.text).filter(x => x.type == 'hashtag').map(x => x.hashtag);
const hashtags = parse(this.text).filter(x => x.node.type === 'hashtag').map(x => x.node.props.hashtag);
const history = JSON.parse(localStorage.getItem('hashtags') || '[]') as string[];
localStorage.setItem('hashtags', JSON.stringify(unique(hashtags.concat(history))));
}

View File

@ -49,7 +49,7 @@ const defaultDeviceSettings = {
roundedCorners: true,
reduceMotion: false,
darkmode: true,
darkTheme: 'dark',
darkTheme: 'bb5a8287-a072-4b0a-8ae5-ea2a0d33f4f2',
lightTheme: 'light',
lineWidth: 1,
fontSize: 0,

View File

@ -3,18 +3,37 @@
name: 'Future',
author: 'syuilo',
desc: 'Sci-fi flavored',
base: 'dark',
vars: {
primary: 'rgb(94, 158, 185)',
secondary: 'rgb(22, 24, 30)',
text: 'rgb(214, 218, 224)',
c0: '#0c0c0c',
c1: 'rgb(255, 105, 78)',
c2: 'rgb(99, 197, 210)',
c4: 'rgb(253, 254, 214)',
c3: 'rgb(204, 254, 253)',
primary: '$c1',
secondary: '#131313',
text: '$c3',
},
props: {
renoteGradient: '#0a2d3c',
renoteText: '$primary',
quoteBorder: '$primary',
bg: '$c0',
noteText: '$c4',
noteHeaderAcct: '$c4',
noteHeaderInfo: '$c4',
subNoteText: ':alpha<0.7<$c4',
renoteGradient: 'rgba(0, 0, 0, 0)',
renoteText: '$c2',
quoteBorder: '$c2',
mfmHashtag: '$c1',
mfmUrl: '$c2',
mfmLink: '$c2',
mfmMention: '$c1',
mfmMentionForeground: '#fff',
notificationIndicator: '$c2',
link: '$c2',
desktopHeaderBg: '$secondary',
},
}

View File

@ -4,7 +4,7 @@ import { deliverQueue, inboxQueue } from '../queue';
const ev = new Xev();
const interval = 1000;
const interval = 3000;
/**
* Report queue stats regularly

View File

@ -1,5 +1,6 @@
import { parseFragment, DefaultTreeDocumentFragment } from 'parse5';
import { URL } from 'url';
import { urlRegex } from './prelude';
export function fromHtml(html: string): string {
if (html == null) return null;
@ -14,7 +15,7 @@ export function fromHtml(html: string): string {
return text.trim();
function getText(node: any) {
function getText(node: any): string {
if (node.nodeName == '#text') return node.value;
if (node.childNodes) {
@ -38,10 +39,11 @@ export function fromHtml(html: string): string {
const txt = getText(node);
const rel = node.attrs.find((x: any) => x.name == 'rel');
const href = node.attrs.find((x: any) => x.name == 'href');
const isHashtag = rel && rel.value.match('tag') !== null;
// ハッシュタグ / hrefがない / txtがURL
if ((rel && rel.value.match('tag') !== null) || !href || href.value == txt) {
text += txt;
if (isHashtag || !href || href.value == txt) {
text += isHashtag || txt.match(urlRegex) ? txt : `<${txt}>`;
// メンション
} else if (txt.startsWith('@') && !(rel && rel.value.match(/^me /))) {
const part = txt.split('@');

File diff suppressed because one or more lines are too long

View File

@ -1,6 +1,6 @@
import * as A from '../prelude/array';
import * as S from '../prelude/string';
import { MfmForest, MfmTree } from './types';
import { MfmForest, MfmTree } from './prelude';
import { createTree, createLeaf } from '../prelude/tree';
function isEmptyTextTree(t: MfmTree): boolean {

View File

@ -1,5 +1,5 @@
import { mfmLanguage } from './language';
import { MfmForest } from './types';
import { MfmForest } from './prelude';
import { normalize } from './normalize';
export function parse(source: string): MfmForest {

View File

@ -35,3 +35,5 @@ export function createLeaf(type: string, props: any): MfmTree {
export function createTree(type: string, children: MfmForest, props: any): MfmTree {
return T.createTree({ type, props }, children);
}
export const urlRegex = /^https?:\/\/[\w\/:%#@\$&\?!\(\)\[\]~\.,=\+\-]+/;

View File

@ -2,7 +2,7 @@ import { JSDOM } from 'jsdom';
import config from '../config';
import { INote } from '../models/note';
import { intersperse } from '../prelude/array';
import { MfmForest, MfmTree } from './types';
import { MfmForest, MfmTree } from './prelude';
export function toHtml(tokens: MfmForest, mentionedRemoteUsers: INote['mentionedRemoteUsers'] = []) {
if (tokens == null) {

View File

@ -1,5 +1,6 @@
import config from '../config';
import { toUnicode, toASCII } from 'punycode';
import { URL } from 'url';
export function getFullApAccount(username: string, host: string) {
return host ? `${username}@${toApHost(host)}` : `${username}@${toApHost(config.host)}`;
@ -10,6 +11,11 @@ export function isSelfHost(host: string) {
return toApHost(config.host) === toApHost(host);
}
export function extractDbHost(uri: string) {
const url = new URL(uri);
return toDbHost(url.hostname);
}
export function toDbHost(host: string) {
if (host == null) return null;
return toUnicode(host.toLowerCase());

1
src/misc/emoji-regex.ts Normal file

File diff suppressed because one or more lines are too long

View File

@ -1,4 +1,4 @@
import { EmojiNode, MfmForest } from '../mfm/types';
import { EmojiNode, MfmForest } from '../mfm/prelude';
import { preorderF } from '../prelude/tree';
import { unique } from '../prelude/array';

View File

@ -1,4 +1,4 @@
import { HashtagNode, MfmForest } from '../mfm/types';
import { HashtagNode, MfmForest } from '../mfm/prelude';
import { preorderF } from '../prelude/tree';
import { unique } from '../prelude/array';

View File

@ -1,6 +1,6 @@
// test is located in test/extract-mentions
import { MentionNode, MfmForest } from '../mfm/types';
import { MentionNode, MfmForest } from '../mfm/prelude';
import { preorderF } from '../prelude/tree';
export default function(mfmForest: MfmForest): MentionNode['props'][] {

View File

@ -13,6 +13,7 @@ const defaultMeta: any = {
originalUsersCount: 0
},
maxNoteTextLength: 1000,
enableEmojiReaction: true,
enableTwitterIntegration: false,
enableGithubIntegration: false,
enableDiscordIntegration: false,

View File

@ -10,6 +10,7 @@ export default function(reaction: string): string {
case 'confused': return '😥';
case 'rip': return '😇';
case 'pudding': return '🍮';
default: return '';
case 'star': return '';
default: return reaction;
}
}

61
src/misc/reaction-lib.ts Normal file
View File

@ -0,0 +1,61 @@
import Emoji from '../models/emoji';
import { emojiRegex } from './emoji-regex';
import fetchMeta from './fetch-meta';
const basic10: Record<string, string> = {
'👍': 'like',
'❤': 'love', // ここに記述する場合は異体字セレクタを入れない
'😆': 'laugh',
'🤔': 'hmm',
'😮': 'surprise',
'🎉': 'congrats',
'💢': 'angry',
'😥': 'confused',
'😇': 'rip',
'🍮': 'pudding',
};
export async function getFallbackReaction(): Promise<string> {
const meta = await fetchMeta();
return meta.useStarForReactionFallback ? 'star' : 'like';
}
export async function toDbReaction(reaction: string, enableEmoji = true): Promise<string> {
if (reaction == null) return await getFallbackReaction();
// 既存の文字列リアクションはそのまま
if (Object.values(basic10).includes(reaction)) return reaction;
if (!enableEmoji) return await getFallbackReaction();
// Unicode絵文字
const match = emojiRegex.exec(reaction);
if (match) {
// 合字を含む1つの絵文字
const unicode = match[0];
// 異体字セレクタ除去後の絵文字
const normalized = unicode.match('\u200d') ? unicode : unicode.replace(/\ufe0f/g, '');
// Unicodeプリンは寿司化不能とするため文字列化しない
if (normalized === '🍮') return normalized;
// プリン以外の既存のリアクションは文字列化する
if (basic10[normalized]) return basic10[normalized];
// それ以外はUnicodeのまま
return normalized;
}
const custom = reaction.match(/:([\w+-]+):/);
if (custom) {
const emoji = await Emoji.findOne({
host: null,
name: custom[1],
});
if (emoji) return reaction;
}
return await getFallbackReaction();
}

View File

@ -194,10 +194,13 @@ export type IMeta = {
disableRegistration?: boolean;
disableLocalTimeline?: boolean;
disableGlobalTimeline?: boolean;
enableEmojiReaction?: boolean;
useStarForReactionFallback?: boolean;
hidedTags?: string[];
mascotImageUrl?: string;
bannerUrl?: string;
errorImageUrl?: string;
iconUrl?: string;
cacheRemoteFiles?: boolean;

View File

@ -20,19 +20,6 @@ export interface INoteReaction {
reaction: string;
}
export const validateReaction = $.str.or([
'like',
'love',
'laugh',
'hmm',
'surprise',
'congrats',
'angry',
'confused',
'rip',
'pudding'
]);
/**
* Pack a reaction for API response
*/

View File

@ -12,6 +12,7 @@ import { packMany as packFileMany, IDriveFile } from './drive-file';
import Following from './following';
import Emoji from './emoji';
import { dbLogger } from '../db/logger';
import { unique, concat } from '../prelude/array';
const Note = db.get<INote>('notes');
Note.createIndex('uri', { sparse: true, unique: true });
@ -41,6 +42,7 @@ export type INote = {
replyId: mongo.ObjectID;
renoteId: mongo.ObjectID;
poll: IPoll;
name?: string;
text: string;
tags: string[];
tagsLower: string[];
@ -241,6 +243,11 @@ export const pack = async (
const id = _note._id;
// Some counts
_note.renoteCount = _note.renoteCount || 0;
_note.repliesCount = _note.repliesCount || 0;
_note.reactionCounts = _note.reactionCounts || {};
// _note._userを消す前か、_note.userを解決した後でないとホストがわからない
if (_note._user) {
const host = _note._user.host;
@ -252,6 +259,8 @@ export const pack = async (
fields: { _id: false }
});
} else {
_note.emojis = unique(concat([_note.emojis, Object.keys(_note.reactionCounts)]));
_note.emojis = Emoji.find({
name: { $in: _note.emojis },
host: host
@ -289,11 +298,6 @@ export const pack = async (
// Populate files
_note.files = packFileMany(_note.fileIds || []);
// Some counts
_note.renoteCount = _note.renoteCount || 0;
_note.repliesCount = _note.repliesCount || 0;
_note.reactionCounts = _note.reactionCounts || {};
// 後方互換性のため
_note.mediaIds = _note.fileIds;
_note.media = _note.files;
@ -391,6 +395,10 @@ export const pack = async (
}
//#endregion
if (_note.name) {
_note.text = `${_note.name}\n${_note.text}`;
}
if (_note.user.isCat && _note.text) {
_note.text = (_note.text
// ja-JP

View File

@ -20,6 +20,7 @@ User.createIndex('createdAt');
User.createIndex('updatedAt');
User.createIndex('followersCount');
User.createIndex('tags');
User.createIndex('isSuspended');
User.createIndex('username');
User.createIndex('usernameLower');
User.createIndex('host');

View File

@ -178,10 +178,10 @@ export function destroy() {
deliverQueue.once('cleaned', (jobs, status) => {
deliverLogger.succ(`Cleaned ${jobs.length} ${status} jobs`);
});
deliverQueue.clean(0, 'wait');
deliverQueue.clean(0, 'delayed');
inboxQueue.once('cleaned', (jobs, status) => {
inboxLogger.succ(`Cleaned ${jobs.length} ${status} jobs`);
});
inboxQueue.clean(0, 'wait');
inboxQueue.clean(0, 'delayed');
}

View File

@ -24,10 +24,8 @@ export default async (actor: IRemoteUser, activity: IAnnounce): Promise<void> =>
switch (object.type) {
case 'Note':
announceNote(resolver, actor, activity, object as INote);
break;
case 'Question':
case 'Article':
announceNote(resolver, actor, activity, object as INote);
break;

View File

@ -5,6 +5,8 @@ import { IAnnounce, INote } from '../../type';
import { fetchNote, resolveNote } from '../../models/note';
import { resolvePerson } from '../../models/person';
import { apLogger } from '../../logger';
import { extractDbHost } from '../../../../misc/convert-host';
import Instance from '../../../../models/instance';
const logger = apLogger;
@ -23,6 +25,11 @@ export default async function(resolver: Resolver, actor: IRemoteUser, activity:
throw new Error('invalid announce');
}
// アナウンス先をブロックしてたら中断
// TODO: いちいちデータベースにアクセスするのはコスト高そうなのでどっかにキャッシュしておく
const instance = await Instance.findOne({ host: extractDbHost(uri) });
if (instance && instance.isBlocked) return;
// 既に同じURIを持つものが登録されていないかチェック
const exist = await fetchNote(uri);
if (exist) {

View File

@ -29,10 +29,8 @@ export default async (actor: IRemoteUser, activity: ICreate): Promise<void> => {
break;
case 'Note':
createNote(resolver, actor, object);
break;
case 'Question':
case 'Article':
createNote(resolver, actor, object);
break;

View File

@ -21,10 +21,8 @@ export default async (actor: IRemoteUser, activity: IDelete): Promise<void> => {
switch (object.type) {
case 'Note':
deleteNote(actor, uri);
break;
case 'Question':
case 'Article':
deleteNote(actor, uri);
break;

View File

@ -3,7 +3,6 @@ import Note from '../../../models/note';
import { IRemoteUser } from '../../../models/user';
import { ILike } from '../type';
import create from '../../../services/note/reaction/create';
import { validateReaction } from '../../../models/note-reaction';
export default async (actor: IRemoteUser, activity: ILike) => {
const id = typeof activity.object == 'string' ? activity.object : activity.object.id;
@ -18,12 +17,5 @@ export default async (actor: IRemoteUser, activity: ILike) => {
throw new Error();
}
let reaction = 'like';
// 他のMisskeyインスタンスからのリアクション
if (activity._misskey_reaction && validateReaction.ok(activity._misskey_reaction)) {
reaction = activity._misskey_reaction;
}
await create(actor, note, reaction);
await create(actor, note, activity._misskey_reaction);
};

View File

@ -1,3 +1,3 @@
import { remoteLogger } from "../logger";
import { remoteLogger } from '../logger';
export const apLogger = remoteLogger.createSubLogger('ap', 'magenta');

View File

@ -19,6 +19,8 @@ import vote from '../../../services/note/polls/vote';
import { apLogger } from '../logger';
import { IDriveFile } from '../../../models/drive-file';
import { deliverQuestionUpdate } from '../../../services/note/polls/update';
import Instance from '../../../models/instance';
import { extractDbHost } from '../../../misc/convert-host';
const logger = apLogger;
@ -55,7 +57,7 @@ export async function createNote(value: any, resolver?: Resolver, silent = false
const object: any = await resolver.resolve(value);
if (!object || !['Note', 'Question'].includes(object.type)) {
if (!object || !['Note', 'Question', 'Article'].includes(object.type)) {
logger.error(`invalid note: ${value}`, {
resolver: {
history: resolver.getHistory()
@ -132,7 +134,15 @@ export async function createNote(value: any, resolver?: Resolver, silent = false
let quote: INote;
if (note._misskey_quote && typeof note._misskey_quote == 'string') {
quote = await resolveNote(note._misskey_quote).catch(() => null);
quote = await resolveNote(note._misskey_quote).catch(e => {
// 4xxの場合は引用してないことにする
if (e.statusCode >= 400 && e.statusCode < 500) {
logger.warn(`Ignored quote target ${note.inReplyTo} - ${e.statusCode} `);
return null;
}
logger.warn(`Error in quote target ${note.inReplyTo} - ${e.statusCode || e}`);
throw e;
});
}
const cw = note.summary === '' ? null : note.summary;
@ -189,6 +199,7 @@ export async function createNote(value: any, resolver?: Resolver, silent = false
files,
reply,
renote: quote,
name: note.name,
cw,
text,
viaMobile: false,
@ -214,6 +225,11 @@ export async function createNote(value: any, resolver?: Resolver, silent = false
export async function resolveNote(value: string | IObject, resolver?: Resolver): Promise<INote> {
const uri = typeof value == 'string' ? value : value.id;
// ブロックしてたら中断
// TODO: いちいちデータベースにアクセスするのはコスト高そうなのでどっかにキャッシュしておく
const instance = await Instance.findOne({ host: extractDbHost(uri) });
if (instance && instance.isBlocked) throw { statusCode: 451 };
//#region このサーバーに既に登録されていたらそれを返す
const exist = await fetchNote(uri);

View File

@ -294,6 +294,13 @@ export async function updatePerson(uri: string, resolver?: Resolver, hint?: obje
}
//#endregion
// 繋がらないインスタンスに何回も試行するのを防ぐ, 後続の同様処理の連続試行を防ぐ ため 試行前にも更新する
await User.update({ _id: exist._id }, {
$set: {
lastFetchedAt: new Date(),
},
});
if (resolver == null) resolver = new Resolver();
const object = hint || await resolver.resolve(uri) as any;

View File

@ -1,3 +1,3 @@
import Logger from "../services/logger";
import Logger from '../services/logger';
export const remoteLogger = new Logger('remote', 'cyan');

View File

@ -17,6 +17,7 @@ import Following from './activitypub/following';
import Featured from './activitypub/featured';
import renderQuestion from '../remote/activitypub/renderer/question';
import { inbox as processInbox } from '../queue';
import { isSelfHost } from '../misc/convert-host';
// Init router
const router = new Router();
@ -79,6 +80,16 @@ router.get('/notes/:note', async (ctx, next) => {
return;
}
// リモートだったらリダイレクト
if (note._user.host != null) {
if (note.uri == null || isSelfHost(note._user.host)) {
ctx.status = 500;
return;
}
ctx.redirect(note.uri);
return;
}
ctx.body = renderActivity(await renderNote(note, false));
ctx.set('Cache-Control', 'public, max-age=180');
setResponseType(ctx);
@ -93,6 +104,7 @@ router.get('/notes/:note/activity', async ctx => {
const note = await Note.findOne({
_id: new ObjectID(ctx.params.note),
'_user.host': null,
visibility: { $in: ['public', 'home'] },
localOnly: { $ne: true }
});
@ -116,6 +128,7 @@ router.get('/questions/:question', async (ctx, next) => {
const poll = await Note.findOne({
_id: new ObjectID(ctx.params.question),
'_user.host': null,
visibility: { $in: ['public', 'home'] },
localOnly: { $ne: true },
poll: {

View File

@ -0,0 +1,40 @@
import $ from 'cafy';
import define from '../../../define';
import { deliverQueue, inboxQueue } from '../../../../../queue';
export const meta = {
tags: ['admin'],
requireCredential: true,
requireModerator: true,
params: {
domain: {
validator: $.str,
},
state: {
validator: $.str,
},
limit: {
validator: $.optional.num,
default: 50
},
}
};
export default define(meta, async (ps) => {
const queue =
ps.domain === 'deliver' ? deliverQueue :
ps.domain === 'inbox' ? inboxQueue :
null;
const jobs = await queue.getJobs([ps.state], 0, ps.limit);
return jobs.map(job => ({
id: job.id,
data: job.data,
attempts: job.attemptsMade,
}));
});

View File

@ -1,7 +1,9 @@
import $ from 'cafy';
import ID, { transform } from '../../../../misc/cafy-id';
import define from '../../define';
import User from '../../../../models/user';
import User, { IUser } from '../../../../models/user';
import Following from '../../../../models/following';
import deleteFollowing from '../../../../services/following/delete';
export const meta = {
desc: {
@ -51,5 +53,25 @@ export default define(meta, async (ps) => {
}
});
unFollowAll(user);
return;
});
async function unFollowAll(follower: IUser) {
const followings = await Following.find({
followerId: follower._id
});
for (const following of followings) {
const followee = await User.findOne({
_id: following.followeeId
});
if (followee == null) {
throw `Cant find followee ${following.followeeId}`;
}
await deleteFollowing(follower, followee, true);
}
}

View File

@ -41,6 +41,20 @@ export const meta = {
}
},
enableEmojiReaction: {
validator: $.optional.nullable.bool,
desc: {
'ja-JP': '絵文字リアクションを有効にするか否か'
}
},
useStarForReactionFallback: {
validator: $.optional.nullable.bool,
desc: {
'ja-JP': '不明なリアクションのフォールバックに star リアクションを使うか'
}
},
hidedTags: {
validator: $.optional.nullable.arr($.str),
desc: {
@ -69,6 +83,13 @@ export const meta = {
}
},
iconUrl: {
validator: $.optional.nullable.str,
desc: {
'ja-JP': 'インスタンスのアイコンURL'
}
},
name: {
validator: $.optional.nullable.str,
desc: {
@ -344,6 +365,14 @@ export default define(meta, async (ps) => {
set.disableGlobalTimeline = ps.disableGlobalTimeline;
}
if (typeof ps.enableEmojiReaction === 'boolean') {
set.enableEmojiReaction = ps.enableEmojiReaction;
}
if (typeof ps.useStarForReactionFallback === 'boolean') {
set.useStarForReactionFallback = ps.useStarForReactionFallback;
}
if (Array.isArray(ps.hidedTags)) {
set.hidedTags = ps.hidedTags;
}
@ -356,6 +385,10 @@ export default define(meta, async (ps) => {
set.bannerUrl = ps.bannerUrl;
}
if (ps.iconUrl !== undefined) {
set.iconUrl = ps.iconUrl;
}
if (ps.name !== undefined) {
set.name = ps.name;
}

View File

@ -8,6 +8,8 @@ import Note, { pack as packNote, INote } from '../../../../models/note';
import { createNote } from '../../../../remote/activitypub/models/note';
import Resolver from '../../../../remote/activitypub/resolver';
import { ApiError } from '../../error';
import Instance from '../../../../models/instance';
import { extractDbHost } from '../../../../misc/convert-host';
export const meta = {
tags: ['federation'],
@ -61,6 +63,10 @@ async function fetchAny(uri: string) {
if (packed !== null) return packed;
}
// ブロックしてたら中断
const instance = await Instance.findOne({ host: extractDbHost(uri) });
if (instance && instance.isBlocked) return null;
// URI(AP Object id)としてDB検索
{
const [user, note] = await Promise.all([
@ -97,7 +103,7 @@ async function fetchAny(uri: string) {
};
}
if (['Note', 'Question'].includes(object.type)) {
if (['Note', 'Question', 'Article'].includes(object.type)) {
const note = await createNote(object.id);
return {
type: 'Note',

View File

@ -70,6 +70,10 @@ export const meta = {
type: 'boolean',
description: 'Whether disabled GTL.',
},
enableEmojiReaction: {
type: 'boolean',
description: 'Whether enabled emoji reaction.',
},
}
}
};
@ -107,6 +111,7 @@ export default define(meta, async (ps, me) => {
disableRegistration: instance.disableRegistration,
disableLocalTimeline: instance.disableLocalTimeline,
disableGlobalTimeline: instance.disableGlobalTimeline,
enableEmojiReaction: instance.enableEmojiReaction,
driveCapacityPerLocalUserMb: instance.localDriveCapacityMb,
driveCapacityPerRemoteUserMb: instance.remoteDriveCapacityMb,
cacheRemoteFiles: instance.cacheRemoteFiles,
@ -116,6 +121,7 @@ export default define(meta, async (ps, me) => {
mascotImageUrl: instance.mascotImageUrl,
bannerUrl: instance.bannerUrl,
errorImageUrl: instance.errorImageUrl,
iconUrl: instance.iconUrl,
maxNoteTextLength: instance.maxNoteTextLength,
emojis: emojis,
enableEmail: instance.enableEmail,
@ -146,6 +152,7 @@ export default define(meta, async (ps, me) => {
}
if (me && (me.isAdmin || me.isModerator)) {
response.useStarForReactionFallback = instance.useStarForReactionFallback;
response.hidedTags = instance.hidedTags;
response.recaptchaSecretKey = instance.recaptchaSecretKey;
response.proxyAccount = instance.proxyAccount;

View File

@ -1,7 +1,6 @@
import $ from 'cafy';
import ID, { transform } from '../../../../../misc/cafy-id';
import createReaction from '../../../../../services/note/reaction/create';
import { validateReaction } from '../../../../../models/note-reaction';
import define from '../../../define';
import { getNote } from '../../../common/getters';
import { ApiError } from '../../../error';
@ -30,7 +29,7 @@ export const meta = {
},
reaction: {
validator: $.str.pipe(validateReaction.ok),
validator: $.str,
desc: {
'ja-JP': 'リアクションの種類'
}

View File

@ -20,7 +20,7 @@ export const meta = {
limit: {
duration: ms('1hour'),
max: 5,
max: 60,
minInterval: ms('3sec')
},

View File

@ -70,7 +70,8 @@ export default define(meta, async (ps, me) => {
users = await User
.find({
host: null,
usernameLower: new RegExp('^' + escapeRegexp(ps.query.replace('@', '').toLowerCase()))
usernameLower: new RegExp('^' + escapeRegexp(ps.query.replace('@', '').toLowerCase())),
isSuspended: { $ne: true }
}, {
limit: ps.limit,
skip: ps.offset
@ -80,7 +81,8 @@ export default define(meta, async (ps, me) => {
const otherUsers = await User
.find({
host: { $ne: null },
usernameLower: new RegExp('^' + escapeRegexp(ps.query.replace('@', '').toLowerCase()))
usernameLower: new RegExp('^' + escapeRegexp(ps.query.replace('@', '').toLowerCase())),
isSuspended: { $ne: true }
}, {
limit: ps.limit - users.length
});

View File

@ -1,3 +1,3 @@
import Logger from "../../services/logger";
import Logger from '../../services/logger';
export const apiLogger = new Logger('api');

View File

@ -392,7 +392,8 @@ export const schemas = {
'angry',
'confused',
'rip',
'pudding'
'pudding',
'star'
],
description: 'The reaction type.'
},

View File

@ -250,7 +250,10 @@ router.get('/reversi', async ctx => ctx.redirect(override(ctx.URL.pathname, 'gam
router.get('*', async ctx => {
const meta = await fetchMeta();
await ctx.render('base', {
img: meta.bannerUrl
img: meta.bannerUrl,
title: meta.name,
desc: meta.description,
icon: meta.iconUrl
});
ctx.set('Cache-Control', 'public, max-age=300');
});

View File

@ -8,17 +8,19 @@ html
head
meta(charset='utf-8')
meta(name='application-name' content='Misskey')
meta(name='application-name' content= title || 'Misskey')
meta(name='referrer' content='origin')
meta(property='og:site_name' content='Misskey')
meta(name='theme-color' content='#105779')
meta(property='og:site_name' content= title || 'Misskey')
link(rel='icon' href= icon || '/favicon.ico')
link(rel='manifest' href='/manifest.json')
title
block title
| Misskey
= title || 'Misskey'
block desc
meta(name='description' content='✨🌎✨ A federated blogging platform ✨🚀✨')
meta(name='description' content= desc || '✨🌎✨ A federated blogging platform ✨🚀✨')
block meta

View File

@ -31,3 +31,5 @@ block meta
if !user.host
link(rel='alternate' href=url type='application/activity+json')
if note.uri
link(rel='alternate' href=note.uri type='application/activity+json')

View File

@ -1,3 +1,3 @@
import Logger from "../logger";
import Logger from '../logger';
export const driveLogger = new Logger('drive', 'blue');

View File

@ -13,7 +13,7 @@ import instanceChart from '../../services/chart/instance';
const logger = new Logger('following/delete');
export default async function(follower: IUser, followee: IUser) {
export default async function(follower: IUser, followee: IUser, silent = false) {
const following = await Following.findOne({
followerId: follower._id,
followeeId: followee._id
@ -71,7 +71,7 @@ export default async function(follower: IUser, followee: IUser) {
perUserFollowingChart.update(follower, followee, false);
// Publish unfollow event
if (isLocalUser(follower)) {
if (!silent && isLocalUser(follower)) {
packUser(followee, follower, {
detail: true
}).then(packed => publishMainStream(follower._id, 'unfollow', packed));

View File

@ -91,6 +91,7 @@ class NotificationManager {
type Option = {
createdAt?: Date;
name?: string;
text?: string;
reply?: INote;
renote?: INote;
@ -437,6 +438,7 @@ async function insertNote(user: IUser, data: Option, tags: string[], emojis: str
fileIds: data.files ? data.files.map(file => file._id) : [],
replyId: data.reply ? data.reply._id : null,
renoteId: data.renote ? data.renote._id : null,
name: data.name,
text: data.text,
poll: data.poll,
cw: data.cw == null ? null : data.cw,

View File

@ -10,6 +10,8 @@ import { deliver } from '../../../queue';
import { renderActivity } from '../../../remote/activitypub/renderer';
import perUserReactionsChart from '../../../services/chart/per-user-reactions';
import { IdentifiableError } from '../../../misc/identifiable-error';
import { toDbReaction } from '../../../misc/reaction-lib';
import fetchMeta from '../../../misc/fetch-meta';
export default async (user: IUser, note: INote, reaction: string) => {
// Myself
@ -17,6 +19,9 @@ export default async (user: IUser, note: INote, reaction: string) => {
throw new IdentifiableError('2d8e7297-1873-4c00-8404-792c68d7bef0', 'cannot react to my note');
}
const meta = await fetchMeta();
reaction = await toDbReaction(reaction, meta.enableEmojiReaction);
// Create reaction
await NoteReaction.insert({
createdAt: new Date(),

View File

@ -12,7 +12,7 @@ import * as assert from 'assert';
import { parse, parsePlain } from '../src/mfm/parse';
import { toHtml } from '../src/mfm/toHtml';
import { createTree as tree, createLeaf as leaf, MfmTree } from '../src/mfm/types';
import { createTree as tree, createLeaf as leaf, MfmTree } from '../src/mfm/prelude';
import { removeOrphanedBrackets } from '../src/mfm/language';
function text(text: string): MfmTree {
@ -840,6 +840,20 @@ describe('MFM', () => {
text(')')
]);
});
it('ignore non-ascii characters contained url without angle brackets', () => {
const tokens = parse('https://大石泉すき.example.com');
assert.deepStrictEqual(tokens, [
text('https://大石泉すき.example.com')
]);
});
it('match non-ascii characters contained url with angle brackets', () => {
const tokens = parse('<https://大石泉すき.example.com>');
assert.deepStrictEqual(tokens, [
leaf('url', { url: 'https://大石泉すき.example.com' })
]);
});
});
describe('link', () => {

91
test/reaction-lib.ts Normal file
View File

@ -0,0 +1,91 @@
/*
* Tests of MFM
*
* How to run the tests:
* > mocha test/reaction-lib.ts --require ts-node/register
*
* To specify test:
* > mocha test/reaction-lib.ts --require ts-node/register -g 'test name'
*/
import * as assert from 'assert';
import { toDbReaction } from '../src/misc/reaction-lib';
describe('toDbReaction', async () => {
it('既存の文字列リアクションはそのまま', async () => {
assert.strictEqual(await toDbReaction('like'), 'like');
});
it('Unicodeプリンは寿司化不能とするため文字列化しない', async () => {
assert.strictEqual(await toDbReaction('🍮'), '🍮');
});
it('プリン以外の既存のリアクションは文字列化する like', async () => {
assert.strictEqual(await toDbReaction('👍'), 'like');
});
it('プリン以外の既存のリアクションは文字列化する love', async () => {
assert.strictEqual(await toDbReaction('❤️'), 'love');
});
it('プリン以外の既存のリアクションは文字列化する love 異体字セレクタなし', async () => {
assert.strictEqual(await toDbReaction('❤'), 'love');
});
it('プリン以外の既存のリアクションは文字列化する laugh', async () => {
assert.strictEqual(await toDbReaction('😆'), 'laugh');
});
it('プリン以外の既存のリアクションは文字列化する hmm', async () => {
assert.strictEqual(await toDbReaction('🤔'), 'hmm');
});
it('プリン以外の既存のリアクションは文字列化する surprise', async () => {
assert.strictEqual(await toDbReaction('😮'), 'surprise');
});
it('プリン以外の既存のリアクションは文字列化する congrats', async () => {
assert.strictEqual(await toDbReaction('🎉'), 'congrats');
});
it('プリン以外の既存のリアクションは文字列化する angry', async () => {
assert.strictEqual(await toDbReaction('💢'), 'angry');
});
it('プリン以外の既存のリアクションは文字列化する confused', async () => {
assert.strictEqual(await toDbReaction('😥'), 'confused');
});
it('プリン以外の既存のリアクションは文字列化する rip', async () => {
assert.strictEqual(await toDbReaction('😇'), 'rip');
});
it('それ以外はUnicodeのまま', async () => {
assert.strictEqual(await toDbReaction('🍅'), '🍅');
});
it('異体字セレクタ除去', async () => {
assert.strictEqual(await toDbReaction('㊗️'), '㊗');
});
it('異体字セレクタ除去 必要なし', async () => {
assert.strictEqual(await toDbReaction('㊗'), '㊗');
});
it('fallback - undefined', async () => {
assert.strictEqual(await toDbReaction(undefined), 'like');
});
it('fallback - null', async () => {
assert.strictEqual(await toDbReaction(null), 'like');
});
it('fallback - empty', async () => {
assert.strictEqual(await toDbReaction(''), 'like');
});
it('fallback - unknown', async () => {
assert.strictEqual(await toDbReaction('unknown'), 'like');
});
});