Compare commits
96 Commits
13.0.0-bet
...
13.0.0-bet
Author | SHA1 | Date | |
---|---|---|---|
![]() |
8cc80faf20 | ||
![]() |
4d66077f85 | ||
![]() |
3ece2dc990 | ||
![]() |
6071e962f4 | ||
![]() |
ed43369797 | ||
![]() |
c65957853b | ||
![]() |
6a18360269 | ||
![]() |
c438bd2e27 | ||
![]() |
462acc9eee | ||
![]() |
e4144a17a4 | ||
![]() |
3cfd017538 | ||
![]() |
403849805a | ||
![]() |
402b234d15 | ||
![]() |
eba6b326fa | ||
![]() |
4c9b93a12f | ||
![]() |
dfee79f841 | ||
![]() |
962373cf06 | ||
![]() |
13aa4b64b4 | ||
![]() |
5ce56886a1 | ||
![]() |
2817ca03f5 | ||
![]() |
e633c3b84b | ||
![]() |
8524e9d735 | ||
![]() |
91ced90fb2 | ||
![]() |
2acb3917ba | ||
![]() |
dd78ac089c | ||
![]() |
10e526ba56 | ||
![]() |
7ed905f76b | ||
![]() |
5d13e2744f | ||
![]() |
1d7e0293a8 | ||
![]() |
8977d87021 | ||
![]() |
809400ff23 | ||
![]() |
4c8dbcc20d | ||
![]() |
416dcf884d | ||
![]() |
09d3ce444a | ||
![]() |
27c2ca5048 | ||
![]() |
fceeb1b108 | ||
![]() |
b442c38f41 | ||
![]() |
7c2d2676f7 | ||
![]() |
1f6a41cea7 | ||
![]() |
0d7ee20a77 | ||
![]() |
dcca2350dd | ||
![]() |
1cfdd4c41a | ||
![]() |
25f4ee7030 | ||
![]() |
5320f23017 | ||
![]() |
4ffbbbe6d8 | ||
![]() |
132e45dff4 | ||
![]() |
01652b72b3 | ||
![]() |
8b1fdb5a3b | ||
![]() |
192add376c | ||
![]() |
244ea9593a | ||
![]() |
f20d7cba74 | ||
![]() |
a3e282bc75 | ||
![]() |
49a95c34bf | ||
![]() |
ecbefce2aa | ||
![]() |
91356b1805 | ||
![]() |
2e2ed1385f | ||
![]() |
49f3090edd | ||
![]() |
4594fb11de | ||
![]() |
b93e56d2e5 | ||
![]() |
c550dafb81 | ||
![]() |
8709574f3d | ||
![]() |
1b7043fa79 | ||
![]() |
55ef2393fb | ||
![]() |
7769095efb | ||
![]() |
b8248bdd65 | ||
![]() |
6f4ad581dc | ||
![]() |
aec94920ab | ||
![]() |
155ca39063 | ||
![]() |
58bfb4dca4 | ||
![]() |
49a0b6c48b | ||
![]() |
799a653b44 | ||
![]() |
d09e1f4925 | ||
![]() |
cac784af8a | ||
![]() |
d7e0ddcbca | ||
![]() |
8c0811a442 | ||
![]() |
bab6f75260 | ||
![]() |
54e3fccd87 | ||
![]() |
6a992b6982 | ||
![]() |
ecd6fc1db8 | ||
![]() |
d99be6697e | ||
![]() |
d2d77b5dc1 | ||
![]() |
91503405b4 | ||
![]() |
c336201084 | ||
![]() |
0f3399753d | ||
![]() |
5ec89ea0c3 | ||
![]() |
a42b03c154 | ||
![]() |
4b181a30da | ||
![]() |
70805e00eb | ||
![]() |
3551ac328e | ||
![]() |
e36e5df635 | ||
![]() |
3e7d8b5f17 | ||
![]() |
5846198eee | ||
![]() |
c14063a921 | ||
![]() |
457670e730 | ||
![]() |
513cef50a2 | ||
![]() |
88c64ece78 |
19
CHANGELOG.md
@@ -12,7 +12,7 @@ You should also include the user name that made the change.
|
|||||||
## 13.0.0 (unreleased)
|
## 13.0.0 (unreleased)
|
||||||
|
|
||||||
### TL;DR
|
### TL;DR
|
||||||
- New features (Play, new widgets, new charts, etc)
|
- New features (Play, new widgets, new charts, 🍪👈, etc)
|
||||||
- Rewriten backend
|
- Rewriten backend
|
||||||
- Better performance (backend and frontend)
|
- Better performance (backend and frontend)
|
||||||
- Various usability improvements
|
- Various usability improvements
|
||||||
@@ -30,15 +30,18 @@ You should also include the user name that made the change.
|
|||||||
|
|
||||||
#### For users
|
#### For users
|
||||||
- ノートのウォッチ機能が削除されました
|
- ノートのウォッチ機能が削除されました
|
||||||
|
- アンケートに投票された際に通知が作成されなくなりました
|
||||||
- 新たに動的なPagesを作ることはできなくなりました
|
- 新たに動的なPagesを作ることはできなくなりました
|
||||||
- 代わりにAiScriptを用いてより柔軟に動的なコンテンツを作成できるMisskey Play機能が実装されています。
|
- 代わりにAiScriptを用いてより柔軟に動的なコンテンツを作成できるMisskey Play機能が実装されています。
|
||||||
- AiScriptが0.12.1にアップデートされました
|
- AiScriptが0.12.2にアップデートされました
|
||||||
- 0.12.xの変更点についてはこちら https://github.com/syuilo/aiscript/blob/master/CHANGELOG.md#0120
|
- 0.12.xの変更点についてはこちら https://github.com/syuilo/aiscript/blob/master/CHANGELOG.md#0120
|
||||||
- 0.12.1未満のプラグインは読み込むことはできません
|
- 0.12.x未満のプラグインは読み込むことはできません
|
||||||
- iOS15以下のデバイスはサポートされなくなりました
|
- iOS15以下のデバイスはサポートされなくなりました
|
||||||
- Firefox109以下はサポートされなくなりました
|
- Firefox109以下はサポートされなくなりました
|
||||||
|
|
||||||
#### For app developers
|
#### For app developers
|
||||||
|
- API: metaのレスポンスに`emojis`プロパティが含まれなくなりました
|
||||||
|
- カスタム絵文字一覧情報を取得するには、`emojis`エンドポイントにリクエストします
|
||||||
- API: カスタム絵文字エンティティに`url`プロパティが含まれなくなりました
|
- API: カスタム絵文字エンティティに`url`プロパティが含まれなくなりました
|
||||||
- 絵文字画像を表示するには、`<instance host>/emoji/<emoji name>.webp`にリクエストすると画像が返ります。
|
- 絵文字画像を表示するには、`<instance host>/emoji/<emoji name>.webp`にリクエストすると画像が返ります。
|
||||||
- e.g. `https://p1.a9z.dev/emoji/misskey.webp`
|
- e.g. `https://p1.a9z.dev/emoji/misskey.webp`
|
||||||
@@ -77,13 +80,17 @@ You should also include the user name that made the change.
|
|||||||
- Client: Improve RSS widget @tamaina
|
- Client: Improve RSS widget @tamaina
|
||||||
- Client: show Unicode emoji tooltip with its name in MkReactionsViewer.reaction @saschanaz
|
- Client: show Unicode emoji tooltip with its name in MkReactionsViewer.reaction @saschanaz
|
||||||
- Client: OpenSearch support @SoniEx2 @chaoticryptidz
|
- Client: OpenSearch support @SoniEx2 @chaoticryptidz
|
||||||
|
- Client: Support remote objects in search @SoniEx2
|
||||||
|
- Client: user activity page @syuilo
|
||||||
- Client: add user list widget @syuilo
|
- Client: add user list widget @syuilo
|
||||||
- Client: add heatmap of daily active users to about page @syuilo
|
- Client: add heatmap of daily active users to about page @syuilo
|
||||||
- Client: introduce fluent emoji @syuilo
|
- Client: introduce fluent emoji @syuilo
|
||||||
|
- Client: add new theme @syuilo
|
||||||
- Client: show fireworks when visit user who today is birthday @syuilo
|
- Client: show fireworks when visit user who today is birthday @syuilo
|
||||||
- Client: show bot warning on screen when logged in as bot account @syuilo
|
- Client: show bot warning on screen when logged in as bot account @syuilo
|
||||||
- Client: improve overall performance of client @syuilo
|
- Client: improve overall performance of client @syuilo
|
||||||
- Client: ui tweaks @syuilo
|
- Client: ui tweaks @syuilo
|
||||||
|
- Client: clicker game @syuilo
|
||||||
|
|
||||||
### Bugfixes
|
### Bugfixes
|
||||||
- Server: 引用内の文章がnyaizeされてしまう問題を修正 @kabo2468
|
- Server: 引用内の文章がnyaizeされてしまう問題を修正 @kabo2468
|
||||||
@@ -94,10 +101,16 @@ You should also include the user name that made the change.
|
|||||||
- Server: アンテナの作成数上限を追加 @syuilo
|
- Server: アンテナの作成数上限を追加 @syuilo
|
||||||
- Server: pages/likeのエラーIDが重複しているのを修正 @syuilo
|
- Server: pages/likeのエラーIDが重複しているのを修正 @syuilo
|
||||||
- Server: pages/updateのパラメータによってはsummaryの値が更新されないのを修正 @syuilo
|
- Server: pages/updateのパラメータによってはsummaryの値が更新されないのを修正 @syuilo
|
||||||
|
- Server: Escape SQL LIKE @mei23
|
||||||
|
- Server: 特定のPNG画像のアップロードに失敗する問題を修正 @usbharu
|
||||||
|
- Server: 非公開のクリップのURLでOGPレンダリングされる問題を修正 @syuilo
|
||||||
|
- Server: アンテナタイムライン(ストリーミング)が、フォローしていないユーザーの鍵投稿も拾ってしまう @syuilo
|
||||||
|
- Client: 日付形式の文字列などがカスタム絵文字として表示されるのを修正 @syuilo
|
||||||
- Client: case insensitive emoji search @saschanaz
|
- Client: case insensitive emoji search @saschanaz
|
||||||
- Client: InAppウィンドウが操作できなくなることがあるのを修正 @tamaina
|
- Client: InAppウィンドウが操作できなくなることがあるのを修正 @tamaina
|
||||||
- Client: use proxied image for instance icon @syuilo
|
- Client: use proxied image for instance icon @syuilo
|
||||||
- Client: Webhookの編集画面で、内容を保存することができない問題を修正 @m-hayabusa
|
- Client: Webhookの編集画面で、内容を保存することができない問題を修正 @m-hayabusa
|
||||||
|
- Client: Page編集でブロックの移動が行えない問題を修正 @syuilo
|
||||||
- Client: update emoji picker immediately on all input @saschanaz
|
- Client: update emoji picker immediately on all input @saschanaz
|
||||||
- Client: チャートのツールチップが画面に残ることがあるのを修正 @syuilo
|
- Client: チャートのツールチップが画面に残ることがあるのを修正 @syuilo
|
||||||
- Client: fix wrong link in tutorial @syuilo
|
- Client: fix wrong link in tutorial @syuilo
|
||||||
|
@@ -920,6 +920,10 @@ like: "Gefällt mir"
|
|||||||
unlike: "\"Gefällt mir\" entfernen"
|
unlike: "\"Gefällt mir\" entfernen"
|
||||||
numberOfLikes: "\"Gefällt mir\"-Anzahl"
|
numberOfLikes: "\"Gefällt mir\"-Anzahl"
|
||||||
show: "Anzeigen"
|
show: "Anzeigen"
|
||||||
|
neverShow: "Nicht wieder anzeigen"
|
||||||
|
remindMeLater: "Vielleicht später"
|
||||||
|
didYouLikeMisskey: "Gefällt dir Misskey?"
|
||||||
|
pleaseDonate: "Misskey ist die kostenlose Software, die von {host} verwendet wird. Wir würden uns über Spenden freuen, damit dessen Entwicklung weitergeführt werden kann!"
|
||||||
_sensitiveMediaDetection:
|
_sensitiveMediaDetection:
|
||||||
description: "Ermöglicht eine Erleichterung der Servermoderation durch die automatische Erkennungen von NSFW-Medien unter Verwendung von Machine Learning. Hierdurch wird die Serverlast etwas erhöht."
|
description: "Ermöglicht eine Erleichterung der Servermoderation durch die automatische Erkennungen von NSFW-Medien unter Verwendung von Machine Learning. Hierdurch wird die Serverlast etwas erhöht."
|
||||||
sensitivity: "Erkennungssensitivität"
|
sensitivity: "Erkennungssensitivität"
|
||||||
|
@@ -920,6 +920,10 @@ like: "Like"
|
|||||||
unlike: "Unlike"
|
unlike: "Unlike"
|
||||||
numberOfLikes: "Likes"
|
numberOfLikes: "Likes"
|
||||||
show: "Show"
|
show: "Show"
|
||||||
|
neverShow: "Don't show again"
|
||||||
|
remindMeLater: "Maybe later"
|
||||||
|
didYouLikeMisskey: "Have you taken a liking to Misskey?"
|
||||||
|
pleaseDonate: "{host} uses the free software, Misskey. We would highly appreciate your donations so development of Misskey can continue!"
|
||||||
_sensitiveMediaDetection:
|
_sensitiveMediaDetection:
|
||||||
description: "Reduces the effort of server moderation through automatically recognizing NSFW media via Machine Learning. This will slightly increase the load on the server."
|
description: "Reduces the effort of server moderation through automatically recognizing NSFW media via Machine Learning. This will slightly increase the load on the server."
|
||||||
sensitivity: "Detection sensitivity"
|
sensitivity: "Detection sensitivity"
|
||||||
|
@@ -28,7 +28,7 @@ timeline: "Timeline"
|
|||||||
noAccountDescription: "L'utente non ha ancora scritto niente nella biografia di profilo."
|
noAccountDescription: "L'utente non ha ancora scritto niente nella biografia di profilo."
|
||||||
login: "Accedi"
|
login: "Accedi"
|
||||||
loggingIn: "Accesso in corso..."
|
loggingIn: "Accesso in corso..."
|
||||||
logout: "Esci"
|
logout: "Uscita"
|
||||||
signup: "Iscriviti"
|
signup: "Iscriviti"
|
||||||
uploading: "Caricamento..."
|
uploading: "Caricamento..."
|
||||||
save: "Salva"
|
save: "Salva"
|
||||||
|
@@ -920,6 +920,10 @@ like: "いいね!"
|
|||||||
unlike: "いいねを解除"
|
unlike: "いいねを解除"
|
||||||
numberOfLikes: "いいね数"
|
numberOfLikes: "いいね数"
|
||||||
show: "表示"
|
show: "表示"
|
||||||
|
neverShow: "今後表示しない"
|
||||||
|
remindMeLater: "また後で"
|
||||||
|
didYouLikeMisskey: "Misskeyを気に入っていただけましたか?"
|
||||||
|
pleaseDonate: "Misskeyは{host}が使用している無料のソフトウェアです。これからも開発を続けられるように、ぜひ寄付をお願いします!"
|
||||||
|
|
||||||
_sensitiveMediaDetection:
|
_sensitiveMediaDetection:
|
||||||
description: "機械学習を使って自動でセンシティブなメディアを検出し、モデレーションに役立てることができます。サーバーの負荷が少し増えます。"
|
description: "機械学習を使って自動でセンシティブなメディアを検出し、モデレーションに役立てることができます。サーバーの負荷が少し増えます。"
|
||||||
@@ -1357,6 +1361,7 @@ _widgets:
|
|||||||
userList: "ユーザーリスト"
|
userList: "ユーザーリスト"
|
||||||
_userList:
|
_userList:
|
||||||
chooseList: "リストを選択"
|
chooseList: "リストを選択"
|
||||||
|
clicker: "クリッカー"
|
||||||
|
|
||||||
_cw:
|
_cw:
|
||||||
hide: "隠す"
|
hide: "隠す"
|
||||||
@@ -1546,7 +1551,6 @@ _notification:
|
|||||||
youGotReply: "{name}からのリプライ"
|
youGotReply: "{name}からのリプライ"
|
||||||
youGotQuote: "{name}による引用"
|
youGotQuote: "{name}による引用"
|
||||||
youRenoted: "{name}がRenoteしました"
|
youRenoted: "{name}がRenoteしました"
|
||||||
youGotPoll: "{name}が投票しました"
|
|
||||||
youGotMessagingMessageFromUser: "{name}からのチャットがあります"
|
youGotMessagingMessageFromUser: "{name}からのチャットがあります"
|
||||||
youGotMessagingMessageFromGroup: "{name}のチャットがあります"
|
youGotMessagingMessageFromGroup: "{name}のチャットがあります"
|
||||||
youWereFollowed: "フォローされました"
|
youWereFollowed: "フォローされました"
|
||||||
@@ -1565,7 +1569,6 @@ _notification:
|
|||||||
renote: "Renote"
|
renote: "Renote"
|
||||||
quote: "引用"
|
quote: "引用"
|
||||||
reaction: "リアクション"
|
reaction: "リアクション"
|
||||||
pollVote: "アンケートに投票された"
|
|
||||||
pollEnded: "アンケートが終了"
|
pollEnded: "アンケートが終了"
|
||||||
receiveFollowRequest: "フォロー申請を受け取った"
|
receiveFollowRequest: "フォロー申請を受け取った"
|
||||||
followRequestAccepted: "フォローが受理された"
|
followRequestAccepted: "フォローが受理された"
|
||||||
|
@@ -920,6 +920,10 @@ like: "좋아요!"
|
|||||||
unlike: "좋아요 취소"
|
unlike: "좋아요 취소"
|
||||||
numberOfLikes: "좋아요 수"
|
numberOfLikes: "좋아요 수"
|
||||||
show: "표시"
|
show: "표시"
|
||||||
|
neverShow: "다시 보지 않기"
|
||||||
|
remindMeLater: "나중에 알림"
|
||||||
|
didYouLikeMisskey: "Misskey가 마음에 드시나요?"
|
||||||
|
pleaseDonate: "{host}은(는) 무료 소프트웨어 Misskey를 사용합니다. 후원을 통해 저희의 개발이 이어질 수 있게 도와주세요!"
|
||||||
_sensitiveMediaDetection:
|
_sensitiveMediaDetection:
|
||||||
description: "기계학습을 통해 자동으로 민감한 미디어를 탐지하여, 모더레이션에 참고할 수 있도록 합니다. 서버의 부하를 약간 증가시킵니다."
|
description: "기계학습을 통해 자동으로 민감한 미디어를 탐지하여, 모더레이션에 참고할 수 있도록 합니다. 서버의 부하를 약간 증가시킵니다."
|
||||||
sensitivity: "탐지 민감도"
|
sensitivity: "탐지 민감도"
|
||||||
|
@@ -913,6 +913,10 @@ tools: "Nástroje"
|
|||||||
cannotLoad: "Nedá sa načítať."
|
cannotLoad: "Nedá sa načítať."
|
||||||
like: "Páči sa mi"
|
like: "Páči sa mi"
|
||||||
show: "Zobraziť"
|
show: "Zobraziť"
|
||||||
|
neverShow: "Nabudúce nezobrazovať"
|
||||||
|
remindMeLater: "Pripomenúť neskôr"
|
||||||
|
didYouLikeMisskey: "Páči sa vám Misskey?"
|
||||||
|
pleaseDonate: "Misskey je bezplatný softvér, ktorý používa {host}. Prosím, prispejte, aby sme ho mohli ďalej rozvíjať!"
|
||||||
_sensitiveMediaDetection:
|
_sensitiveMediaDetection:
|
||||||
description: "Strojové učenie sa použije na automatickú detekciu citlivých médií na účely ich moderovania. Mierne sa zvýši zaťaženie servera."
|
description: "Strojové učenie sa použije na automatickú detekciu citlivých médií na účely ich moderovania. Mierne sa zvýši zaťaženie servera."
|
||||||
sensitivity: "Citlivosť detekcie"
|
sensitivity: "Citlivosť detekcie"
|
||||||
|
@@ -917,6 +917,8 @@ tools: "เครื่องมือ"
|
|||||||
cannotLoad: "ไม่สามารถโหลดได้"
|
cannotLoad: "ไม่สามารถโหลดได้"
|
||||||
numberOfProfileView: "มุมมองโปรไฟล์"
|
numberOfProfileView: "มุมมองโปรไฟล์"
|
||||||
like: "ชื่นชอบ"
|
like: "ชื่นชอบ"
|
||||||
|
unlike: "ไม่ชอบ"
|
||||||
|
numberOfLikes: "จำนวนไลค์"
|
||||||
show: "แสดงผล"
|
show: "แสดงผล"
|
||||||
_sensitiveMediaDetection:
|
_sensitiveMediaDetection:
|
||||||
description: "ลดความพยายามในการดูแลเซิร์ฟเวอร์ผ่านการจดจำสื่อ NSFW โดยอัตโนมัติผ่านการเรียนรู้ของเครื่อง การทำสิ่งนี้อาจจะเพิ่มภาระบนเซิร์ฟเวอร์เล็กน้อย"
|
description: "ลดความพยายามในการดูแลเซิร์ฟเวอร์ผ่านการจดจำสื่อ NSFW โดยอัตโนมัติผ่านการเรียนรู้ของเครื่อง การทำสิ่งนี้อาจจะเพิ่มภาระบนเซิร์ฟเวอร์เล็กน้อย"
|
||||||
@@ -1317,6 +1319,7 @@ _widgets:
|
|||||||
jobQueue: "คิวงาน"
|
jobQueue: "คิวงาน"
|
||||||
serverMetric: "ตัวชี้วัดเซิร์ฟเวอร์"
|
serverMetric: "ตัวชี้วัดเซิร์ฟเวอร์"
|
||||||
aiscript: "AiScript คอนโซล"
|
aiscript: "AiScript คอนโซล"
|
||||||
|
aiscriptApp: "AiScript แอพ"
|
||||||
aichan: "เอไอ"
|
aichan: "เอไอ"
|
||||||
userList: "รายชื่อผู้ใช้"
|
userList: "รายชื่อผู้ใช้"
|
||||||
_userList:
|
_userList:
|
||||||
@@ -1423,7 +1426,16 @@ _timelines:
|
|||||||
social: "โซเชี่ยล"
|
social: "โซเชี่ยล"
|
||||||
global: "ทั่วโลก"
|
global: "ทั่วโลก"
|
||||||
_play:
|
_play:
|
||||||
|
new: "สร้างการเล่น"
|
||||||
|
edit: "แก้ไขเล่น"
|
||||||
|
created: "สร้างการเล่นแล้ว"
|
||||||
|
updated: "แก้ไขการเล่นแล้ว"
|
||||||
|
deleted: "ลบการเล่นแล้ว"
|
||||||
|
pageSetting: "ตั้งค่าการเล่น"
|
||||||
|
editThisPage: "แก้ไข Play นี้"
|
||||||
viewSource: "ดูต้นฉบับ"
|
viewSource: "ดูต้นฉบับ"
|
||||||
|
my: "มาย เพลย์"
|
||||||
|
liked: "ไลค์ เพลย์"
|
||||||
featured: "เป็นที่นิยม"
|
featured: "เป็นที่นิยม"
|
||||||
title: "หัวข้อ"
|
title: "หัวข้อ"
|
||||||
script: "สคริปต์"
|
script: "สคริปต์"
|
||||||
|
@@ -918,7 +918,11 @@ cannotLoad: "無法載入"
|
|||||||
numberOfProfileView: "個人檔案檢視次數"
|
numberOfProfileView: "個人檔案檢視次數"
|
||||||
like: "讚"
|
like: "讚"
|
||||||
unlike: "收回讚"
|
unlike: "收回讚"
|
||||||
|
numberOfLikes: "讚數"
|
||||||
show: "檢視"
|
show: "檢視"
|
||||||
|
neverShow: "不再顯示"
|
||||||
|
remindMeLater: "以後再說"
|
||||||
|
didYouLikeMisskey: "您是否喜愛Misskey呢?"
|
||||||
_sensitiveMediaDetection:
|
_sensitiveMediaDetection:
|
||||||
description: "您可以使用機器學習自動檢測敏感媒體並將其用於審核。 伺服器的負荷會稍微增加。"
|
description: "您可以使用機器學習自動檢測敏感媒體並將其用於審核。 伺服器的負荷會稍微增加。"
|
||||||
sensitivity: "檢測敏感度"
|
sensitivity: "檢測敏感度"
|
||||||
@@ -1318,6 +1322,7 @@ _widgets:
|
|||||||
jobQueue: "佇列"
|
jobQueue: "佇列"
|
||||||
serverMetric: "服務器指標 "
|
serverMetric: "服務器指標 "
|
||||||
aiscript: "AiScript控制台"
|
aiscript: "AiScript控制台"
|
||||||
|
aiscriptApp: "AiScript App"
|
||||||
aichan: "小藍"
|
aichan: "小藍"
|
||||||
userList: "使用者列表"
|
userList: "使用者列表"
|
||||||
_userList:
|
_userList:
|
||||||
@@ -1424,7 +1429,16 @@ _timelines:
|
|||||||
social: "社群"
|
social: "社群"
|
||||||
global: "公開"
|
global: "公開"
|
||||||
_play:
|
_play:
|
||||||
|
new: "新增Play"
|
||||||
|
edit: "編輯Play"
|
||||||
|
created: "已新增Play"
|
||||||
|
updated: "已更新Play"
|
||||||
|
deleted: "已刪除Play"
|
||||||
|
pageSetting: "Play設定"
|
||||||
|
editThisPage: "編輯這個Play"
|
||||||
viewSource: "檢視原始碼"
|
viewSource: "檢視原始碼"
|
||||||
|
my: "自己的Play"
|
||||||
|
liked: "按了讚的Play"
|
||||||
featured: "人氣"
|
featured: "人氣"
|
||||||
title: "標題"
|
title: "標題"
|
||||||
script: "腳本"
|
script: "腳本"
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "misskey",
|
"name": "misskey",
|
||||||
"version": "13.0.0-beta.25",
|
"version": "13.0.0-beta.35",
|
||||||
"codename": "indigo",
|
"codename": "indigo",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
@@ -53,10 +53,10 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/gulp": "4.0.10",
|
"@types/gulp": "4.0.10",
|
||||||
"@types/gulp-rename": "2.0.1",
|
"@types/gulp-rename": "2.0.1",
|
||||||
"@typescript-eslint/eslint-plugin": "5.47.1",
|
"@typescript-eslint/eslint-plugin": "5.48.0",
|
||||||
"@typescript-eslint/parser": "5.47.1",
|
"@typescript-eslint/parser": "5.48.0",
|
||||||
"cross-env": "7.0.3",
|
"cross-env": "7.0.3",
|
||||||
"cypress": "12.2.0",
|
"cypress": "12.3.0",
|
||||||
"eslint": "^8.31.0",
|
"eslint": "^8.31.0",
|
||||||
"start-server-and-test": "1.15.2",
|
"start-server-and-test": "1.15.2",
|
||||||
"typescript": "4.9.4"
|
"typescript": "4.9.4"
|
||||||
|
BIN
packages/backend/assets/emoji-unknown.png
Normal file
After Width: | Height: | Size: 3.4 KiB |
@@ -1,5 +0,0 @@
|
|||||||
Font Awesome Icons
|
|
||||||
-------------------------
|
|
||||||
|
|
||||||
Ⓒ Font Awesome
|
|
||||||
CC BY 4.0 (https://creativecommons.org/licenses/by/4.0/)
|
|
Before Width: | Height: | Size: 1.7 KiB |
Before Width: | Height: | Size: 577 B |
Before Width: | Height: | Size: 1.4 KiB |
Before Width: | Height: | Size: 1.1 KiB |
Before Width: | Height: | Size: 1.1 KiB |
Before Width: | Height: | Size: 844 B |
Before Width: | Height: | Size: 507 B |
Before Width: | Height: | Size: 689 B |
Before Width: | Height: | Size: 772 B |
Before Width: | Height: | Size: 930 B |
Before Width: | Height: | Size: 798 B |
Before Width: | Height: | Size: 1.7 KiB |
Before Width: | Height: | Size: 991 B |
24
packages/backend/assets/tabler-badges/LICENSE
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
Tabler Icons
|
||||||
|
https://github.com/tabler/tabler-icons/blob/master/LICENSE
|
||||||
|
====
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2020-2022 Paweł Kuna
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
BIN
packages/backend/assets/tabler-badges/antenna.png
Normal file
After Width: | Height: | Size: 516 B |
BIN
packages/backend/assets/tabler-badges/arrow-back-up.png
Normal file
After Width: | Height: | Size: 952 B |
BIN
packages/backend/assets/tabler-badges/at.png
Normal file
After Width: | Height: | Size: 2.8 KiB |
BIN
packages/backend/assets/tabler-badges/chart-arrows.png
Normal file
After Width: | Height: | Size: 829 B |
BIN
packages/backend/assets/tabler-badges/circle-check.png
Normal file
After Width: | Height: | Size: 2.3 KiB |
BIN
packages/backend/assets/tabler-badges/messages.png
Normal file
After Width: | Height: | Size: 1.0 KiB |
Before Width: | Height: | Size: 174 B After Width: | Height: | Size: 174 B |
BIN
packages/backend/assets/tabler-badges/plus.png
Normal file
After Width: | Height: | Size: 414 B |
BIN
packages/backend/assets/tabler-badges/quote.png
Normal file
After Width: | Height: | Size: 1011 B |
BIN
packages/backend/assets/tabler-badges/repeat.png
Normal file
After Width: | Height: | Size: 1.2 KiB |
BIN
packages/backend/assets/tabler-badges/user-plus.png
Normal file
After Width: | Height: | Size: 1.4 KiB |
BIN
packages/backend/assets/tabler-badges/users.png
Normal file
After Width: | Height: | Size: 1.9 KiB |
@@ -21,9 +21,9 @@
|
|||||||
"@tensorflow/tfjs-node": "4.1.0"
|
"@tensorflow/tfjs-node": "4.1.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@bull-board/api": "^4.10.0",
|
"@bull-board/api": "^4.10.1",
|
||||||
"@bull-board/fastify": "^4.10.0",
|
"@bull-board/fastify": "^4.10.1",
|
||||||
"@bull-board/ui": "^4.10.0",
|
"@bull-board/ui": "^4.10.1",
|
||||||
"@discordapp/twemoji": "14.0.2",
|
"@discordapp/twemoji": "14.0.2",
|
||||||
"@fastify/accepts": "4.1.0",
|
"@fastify/accepts": "4.1.0",
|
||||||
"@fastify/cookie": "^8.3.0",
|
"@fastify/cookie": "^8.3.0",
|
||||||
@@ -38,10 +38,10 @@
|
|||||||
"@peertube/http-signature": "1.7.0",
|
"@peertube/http-signature": "1.7.0",
|
||||||
"@sinonjs/fake-timers": "10.0.2",
|
"@sinonjs/fake-timers": "10.0.2",
|
||||||
"accepts": "^1.3.8",
|
"accepts": "^1.3.8",
|
||||||
"ajv": "8.11.2",
|
"ajv": "8.12.0",
|
||||||
"archiver": "5.3.1",
|
"archiver": "5.3.1",
|
||||||
"autwh": "0.1.0",
|
"autwh": "0.1.0",
|
||||||
"aws-sdk": "2.1286.0",
|
"aws-sdk": "2.1289.0",
|
||||||
"bcryptjs": "2.4.3",
|
"bcryptjs": "2.4.3",
|
||||||
"blurhash": "2.0.4",
|
"blurhash": "2.0.4",
|
||||||
"bull": "4.10.2",
|
"bull": "4.10.2",
|
||||||
@@ -72,7 +72,7 @@
|
|||||||
"json5-loader": "4.0.1",
|
"json5-loader": "4.0.1",
|
||||||
"jsonld": "8.1.0",
|
"jsonld": "8.1.0",
|
||||||
"jsrsasign": "10.6.1",
|
"jsrsasign": "10.6.1",
|
||||||
"mfm-js": "0.23.0",
|
"mfm-js": "0.23.1",
|
||||||
"mime-types": "2.1.35",
|
"mime-types": "2.1.35",
|
||||||
"misskey-js": "0.0.14",
|
"misskey-js": "0.0.14",
|
||||||
"ms": "3.0.0-canary.1",
|
"ms": "3.0.0-canary.1",
|
||||||
@@ -110,8 +110,8 @@
|
|||||||
"stringz": "2.1.0",
|
"stringz": "2.1.0",
|
||||||
"summaly": "2.7.0",
|
"summaly": "2.7.0",
|
||||||
"syslog-pro": "git+https://github.com/misskey-dev/SyslogPro#0.2.9-misskey.2",
|
"syslog-pro": "git+https://github.com/misskey-dev/SyslogPro#0.2.9-misskey.2",
|
||||||
"systeminformation": "5.16.9",
|
"systeminformation": "5.17.1",
|
||||||
"tinycolor2": "1.5.1",
|
"tinycolor2": "1.5.2",
|
||||||
"tmp": "0.2.1",
|
"tmp": "0.2.1",
|
||||||
"tsc-alias": "1.8.2",
|
"tsc-alias": "1.8.2",
|
||||||
"tsconfig-paths": "4.1.2",
|
"tsconfig-paths": "4.1.2",
|
||||||
@@ -128,7 +128,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@redocly/openapi-core": "1.0.0-beta.117",
|
"@redocly/openapi-core": "1.0.0-beta.117",
|
||||||
"@swc/core": "1.3.24",
|
"@swc/core": "1.3.25",
|
||||||
"@swc/jest": "0.2.24",
|
"@swc/jest": "0.2.24",
|
||||||
"@types/accepts": "1.3.5",
|
"@types/accepts": "1.3.5",
|
||||||
"@types/archiver": "5.3.1",
|
"@types/archiver": "5.3.1",
|
||||||
@@ -172,8 +172,8 @@
|
|||||||
"@types/web-push": "3.3.2",
|
"@types/web-push": "3.3.2",
|
||||||
"@types/websocket": "1.0.5",
|
"@types/websocket": "1.0.5",
|
||||||
"@types/ws": "8.5.4",
|
"@types/ws": "8.5.4",
|
||||||
"@typescript-eslint/eslint-plugin": "5.47.1",
|
"@typescript-eslint/eslint-plugin": "5.48.0",
|
||||||
"@typescript-eslint/parser": "5.47.1",
|
"@typescript-eslint/parser": "5.48.0",
|
||||||
"cross-env": "7.0.3",
|
"cross-env": "7.0.3",
|
||||||
"eslint": "8.31.0",
|
"eslint": "8.31.0",
|
||||||
"eslint-plugin-import": "2.26.0",
|
"eslint-plugin-import": "2.26.0",
|
||||||
|
@@ -15,8 +15,8 @@ import type { Packed } from '@/misc/schema.js';
|
|||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import type { MutingsRepository, BlockingsRepository, NotesRepository, AntennaNotesRepository, AntennasRepository, UserGroupJoiningsRepository, UserListJoiningsRepository } from '@/models/index.js';
|
import type { MutingsRepository, BlockingsRepository, NotesRepository, AntennaNotesRepository, AntennasRepository, UserGroupJoiningsRepository, UserListJoiningsRepository } from '@/models/index.js';
|
||||||
import { UtilityService } from '@/core/UtilityService.js';
|
import { UtilityService } from '@/core/UtilityService.js';
|
||||||
import type { OnApplicationShutdown } from '@nestjs/common';
|
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
|
import type { OnApplicationShutdown } from '@nestjs/common';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class AntennaService implements OnApplicationShutdown {
|
export class AntennaService implements OnApplicationShutdown {
|
||||||
@@ -135,7 +135,7 @@ export class AntennaService implements OnApplicationShutdown {
|
|||||||
this.globalEventServie.publishMainStream(antenna.userId, 'unreadAntenna', antenna);
|
this.globalEventServie.publishMainStream(antenna.userId, 'unreadAntenna', antenna);
|
||||||
this.pushNotificationService.pushNotification(antenna.userId, 'unreadAntennaNote', {
|
this.pushNotificationService.pushNotification(antenna.userId, 'unreadAntennaNote', {
|
||||||
antenna: { id: antenna.id, name: antenna.name },
|
antenna: { id: antenna.id, name: antenna.name },
|
||||||
note: await this.noteEntityService.pack(note)
|
note: await this.noteEntityService.pack(note),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, 2000);
|
}, 2000);
|
||||||
@@ -144,27 +144,19 @@ export class AntennaService implements OnApplicationShutdown {
|
|||||||
|
|
||||||
// NOTE: フォローしているユーザーのノート、リストのユーザーのノート、グループのユーザーのノート指定はパフォーマンス上の理由で無効になっている
|
// NOTE: フォローしているユーザーのノート、リストのユーザーのノート、グループのユーザーのノート指定はパフォーマンス上の理由で無効になっている
|
||||||
|
|
||||||
/**
|
|
||||||
* noteUserFollowers / antennaUserFollowing はどちらか一方が指定されていればよい
|
|
||||||
*/
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async checkHitAntenna(antenna: Antenna, note: (Note | Packed<'Note'>), noteUser: { id: User['id']; username: string; host: string | null; }, noteUserFollowers?: User['id'][], antennaUserFollowing?: User['id'][]): Promise<boolean> {
|
public async checkHitAntenna(antenna: Antenna, note: (Note | Packed<'Note'>), noteUser: { id: User['id']; username: string; host: string | null; }): Promise<boolean> {
|
||||||
if (note.visibility === 'specified') return false;
|
if (note.visibility === 'specified') return false;
|
||||||
|
if (note.visibility === 'followers') return false;
|
||||||
|
|
||||||
// アンテナ作成者がノート作成者にブロックされていたらスキップ
|
// アンテナ作成者がノート作成者にブロックされていたらスキップ
|
||||||
const blockings = await this.blockingCache.fetch(noteUser.id, () => this.blockingsRepository.findBy({ blockerId: noteUser.id }).then(res => res.map(x => x.blockeeId)));
|
const blockings = await this.blockingCache.fetch(noteUser.id, () => this.blockingsRepository.findBy({ blockerId: noteUser.id }).then(res => res.map(x => x.blockeeId)));
|
||||||
if (blockings.some(blocking => blocking === antenna.userId)) return false;
|
if (blockings.some(blocking => blocking === antenna.userId)) return false;
|
||||||
|
|
||||||
if (note.visibility === 'followers') {
|
|
||||||
if (noteUserFollowers && !noteUserFollowers.includes(antenna.userId)) return false;
|
|
||||||
if (antennaUserFollowing && !antennaUserFollowing.includes(note.userId)) return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!antenna.withReplies && note.replyId != null) return false;
|
if (!antenna.withReplies && note.replyId != null) return false;
|
||||||
|
|
||||||
if (antenna.src === 'home') {
|
if (antenna.src === 'home') {
|
||||||
if (noteUserFollowers && !noteUserFollowers.includes(antenna.userId)) return false;
|
// TODO
|
||||||
if (antennaUserFollowing && !antennaUserFollowing.includes(note.userId)) return false;
|
|
||||||
} else if (antenna.src === 'list') {
|
} else if (antenna.src === 'list') {
|
||||||
const listUsers = (await this.userListJoiningsRepository.findBy({
|
const listUsers = (await this.userListJoiningsRepository.findBy({
|
||||||
userListId: antenna.userListId!,
|
userListId: antenna.userListId!,
|
||||||
|
@@ -398,13 +398,13 @@ export class FileInfoService {
|
|||||||
.raw()
|
.raw()
|
||||||
.ensureAlpha()
|
.ensureAlpha()
|
||||||
.resize(64, 64, { fit: 'inside' })
|
.resize(64, 64, { fit: 'inside' })
|
||||||
.toBuffer((err, buffer, { width, height }) => {
|
.toBuffer((err, buffer, info) => {
|
||||||
if (err) return reject(err);
|
if (err) return reject(err);
|
||||||
|
|
||||||
let hash;
|
let hash;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
hash = encode(new Uint8ClampedArray(buffer), width, height, 5, 5);
|
hash = encode(new Uint8ClampedArray(buffer), info.width, info.height, 5, 5);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return reject(e);
|
return reject(e);
|
||||||
}
|
}
|
||||||
|
@@ -92,13 +92,6 @@ export class PollService {
|
|||||||
choice: choice,
|
choice: choice,
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Notify
|
|
||||||
this.createNotificationService.createNotification(note.userId, 'pollVote', {
|
|
||||||
notifierId: user.id,
|
|
||||||
noteId: note.id,
|
|
||||||
choice: choice,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
|
@@ -22,23 +22,25 @@ export class EmojiEntityService {
|
|||||||
@bindThis
|
@bindThis
|
||||||
public async pack(
|
public async pack(
|
||||||
src: Emoji['id'] | Emoji,
|
src: Emoji['id'] | Emoji,
|
||||||
|
opts: { omitHost?: boolean; omitId?: boolean; } = {},
|
||||||
): Promise<Packed<'Emoji'>> {
|
): Promise<Packed<'Emoji'>> {
|
||||||
const emoji = typeof src === 'object' ? src : await this.emojisRepository.findOneByOrFail({ id: src });
|
const emoji = typeof src === 'object' ? src : await this.emojisRepository.findOneByOrFail({ id: src });
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: emoji.id,
|
id: opts.omitId ? undefined : emoji.id,
|
||||||
aliases: emoji.aliases,
|
aliases: emoji.aliases,
|
||||||
name: emoji.name,
|
name: emoji.name,
|
||||||
category: emoji.category,
|
category: emoji.category,
|
||||||
host: emoji.host,
|
host: opts.omitHost ? undefined : emoji.host,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public packMany(
|
public packMany(
|
||||||
emojis: any[],
|
emojis: any[],
|
||||||
|
opts: { omitHost?: boolean; omitId?: boolean; } = {},
|
||||||
) {
|
) {
|
||||||
return Promise.all(emojis.map(x => this.pack(x)));
|
return Promise.all(emojis.map(x => this.pack(x, opts)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -98,7 +98,7 @@ export class NotificationEntityService implements OnModuleInit {
|
|||||||
}),
|
}),
|
||||||
reaction: notification.reaction,
|
reaction: notification.reaction,
|
||||||
} : {}),
|
} : {}),
|
||||||
...(notification.type === 'pollVote' ? {
|
...(notification.type === 'pollVote' ? { // TODO: そのうち消す
|
||||||
note: this.noteEntityService.pack(notification.note ?? notification.noteId!, { id: notification.notifieeId }, {
|
note: this.noteEntityService.pack(notification.note ?? notification.noteId!, { id: notification.notifieeId }, {
|
||||||
detail: true,
|
detail: true,
|
||||||
_hint_: options._hintForEachNotes_,
|
_hint_: options._hintForEachNotes_,
|
||||||
|
@@ -3,6 +3,7 @@ import { FILE_TYPE_BROWSERSAFE } from '@/const.js';
|
|||||||
const dictionary = {
|
const dictionary = {
|
||||||
'safe-file': FILE_TYPE_BROWSERSAFE,
|
'safe-file': FILE_TYPE_BROWSERSAFE,
|
||||||
'sharp-convertible-image': ['image/jpeg', 'image/png', 'image/gif', 'image/apng', 'image/vnd.mozilla.apng', 'image/webp', 'image/avif', 'image/svg+xml'],
|
'sharp-convertible-image': ['image/jpeg', 'image/png', 'image/gif', 'image/apng', 'image/vnd.mozilla.apng', 'image/webp', 'image/avif', 'image/svg+xml'],
|
||||||
|
'sharp-animation-convertible-image': ['image/jpeg', 'image/png', 'image/gif', 'image/webp', 'image/avif', 'image/svg+xml'],
|
||||||
};
|
};
|
||||||
|
|
||||||
export const isMimeImage = (mime: string, type: keyof typeof dictionary): boolean => dictionary[type].includes(mime);
|
export const isMimeImage = (mime: string, type: keyof typeof dictionary): boolean => dictionary[type].includes(mime);
|
||||||
|
3
packages/backend/src/misc/sql-like-escape.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export function sqlLikeEscape(s: string) {
|
||||||
|
return s.replace(/([%_])/g, '\\$1');
|
||||||
|
}
|
@@ -55,11 +55,11 @@ export class Notification {
|
|||||||
* 通知の種類。
|
* 通知の種類。
|
||||||
* follow - フォローされた
|
* follow - フォローされた
|
||||||
* mention - 投稿で自分が言及された
|
* mention - 投稿で自分が言及された
|
||||||
* reply - (自分または自分がWatchしている)投稿が返信された
|
* reply - 投稿に返信された
|
||||||
* renote - (自分または自分がWatchしている)投稿がRenoteされた
|
* renote - 投稿がRenoteされた
|
||||||
* quote - (自分または自分がWatchしている)投稿が引用Renoteされた
|
* quote - 投稿が引用Renoteされた
|
||||||
* reaction - (自分または自分がWatchしている)投稿にリアクションされた
|
* reaction - 投稿にリアクションされた
|
||||||
* pollVote - (自分または自分がWatchしている)投稿のアンケートに投票された
|
* pollVote - 投稿のアンケートに投票された (廃止)
|
||||||
* pollEnded - 自分のアンケートもしくは自分が投票したアンケートが終了した
|
* pollEnded - 自分のアンケートもしくは自分が投票したアンケートが終了した
|
||||||
* receiveFollowRequest - フォローリクエストされた
|
* receiveFollowRequest - フォローリクエストされた
|
||||||
* followRequestAccepted - 自分の送ったフォローリクエストが承認された
|
* followRequestAccepted - 自分の送ったフォローリクエストが承認された
|
||||||
|
@@ -3,7 +3,7 @@ export const packedEmojiSchema = {
|
|||||||
properties: {
|
properties: {
|
||||||
id: {
|
id: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
optional: false, nullable: false,
|
optional: true, nullable: false,
|
||||||
format: 'id',
|
format: 'id',
|
||||||
example: 'xxxxxxxxxx',
|
example: 'xxxxxxxxxx',
|
||||||
},
|
},
|
||||||
@@ -26,12 +26,8 @@ export const packedEmojiSchema = {
|
|||||||
},
|
},
|
||||||
host: {
|
host: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
optional: false, nullable: true,
|
optional: true, nullable: true,
|
||||||
description: 'The local host is represented with `null`.',
|
description: 'The local host is represented with `null`.',
|
||||||
},
|
},
|
||||||
url: {
|
|
||||||
type: 'string',
|
|
||||||
optional: true, nullable: false,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
@@ -79,10 +79,18 @@ export class MediaProxyServerService {
|
|||||||
|
|
||||||
const { mime, ext } = await this.fileInfoService.detectType(path);
|
const { mime, ext } = await this.fileInfoService.detectType(path);
|
||||||
const isConvertibleImage = isMimeImage(mime, 'sharp-convertible-image');
|
const isConvertibleImage = isMimeImage(mime, 'sharp-convertible-image');
|
||||||
|
const isAnimationConvertibleImage = isMimeImage(mime, 'sharp-animation-convertible-image');
|
||||||
|
|
||||||
let image: IImage;
|
let image: IImage;
|
||||||
if ('emoji' in request.query && isConvertibleImage) {
|
if ('emoji' in request.query && isConvertibleImage) {
|
||||||
const data = await sharp(path, { animated: !('static' in request.query) })
|
if (!isAnimationConvertibleImage && !('static' in request.query)) {
|
||||||
|
image = {
|
||||||
|
data: fs.readFileSync(path),
|
||||||
|
ext,
|
||||||
|
type: mime,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
const data = await sharp(path, { animated: !('static' in request.query) })
|
||||||
.resize({
|
.resize({
|
||||||
height: 128,
|
height: 128,
|
||||||
withoutEnlargement: true,
|
withoutEnlargement: true,
|
||||||
@@ -90,11 +98,12 @@ export class MediaProxyServerService {
|
|||||||
.webp(webpDefault)
|
.webp(webpDefault)
|
||||||
.toBuffer();
|
.toBuffer();
|
||||||
|
|
||||||
image = {
|
image = {
|
||||||
data,
|
data,
|
||||||
ext: 'webp',
|
ext: 'webp',
|
||||||
type: 'image/webp',
|
type: 'image/webp',
|
||||||
};
|
};
|
||||||
|
}
|
||||||
} else if ('static' in request.query && isConvertibleImage) {
|
} else if ('static' in request.query && isConvertibleImage) {
|
||||||
image = await this.imageProcessingService.convertToWebp(path, 498, 280);
|
image = await this.imageProcessingService.convertToWebp(path, 498, 280);
|
||||||
} else if ('preview' in request.query && isConvertibleImage) {
|
} else if ('preview' in request.query && isConvertibleImage) {
|
||||||
|
@@ -1,12 +1,11 @@
|
|||||||
import cluster from 'node:cluster';
|
import cluster from 'node:cluster';
|
||||||
import * as fs from 'node:fs';
|
import * as fs from 'node:fs';
|
||||||
import * as http from 'node:http';
|
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import Fastify from 'fastify';
|
import Fastify from 'fastify';
|
||||||
import { IsNull } from 'typeorm';
|
import { IsNull } from 'typeorm';
|
||||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||||
import type { Config } from '@/config.js';
|
import type { Config } from '@/config.js';
|
||||||
import type { UserProfilesRepository, UsersRepository } from '@/models/index.js';
|
import type { EmojisRepository, UserProfilesRepository, UsersRepository } from '@/models/index.js';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import type Logger from '@/logger.js';
|
import type Logger from '@/logger.js';
|
||||||
import { envOption } from '@/env.js';
|
import { envOption } from '@/env.js';
|
||||||
@@ -39,6 +38,9 @@ export class ServerService {
|
|||||||
@Inject(DI.userProfilesRepository)
|
@Inject(DI.userProfilesRepository)
|
||||||
private userProfilesRepository: UserProfilesRepository,
|
private userProfilesRepository: UserProfilesRepository,
|
||||||
|
|
||||||
|
@Inject(DI.emojisRepository)
|
||||||
|
private emojisRepository: EmojisRepository,
|
||||||
|
|
||||||
private userEntityService: UserEntityService,
|
private userEntityService: UserEntityService,
|
||||||
private apiServerService: ApiServerService,
|
private apiServerService: ApiServerService,
|
||||||
private streamingApiServerService: StreamingApiServerService,
|
private streamingApiServerService: StreamingApiServerService,
|
||||||
@@ -77,6 +79,43 @@ export class ServerService {
|
|||||||
fastify.register(this.nodeinfoServerService.createServer);
|
fastify.register(this.nodeinfoServerService.createServer);
|
||||||
fastify.register(this.wellKnownServerService.createServer);
|
fastify.register(this.wellKnownServerService.createServer);
|
||||||
|
|
||||||
|
fastify.get<{ Params: { path: string }; Querystring: { static?: any; }; }>('/emoji/:path(.*)', async (request, reply) => {
|
||||||
|
const path = request.params.path;
|
||||||
|
|
||||||
|
if (!path.match(/^[a-zA-Z0-9\-_@\.]+?\.webp$/)) {
|
||||||
|
reply.code(404);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
reply.header('Cache-Control', 'public, max-age=86400');
|
||||||
|
|
||||||
|
const name = path.split('@')[0].replace('.webp', '');
|
||||||
|
const host = path.split('@')[1]?.replace('.webp', '');
|
||||||
|
|
||||||
|
const emoji = await this.emojisRepository.findOneBy({
|
||||||
|
// `@.` is the spec of ReactionService.decodeReaction
|
||||||
|
host: (host == null || host === '.') ? IsNull() : host,
|
||||||
|
name: name,
|
||||||
|
});
|
||||||
|
|
||||||
|
reply.header('Content-Security-Policy', 'default-src \'none\'; style-src \'unsafe-inline\'');
|
||||||
|
|
||||||
|
if (emoji == null) {
|
||||||
|
return await reply.redirect('/static-assets/emoji-unknown.png');
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = new URL('/proxy/emoji.webp', this.config.url);
|
||||||
|
// || emoji.originalUrl してるのは後方互換性のため(publicUrlはstringなので??はだめ)
|
||||||
|
url.searchParams.set('url', emoji.publicUrl || emoji.originalUrl);
|
||||||
|
url.searchParams.set('emoji', '1');
|
||||||
|
if ('static' in request.query) url.searchParams.set('static', '1');
|
||||||
|
|
||||||
|
return await reply.redirect(
|
||||||
|
301,
|
||||||
|
url.toString(),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
fastify.get<{ Params: { acct: string } }>('/avatar/@:acct', async (request, reply) => {
|
fastify.get<{ Params: { acct: string } }>('/avatar/@:acct', async (request, reply) => {
|
||||||
const { username, host } = Acct.parse(request.params.acct);
|
const { username, host } = Acct.parse(request.params.acct);
|
||||||
const user = await this.usersRepository.findOne({
|
const user = await this.usersRepository.findOne({
|
||||||
|
@@ -220,6 +220,7 @@ import * as ep___messaging_messages_create from './endpoints/messaging/messages/
|
|||||||
import * as ep___messaging_messages_delete from './endpoints/messaging/messages/delete.js';
|
import * as ep___messaging_messages_delete from './endpoints/messaging/messages/delete.js';
|
||||||
import * as ep___messaging_messages_read from './endpoints/messaging/messages/read.js';
|
import * as ep___messaging_messages_read from './endpoints/messaging/messages/read.js';
|
||||||
import * as ep___meta from './endpoints/meta.js';
|
import * as ep___meta from './endpoints/meta.js';
|
||||||
|
import * as ep___emojis from './endpoints/emojis.js';
|
||||||
import * as ep___miauth_genToken from './endpoints/miauth/gen-token.js';
|
import * as ep___miauth_genToken from './endpoints/miauth/gen-token.js';
|
||||||
import * as ep___mute_create from './endpoints/mute/create.js';
|
import * as ep___mute_create from './endpoints/mute/create.js';
|
||||||
import * as ep___mute_delete from './endpoints/mute/delete.js';
|
import * as ep___mute_delete from './endpoints/mute/delete.js';
|
||||||
@@ -550,6 +551,7 @@ const $messaging_messages_create: Provider = { provide: 'ep:messaging/messages/c
|
|||||||
const $messaging_messages_delete: Provider = { provide: 'ep:messaging/messages/delete', useClass: ep___messaging_messages_delete.default };
|
const $messaging_messages_delete: Provider = { provide: 'ep:messaging/messages/delete', useClass: ep___messaging_messages_delete.default };
|
||||||
const $messaging_messages_read: Provider = { provide: 'ep:messaging/messages/read', useClass: ep___messaging_messages_read.default };
|
const $messaging_messages_read: Provider = { provide: 'ep:messaging/messages/read', useClass: ep___messaging_messages_read.default };
|
||||||
const $meta: Provider = { provide: 'ep:meta', useClass: ep___meta.default };
|
const $meta: Provider = { provide: 'ep:meta', useClass: ep___meta.default };
|
||||||
|
const $emojis: Provider = { provide: 'ep:emojis', useClass: ep___emojis.default };
|
||||||
const $miauth_genToken: Provider = { provide: 'ep:miauth/gen-token', useClass: ep___miauth_genToken.default };
|
const $miauth_genToken: Provider = { provide: 'ep:miauth/gen-token', useClass: ep___miauth_genToken.default };
|
||||||
const $mute_create: Provider = { provide: 'ep:mute/create', useClass: ep___mute_create.default };
|
const $mute_create: Provider = { provide: 'ep:mute/create', useClass: ep___mute_create.default };
|
||||||
const $mute_delete: Provider = { provide: 'ep:mute/delete', useClass: ep___mute_delete.default };
|
const $mute_delete: Provider = { provide: 'ep:mute/delete', useClass: ep___mute_delete.default };
|
||||||
@@ -884,6 +886,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
|
|||||||
$messaging_messages_delete,
|
$messaging_messages_delete,
|
||||||
$messaging_messages_read,
|
$messaging_messages_read,
|
||||||
$meta,
|
$meta,
|
||||||
|
$emojis,
|
||||||
$miauth_genToken,
|
$miauth_genToken,
|
||||||
$mute_create,
|
$mute_create,
|
||||||
$mute_delete,
|
$mute_delete,
|
||||||
@@ -1212,6 +1215,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
|
|||||||
$messaging_messages_delete,
|
$messaging_messages_delete,
|
||||||
$messaging_messages_read,
|
$messaging_messages_read,
|
||||||
$meta,
|
$meta,
|
||||||
|
$emojis,
|
||||||
$miauth_genToken,
|
$miauth_genToken,
|
||||||
$mute_create,
|
$mute_create,
|
||||||
$mute_delete,
|
$mute_delete,
|
||||||
|
@@ -219,6 +219,7 @@ import * as ep___messaging_messages_create from './endpoints/messaging/messages/
|
|||||||
import * as ep___messaging_messages_delete from './endpoints/messaging/messages/delete.js';
|
import * as ep___messaging_messages_delete from './endpoints/messaging/messages/delete.js';
|
||||||
import * as ep___messaging_messages_read from './endpoints/messaging/messages/read.js';
|
import * as ep___messaging_messages_read from './endpoints/messaging/messages/read.js';
|
||||||
import * as ep___meta from './endpoints/meta.js';
|
import * as ep___meta from './endpoints/meta.js';
|
||||||
|
import * as ep___emojis from './endpoints/emojis.js';
|
||||||
import * as ep___miauth_genToken from './endpoints/miauth/gen-token.js';
|
import * as ep___miauth_genToken from './endpoints/miauth/gen-token.js';
|
||||||
import * as ep___mute_create from './endpoints/mute/create.js';
|
import * as ep___mute_create from './endpoints/mute/create.js';
|
||||||
import * as ep___mute_delete from './endpoints/mute/delete.js';
|
import * as ep___mute_delete from './endpoints/mute/delete.js';
|
||||||
@@ -547,6 +548,7 @@ const eps = [
|
|||||||
['messaging/messages/delete', ep___messaging_messages_delete],
|
['messaging/messages/delete', ep___messaging_messages_delete],
|
||||||
['messaging/messages/read', ep___messaging_messages_read],
|
['messaging/messages/read', ep___messaging_messages_read],
|
||||||
['meta', ep___meta],
|
['meta', ep___meta],
|
||||||
|
['emojis', ep___emojis],
|
||||||
['miauth/gen-token', ep___miauth_genToken],
|
['miauth/gen-token', ep___miauth_genToken],
|
||||||
['mute/create', ep___mute_create],
|
['mute/create', ep___mute_create],
|
||||||
['mute/delete', ep___mute_delete],
|
['mute/delete', ep___mute_delete],
|
||||||
|
@@ -5,6 +5,7 @@ import { QueryService } from '@/core/QueryService.js';
|
|||||||
import { UtilityService } from '@/core/UtilityService.js';
|
import { UtilityService } from '@/core/UtilityService.js';
|
||||||
import { EmojiEntityService } from '@/core/entities/EmojiEntityService.js';
|
import { EmojiEntityService } from '@/core/entities/EmojiEntityService.js';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
|
import { sqlLikeEscape } from '@/misc/sql-like-escape.js';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
tags: ['admin'],
|
tags: ['admin'],
|
||||||
@@ -92,7 +93,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (ps.query) {
|
if (ps.query) {
|
||||||
q.andWhere('emoji.name like :query', { query: '%' + ps.query + '%' });
|
q.andWhere('emoji.name like :query', { query: '%' + sqlLikeEscape(ps.query) + '%' });
|
||||||
}
|
}
|
||||||
|
|
||||||
const emojis = await q
|
const emojis = await q
|
||||||
|
@@ -5,6 +5,7 @@ import type { Emoji } from '@/models/entities/Emoji.js';
|
|||||||
import { QueryService } from '@/core/QueryService.js';
|
import { QueryService } from '@/core/QueryService.js';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import { EmojiEntityService } from '@/core/entities/EmojiEntityService.js';
|
import { EmojiEntityService } from '@/core/entities/EmojiEntityService.js';
|
||||||
|
//import { sqlLikeEscape } from '@/misc/sql-like-escape.js';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
tags: ['admin'],
|
tags: ['admin'],
|
||||||
@@ -82,7 +83,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
|||||||
let emojis: Emoji[];
|
let emojis: Emoji[];
|
||||||
|
|
||||||
if (ps.query) {
|
if (ps.query) {
|
||||||
//q.andWhere('emoji.name ILIKE :q', { q: `%${ps.query}%` });
|
//q.andWhere('emoji.name ILIKE :q', { q: `%${ sqlLikeEscape(ps.query) }%` });
|
||||||
//const emojis = await q.take(ps.limit).getMany();
|
//const emojis = await q.take(ps.limit).getMany();
|
||||||
|
|
||||||
emojis = await q.getMany();
|
emojis = await q.getMany();
|
||||||
|
@@ -3,6 +3,7 @@ import type { UsersRepository } from '@/models/index.js';
|
|||||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||||
|
import { sqlLikeEscape } from '@/misc/sql-like-escape.js';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
tags: ['admin'],
|
tags: ['admin'],
|
||||||
@@ -68,7 +69,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (ps.username) {
|
if (ps.username) {
|
||||||
query.andWhere('user.usernameLower like :username', { username: ps.username.toLowerCase() + '%' });
|
query.andWhere('user.usernameLower like :username', { username: sqlLikeEscape(ps.username.toLowerCase()) + '%' });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ps.hostname) {
|
if (ps.hostname) {
|
||||||
|
91
packages/backend/src/server/api/endpoints/emojis.ts
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
import { IsNull, MoreThan } from 'typeorm';
|
||||||
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
|
import type { EmojisRepository } from '@/models/index.js';
|
||||||
|
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||||
|
import { EmojiEntityService } from '@/core/entities/EmojiEntityService.js';
|
||||||
|
import type { Config } from '@/config.js';
|
||||||
|
import { DI } from '@/di-symbols.js';
|
||||||
|
|
||||||
|
export const meta = {
|
||||||
|
tags: ['meta'],
|
||||||
|
|
||||||
|
requireCredential: false,
|
||||||
|
|
||||||
|
res: {
|
||||||
|
type: 'object',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
properties: {
|
||||||
|
emojis: {
|
||||||
|
type: 'array',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
items: {
|
||||||
|
type: 'object',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
properties: {
|
||||||
|
id: {
|
||||||
|
type: 'string',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
format: 'id',
|
||||||
|
},
|
||||||
|
aliases: {
|
||||||
|
type: 'array',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
items: {
|
||||||
|
type: 'string',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
category: {
|
||||||
|
type: 'string',
|
||||||
|
optional: false, nullable: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const paramDef = {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
},
|
||||||
|
required: [],
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
// eslint-disable-next-line import/no-default-export
|
||||||
|
@Injectable()
|
||||||
|
export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||||
|
constructor(
|
||||||
|
@Inject(DI.config)
|
||||||
|
private config: Config,
|
||||||
|
|
||||||
|
@Inject(DI.emojisRepository)
|
||||||
|
private emojisRepository: EmojisRepository,
|
||||||
|
|
||||||
|
private emojiEntityService: EmojiEntityService,
|
||||||
|
) {
|
||||||
|
super(meta, paramDef, async (ps, me) => {
|
||||||
|
const emojis = await this.emojisRepository.find({
|
||||||
|
where: {
|
||||||
|
host: IsNull(),
|
||||||
|
},
|
||||||
|
order: {
|
||||||
|
category: 'ASC',
|
||||||
|
name: 'ASC',
|
||||||
|
},
|
||||||
|
cache: {
|
||||||
|
id: 'meta_emojis',
|
||||||
|
milliseconds: 3600000, // 1 hour
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
emojis: await this.emojiEntityService.packMany(emojis, {
|
||||||
|
omitId: true,
|
||||||
|
omitHost: true,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@@ -4,6 +4,7 @@ import type { InstancesRepository } from '@/models/index.js';
|
|||||||
import { InstanceEntityService } from '@/core/entities/InstanceEntityService.js';
|
import { InstanceEntityService } from '@/core/entities/InstanceEntityService.js';
|
||||||
import { MetaService } from '@/core/MetaService.js';
|
import { MetaService } from '@/core/MetaService.js';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
|
import { sqlLikeEscape } from '@/misc/sql-like-escape.js';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
tags: ['federation'],
|
tags: ['federation'],
|
||||||
@@ -120,7 +121,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (ps.host) {
|
if (ps.host) {
|
||||||
query.andWhere('instance.host like :host', { host: '%' + ps.host.toLowerCase() + '%' });
|
query.andWhere('instance.host like :host', { host: '%' + sqlLikeEscape(ps.host.toLowerCase()) + '%' });
|
||||||
}
|
}
|
||||||
|
|
||||||
const instances = await query.take(ps.limit).skip(ps.offset).getMany();
|
const instances = await query.take(ps.limit).skip(ps.offset).getMany();
|
||||||
|
@@ -2,6 +2,7 @@ import { Inject, Injectable } from '@nestjs/common';
|
|||||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||||
import type { HashtagsRepository } from '@/models/index.js';
|
import type { HashtagsRepository } from '@/models/index.js';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
|
import { sqlLikeEscape } from '@/misc/sql-like-escape.js';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
tags: ['hashtags'],
|
tags: ['hashtags'],
|
||||||
@@ -37,7 +38,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
|||||||
) {
|
) {
|
||||||
super(meta, paramDef, async (ps, me) => {
|
super(meta, paramDef, async (ps, me) => {
|
||||||
const hashtags = await this.hashtagsRepository.createQueryBuilder('tag')
|
const hashtags = await this.hashtagsRepository.createQueryBuilder('tag')
|
||||||
.where('tag.name like :q', { q: ps.query.toLowerCase() + '%' })
|
.where('tag.name like :q', { q: sqlLikeEscape(ps.query.toLowerCase()) + '%' })
|
||||||
.orderBy('tag.count', 'DESC')
|
.orderBy('tag.count', 'DESC')
|
||||||
.groupBy('tag.id')
|
.groupBy('tag.id')
|
||||||
.take(ps.limit)
|
.take(ps.limit)
|
||||||
|
@@ -4,7 +4,6 @@ import type { AdsRepository, EmojisRepository, UsersRepository } from '@/models/
|
|||||||
import { MAX_NOTE_TEXT_LENGTH, DB_MAX_NOTE_TEXT_LENGTH } from '@/const.js';
|
import { MAX_NOTE_TEXT_LENGTH, DB_MAX_NOTE_TEXT_LENGTH } from '@/const.js';
|
||||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||||
import { EmojiEntityService } from '@/core/entities/EmojiEntityService.js';
|
|
||||||
import { MetaService } from '@/core/MetaService.js';
|
import { MetaService } from '@/core/MetaService.js';
|
||||||
import type { Config } from '@/config.js';
|
import type { Config } from '@/config.js';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
@@ -152,43 +151,6 @@ export const meta = {
|
|||||||
type: 'number',
|
type: 'number',
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
},
|
},
|
||||||
emojis: {
|
|
||||||
type: 'array',
|
|
||||||
optional: false, nullable: false,
|
|
||||||
items: {
|
|
||||||
type: 'object',
|
|
||||||
optional: false, nullable: false,
|
|
||||||
properties: {
|
|
||||||
id: {
|
|
||||||
type: 'string',
|
|
||||||
optional: false, nullable: false,
|
|
||||||
format: 'id',
|
|
||||||
},
|
|
||||||
aliases: {
|
|
||||||
type: 'array',
|
|
||||||
optional: false, nullable: false,
|
|
||||||
items: {
|
|
||||||
type: 'string',
|
|
||||||
optional: false, nullable: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
category: {
|
|
||||||
type: 'string',
|
|
||||||
optional: false, nullable: true,
|
|
||||||
},
|
|
||||||
host: {
|
|
||||||
type: 'string',
|
|
||||||
optional: false, nullable: true,
|
|
||||||
description: 'The local host is represented with `null`.',
|
|
||||||
},
|
|
||||||
url: {
|
|
||||||
type: 'string',
|
|
||||||
optional: false, nullable: false,
|
|
||||||
format: 'url',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
ads: {
|
ads: {
|
||||||
type: 'array',
|
type: 'array',
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
@@ -326,30 +288,12 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
|||||||
@Inject(DI.adsRepository)
|
@Inject(DI.adsRepository)
|
||||||
private adsRepository: AdsRepository,
|
private adsRepository: AdsRepository,
|
||||||
|
|
||||||
@Inject(DI.emojisRepository)
|
|
||||||
private emojisRepository: EmojisRepository,
|
|
||||||
|
|
||||||
private userEntityService: UserEntityService,
|
private userEntityService: UserEntityService,
|
||||||
private emojiEntityService: EmojiEntityService,
|
|
||||||
private metaService: MetaService,
|
private metaService: MetaService,
|
||||||
) {
|
) {
|
||||||
super(meta, paramDef, async (ps, me) => {
|
super(meta, paramDef, async (ps, me) => {
|
||||||
const instance = await this.metaService.fetch(true);
|
const instance = await this.metaService.fetch(true);
|
||||||
|
|
||||||
const emojis = await this.emojisRepository.find({
|
|
||||||
where: {
|
|
||||||
host: IsNull(),
|
|
||||||
},
|
|
||||||
order: {
|
|
||||||
category: 'ASC',
|
|
||||||
name: 'ASC',
|
|
||||||
},
|
|
||||||
cache: {
|
|
||||||
id: 'meta_emojis',
|
|
||||||
milliseconds: 3600000, // 1 hour
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const ads = await this.adsRepository.find({
|
const ads = await this.adsRepository.find({
|
||||||
where: {
|
where: {
|
||||||
expiresAt: MoreThan(new Date()),
|
expiresAt: MoreThan(new Date()),
|
||||||
@@ -390,7 +334,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
|||||||
backgroundImageUrl: instance.backgroundImageUrl,
|
backgroundImageUrl: instance.backgroundImageUrl,
|
||||||
logoImageUrl: instance.logoImageUrl,
|
logoImageUrl: instance.logoImageUrl,
|
||||||
maxNoteTextLength: MAX_NOTE_TEXT_LENGTH, // 後方互換性のため
|
maxNoteTextLength: MAX_NOTE_TEXT_LENGTH, // 後方互換性のため
|
||||||
emojis: await this.emojiEntityService.packMany(emojis),
|
|
||||||
defaultLightTheme: instance.defaultLightTheme,
|
defaultLightTheme: instance.defaultLightTheme,
|
||||||
defaultDarkTheme: instance.defaultDarkTheme,
|
defaultDarkTheme: instance.defaultDarkTheme,
|
||||||
ads: ads.map(ad => ({
|
ads: ads.map(ad => ({
|
||||||
|
@@ -162,13 +162,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
|||||||
userId: me.id,
|
userId: me.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Notify
|
|
||||||
this.createNotificationService.createNotification(note.userId, 'pollVote', {
|
|
||||||
notifierId: me.id,
|
|
||||||
noteId: note.id,
|
|
||||||
choice: ps.choice,
|
|
||||||
});
|
|
||||||
|
|
||||||
// リモート投票の場合リプライ送信
|
// リモート投票の場合リプライ送信
|
||||||
if (note.userHost != null) {
|
if (note.userHost != null) {
|
||||||
const pollOwner = await this.usersRepository.findOneByOrFail({ id: note.userId }) as IRemoteUser;
|
const pollOwner = await this.usersRepository.findOneByOrFail({ id: note.userId }) as IRemoteUser;
|
||||||
|
@@ -6,6 +6,7 @@ import { QueryService } from '@/core/QueryService.js';
|
|||||||
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
||||||
import type { Config } from '@/config.js';
|
import type { Config } from '@/config.js';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
|
import { sqlLikeEscape } from '@/misc/sql-like-escape.js';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
tags: ['notes'],
|
tags: ['notes'],
|
||||||
@@ -70,7 +71,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
query
|
query
|
||||||
.andWhere('note.text ILIKE :q', { q: `%${ps.query}%` })
|
.andWhere('note.text ILIKE :q', { q: `%${ sqlLikeEscape(ps.query) }%` })
|
||||||
.innerJoinAndSelect('note.user', 'user')
|
.innerJoinAndSelect('note.user', 'user')
|
||||||
.leftJoinAndSelect('user.avatar', 'avatar')
|
.leftJoinAndSelect('user.avatar', 'avatar')
|
||||||
.leftJoinAndSelect('user.banner', 'banner')
|
.leftJoinAndSelect('user.banner', 'banner')
|
||||||
|
@@ -6,6 +6,7 @@ import type { User } from '@/models/entities/User.js';
|
|||||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
|
import { sqlLikeEscape } from '@/misc/sql-like-escape.js';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
tags: ['users'],
|
tags: ['users'],
|
||||||
@@ -59,10 +60,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
|||||||
if (ps.host) {
|
if (ps.host) {
|
||||||
const q = this.usersRepository.createQueryBuilder('user')
|
const q = this.usersRepository.createQueryBuilder('user')
|
||||||
.where('user.isSuspended = FALSE')
|
.where('user.isSuspended = FALSE')
|
||||||
.andWhere('user.host LIKE :host', { host: ps.host.toLowerCase() + '%' });
|
.andWhere('user.host LIKE :host', { host: sqlLikeEscape(ps.host.toLowerCase()) + '%' });
|
||||||
|
|
||||||
if (ps.username) {
|
if (ps.username) {
|
||||||
q.andWhere('user.usernameLower LIKE :username', { username: ps.username.toLowerCase() + '%' });
|
q.andWhere('user.usernameLower LIKE :username', { username: sqlLikeEscape(ps.username.toLowerCase()) + '%' });
|
||||||
}
|
}
|
||||||
|
|
||||||
q.andWhere('user.updatedAt IS NOT NULL');
|
q.andWhere('user.updatedAt IS NOT NULL');
|
||||||
@@ -83,7 +84,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
|||||||
.where(`user.id IN (${ followingQuery.getQuery() })`)
|
.where(`user.id IN (${ followingQuery.getQuery() })`)
|
||||||
.andWhere('user.id != :meId', { meId: me.id })
|
.andWhere('user.id != :meId', { meId: me.id })
|
||||||
.andWhere('user.isSuspended = FALSE')
|
.andWhere('user.isSuspended = FALSE')
|
||||||
.andWhere('user.usernameLower LIKE :username', { username: ps.username.toLowerCase() + '%' })
|
.andWhere('user.usernameLower LIKE :username', { username: sqlLikeEscape(ps.username.toLowerCase()) + '%' })
|
||||||
.andWhere(new Brackets(qb => { qb
|
.andWhere(new Brackets(qb => { qb
|
||||||
.where('user.updatedAt IS NULL')
|
.where('user.updatedAt IS NULL')
|
||||||
.orWhere('user.updatedAt > :activeThreshold', { activeThreshold: activeThreshold });
|
.orWhere('user.updatedAt > :activeThreshold', { activeThreshold: activeThreshold });
|
||||||
@@ -101,7 +102,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
|||||||
.where(`user.id NOT IN (${ followingQuery.getQuery() })`)
|
.where(`user.id NOT IN (${ followingQuery.getQuery() })`)
|
||||||
.andWhere('user.id != :meId', { meId: me.id })
|
.andWhere('user.id != :meId', { meId: me.id })
|
||||||
.andWhere('user.isSuspended = FALSE')
|
.andWhere('user.isSuspended = FALSE')
|
||||||
.andWhere('user.usernameLower LIKE :username', { username: ps.username.toLowerCase() + '%' })
|
.andWhere('user.usernameLower LIKE :username', { username: sqlLikeEscape(ps.username.toLowerCase()) + '%' })
|
||||||
.andWhere('user.updatedAt IS NOT NULL');
|
.andWhere('user.updatedAt IS NOT NULL');
|
||||||
|
|
||||||
otherQuery.setParameters(followingQuery.getParameters());
|
otherQuery.setParameters(followingQuery.getParameters());
|
||||||
@@ -116,7 +117,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
|||||||
} else {
|
} else {
|
||||||
users = await this.usersRepository.createQueryBuilder('user')
|
users = await this.usersRepository.createQueryBuilder('user')
|
||||||
.where('user.isSuspended = FALSE')
|
.where('user.isSuspended = FALSE')
|
||||||
.andWhere('user.usernameLower LIKE :username', { username: ps.username.toLowerCase() + '%' })
|
.andWhere('user.usernameLower LIKE :username', { username: sqlLikeEscape(ps.username.toLowerCase()) + '%' })
|
||||||
.andWhere('user.updatedAt IS NOT NULL')
|
.andWhere('user.updatedAt IS NOT NULL')
|
||||||
.orderBy('user.updatedAt', 'DESC')
|
.orderBy('user.updatedAt', 'DESC')
|
||||||
.take(ps.limit - users.length)
|
.take(ps.limit - users.length)
|
||||||
|
@@ -5,6 +5,7 @@ import type { User } from '@/models/entities/User.js';
|
|||||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
|
import { sqlLikeEscape } from '@/misc/sql-like-escape.js';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
tags: ['users'],
|
tags: ['users'],
|
||||||
@@ -57,7 +58,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
|||||||
|
|
||||||
if (isUsername) {
|
if (isUsername) {
|
||||||
const usernameQuery = this.usersRepository.createQueryBuilder('user')
|
const usernameQuery = this.usersRepository.createQueryBuilder('user')
|
||||||
.where('user.usernameLower LIKE :username', { username: ps.query.replace('@', '').toLowerCase() + '%' })
|
.where('user.usernameLower LIKE :username', { username: sqlLikeEscape(ps.query.replace('@', '').toLowerCase()) + '%' })
|
||||||
.andWhere(new Brackets(qb => { qb
|
.andWhere(new Brackets(qb => { qb
|
||||||
.where('user.updatedAt IS NULL')
|
.where('user.updatedAt IS NULL')
|
||||||
.orWhere('user.updatedAt > :activeThreshold', { activeThreshold: activeThreshold });
|
.orWhere('user.updatedAt > :activeThreshold', { activeThreshold: activeThreshold });
|
||||||
@@ -78,11 +79,11 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
|||||||
} else {
|
} else {
|
||||||
const nameQuery = this.usersRepository.createQueryBuilder('user')
|
const nameQuery = this.usersRepository.createQueryBuilder('user')
|
||||||
.where(new Brackets(qb => {
|
.where(new Brackets(qb => {
|
||||||
qb.where('user.name ILIKE :query', { query: '%' + ps.query + '%' });
|
qb.where('user.name ILIKE :query', { query: '%' + sqlLikeEscape(ps.query) + '%' });
|
||||||
|
|
||||||
// Also search username if it qualifies as username
|
// Also search username if it qualifies as username
|
||||||
if (this.userEntityService.validateLocalUsername(ps.query)) {
|
if (this.userEntityService.validateLocalUsername(ps.query)) {
|
||||||
qb.orWhere('user.usernameLower LIKE :username', { username: '%' + ps.query.toLowerCase() + '%' });
|
qb.orWhere('user.usernameLower LIKE :username', { username: '%' + sqlLikeEscape(ps.query.toLowerCase()) + '%' });
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
.andWhere(new Brackets(qb => { qb
|
.andWhere(new Brackets(qb => { qb
|
||||||
@@ -106,7 +107,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
|||||||
if (users.length < ps.limit) {
|
if (users.length < ps.limit) {
|
||||||
const profQuery = this.userProfilesRepository.createQueryBuilder('prof')
|
const profQuery = this.userProfilesRepository.createQueryBuilder('prof')
|
||||||
.select('prof.userId')
|
.select('prof.userId')
|
||||||
.where('prof.description ILIKE :query', { query: '%' + ps.query + '%' });
|
.where('prof.description ILIKE :query', { query: '%' + sqlLikeEscape(ps.query) + '%' });
|
||||||
|
|
||||||
if (ps.origin === 'local') {
|
if (ps.origin === 'local') {
|
||||||
profQuery.andWhere('prof.userHost IS NULL');
|
profQuery.andWhere('prof.userHost IS NULL');
|
||||||
|
@@ -1,6 +1,5 @@
|
|||||||
import { dirname } from 'node:path';
|
import { dirname } from 'node:path';
|
||||||
import { fileURLToPath } from 'node:url';
|
import { fileURLToPath } from 'node:url';
|
||||||
import { PathOrFileDescriptor, readFileSync } from 'node:fs';
|
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import { createBullBoard } from '@bull-board/api';
|
import { createBullBoard } from '@bull-board/api';
|
||||||
import { BullAdapter } from '@bull-board/api/bullAdapter.js';
|
import { BullAdapter } from '@bull-board/api/bullAdapter.js';
|
||||||
@@ -71,9 +70,6 @@ export class ClientServerService {
|
|||||||
@Inject(DI.pagesRepository)
|
@Inject(DI.pagesRepository)
|
||||||
private pagesRepository: PagesRepository,
|
private pagesRepository: PagesRepository,
|
||||||
|
|
||||||
@Inject(DI.emojisRepository)
|
|
||||||
private emojisRepository: EmojisRepository,
|
|
||||||
|
|
||||||
@Inject(DI.flashsRepository)
|
@Inject(DI.flashsRepository)
|
||||||
private flashsRepository: FlashsRepository,
|
private flashsRepository: FlashsRepository,
|
||||||
|
|
||||||
@@ -225,49 +221,6 @@ export class ClientServerService {
|
|||||||
return reply.sendFile('/apple-touch-icon.png', staticAssets);
|
return reply.sendFile('/apple-touch-icon.png', staticAssets);
|
||||||
});
|
});
|
||||||
|
|
||||||
fastify.get<{ Params: { path: string }; Querystring: { static?: any; }; }>('/emoji/:path(.*)', async (request, reply) => {
|
|
||||||
const path = request.params.path;
|
|
||||||
|
|
||||||
if (!path.match(/^[a-zA-Z0-9\-_@\.]+?\.webp$/)) {
|
|
||||||
reply.code(404);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
reply.header('Cache-Control', 'public, max-age=86400');
|
|
||||||
|
|
||||||
const name = path.split('@')[0].replace('.webp', '');
|
|
||||||
const host = path.split('@')[1]?.replace('.webp', '');
|
|
||||||
|
|
||||||
const emoji = await this.emojisRepository.findOneBy({
|
|
||||||
// `@.` is the spec of ReactionService.decodeReaction
|
|
||||||
host: (host == null || host === '.') ? IsNull() : host,
|
|
||||||
name: name,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (emoji == null) {
|
|
||||||
reply.code(404);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
reply.header('Content-Security-Policy', 'default-src \'none\'; style-src \'unsafe-inline\'');
|
|
||||||
|
|
||||||
// || emoji.originalUrl してるのは後方互換性のため(publicUrlはstringなので??はだめ)
|
|
||||||
return await reply.redirect(301, emoji.publicUrl ?? emoji.originalUrl);
|
|
||||||
|
|
||||||
/* https://github.com/misskey-dev/misskey/pull/9431#issuecomment-1373006446
|
|
||||||
const url = new URL('/proxy/emoji.webp', this.config.url);
|
|
||||||
// || emoji.originalUrl してるのは後方互換性のため(publicUrlはstringなので??はだめ)
|
|
||||||
url.searchParams.set('url', emoji.publicUrl || emoji.originalUrl);
|
|
||||||
url.searchParams.set('emoji', '1');
|
|
||||||
if ('static' in request.query) url.searchParams.set('static', '1');
|
|
||||||
|
|
||||||
return await reply.redirect(
|
|
||||||
301,
|
|
||||||
url.toString(),
|
|
||||||
);
|
|
||||||
*/
|
|
||||||
});
|
|
||||||
|
|
||||||
fastify.get<{ Params: { path: string } }>('/fluent-emoji/:path(.*)', async (request, reply) => {
|
fastify.get<{ Params: { path: string } }>('/fluent-emoji/:path(.*)', async (request, reply) => {
|
||||||
const path = request.params.path;
|
const path = request.params.path;
|
||||||
|
|
||||||
@@ -359,10 +312,10 @@ export class ClientServerService {
|
|||||||
fastify.get('/opensearch.xml', async (request, reply) => {
|
fastify.get('/opensearch.xml', async (request, reply) => {
|
||||||
const meta = await this.metaService.fetch();
|
const meta = await this.metaService.fetch();
|
||||||
|
|
||||||
const name = meta.name || 'Misskey';
|
const name = meta.name ?? 'Misskey';
|
||||||
let content = '';
|
let content = '';
|
||||||
content += '<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/" xmlns:moz="http://www.mozilla.org/2006/browser/search/">';
|
content += '<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/" xmlns:moz="http://www.mozilla.org/2006/browser/search/">';
|
||||||
content += `<ShortName>${name} Search</ShortName>`;
|
content += `<ShortName>${name}</ShortName>`;
|
||||||
content += `<Description>${name} Search</Description>`;
|
content += `<Description>${name} Search</Description>`;
|
||||||
content += '<InputEncoding>UTF-8</InputEncoding>';
|
content += '<InputEncoding>UTF-8</InputEncoding>';
|
||||||
content += `<Image width="16" height="16" type="image/x-icon">${this.config.url}/favicon.ico</Image>`;
|
content += `<Image width="16" height="16" type="image/x-icon">${this.config.url}/favicon.ico</Image>`;
|
||||||
@@ -580,13 +533,12 @@ export class ClientServerService {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Clip
|
// Clip
|
||||||
// TODO: 非publicなclipのハンドリング
|
|
||||||
fastify.get<{ Params: { clip: string; } }>('/clips/:clip', async (request, reply) => {
|
fastify.get<{ Params: { clip: string; } }>('/clips/:clip', async (request, reply) => {
|
||||||
const clip = await this.clipsRepository.findOneBy({
|
const clip = await this.clipsRepository.findOneBy({
|
||||||
id: request.params.clip,
|
id: request.params.clip,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (clip) {
|
if (clip && clip.isPublic) {
|
||||||
const _clip = await this.clipEntityService.pack(clip);
|
const _clip = await this.clipEntityService.pack(clip);
|
||||||
const profile = await this.userProfilesRepository.findOneByOrFail({ userId: clip.userId });
|
const profile = await this.userProfilesRepository.findOneByOrFail({ userId: clip.userId });
|
||||||
const meta = await this.metaService.fetch();
|
const meta = await this.metaService.fetch();
|
||||||
|
@@ -31,7 +31,7 @@ html
|
|||||||
link(rel='icon' href= icon || '/favicon.ico')
|
link(rel='icon' href= icon || '/favicon.ico')
|
||||||
link(rel='apple-touch-icon' href= icon || '/apple-touch-icon.png')
|
link(rel='apple-touch-icon' href= icon || '/apple-touch-icon.png')
|
||||||
link(rel='manifest' href='/manifest.json')
|
link(rel='manifest' href='/manifest.json')
|
||||||
link(rel='search' type='application/opensearchdescription+xml' title=((title || "Misskey") + " Search") href=`${url}/opensearch.xml`)
|
link(rel='search' type='application/opensearchdescription+xml' title=(title || "Misskey") href=`${url}/opensearch.xml`)
|
||||||
link(rel='prefetch' href='https://xn--931a.moe/assets/info.jpg')
|
link(rel='prefetch' href='https://xn--931a.moe/assets/info.jpg')
|
||||||
link(rel='prefetch' href='https://xn--931a.moe/assets/not-found.jpg')
|
link(rel='prefetch' href='https://xn--931a.moe/assets/not-found.jpg')
|
||||||
link(rel='prefetch' href='https://xn--931a.moe/assets/error.jpg')
|
link(rel='prefetch' href='https://xn--931a.moe/assets/error.jpg')
|
||||||
|
BIN
packages/frontend/assets/cookie.png
Normal file
After Width: | Height: | Size: 38 KiB |
@@ -11,7 +11,7 @@
|
|||||||
"@rollup/plugin-alias": "4.0.2",
|
"@rollup/plugin-alias": "4.0.2",
|
||||||
"@rollup/plugin-json": "6.0.0",
|
"@rollup/plugin-json": "6.0.0",
|
||||||
"@rollup/pluginutils": "5.0.2",
|
"@rollup/pluginutils": "5.0.2",
|
||||||
"@syuilo/aiscript": "0.12.1",
|
"@syuilo/aiscript": "0.12.2",
|
||||||
"@tabler/icons": "^1.118.0",
|
"@tabler/icons": "^1.118.0",
|
||||||
"@vitejs/plugin-vue": "4.0.0",
|
"@vitejs/plugin-vue": "4.0.0",
|
||||||
"@vue/compiler-sfc": "3.2.45",
|
"@vue/compiler-sfc": "3.2.45",
|
||||||
@@ -21,7 +21,7 @@
|
|||||||
"broadcast-channel": "4.19.1",
|
"broadcast-channel": "4.19.1",
|
||||||
"browser-image-resizer": "git+https://github.com/misskey-dev/browser-image-resizer#v2.2.1-misskey.3",
|
"browser-image-resizer": "git+https://github.com/misskey-dev/browser-image-resizer#v2.2.1-misskey.3",
|
||||||
"canvas-confetti": "^1.6.0",
|
"canvas-confetti": "^1.6.0",
|
||||||
"chart.js": "4.1.1",
|
"chart.js": "4.1.2",
|
||||||
"chartjs-adapter-date-fns": "3.0.0",
|
"chartjs-adapter-date-fns": "3.0.0",
|
||||||
"chartjs-chart-matrix": "^1.3.0",
|
"chartjs-chart-matrix": "^1.3.0",
|
||||||
"chartjs-plugin-gradient": "0.6.1",
|
"chartjs-plugin-gradient": "0.6.1",
|
||||||
@@ -36,16 +36,16 @@
|
|||||||
"insert-text-at-cursor": "0.3.0",
|
"insert-text-at-cursor": "0.3.0",
|
||||||
"is-file-animated": "1.0.2",
|
"is-file-animated": "1.0.2",
|
||||||
"json5": "2.2.3",
|
"json5": "2.2.3",
|
||||||
"katex": "0.15.6",
|
"katex": "0.16.4",
|
||||||
"matter-js": "0.18.0",
|
"matter-js": "0.18.0",
|
||||||
"mfm-js": "0.23.0",
|
"mfm-js": "0.23.1",
|
||||||
"misskey-js": "0.0.14",
|
"misskey-js": "0.0.14",
|
||||||
"photoswipe": "5.3.4",
|
"photoswipe": "5.3.4",
|
||||||
"prismjs": "1.29.0",
|
"prismjs": "1.29.0",
|
||||||
"punycode": "2.1.1",
|
"punycode": "2.1.1",
|
||||||
"querystring": "0.2.1",
|
"querystring": "0.2.1",
|
||||||
"rndstr": "1.0.0",
|
"rndstr": "1.0.0",
|
||||||
"rollup": "3.9.0",
|
"rollup": "3.9.1",
|
||||||
"s-age": "1.1.2",
|
"s-age": "1.1.2",
|
||||||
"sanitize-html": "^2.8.1",
|
"sanitize-html": "^2.8.1",
|
||||||
"sass": "1.57.1",
|
"sass": "1.57.1",
|
||||||
@@ -56,14 +56,14 @@
|
|||||||
"textarea-caret": "3.1.0",
|
"textarea-caret": "3.1.0",
|
||||||
"three": "0.148.0",
|
"three": "0.148.0",
|
||||||
"throttle-debounce": "5.0.0",
|
"throttle-debounce": "5.0.0",
|
||||||
"tinycolor2": "1.5.1",
|
"tinycolor2": "1.5.2",
|
||||||
"tsc-alias": "1.8.2",
|
"tsc-alias": "1.8.2",
|
||||||
"tsconfig-paths": "4.1.2",
|
"tsconfig-paths": "4.1.2",
|
||||||
"twemoji-parser": "14.0.0",
|
"twemoji-parser": "14.0.0",
|
||||||
"typescript": "4.9.4",
|
"typescript": "4.9.4",
|
||||||
"uuid": "9.0.0",
|
"uuid": "9.0.0",
|
||||||
"vanilla-tilt": "1.8.0",
|
"vanilla-tilt": "1.8.0",
|
||||||
"vite": "4.0.3",
|
"vite": "4.0.4",
|
||||||
"vue": "3.2.45",
|
"vue": "3.2.45",
|
||||||
"vue-prism-editor": "2.0.0-alpha.2",
|
"vue-prism-editor": "2.0.0-alpha.2",
|
||||||
"vuedraggable": "next"
|
"vuedraggable": "next"
|
||||||
@@ -73,7 +73,7 @@
|
|||||||
"@types/glob": "8.0.0",
|
"@types/glob": "8.0.0",
|
||||||
"@types/gulp": "4.0.10",
|
"@types/gulp": "4.0.10",
|
||||||
"@types/gulp-rename": "2.0.1",
|
"@types/gulp-rename": "2.0.1",
|
||||||
"@types/katex": "0.14.0",
|
"@types/katex": "0.16.0",
|
||||||
"@types/matter-js": "0.18.2",
|
"@types/matter-js": "0.18.2",
|
||||||
"@types/punycode": "2.1.0",
|
"@types/punycode": "2.1.0",
|
||||||
"@types/sanitize-html": "^2.8.0",
|
"@types/sanitize-html": "^2.8.0",
|
||||||
@@ -83,16 +83,16 @@
|
|||||||
"@types/uuid": "9.0.0",
|
"@types/uuid": "9.0.0",
|
||||||
"@types/websocket": "1.0.5",
|
"@types/websocket": "1.0.5",
|
||||||
"@types/ws": "8.5.4",
|
"@types/ws": "8.5.4",
|
||||||
"@typescript-eslint/eslint-plugin": "5.47.1",
|
"@typescript-eslint/eslint-plugin": "5.48.0",
|
||||||
"@typescript-eslint/parser": "5.47.1",
|
"@typescript-eslint/parser": "5.48.0",
|
||||||
"@vue/runtime-core": "3.2.45",
|
"@vue/runtime-core": "3.2.45",
|
||||||
"cross-env": "7.0.3",
|
"cross-env": "7.0.3",
|
||||||
"cypress": "12.2.0",
|
"cypress": "12.3.0",
|
||||||
"eslint": "8.31.0",
|
"eslint": "8.31.0",
|
||||||
"eslint-plugin-import": "2.26.0",
|
"eslint-plugin-import": "2.26.0",
|
||||||
"eslint-plugin-vue": "9.8.0",
|
"eslint-plugin-vue": "9.8.0",
|
||||||
"start-server-and-test": "1.15.2",
|
"start-server-and-test": "1.15.2",
|
||||||
"vue-eslint-parser": "^9.1.0",
|
"vue-eslint-parser": "^9.1.0",
|
||||||
"vue-tsc": "^1.0.19"
|
"vue-tsc": "^1.0.22"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -6,12 +6,13 @@ import { del, get, set } from '@/scripts/idb-proxy';
|
|||||||
import { apiUrl } from '@/config';
|
import { apiUrl } from '@/config';
|
||||||
import { waiting, api, popup, popupMenu, success, alert } from '@/os';
|
import { waiting, api, popup, popupMenu, success, alert } from '@/os';
|
||||||
import { unisonReload, reloadChannel } from '@/scripts/unison-reload';
|
import { unisonReload, reloadChannel } from '@/scripts/unison-reload';
|
||||||
|
import { miLocalStorage } from './local-storage';
|
||||||
|
|
||||||
// TODO: 他のタブと永続化されたstateを同期
|
// TODO: 他のタブと永続化されたstateを同期
|
||||||
|
|
||||||
type Account = misskey.entities.MeDetailed;
|
type Account = misskey.entities.MeDetailed;
|
||||||
|
|
||||||
const accountData = localStorage.getItem('account');
|
const accountData = miLocalStorage.getItem('account');
|
||||||
|
|
||||||
// TODO: 外部からはreadonlyに
|
// TODO: 外部からはreadonlyに
|
||||||
export const $i = accountData ? reactive(JSON.parse(accountData) as Account) : null;
|
export const $i = accountData ? reactive(JSON.parse(accountData) as Account) : null;
|
||||||
@@ -21,7 +22,7 @@ export const iAmAdmin = $i != null && $i.isAdmin;
|
|||||||
|
|
||||||
export async function signout() {
|
export async function signout() {
|
||||||
waiting();
|
waiting();
|
||||||
localStorage.removeItem('account');
|
miLocalStorage.removeItem('account');
|
||||||
|
|
||||||
await removeAccount($i.id);
|
await removeAccount($i.id);
|
||||||
|
|
||||||
@@ -119,7 +120,7 @@ export function updateAccount(accountData) {
|
|||||||
for (const [key, value] of Object.entries(accountData)) {
|
for (const [key, value] of Object.entries(accountData)) {
|
||||||
$i[key] = value;
|
$i[key] = value;
|
||||||
}
|
}
|
||||||
localStorage.setItem('account', JSON.stringify($i));
|
miLocalStorage.setItem('account', JSON.stringify($i));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function refreshAccount() {
|
export function refreshAccount() {
|
||||||
@@ -130,7 +131,7 @@ export async function login(token: Account['token'], redirect?: string) {
|
|||||||
waiting();
|
waiting();
|
||||||
if (_DEV_) console.log('logging as token ', token);
|
if (_DEV_) console.log('logging as token ', token);
|
||||||
const me = await fetchAccount(token);
|
const me = await fetchAccount(token);
|
||||||
localStorage.setItem('account', JSON.stringify(me));
|
miLocalStorage.setItem('account', JSON.stringify(me));
|
||||||
document.cookie = `token=${token}; path=/; max-age=31536000`; // bull dashboardの認証とかで使う
|
document.cookie = `token=${token}; path=/; max-age=31536000`; // bull dashboardの認証とかで使う
|
||||||
await addAccount(me.id, token);
|
await addAccount(me.id, token);
|
||||||
|
|
||||||
|
@@ -37,7 +37,7 @@
|
|||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import MkButton from '@/components/MkButton.vue';
|
import MkButton from '@/components/MkButton.vue';
|
||||||
import MkSwitch from '@/components/form/switch.vue';
|
import MkSwitch from '@/components/MkSwitch.vue';
|
||||||
import MkKeyValue from '@/components/MkKeyValue.vue';
|
import MkKeyValue from '@/components/MkKeyValue.vue';
|
||||||
import { acct, userPage } from '@/filters/user';
|
import { acct, userPage } from '@/filters/user';
|
||||||
import * as os from '@/os';
|
import * as os from '@/os';
|
||||||
|
@@ -28,7 +28,7 @@
|
|||||||
import { ref, shallowRef } from 'vue';
|
import { ref, shallowRef } from 'vue';
|
||||||
import * as Misskey from 'misskey-js';
|
import * as Misskey from 'misskey-js';
|
||||||
import MkWindow from '@/components/MkWindow.vue';
|
import MkWindow from '@/components/MkWindow.vue';
|
||||||
import MkTextarea from '@/components/form/textarea.vue';
|
import MkTextarea from '@/components/MkTextarea.vue';
|
||||||
import MkButton from '@/components/MkButton.vue';
|
import MkButton from '@/components/MkButton.vue';
|
||||||
import * as os from '@/os';
|
import * as os from '@/os';
|
||||||
import { i18n } from '@/i18n';
|
import { i18n } from '@/i18n';
|
||||||
|
@@ -33,12 +33,12 @@
|
|||||||
<option v-for="item in c.items" :key="item.value" :value="item.value">{{ item.text }}</option>
|
<option v-for="item in c.items" :key="item.value" :value="item.value">{{ item.text }}</option>
|
||||||
</MkSelect>
|
</MkSelect>
|
||||||
<MkButton v-else-if="c.type === 'postFormButton'" :primary="c.primary" :rounded="c.rounded" :small="size === 'small'" @click="openPostForm">{{ c.text }}</MkButton>
|
<MkButton v-else-if="c.type === 'postFormButton'" :primary="c.primary" :rounded="c.rounded" :small="size === 'small'" @click="openPostForm">{{ c.text }}</MkButton>
|
||||||
<FormFolder v-else-if="c.type === 'folder'" :default-open="c.opened">
|
<MkFolder v-else-if="c.type === 'folder'" :default-open="c.opened">
|
||||||
<template #label>{{ c.title }}</template>
|
<template #label>{{ c.title }}</template>
|
||||||
<template v-for="child in c.children" :key="child">
|
<template v-for="child in c.children" :key="child">
|
||||||
<MkAsUi v-if="!g(child).hidden" :component="g(child)" :components="props.components" :size="size"/>
|
<MkAsUi v-if="!g(child).hidden" :component="g(child)" :components="props.components" :size="size"/>
|
||||||
</template>
|
</template>
|
||||||
</FormFolder>
|
</MkFolder>
|
||||||
<div v-else-if="c.type === 'container'" :class="[$style.container, { [$style.fontSerif]: c.font === 'serif', [$style.fontMonospace]: c.font === 'monospace', [$style.containerCenter]: c.align === 'center' }]" :style="{ backgroundColor: c.bgColor ?? null, color: c.fgColor ?? null, borderWidth: c.borderWidth ? `${c.borderWidth}px` : 0, borderColor: c.borderColor ?? 'var(--divider)', padding: c.padding ? `${c.padding}px` : 0, borderRadius: c.rounded ? '8px' : 0 }">
|
<div v-else-if="c.type === 'container'" :class="[$style.container, { [$style.fontSerif]: c.font === 'serif', [$style.fontMonospace]: c.font === 'monospace', [$style.containerCenter]: c.align === 'center' }]" :style="{ backgroundColor: c.bgColor ?? null, color: c.fgColor ?? null, borderWidth: c.borderWidth ? `${c.borderWidth}px` : 0, borderColor: c.borderColor ?? 'var(--divider)', padding: c.padding ? `${c.padding}px` : 0, borderRadius: c.rounded ? '8px' : 0 }">
|
||||||
<template v-for="child in c.children" :key="child">
|
<template v-for="child in c.children" :key="child">
|
||||||
<MkAsUi v-if="!g(child).hidden" :component="g(child)" :components="props.components" :size="size"/>
|
<MkAsUi v-if="!g(child).hidden" :component="g(child)" :components="props.components" :size="size"/>
|
||||||
@@ -51,12 +51,12 @@
|
|||||||
import { computed, defineAsyncComponent, onMounted, onUnmounted, Ref } from 'vue';
|
import { computed, defineAsyncComponent, onMounted, onUnmounted, Ref } from 'vue';
|
||||||
import * as os from '@/os';
|
import * as os from '@/os';
|
||||||
import MkButton from '@/components/MkButton.vue';
|
import MkButton from '@/components/MkButton.vue';
|
||||||
import MkInput from '@/components/form/input.vue';
|
import MkInput from '@/components/MkInput.vue';
|
||||||
import MkSwitch from '@/components/form/switch.vue';
|
import MkSwitch from '@/components/MkSwitch.vue';
|
||||||
import MkTextarea from '@/components/form/textarea.vue';
|
import MkTextarea from '@/components/MkTextarea.vue';
|
||||||
import MkSelect from '@/components/form/select.vue';
|
import MkSelect from '@/components/MkSelect.vue';
|
||||||
import { AsUiComponent } from '@/scripts/aiscript/ui';
|
import { AsUiComponent } from '@/scripts/aiscript/ui';
|
||||||
import FormFolder from '@/components/form/folder.vue';
|
import MkFolder from '@/components/MkFolder.vue';
|
||||||
|
|
||||||
const props = withDefaults(defineProps<{
|
const props = withDefaults(defineProps<{
|
||||||
component: AsUiComponent;
|
component: AsUiComponent;
|
||||||
|
@@ -46,6 +46,10 @@ import { defaultStore } from '@/store';
|
|||||||
import { emojilist } from '@/scripts/emojilist';
|
import { emojilist } from '@/scripts/emojilist';
|
||||||
import { instance } from '@/instance';
|
import { instance } from '@/instance';
|
||||||
import { i18n } from '@/i18n';
|
import { i18n } from '@/i18n';
|
||||||
|
import { miLocalStorage } from '@/local-storage';
|
||||||
|
import { getCustomEmojis } from '@/custom-emojis';
|
||||||
|
|
||||||
|
const customEmojis = await getCustomEmojis();
|
||||||
|
|
||||||
type EmojiDef = {
|
type EmojiDef = {
|
||||||
emoji: string;
|
emoji: string;
|
||||||
@@ -85,7 +89,6 @@ for (const x of lib) {
|
|||||||
emjdb.sort((a, b) => a.name.length - b.name.length);
|
emjdb.sort((a, b) => a.name.length - b.name.length);
|
||||||
|
|
||||||
//#region Construct Emoji DB
|
//#region Construct Emoji DB
|
||||||
const customEmojis = instance.emojis;
|
|
||||||
const emojiDefinitions: EmojiDef[] = [];
|
const emojiDefinitions: EmojiDef[] = [];
|
||||||
|
|
||||||
for (const x of customEmojis) {
|
for (const x of customEmojis) {
|
||||||
@@ -116,7 +119,6 @@ export default {
|
|||||||
emojiDb,
|
emojiDb,
|
||||||
emojiDefinitions,
|
emojiDefinitions,
|
||||||
emojilist,
|
emojilist,
|
||||||
customEmojis,
|
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -208,7 +210,7 @@ function exec() {
|
|||||||
}
|
}
|
||||||
} else if (props.type === 'hashtag') {
|
} else if (props.type === 'hashtag') {
|
||||||
if (!props.q || props.q === '') {
|
if (!props.q || props.q === '') {
|
||||||
hashtags.value = JSON.parse(localStorage.getItem('hashtags') || '[]');
|
hashtags.value = JSON.parse(miLocalStorage.getItem('hashtags') || '[]');
|
||||||
fetching.value = false;
|
fetching.value = false;
|
||||||
} else {
|
} else {
|
||||||
const cacheKey = `autocomplete:hashtag:${props.q}`;
|
const cacheKey = `autocomplete:hashtag:${props.q}`;
|
||||||
|
@@ -16,7 +16,6 @@
|
|||||||
*/
|
*/
|
||||||
import { onMounted, ref, shallowRef, watch, PropType, onUnmounted } from 'vue';
|
import { onMounted, ref, shallowRef, watch, PropType, onUnmounted } from 'vue';
|
||||||
import { Chart } from 'chart.js';
|
import { Chart } from 'chart.js';
|
||||||
import { enUS } from 'date-fns/locale';
|
|
||||||
import gradient from 'chartjs-plugin-gradient';
|
import gradient from 'chartjs-plugin-gradient';
|
||||||
import * as os from '@/os';
|
import * as os from '@/os';
|
||||||
import { defaultStore } from '@/store';
|
import { defaultStore } from '@/store';
|
||||||
@@ -186,6 +185,10 @@ const render = () => {
|
|||||||
time: {
|
time: {
|
||||||
stepSize: 1,
|
stepSize: 1,
|
||||||
unit: props.span === 'day' ? 'month' : 'day',
|
unit: props.span === 'day' ? 'month' : 'day',
|
||||||
|
displayFormats: {
|
||||||
|
day: 'M/d',
|
||||||
|
month: 'Y/M',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
grid: {
|
grid: {
|
||||||
},
|
},
|
||||||
@@ -194,11 +197,6 @@ const render = () => {
|
|||||||
maxRotation: 0,
|
maxRotation: 0,
|
||||||
autoSkipPadding: 16,
|
autoSkipPadding: 16,
|
||||||
},
|
},
|
||||||
adapters: {
|
|
||||||
date: {
|
|
||||||
locale: enUS,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
min: getDate(props.limit).getTime(),
|
min: getDate(props.limit).getTime(),
|
||||||
},
|
},
|
||||||
y: {
|
y: {
|
||||||
|
@@ -72,4 +72,11 @@ defineExpose({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@container (max-width: 500px) {
|
||||||
|
.root {
|
||||||
|
font-size: 90%;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
92
packages/frontend/src/components/MkClickerGame.vue
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div v-if="game.ready" :class="$style.game">
|
||||||
|
<div :class="$style.cps" class="">{{ number(cps) }}cps</div>
|
||||||
|
<div :class="$style.count" class=""><i class="ti ti-cookie" style="font-size: 70%;"></i> {{ number(cookies) }}</div>
|
||||||
|
<button v-click-anime class="_button" :class="$style.button" @click="onClick">
|
||||||
|
<img src="/client-assets/cookie.png" :class="$style.img">
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div v-else>
|
||||||
|
<MkLoading/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { computed, defineAsyncComponent, onMounted, onUnmounted } from 'vue';
|
||||||
|
import MkPlusOneEffect from '@/components/MkPlusOneEffect.vue';
|
||||||
|
import * as os from '@/os';
|
||||||
|
import { useInterval } from '@/scripts/use-interval';
|
||||||
|
import * as game from '@/scripts/clicker-game';
|
||||||
|
import number from '@/filters/number';
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const saveData = game.saveData;
|
||||||
|
const cookies = computed(() => saveData.value?.cookies);
|
||||||
|
let cps = $ref(0);
|
||||||
|
let prevCookies = $ref(0);
|
||||||
|
|
||||||
|
function onClick(ev: MouseEvent) {
|
||||||
|
saveData.value!.cookies++;
|
||||||
|
saveData.value!.totalCookies++;
|
||||||
|
saveData.value!.totalHandmadeCookies++;
|
||||||
|
saveData.value!.clicked++;
|
||||||
|
|
||||||
|
const x = ev.clientX;
|
||||||
|
const y = ev.clientY;
|
||||||
|
os.popup(MkPlusOneEffect, { x, y }, {}, 'end');
|
||||||
|
}
|
||||||
|
|
||||||
|
useInterval(() => {
|
||||||
|
const diff = saveData.value!.cookies - prevCookies;
|
||||||
|
cps = diff;
|
||||||
|
prevCookies = saveData.value!.cookies;
|
||||||
|
}, 1000, {
|
||||||
|
immediate: false,
|
||||||
|
afterMounted: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
useInterval(game.save, 1000 * 5, {
|
||||||
|
immediate: false,
|
||||||
|
afterMounted: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
await game.load();
|
||||||
|
prevCookies = saveData.value!.cookies;
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
game.save();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" module>
|
||||||
|
.game {
|
||||||
|
padding: 16px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cps {
|
||||||
|
position: absolute;
|
||||||
|
top: 12px;
|
||||||
|
left: 12px;
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.count {
|
||||||
|
font-size: 1.3em;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.img {
|
||||||
|
max-width: 90px;
|
||||||
|
}
|
||||||
|
</style>
|
@@ -74,7 +74,7 @@ function onMousedown(evt: Event) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.fade-enter-active, .fade-leave-active {
|
.fade-enter-active, .fade-leave-active {
|
||||||
transition: opacity 0.5s cubic-bezier(0.16, 1, 0.3, 1), transform 0.5s cubic-bezier(0.16, 1, 0.3, 1);
|
transition: opacity 0.3s cubic-bezier(0.16, 1, 0.3, 1), transform 0.3s cubic-bezier(0.16, 1, 0.3, 1);
|
||||||
transform-origin: left top;
|
transform-origin: left top;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -42,8 +42,8 @@
|
|||||||
import { onBeforeUnmount, onMounted, ref, shallowRef } from 'vue';
|
import { onBeforeUnmount, onMounted, ref, shallowRef } from 'vue';
|
||||||
import MkModal from '@/components/MkModal.vue';
|
import MkModal from '@/components/MkModal.vue';
|
||||||
import MkButton from '@/components/MkButton.vue';
|
import MkButton from '@/components/MkButton.vue';
|
||||||
import MkInput from '@/components/form/input.vue';
|
import MkInput from '@/components/MkInput.vue';
|
||||||
import MkSelect from '@/components/form/select.vue';
|
import MkSelect from '@/components/MkSelect.vue';
|
||||||
import { i18n } from '@/i18n';
|
import { i18n } from '@/i18n';
|
||||||
|
|
||||||
type Input = {
|
type Input = {
|
||||||
|
109
packages/frontend/src/components/MkDonation.vue
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
<template>
|
||||||
|
<div class="_panel _shadow" :class="$style.root">
|
||||||
|
<!-- TODO: インスタンス運営者が任意のテキストとリンクを設定できるようにする -->
|
||||||
|
<div :class="$style.icon">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-pig-money" width="40" height="40" viewBox="0 0 24 24" stroke-width="1" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||||
|
<path d="M15 11v.01"></path>
|
||||||
|
<path d="M5.173 8.378a3 3 0 1 1 4.656 -1.377"></path>
|
||||||
|
<path d="M16 4v3.803a6.019 6.019 0 0 1 2.658 3.197h1.341a1 1 0 0 1 1 1v2a1 1 0 0 1 -1 1h-1.342c-.336 .95 -.907 1.8 -1.658 2.473v2.027a1.5 1.5 0 0 1 -3 0v-.583a6.04 6.04 0 0 1 -1 .083h-4a6.04 6.04 0 0 1 -1 -.083v.583a1.5 1.5 0 0 1 -3 0v-2l.001 -.027a6 6 0 0 1 3.999 -10.473h2.5l4.5 -3h.001z"></path>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div :class="$style.main">
|
||||||
|
<div :class="$style.title">{{ i18n.ts.didYouLikeMisskey }}</div>
|
||||||
|
<div :class="$style.text">
|
||||||
|
<I18n :src="i18n.ts.pleaseDonate" tag="span">
|
||||||
|
<template #host>
|
||||||
|
{{ $instance.name ?? host }}
|
||||||
|
</template>
|
||||||
|
</I18n>
|
||||||
|
<div style="margin-top: 0.2em;">
|
||||||
|
<MkLink target="_blank" url="https://misskey-hub.net/docs/donate.html">{{ i18n.ts.learnMore }}</MkLink>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="_buttons">
|
||||||
|
<MkButton @click="close">{{ i18n.ts.remindMeLater }}</MkButton>
|
||||||
|
<MkButton @click="neverShow">{{ i18n.ts.neverShow }}</MkButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button class="_button" :class="$style.close" @click="close"><i class="ti ti-x"></i></button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { onMounted, shallowRef } from 'vue';
|
||||||
|
import MkButton from '@/components/MkButton.vue';
|
||||||
|
import MkLink from '@/components/MkLink.vue';
|
||||||
|
import { host } from '@/config';
|
||||||
|
import { i18n } from '@/i18n';
|
||||||
|
import * as os from '@/os';
|
||||||
|
import { miLocalStorage } from '@/local-storage';
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(ev: 'closed'): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const zIndex = os.claimZIndex('low');
|
||||||
|
|
||||||
|
function close() {
|
||||||
|
miLocalStorage.setItem('latestDonationInfoShownAt', Date.now().toString());
|
||||||
|
emit('closed');
|
||||||
|
}
|
||||||
|
|
||||||
|
function neverShow() {
|
||||||
|
miLocalStorage.setItem('neverShowDonationInfo', 'true')
|
||||||
|
close();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" module>
|
||||||
|
.root {
|
||||||
|
position: fixed;
|
||||||
|
z-index: v-bind(zIndex);
|
||||||
|
bottom: var(--margin);
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
margin: auto;
|
||||||
|
box-sizing: border-box;
|
||||||
|
width: calc(100% - (var(--margin) * 2));
|
||||||
|
max-width: 500px;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
text-align: center;
|
||||||
|
padding-top: 25px;
|
||||||
|
width: 100px;
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
|
@media (max-width: 500px) {
|
||||||
|
.icon {
|
||||||
|
width: 80px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@media (max-width: 450px) {
|
||||||
|
.icon {
|
||||||
|
width: 70px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.main {
|
||||||
|
padding: 25px 25px 25px 0;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.close {
|
||||||
|
position: absolute;
|
||||||
|
top: 8px;
|
||||||
|
right: 8px;
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text {
|
||||||
|
margin: 0.7em 0 1em 0;
|
||||||
|
}
|
||||||
|
</style>
|
@@ -1,12 +1,12 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="omfetrab" :class="['s' + size, 'w' + width, 'h' + height, { asDrawer }]" :style="{ maxHeight: maxHeight ? maxHeight + 'px' : undefined }">
|
<div class="omfetrab" :class="['s' + size, 'w' + width, 'h' + height, { asDrawer, asWindow }]" :style="{ maxHeight: maxHeight ? maxHeight + 'px' : undefined }">
|
||||||
<input ref="search" :value="q" class="search" data-prevent-emoji-insert :class="{ filled: q != null && q != '' }" :placeholder="i18n.ts.search" type="search" @input="input()" @paste.stop="paste" @keyup.enter="done()">
|
<input ref="search" :value="q" class="search" data-prevent-emoji-insert :class="{ filled: q != null && q != '' }" :placeholder="i18n.ts.search" type="search" @input="input()" @paste.stop="paste" @keyup.enter="done()">
|
||||||
<div ref="emojis" class="emojis">
|
<div v-if="customEmojis != null && customEmojiCategories != null" ref="emojisEl" class="emojis">
|
||||||
<section class="result">
|
<section class="result">
|
||||||
<div v-if="searchResultCustom.length > 0" class="body">
|
<div v-if="searchResultCustom.length > 0" class="body">
|
||||||
<button
|
<button
|
||||||
v-for="emoji in searchResultCustom"
|
v-for="emoji in searchResultCustom"
|
||||||
:key="emoji.id"
|
:key="emoji.name"
|
||||||
class="_button item"
|
class="_button item"
|
||||||
:title="emoji.name"
|
:title="emoji.name"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
@@ -85,15 +85,17 @@ import MkRippleEffect from '@/components/MkRippleEffect.vue';
|
|||||||
import * as os from '@/os';
|
import * as os from '@/os';
|
||||||
import { isTouchUsing } from '@/scripts/touch';
|
import { isTouchUsing } from '@/scripts/touch';
|
||||||
import { deviceKind } from '@/scripts/device-kind';
|
import { deviceKind } from '@/scripts/device-kind';
|
||||||
import { emojiCategories, instance } from '@/instance';
|
import { instance } from '@/instance';
|
||||||
import { i18n } from '@/i18n';
|
import { i18n } from '@/i18n';
|
||||||
import { defaultStore } from '@/store';
|
import { defaultStore } from '@/store';
|
||||||
|
import { getCustomEmojiCategories, getCustomEmojis } from '@/custom-emojis';
|
||||||
|
|
||||||
const props = withDefaults(defineProps<{
|
const props = withDefaults(defineProps<{
|
||||||
showPinned?: boolean;
|
showPinned?: boolean;
|
||||||
asReactionPicker?: boolean;
|
asReactionPicker?: boolean;
|
||||||
maxHeight?: number;
|
maxHeight?: number;
|
||||||
asDrawer?: boolean;
|
asDrawer?: boolean;
|
||||||
|
asWindow?: boolean;
|
||||||
}>(), {
|
}>(), {
|
||||||
showPinned: true,
|
showPinned: true,
|
||||||
});
|
});
|
||||||
@@ -102,8 +104,17 @@ const emit = defineEmits<{
|
|||||||
(ev: 'chosen', v: string): void;
|
(ev: 'chosen', v: string): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
let customEmojis = $ref(null);
|
||||||
|
getCustomEmojis().then((x) => {
|
||||||
|
customEmojis = x;
|
||||||
|
});
|
||||||
|
let customEmojiCategories = $ref(null);
|
||||||
|
getCustomEmojiCategories().then((x) => {
|
||||||
|
customEmojiCategories = x;
|
||||||
|
});
|
||||||
|
|
||||||
const search = shallowRef<HTMLInputElement>();
|
const search = shallowRef<HTMLInputElement>();
|
||||||
const emojis = shallowRef<HTMLDivElement>();
|
const emojisEl = shallowRef<HTMLDivElement>();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
reactions: pinned,
|
reactions: pinned,
|
||||||
@@ -117,15 +128,13 @@ const {
|
|||||||
const size = computed(() => props.asReactionPicker ? reactionPickerSize.value : 1);
|
const size = computed(() => props.asReactionPicker ? reactionPickerSize.value : 1);
|
||||||
const width = computed(() => props.asReactionPicker ? reactionPickerWidth.value : 3);
|
const width = computed(() => props.asReactionPicker ? reactionPickerWidth.value : 3);
|
||||||
const height = computed(() => props.asReactionPicker ? reactionPickerHeight.value : 2);
|
const height = computed(() => props.asReactionPicker ? reactionPickerHeight.value : 2);
|
||||||
const customEmojiCategories = emojiCategories;
|
|
||||||
const customEmojis = instance.emojis;
|
|
||||||
const q = ref<string>('');
|
const q = ref<string>('');
|
||||||
const searchResultCustom = ref<Misskey.entities.CustomEmoji[]>([]);
|
const searchResultCustom = ref<Misskey.entities.CustomEmoji[]>([]);
|
||||||
const searchResultUnicode = ref<UnicodeEmojiDef[]>([]);
|
const searchResultUnicode = ref<UnicodeEmojiDef[]>([]);
|
||||||
const tab = ref<'index' | 'custom' | 'unicode' | 'tags'>('index');
|
const tab = ref<'index' | 'custom' | 'unicode' | 'tags'>('index');
|
||||||
|
|
||||||
watch(q, () => {
|
watch(q, () => {
|
||||||
if (emojis.value) emojis.value.scrollTop = 0;
|
if (emojisEl.value) emojisEl.value.scrollTop = 0;
|
||||||
|
|
||||||
if (q.value === '') {
|
if (q.value === '') {
|
||||||
searchResultCustom.value = [];
|
searchResultCustom.value = [];
|
||||||
@@ -274,7 +283,7 @@ function focus() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function reset() {
|
function reset() {
|
||||||
if (emojis.value) emojis.value.scrollTop = 0;
|
if (emojisEl.value) emojisEl.value.scrollTop = 0;
|
||||||
q.value = '';
|
q.value = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -440,6 +449,28 @@ defineExpose({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.asWindow {
|
||||||
|
width: 100% !important;
|
||||||
|
height: 100% !important;
|
||||||
|
|
||||||
|
> .emojis {
|
||||||
|
::v-deep(section) {
|
||||||
|
> .body {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: var(--columns);
|
||||||
|
font-size: 30px;
|
||||||
|
|
||||||
|
> .item {
|
||||||
|
aspect-ratio: 1 / 1;
|
||||||
|
width: auto;
|
||||||
|
height: auto;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
> .search {
|
> .search {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
|
@@ -1,13 +1,13 @@
|
|||||||
<template>
|
<template>
|
||||||
<MkWindow ref="window"
|
<MkWindow ref="window"
|
||||||
:initial-width="null"
|
:initial-width="300"
|
||||||
:initial-height="null"
|
:initial-height="290"
|
||||||
:can-resize="false"
|
:can-resize="true"
|
||||||
:mini="true"
|
:mini="true"
|
||||||
:front="true"
|
:front="true"
|
||||||
@closed="emit('closed')"
|
@closed="emit('closed')"
|
||||||
>
|
>
|
||||||
<MkEmojiPicker :show-pinned="showPinned" :as-reaction-picker="asReactionPicker" @chosen="chosen"/>
|
<MkEmojiPicker :show-pinned="showPinned" :as-reaction-picker="asReactionPicker" as-window @chosen="chosen" :class="$style.picker"/>
|
||||||
</MkWindow>
|
</MkWindow>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -34,147 +34,8 @@ function chosen(emoji: any) {
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" module>
|
||||||
.omfetrab {
|
.picker {
|
||||||
$pad: 8px;
|
height: 100%;
|
||||||
--eachSize: 40px;
|
|
||||||
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
contain: content;
|
|
||||||
|
|
||||||
&.big {
|
|
||||||
--eachSize: 44px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.w1 {
|
|
||||||
width: calc((var(--eachSize) * 5) + (#{$pad} * 2));
|
|
||||||
}
|
|
||||||
|
|
||||||
&.w2 {
|
|
||||||
width: calc((var(--eachSize) * 6) + (#{$pad} * 2));
|
|
||||||
}
|
|
||||||
|
|
||||||
&.w3 {
|
|
||||||
width: calc((var(--eachSize) * 7) + (#{$pad} * 2));
|
|
||||||
}
|
|
||||||
|
|
||||||
&.h1 {
|
|
||||||
--height: calc((var(--eachSize) * 4) + (#{$pad} * 2));
|
|
||||||
}
|
|
||||||
|
|
||||||
&.h2 {
|
|
||||||
--height: calc((var(--eachSize) * 6) + (#{$pad} * 2));
|
|
||||||
}
|
|
||||||
|
|
||||||
&.h3 {
|
|
||||||
--height: calc((var(--eachSize) * 8) + (#{$pad} * 2));
|
|
||||||
}
|
|
||||||
|
|
||||||
> .search {
|
|
||||||
width: 100%;
|
|
||||||
padding: 12px;
|
|
||||||
box-sizing: border-box;
|
|
||||||
font-size: 1em;
|
|
||||||
outline: none;
|
|
||||||
border: none;
|
|
||||||
background: transparent;
|
|
||||||
color: var(--fg);
|
|
||||||
|
|
||||||
&:not(.filled) {
|
|
||||||
order: 1;
|
|
||||||
z-index: 2;
|
|
||||||
box-shadow: 0px -1px 0 0px var(--divider);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
> .emojis {
|
|
||||||
height: var(--height);
|
|
||||||
overflow-y: auto;
|
|
||||||
overflow-x: hidden;
|
|
||||||
|
|
||||||
scrollbar-width: none;
|
|
||||||
|
|
||||||
&::-webkit-scrollbar {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
> .index {
|
|
||||||
min-height: var(--height);
|
|
||||||
position: relative;
|
|
||||||
border-bottom: solid 0.5px var(--divider);
|
|
||||||
|
|
||||||
> .arrow {
|
|
||||||
position: absolute;
|
|
||||||
bottom: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
padding: 16px 0;
|
|
||||||
text-align: center;
|
|
||||||
opacity: 0.5;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
section {
|
|
||||||
> header {
|
|
||||||
position: sticky;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
z-index: 1;
|
|
||||||
padding: 8px;
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
> div {
|
|
||||||
padding: $pad;
|
|
||||||
|
|
||||||
> button {
|
|
||||||
position: relative;
|
|
||||||
padding: 0;
|
|
||||||
width: var(--eachSize);
|
|
||||||
height: var(--eachSize);
|
|
||||||
border-radius: 4px;
|
|
||||||
|
|
||||||
&:focus-visible {
|
|
||||||
outline: solid 2px var(--focus);
|
|
||||||
z-index: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background: rgba(0, 0, 0, 0.05);
|
|
||||||
}
|
|
||||||
|
|
||||||
&:active {
|
|
||||||
background: var(--accent);
|
|
||||||
box-shadow: inset 0 0.15em 0.3em rgba(27, 31, 35, 0.15);
|
|
||||||
}
|
|
||||||
|
|
||||||
> * {
|
|
||||||
font-size: 24px;
|
|
||||||
height: 1.25em;
|
|
||||||
vertical-align: -.25em;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.result {
|
|
||||||
border-bottom: solid 0.5px var(--divider);
|
|
||||||
|
|
||||||
&:empty {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.unicode {
|
|
||||||
min-height: 384px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.custom {
|
|
||||||
min-height: 64px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@@ -23,7 +23,7 @@
|
|||||||
import { } from 'vue';
|
import { } from 'vue';
|
||||||
import * as Misskey from 'misskey-js';
|
import * as Misskey from 'misskey-js';
|
||||||
import MkModalWindow from '@/components/MkModalWindow.vue';
|
import MkModalWindow from '@/components/MkModalWindow.vue';
|
||||||
import MkTextarea from '@/components/form/textarea.vue';
|
import MkTextarea from '@/components/MkTextarea.vue';
|
||||||
import MkDriveFileThumbnail from '@/components/MkDriveFileThumbnail.vue';
|
import MkDriveFileThumbnail from '@/components/MkDriveFileThumbnail.vue';
|
||||||
import { i18n } from '@/i18n';
|
import { i18n } from '@/i18n';
|
||||||
|
|
||||||
|
154
packages/frontend/src/components/MkFoldableSection.vue
Normal file
@@ -0,0 +1,154 @@
|
|||||||
|
<template>
|
||||||
|
<div class="ssazuxis">
|
||||||
|
<header class="_button" :style="{ background: bg }" @click="showBody = !showBody">
|
||||||
|
<div class="title"><div><slot name="header"></slot></div></div>
|
||||||
|
<div class="divider"></div>
|
||||||
|
<button class="_button">
|
||||||
|
<template v-if="showBody"><i class="ti ti-chevron-up"></i></template>
|
||||||
|
<template v-else><i class="ti ti-chevron-down"></i></template>
|
||||||
|
</button>
|
||||||
|
</header>
|
||||||
|
<Transition
|
||||||
|
:name="$store.state.animation ? 'folder-toggle' : ''"
|
||||||
|
@enter="enter"
|
||||||
|
@after-enter="afterEnter"
|
||||||
|
@leave="leave"
|
||||||
|
@after-leave="afterLeave"
|
||||||
|
>
|
||||||
|
<div v-show="showBody">
|
||||||
|
<slot></slot>
|
||||||
|
</div>
|
||||||
|
</Transition>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent } from 'vue';
|
||||||
|
import tinycolor from 'tinycolor2';
|
||||||
|
import { miLocalStorage } from '@/local-storage';
|
||||||
|
|
||||||
|
const miLocalStoragePrefix = 'ui:folder:' as const;
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
props: {
|
||||||
|
expanded: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
persistKey: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
bg: null,
|
||||||
|
showBody: (this.persistKey && miLocalStorage.getItem(`${miLocalStoragePrefix}${this.persistKey}`)) ? (miLocalStorage.getItem(`${miLocalStoragePrefix}${this.persistKey}`) === 't') : this.expanded,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
showBody() {
|
||||||
|
if (this.persistKey) {
|
||||||
|
miLocalStorage.setItem(`${miLocalStoragePrefix}${this.persistKey}`, this.showBody ? 't' : 'f');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
function getParentBg(el: Element | null): string {
|
||||||
|
if (el == null || el.tagName === 'BODY') return 'var(--bg)';
|
||||||
|
const bg = el.style.background || el.style.backgroundColor;
|
||||||
|
if (bg) {
|
||||||
|
return bg;
|
||||||
|
} else {
|
||||||
|
return getParentBg(el.parentElement);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const rawBg = getParentBg(this.$el);
|
||||||
|
const bg = tinycolor(rawBg.startsWith('var(') ? getComputedStyle(document.documentElement).getPropertyValue(rawBg.slice(4, -1)) : rawBg);
|
||||||
|
bg.setAlpha(0.85);
|
||||||
|
this.bg = bg.toRgbString();
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
toggleContent(show: boolean) {
|
||||||
|
this.showBody = show;
|
||||||
|
},
|
||||||
|
|
||||||
|
enter(el) {
|
||||||
|
const elementHeight = el.getBoundingClientRect().height;
|
||||||
|
el.style.height = 0;
|
||||||
|
el.offsetHeight; // reflow
|
||||||
|
el.style.height = elementHeight + 'px';
|
||||||
|
},
|
||||||
|
afterEnter(el) {
|
||||||
|
el.style.height = null;
|
||||||
|
},
|
||||||
|
leave(el) {
|
||||||
|
const elementHeight = el.getBoundingClientRect().height;
|
||||||
|
el.style.height = elementHeight + 'px';
|
||||||
|
el.offsetHeight; // reflow
|
||||||
|
el.style.height = 0;
|
||||||
|
},
|
||||||
|
afterLeave(el) {
|
||||||
|
el.style.height = null;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.folder-toggle-enter-active, .folder-toggle-leave-active {
|
||||||
|
overflow-y: clip;
|
||||||
|
transition: opacity 0.5s, height 0.5s !important;
|
||||||
|
}
|
||||||
|
.folder-toggle-enter-from {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
.folder-toggle-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ssazuxis {
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
> header {
|
||||||
|
display: flex;
|
||||||
|
position: relative;
|
||||||
|
z-index: 10;
|
||||||
|
position: sticky;
|
||||||
|
top: var(--stickyTop, 0px);
|
||||||
|
padding: var(--x-padding);
|
||||||
|
-webkit-backdrop-filter: var(--blur, blur(8px));
|
||||||
|
backdrop-filter: var(--blur, blur(20px));
|
||||||
|
|
||||||
|
> .title {
|
||||||
|
display: grid;
|
||||||
|
place-content: center;
|
||||||
|
margin: 0;
|
||||||
|
padding: 12px 16px 12px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
> .divider {
|
||||||
|
flex: 1;
|
||||||
|
margin: auto;
|
||||||
|
height: 1px;
|
||||||
|
background: var(--divider);
|
||||||
|
}
|
||||||
|
|
||||||
|
> button {
|
||||||
|
padding: 12px 0 12px 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@container (max-width: 500px) {
|
||||||
|
.ssazuxis {
|
||||||
|
> header {
|
||||||
|
> .title {
|
||||||
|
padding: 8px 10px 8px 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
@@ -1,160 +1,177 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="ssazuxis">
|
<div ref="rootEl" class="dwzlatin" :class="{ opened }">
|
||||||
<header class="_button" :style="{ background: bg }" @click="showBody = !showBody">
|
<div class="header _button" @click="toggle">
|
||||||
<div class="title"><slot name="header"></slot></div>
|
<span class="icon"><slot name="icon"></slot></span>
|
||||||
<div class="divider"></div>
|
<span class="text"><slot name="label"></slot></span>
|
||||||
<button class="_button">
|
<span class="right">
|
||||||
<template v-if="showBody"><i class="ti ti-chevron-up"></i></template>
|
<span class="text"><slot name="suffix"></slot></span>
|
||||||
<template v-else><i class="ti ti-chevron-down"></i></template>
|
<i v-if="opened" class="ti ti-chevron-up icon"></i>
|
||||||
</button>
|
<i v-else class="ti ti-chevron-down icon"></i>
|
||||||
</header>
|
</span>
|
||||||
<Transition
|
</div>
|
||||||
:name="$store.state.animation ? 'folder-toggle' : ''"
|
<div v-if="openedAtLeastOnce" class="body" :class="{ bgSame }" :style="{ maxHeight: maxHeight ? `${maxHeight}px` : null }">
|
||||||
@enter="enter"
|
<Transition
|
||||||
@after-enter="afterEnter"
|
:name="$store.state.animation ? 'folder-toggle' : ''"
|
||||||
@leave="leave"
|
@enter="enter"
|
||||||
@after-leave="afterLeave"
|
@after-enter="afterEnter"
|
||||||
>
|
@leave="leave"
|
||||||
<div v-show="showBody">
|
@after-leave="afterLeave"
|
||||||
<slot></slot>
|
>
|
||||||
</div>
|
<KeepAlive>
|
||||||
</Transition>
|
<div v-show="opened">
|
||||||
|
<MkSpacer :margin-min="14" :margin-max="22">
|
||||||
|
<slot></slot>
|
||||||
|
</MkSpacer>
|
||||||
|
</div>
|
||||||
|
</KeepAlive>
|
||||||
|
</Transition>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts" setup>
|
||||||
import { defineComponent } from 'vue';
|
import { nextTick, onMounted } from 'vue';
|
||||||
import tinycolor from 'tinycolor2';
|
|
||||||
|
|
||||||
const localStoragePrefix = 'ui:folder:';
|
const props = withDefaults(defineProps<{
|
||||||
|
defaultOpen: boolean;
|
||||||
|
maxHeight: number | null;
|
||||||
|
}>(), {
|
||||||
|
defaultOpen: false,
|
||||||
|
maxHeight: null,
|
||||||
|
});
|
||||||
|
|
||||||
export default defineComponent({
|
const getBgColor = (el: HTMLElement) => {
|
||||||
props: {
|
const style = window.getComputedStyle(el);
|
||||||
expanded: {
|
if (style.backgroundColor && !['rgba(0, 0, 0, 0)', 'rgba(0,0,0,0)', 'transparent'].includes(style.backgroundColor)) {
|
||||||
type: Boolean,
|
return style.backgroundColor;
|
||||||
required: false,
|
} else {
|
||||||
default: true,
|
return el.parentElement ? getBgColor(el.parentElement) : 'transparent';
|
||||||
},
|
}
|
||||||
persistKey: {
|
};
|
||||||
type: String,
|
|
||||||
required: false,
|
|
||||||
default: null,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
bg: null,
|
|
||||||
showBody: (this.persistKey && localStorage.getItem(localStoragePrefix + this.persistKey)) ? localStorage.getItem(localStoragePrefix + this.persistKey) === 't' : this.expanded,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
watch: {
|
|
||||||
showBody() {
|
|
||||||
if (this.persistKey) {
|
|
||||||
localStorage.setItem(localStoragePrefix + this.persistKey, this.showBody ? 't' : 'f');
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
mounted() {
|
|
||||||
function getParentBg(el: Element | null): string {
|
|
||||||
if (el == null || el.tagName === 'BODY') return 'var(--bg)';
|
|
||||||
const bg = el.style.background || el.style.backgroundColor;
|
|
||||||
if (bg) {
|
|
||||||
return bg;
|
|
||||||
} else {
|
|
||||||
return getParentBg(el.parentElement);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const rawBg = getParentBg(this.$el);
|
|
||||||
const bg = tinycolor(rawBg.startsWith('var(') ? getComputedStyle(document.documentElement).getPropertyValue(rawBg.slice(4, -1)) : rawBg);
|
|
||||||
bg.setAlpha(0.85);
|
|
||||||
this.bg = bg.toRgbString();
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
toggleContent(show: boolean) {
|
|
||||||
this.showBody = show;
|
|
||||||
},
|
|
||||||
|
|
||||||
enter(el) {
|
let rootEl = $ref<HTMLElement>();
|
||||||
const elementHeight = el.getBoundingClientRect().height;
|
let bgSame = $ref(false);
|
||||||
el.style.height = 0;
|
let opened = $ref(props.defaultOpen);
|
||||||
el.offsetHeight; // reflow
|
let openedAtLeastOnce = $ref(props.defaultOpen);
|
||||||
el.style.height = elementHeight + 'px';
|
|
||||||
},
|
function enter(el) {
|
||||||
afterEnter(el) {
|
const elementHeight = el.getBoundingClientRect().height;
|
||||||
el.style.height = null;
|
el.style.height = 0;
|
||||||
},
|
el.offsetHeight; // reflow
|
||||||
leave(el) {
|
el.style.height = Math.min(elementHeight, props.maxHeight ?? Infinity) + 'px';
|
||||||
const elementHeight = el.getBoundingClientRect().height;
|
}
|
||||||
el.style.height = elementHeight + 'px';
|
|
||||||
el.offsetHeight; // reflow
|
function afterEnter(el) {
|
||||||
el.style.height = 0;
|
el.style.height = null;
|
||||||
},
|
}
|
||||||
afterLeave(el) {
|
|
||||||
el.style.height = null;
|
function leave(el) {
|
||||||
},
|
const elementHeight = el.getBoundingClientRect().height;
|
||||||
},
|
el.style.height = elementHeight + 'px';
|
||||||
|
el.offsetHeight; // reflow
|
||||||
|
el.style.height = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function afterLeave(el) {
|
||||||
|
el.style.height = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggle() {
|
||||||
|
if (!opened) {
|
||||||
|
openedAtLeastOnce = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
nextTick(() => {
|
||||||
|
opened = !opened;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
const computedStyle = getComputedStyle(document.documentElement);
|
||||||
|
const parentBg = getBgColor(rootEl.parentElement);
|
||||||
|
const myBg = computedStyle.getPropertyValue('--panel');
|
||||||
|
bgSame = parentBg === myBg;
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.folder-toggle-enter-active, .folder-toggle-leave-active {
|
.folder-toggle-enter-active, .folder-toggle-leave-active {
|
||||||
overflow-y: clip;
|
overflow-y: clip;
|
||||||
transition: opacity 0.5s, height 0.5s !important;
|
transition: opacity 0.3s, height 0.3s, transform 0.3s !important;
|
||||||
}
|
}
|
||||||
.folder-toggle-enter-from {
|
.folder-toggle-enter-from, .folder-toggle-leave-to {
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
.folder-toggle-leave-to {
|
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ssazuxis {
|
.dwzlatin {
|
||||||
position: relative;
|
display: block;
|
||||||
|
|
||||||
> header {
|
> .header {
|
||||||
display: flex;
|
display: flex;
|
||||||
position: relative;
|
align-items: center;
|
||||||
z-index: 10;
|
width: 100%;
|
||||||
position: sticky;
|
box-sizing: border-box;
|
||||||
top: var(--stickyTop, 0px);
|
padding: 10px 14px 10px 14px;
|
||||||
padding: var(--x-padding);
|
background: var(--buttonBg);
|
||||||
-webkit-backdrop-filter: var(--blur, blur(8px));
|
border-radius: 6px;
|
||||||
backdrop-filter: var(--blur, blur(20px));
|
|
||||||
|
|
||||||
> .title {
|
&:hover {
|
||||||
display: grid;
|
text-decoration: none;
|
||||||
place-content: center;
|
background: var(--buttonHoverBg);
|
||||||
margin: 0;
|
}
|
||||||
padding: 12px 16px 12px 0;
|
|
||||||
|
|
||||||
> i {
|
&.active {
|
||||||
margin-right: 6px;
|
color: var(--accent);
|
||||||
}
|
background: var(--buttonHoverBg);
|
||||||
|
}
|
||||||
|
|
||||||
|
> .icon {
|
||||||
|
margin-right: 0.75em;
|
||||||
|
flex-shrink: 0;
|
||||||
|
text-align: center;
|
||||||
|
opacity: 0.8;
|
||||||
|
|
||||||
&:empty {
|
&:empty {
|
||||||
display: none;
|
display: none;
|
||||||
|
|
||||||
|
& + .text {
|
||||||
|
padding-left: 4px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
> .divider {
|
> .text {
|
||||||
flex: 1;
|
white-space: nowrap;
|
||||||
margin: auto;
|
text-overflow: ellipsis;
|
||||||
height: 1px;
|
overflow: hidden;
|
||||||
background: var(--divider);
|
padding-right: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
> button {
|
> .right {
|
||||||
padding: 12px 0 12px 16px;
|
margin-left: auto;
|
||||||
|
opacity: 0.7;
|
||||||
|
white-space: nowrap;
|
||||||
|
|
||||||
|
> .text:not(:empty) {
|
||||||
|
margin-right: 0.75em;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
@container (max-width: 500px) {
|
> .body {
|
||||||
.ssazuxis {
|
background: var(--panel);
|
||||||
> header {
|
border-radius: 0 0 6px 6px;
|
||||||
> .title {
|
container-type: inline-size;
|
||||||
padding: 8px 10px 8px 0;
|
overflow: auto;
|
||||||
}
|
|
||||||
|
&.bgSame {
|
||||||
|
background: var(--bg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.opened {
|
||||||
|
> .header {
|
||||||
|
border-radius: 6px 6px 0 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -36,7 +36,7 @@
|
|||||||
import { } from 'vue';
|
import { } from 'vue';
|
||||||
import MkModalWindow from '@/components/MkModalWindow.vue';
|
import MkModalWindow from '@/components/MkModalWindow.vue';
|
||||||
import MkButton from '@/components/MkButton.vue';
|
import MkButton from '@/components/MkButton.vue';
|
||||||
import MkInput from '@/components/form/input.vue';
|
import MkInput from '@/components/MkInput.vue';
|
||||||
import * as os from '@/os';
|
import * as os from '@/os';
|
||||||
import { instance } from '@/instance';
|
import { instance } from '@/instance';
|
||||||
import { i18n } from '@/i18n';
|
import { i18n } from '@/i18n';
|
||||||
|
@@ -17,34 +17,34 @@
|
|||||||
<MkSpacer :margin-min="20" :margin-max="32">
|
<MkSpacer :margin-min="20" :margin-max="32">
|
||||||
<div class="xkpnjxcv _gaps_m">
|
<div class="xkpnjxcv _gaps_m">
|
||||||
<template v-for="item in Object.keys(form).filter(item => !form[item].hidden)">
|
<template v-for="item in Object.keys(form).filter(item => !form[item].hidden)">
|
||||||
<FormInput v-if="form[item].type === 'number'" v-model="values[item]" type="number" :step="form[item].step || 1">
|
<MkInput v-if="form[item].type === 'number'" v-model="values[item]" type="number" :step="form[item].step || 1">
|
||||||
<template #label><span v-text="form[item].label || item"></span><span v-if="form[item].required === false"> ({{ $ts.optional }})</span></template>
|
<template #label><span v-text="form[item].label || item"></span><span v-if="form[item].required === false"> ({{ $ts.optional }})</span></template>
|
||||||
<template v-if="form[item].description" #caption>{{ form[item].description }}</template>
|
<template v-if="form[item].description" #caption>{{ form[item].description }}</template>
|
||||||
</FormInput>
|
</MkInput>
|
||||||
<FormInput v-else-if="form[item].type === 'string' && !form[item].multiline" v-model="values[item]" type="text">
|
<MkInput v-else-if="form[item].type === 'string' && !form[item].multiline" v-model="values[item]" type="text">
|
||||||
<template #label><span v-text="form[item].label || item"></span><span v-if="form[item].required === false"> ({{ $ts.optional }})</span></template>
|
<template #label><span v-text="form[item].label || item"></span><span v-if="form[item].required === false"> ({{ $ts.optional }})</span></template>
|
||||||
<template v-if="form[item].description" #caption>{{ form[item].description }}</template>
|
<template v-if="form[item].description" #caption>{{ form[item].description }}</template>
|
||||||
</FormInput>
|
</MkInput>
|
||||||
<FormTextarea v-else-if="form[item].type === 'string' && form[item].multiline" v-model="values[item]">
|
<MkTextarea v-else-if="form[item].type === 'string' && form[item].multiline" v-model="values[item]">
|
||||||
<template #label><span v-text="form[item].label || item"></span><span v-if="form[item].required === false"> ({{ $ts.optional }})</span></template>
|
<template #label><span v-text="form[item].label || item"></span><span v-if="form[item].required === false"> ({{ $ts.optional }})</span></template>
|
||||||
<template v-if="form[item].description" #caption>{{ form[item].description }}</template>
|
<template v-if="form[item].description" #caption>{{ form[item].description }}</template>
|
||||||
</FormTextarea>
|
</MkTextarea>
|
||||||
<FormSwitch v-else-if="form[item].type === 'boolean'" v-model="values[item]">
|
<MkSwitch v-else-if="form[item].type === 'boolean'" v-model="values[item]">
|
||||||
<span v-text="form[item].label || item"></span>
|
<span v-text="form[item].label || item"></span>
|
||||||
<template v-if="form[item].description" #caption>{{ form[item].description }}</template>
|
<template v-if="form[item].description" #caption>{{ form[item].description }}</template>
|
||||||
</FormSwitch>
|
</MkSwitch>
|
||||||
<FormSelect v-else-if="form[item].type === 'enum'" v-model="values[item]">
|
<MkSelect v-else-if="form[item].type === 'enum'" v-model="values[item]">
|
||||||
<template #label><span v-text="form[item].label || item"></span><span v-if="form[item].required === false"> ({{ $ts.optional }})</span></template>
|
<template #label><span v-text="form[item].label || item"></span><span v-if="form[item].required === false"> ({{ $ts.optional }})</span></template>
|
||||||
<option v-for="item in form[item].enum" :key="item.value" :value="item.value">{{ item.label }}</option>
|
<option v-for="item in form[item].enum" :key="item.value" :value="item.value">{{ item.label }}</option>
|
||||||
</FormSelect>
|
</MkSelect>
|
||||||
<FormRadios v-else-if="form[item].type === 'radio'" v-model="values[item]">
|
<MkRadios v-else-if="form[item].type === 'radio'" v-model="values[item]">
|
||||||
<template #label><span v-text="form[item].label || item"></span><span v-if="form[item].required === false"> ({{ $ts.optional }})</span></template>
|
<template #label><span v-text="form[item].label || item"></span><span v-if="form[item].required === false"> ({{ $ts.optional }})</span></template>
|
||||||
<option v-for="item in form[item].options" :key="item.value" :value="item.value">{{ item.label }}</option>
|
<option v-for="item in form[item].options" :key="item.value" :value="item.value">{{ item.label }}</option>
|
||||||
</FormRadios>
|
</MkRadios>
|
||||||
<FormRange v-else-if="form[item].type === 'range'" v-model="values[item]" :min="form[item].min" :max="form[item].max" :step="form[item].step" :text-converter="form[item].textConverter">
|
<MkRange v-else-if="form[item].type === 'range'" v-model="values[item]" :min="form[item].min" :max="form[item].max" :step="form[item].step" :text-converter="form[item].textConverter">
|
||||||
<template #label><span v-text="form[item].label || item"></span><span v-if="form[item].required === false"> ({{ $ts.optional }})</span></template>
|
<template #label><span v-text="form[item].label || item"></span><span v-if="form[item].required === false"> ({{ $ts.optional }})</span></template>
|
||||||
<template v-if="form[item].description" #caption>{{ form[item].description }}</template>
|
<template v-if="form[item].description" #caption>{{ form[item].description }}</template>
|
||||||
</FormRange>
|
</MkRange>
|
||||||
<MkButton v-else-if="form[item].type === 'button'" @click="form[item].action($event, values)">
|
<MkButton v-else-if="form[item].type === 'button'" @click="form[item].action($event, values)">
|
||||||
<span v-text="form[item].content || item"></span>
|
<span v-text="form[item].content || item"></span>
|
||||||
</MkButton>
|
</MkButton>
|
||||||
@@ -56,25 +56,25 @@
|
|||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent } from 'vue';
|
import { defineComponent } from 'vue';
|
||||||
import FormInput from './form/input.vue';
|
import MkInput from './MkInput.vue';
|
||||||
import FormTextarea from './form/textarea.vue';
|
import MkTextarea from './MkTextarea.vue';
|
||||||
import FormSwitch from './form/switch.vue';
|
import MkSwitch from './MkSwitch.vue';
|
||||||
import FormSelect from './form/select.vue';
|
import MkSelect from './MkSelect.vue';
|
||||||
import FormRange from './form/range.vue';
|
import MkRange from './MkRange.vue';
|
||||||
import MkButton from './MkButton.vue';
|
import MkButton from './MkButton.vue';
|
||||||
import FormRadios from './form/radios.vue';
|
import MkRadios from './MkRadios.vue';
|
||||||
import MkModalWindow from '@/components/MkModalWindow.vue';
|
import MkModalWindow from '@/components/MkModalWindow.vue';
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
components: {
|
components: {
|
||||||
MkModalWindow,
|
MkModalWindow,
|
||||||
FormInput,
|
MkInput,
|
||||||
FormTextarea,
|
MkTextarea,
|
||||||
FormSwitch,
|
MkSwitch,
|
||||||
FormSelect,
|
MkSelect,
|
||||||
FormRange,
|
MkRange,
|
||||||
MkButton,
|
MkButton,
|
||||||
FormRadios,
|
MkRadios,
|
||||||
},
|
},
|
||||||
|
|
||||||
props: {
|
props: {
|
||||||
|
@@ -10,7 +10,6 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { markRaw, version as vueVersion, onMounted, onBeforeUnmount, nextTick, watch } from 'vue';
|
import { markRaw, version as vueVersion, onMounted, onBeforeUnmount, nextTick, watch } from 'vue';
|
||||||
import { Chart } from 'chart.js';
|
import { Chart } from 'chart.js';
|
||||||
import { enUS } from 'date-fns/locale';
|
|
||||||
import tinycolor from 'tinycolor2';
|
import tinycolor from 'tinycolor2';
|
||||||
import { MatrixController, MatrixElement } from 'chartjs-chart-matrix';
|
import { MatrixController, MatrixElement } from 'chartjs-chart-matrix';
|
||||||
import * as os from '@/os';
|
import * as os from '@/os';
|
||||||
@@ -149,7 +148,9 @@ async function renderChart() {
|
|||||||
round: 'week',
|
round: 'week',
|
||||||
isoWeekday: 0,
|
isoWeekday: 0,
|
||||||
displayFormats: {
|
displayFormats: {
|
||||||
week: 'MMM dd',
|
day: 'M/d',
|
||||||
|
month: 'Y/M',
|
||||||
|
week: 'M/d',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
grid: {
|
grid: {
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div :class="[$style.root, { yellow: instance.isNotResponding, red: instance.isBlocked, gray: instance.isSuspended }]">
|
<div :class="[$style.root, { yellow: instance.isNotResponding, red: instance.isBlocked, gray: instance.isSuspended }]">
|
||||||
<img class="icon" :src="getInstanceIcon(instance)" alt=""/>
|
<img class="icon" :src="getInstanceIcon(instance)" alt="" loading="lazy"/>
|
||||||
<div class="body">
|
<div class="body">
|
||||||
<span class="host">{{ instance.name ?? instance.host }}</span>
|
<span class="host">{{ instance.name ?? instance.host }}</span>
|
||||||
<span class="sub _monospace"><b>{{ instance.host }}</b> / {{ instance.softwareName || '?' }} {{ instance.softwareVersion }}</span>
|
<span class="sub _monospace"><b>{{ instance.host }}</b> / {{ instance.softwareName || '?' }} {{ instance.softwareVersion }}</span>
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div :class="$style.root">
|
<div :class="$style.root">
|
||||||
<MkFolder class="item">
|
<MkFoldableSection class="item">
|
||||||
<template #header>Chart</template>
|
<template #header>Chart</template>
|
||||||
<div :class="$style.chart">
|
<div :class="$style.chart">
|
||||||
<div class="selects">
|
<div class="selects">
|
||||||
@@ -34,9 +34,9 @@
|
|||||||
<MkChart :src="chartSrc" :span="chartSpan" :limit="chartLimit" :detailed="true"></MkChart>
|
<MkChart :src="chartSrc" :span="chartSpan" :limit="chartLimit" :detailed="true"></MkChart>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</MkFolder>
|
</MkFoldableSection>
|
||||||
|
|
||||||
<MkFolder class="item">
|
<MkFoldableSection class="item">
|
||||||
<template #header>Active users heatmap</template>
|
<template #header>Active users heatmap</template>
|
||||||
<MkSelect v-model="heatmapSrc" style="margin: 0 0 12px 0;">
|
<MkSelect v-model="heatmapSrc" style="margin: 0 0 12px 0;">
|
||||||
<option value="active-users">Active users</option>
|
<option value="active-users">Active users</option>
|
||||||
@@ -48,16 +48,16 @@
|
|||||||
<div class="_panel" :class="$style.heatmap">
|
<div class="_panel" :class="$style.heatmap">
|
||||||
<MkHeatmap :src="heatmapSrc"/>
|
<MkHeatmap :src="heatmapSrc"/>
|
||||||
</div>
|
</div>
|
||||||
</MkFolder>
|
</MkFoldableSection>
|
||||||
|
|
||||||
<MkFolder class="item">
|
<MkFoldableSection class="item">
|
||||||
<template #header>Retention rate</template>
|
<template #header>Retention rate</template>
|
||||||
<div class="_panel" :class="$style.retention">
|
<div class="_panel" :class="$style.retention">
|
||||||
<MkRetentionHeatmap/>
|
<MkRetentionHeatmap/>
|
||||||
</div>
|
</div>
|
||||||
</MkFolder>
|
</MkFoldableSection>
|
||||||
|
|
||||||
<MkFolder class="item">
|
<MkFoldableSection class="item">
|
||||||
<template #header>Federation</template>
|
<template #header>Federation</template>
|
||||||
<div :class="$style.federation">
|
<div :class="$style.federation">
|
||||||
<div class="pies">
|
<div class="pies">
|
||||||
@@ -71,20 +71,20 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</MkFolder>
|
</MkFoldableSection>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { onMounted } from 'vue';
|
import { onMounted } from 'vue';
|
||||||
import { Chart } from 'chart.js';
|
import { Chart } from 'chart.js';
|
||||||
import MkSelect from '@/components/form/select.vue';
|
import MkSelect from '@/components/MkSelect.vue';
|
||||||
import MkChart from '@/components/MkChart.vue';
|
import MkChart from '@/components/MkChart.vue';
|
||||||
import { useChartTooltip } from '@/scripts/use-chart-tooltip';
|
import { useChartTooltip } from '@/scripts/use-chart-tooltip';
|
||||||
import * as os from '@/os';
|
import * as os from '@/os';
|
||||||
import { i18n } from '@/i18n';
|
import { i18n } from '@/i18n';
|
||||||
import MkHeatmap from '@/components/MkHeatmap.vue';
|
import MkHeatmap from '@/components/MkHeatmap.vue';
|
||||||
import MkFolder from '@/components/MkFolder.vue';
|
import MkFoldableSection from '@/components/MkFoldableSection.vue';
|
||||||
import MkRetentionHeatmap from '@/components/MkRetentionHeatmap.vue';
|
import MkRetentionHeatmap from '@/components/MkRetentionHeatmap.vue';
|
||||||
import { initChart } from '@/scripts/init-chart';
|
import { initChart } from '@/scripts/init-chart';
|
||||||
|
|
||||||
|
@@ -31,7 +31,7 @@
|
|||||||
<span v-if="item.indicate" class="indicator"><i class="_indicatorCircle"></i></span>
|
<span v-if="item.indicate" class="indicator"><i class="_indicatorCircle"></i></span>
|
||||||
</button>
|
</button>
|
||||||
<span v-else-if="item.type === 'switch'" :tabindex="i" class="item" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
|
<span v-else-if="item.type === 'switch'" :tabindex="i" class="item" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
|
||||||
<FormSwitch v-model="item.ref" :disabled="item.disabled" class="form-switch">{{ item.text }}</FormSwitch>
|
<MkSwitch v-model="item.ref" :disabled="item.disabled" class="form-switch">{{ item.text }}</MkSwitch>
|
||||||
</span>
|
</span>
|
||||||
<button v-else-if="item.type === 'parent'" :tabindex="i" class="_button item parent" :class="{ childShowing: childShowingItem === item }" @mouseenter="showChildren(item, $event)">
|
<button v-else-if="item.type === 'parent'" :tabindex="i" class="_button item parent" :class="{ childShowing: childShowingItem === item }" @mouseenter="showChildren(item, $event)">
|
||||||
<i v-if="item.icon" class="ti-fw" :class="item.icon"></i>
|
<i v-if="item.icon" class="ti-fw" :class="item.icon"></i>
|
||||||
@@ -58,7 +58,7 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { defineAsyncComponent, nextTick, onBeforeUnmount, onMounted, onUnmounted, Ref, ref, watch } from 'vue';
|
import { defineAsyncComponent, nextTick, onBeforeUnmount, onMounted, onUnmounted, Ref, ref, watch } from 'vue';
|
||||||
import { focusPrev, focusNext } from '@/scripts/focus';
|
import { focusPrev, focusNext } from '@/scripts/focus';
|
||||||
import FormSwitch from '@/components/form/switch.vue';
|
import MkSwitch from '@/components/MkSwitch.vue';
|
||||||
import { MenuItem, InnerMenuItem, MenuPending, MenuAction } from '@/types/menu';
|
import { MenuItem, InnerMenuItem, MenuPending, MenuAction } from '@/types/menu';
|
||||||
import * as os from '@/os';
|
import * as os from '@/os';
|
||||||
import { i18n } from '@/i18n';
|
import { i18n } from '@/i18n';
|
||||||
@@ -251,17 +251,18 @@ onBeforeUnmount(() => {
|
|||||||
color: #fff;
|
color: #fff;
|
||||||
|
|
||||||
&:before {
|
&:before {
|
||||||
background: #d42e2e;
|
background: #d42e2e !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&:active,
|
||||||
&.active {
|
&.active {
|
||||||
color: var(--fgOnAccent);
|
color: var(--fgOnAccent) !important;
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
|
|
||||||
&:before {
|
&:before {
|
||||||
background: var(--accent);
|
background: var(--accent) !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<Transition :name="transitionName" :duration="transitionDuration" appear @after-leave="emit('closed')" @enter="emit('opening')" @after-enter="onOpened">
|
<Transition :name="transitionName" :duration="transitionDuration" appear @after-leave="emit('closed')" @enter="emit('opening')" @after-enter="onOpened">
|
||||||
<div v-show="manualShowing != null ? manualShowing : showing" v-hotkey.global="keymap" class="qzhlnise" :class="{ drawer: type === 'drawer', dialog: type === 'dialog' || type === 'dialog:top', popup: type === 'popup' }" :style="{ zIndex, pointerEvents: (manualShowing != null ? manualShowing : showing) ? 'auto' : 'none', '--transformOrigin': transformOrigin }">
|
<div v-show="manualShowing != null ? manualShowing : showing" v-hotkey.global="keymap" class="qzhlnise" :class="{ drawer: type === 'drawer', dialog: type === 'dialog' || type === 'dialog:top', popup: type === 'popup' }" :style="{ zIndex, pointerEvents: (manualShowing != null ? manualShowing : showing) ? 'auto' : 'none', '--transformOrigin': transformOrigin }">
|
||||||
<div class="bg _modalBg" :class="{ transparent: transparentBg && (type === 'popup') }" :style="{ zIndex }" @click="onBgClick" @contextmenu.prevent.stop="() => {}"></div>
|
<div class="bg _modalBg" :class="{ transparent: transparentBg && (type === 'popup') }" :style="{ zIndex }" @click="onBgClick" @mousedown="onBgClick" @contextmenu.prevent.stop="() => {}"></div>
|
||||||
<div ref="content" class="content" :class="{ fixed, top: type === 'dialog:top' }" :style="{ zIndex }" @click.self="onBgClick">
|
<div ref="content" class="content" :class="{ fixed, top: type === 'dialog:top' }" :style="{ zIndex }" @click.self="onBgClick">
|
||||||
<slot :max-height="maxHeight" :type="type"></slot>
|
<slot :max-height="maxHeight" :type="type"></slot>
|
||||||
</div>
|
</div>
|
||||||
@@ -63,6 +63,7 @@ let transformOrigin = $ref('center');
|
|||||||
let showing = $ref(true);
|
let showing = $ref(true);
|
||||||
let content = $shallowRef<HTMLElement>();
|
let content = $shallowRef<HTMLElement>();
|
||||||
const zIndex = os.claimZIndex(props.zPriority);
|
const zIndex = os.claimZIndex(props.zPriority);
|
||||||
|
let useSendAnime = $ref(false);
|
||||||
const type = $computed<ModalTypes>(() => {
|
const type = $computed<ModalTypes>(() => {
|
||||||
if (props.preferType === 'auto') {
|
if (props.preferType === 'auto') {
|
||||||
if (!defaultStore.state.disableDrawer && isTouchUsing && deviceKind === 'smartphone') {
|
if (!defaultStore.state.disableDrawer && isTouchUsing && deviceKind === 'smartphone') {
|
||||||
@@ -74,15 +75,34 @@ const type = $computed<ModalTypes>(() => {
|
|||||||
return props.preferType!;
|
return props.preferType!;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
let transitionName = $ref(defaultStore.state.animation ? (type === 'drawer') ? 'modal-drawer' : (type === 'popup') ? 'modal-popup' : 'modal' : '');
|
let transitionName = $computed((() =>
|
||||||
let transitionDuration = $ref(defaultStore.state.animation ? 200 : 0);
|
defaultStore.state.animation
|
||||||
|
? useSendAnime
|
||||||
|
? 'send'
|
||||||
|
: type === 'drawer'
|
||||||
|
? 'modal-drawer'
|
||||||
|
: type === 'popup'
|
||||||
|
? 'modal-popup'
|
||||||
|
: 'modal'
|
||||||
|
: ''
|
||||||
|
));
|
||||||
|
let transitionDuration = $computed((() =>
|
||||||
|
transitionName === 'send'
|
||||||
|
? 400
|
||||||
|
: transitionName === 'modal-popup'
|
||||||
|
? 100
|
||||||
|
: transitionName === 'modal'
|
||||||
|
? 200
|
||||||
|
: transitionName === 'modal-drawer'
|
||||||
|
? 200
|
||||||
|
: 0
|
||||||
|
));
|
||||||
|
|
||||||
let contentClicking = false;
|
let contentClicking = false;
|
||||||
|
|
||||||
function close(opts: { useSendAnimation?: boolean } = {}) {
|
function close(opts: { useSendAnimation?: boolean } = {}) {
|
||||||
if (opts.useSendAnimation) {
|
if (opts.useSendAnimation) {
|
||||||
transitionName = 'send';
|
useSendAnime = true;
|
||||||
transitionDuration = 400;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line vue/no-mutating-props
|
// eslint-disable-next-line vue/no-mutating-props
|
||||||
@@ -267,9 +287,8 @@ defineExpose({
|
|||||||
}
|
}
|
||||||
|
|
||||||
> .content {
|
> .content {
|
||||||
transform-style: preserve-3d;
|
transform: translateY(0px);
|
||||||
transform: perspective(50cm) translateZ(0px) translateY(0px) rotateX(0deg);
|
transition: opacity 0.3s ease-in, transform 0.3s cubic-bezier(.5,-0.5,1,.5) !important;
|
||||||
transition: opacity 0.4s cubic-bezier(.5,-0.5,.75,1), transform 0.4s cubic-bezier(.5,-0.5,.75,1) !important;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.send-enter-from, .send-leave-to {
|
.send-enter-from, .send-leave-to {
|
||||||
@@ -280,8 +299,7 @@ defineExpose({
|
|||||||
> .content {
|
> .content {
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transform-style: preserve-3d;
|
transform: translateY(-300px);
|
||||||
transform: perspective(50cm) translateZ(-300px) translateY(-200px) rotateX(40deg);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -310,12 +328,12 @@ defineExpose({
|
|||||||
|
|
||||||
.modal-popup-enter-active, .modal-popup-leave-active {
|
.modal-popup-enter-active, .modal-popup-leave-active {
|
||||||
> .bg {
|
> .bg {
|
||||||
transition: opacity 0.2s !important;
|
transition: opacity 0.1s !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
> .content {
|
> .content {
|
||||||
transform-origin: var(--transformOrigin);
|
transform-origin: var(--transformOrigin);
|
||||||
transition: opacity 0.2s cubic-bezier(0, 0, 0.2, 1), transform 0.2s cubic-bezier(0, 0, 0.2, 1) !important;
|
transition: opacity 0.1s cubic-bezier(0, 0, 0.2, 1), transform 0.1s cubic-bezier(0, 0, 0.2, 1) !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.modal-popup-enter-from, .modal-popup-leave-to {
|
.modal-popup-enter-from, .modal-popup-leave-to {
|
||||||
|
@@ -13,7 +13,7 @@
|
|||||||
<div v-if="appearNote._prId_" class="info"><i class="fas fa-bullhorn"></i> {{ i18n.ts.promotion }}<button class="_textButton hide" @click="readPromo()">{{ i18n.ts.hideThisNote }} <i class="ti ti-x"></i></button></div>
|
<div v-if="appearNote._prId_" class="info"><i class="fas fa-bullhorn"></i> {{ i18n.ts.promotion }}<button class="_textButton hide" @click="readPromo()">{{ i18n.ts.hideThisNote }} <i class="ti ti-x"></i></button></div>
|
||||||
<div v-if="appearNote._featuredId_" class="info"><i class="ti ti-bolt"></i> {{ i18n.ts.featured }}</div>
|
<div v-if="appearNote._featuredId_" class="info"><i class="ti ti-bolt"></i> {{ i18n.ts.featured }}</div>
|
||||||
<div v-if="isRenote" class="renote">
|
<div v-if="isRenote" class="renote">
|
||||||
<MkAvatar class="avatar" :user="note.user"/>
|
<MkAvatar v-once class="avatar" :user="note.user"/>
|
||||||
<i class="ti ti-repeat"></i>
|
<i class="ti ti-repeat"></i>
|
||||||
<I18n :src="i18n.ts.renotedBy" tag="span">
|
<I18n :src="i18n.ts.renotedBy" tag="span">
|
||||||
<template #user>
|
<template #user>
|
||||||
@@ -27,11 +27,16 @@
|
|||||||
<i v-if="isMyRenote" class="ti ti-dots dropdownIcon"></i>
|
<i v-if="isMyRenote" class="ti ti-dots dropdownIcon"></i>
|
||||||
<MkTime :time="note.createdAt"/>
|
<MkTime :time="note.createdAt"/>
|
||||||
</button>
|
</button>
|
||||||
<MkVisibility :note="note"/>
|
<span v-if="note.visibility !== 'public'" style="margin-left: 0.5em;" :title="i18n.ts._visibility[note.visibility]">
|
||||||
|
<i v-if="note.visibility === 'home'" class="ti ti-home"></i>
|
||||||
|
<i v-else-if="note.visibility === 'followers'" class="ti ti-lock"></i>
|
||||||
|
<i v-else-if="note.visibility === 'specified'" ref="specified" class="ti ti-mail"></i>
|
||||||
|
</span>
|
||||||
|
<span v-if="note.localOnly" style="margin-left: 0.5em;" :title="i18n.ts._visibility['localOnly']"><i class="ti ti-world-off"></i></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<article class="article" @contextmenu.stop="onContextmenu">
|
<article class="article" @contextmenu.stop="onContextmenu">
|
||||||
<MkAvatar class="avatar" :user="appearNote.user"/>
|
<MkAvatar v-once class="avatar" :user="appearNote.user"/>
|
||||||
<div class="main">
|
<div class="main">
|
||||||
<MkNoteHeader class="header" :note="appearNote" :mini="true"/>
|
<MkNoteHeader class="header" :note="appearNote" :mini="true"/>
|
||||||
<MkInstanceTicker v-if="showTicker" class="ticker" :instance="appearNote.user.instance"/>
|
<MkInstanceTicker v-if="showTicker" class="ticker" :instance="appearNote.user.instance"/>
|
||||||
@@ -44,7 +49,7 @@
|
|||||||
<div class="text">
|
<div class="text">
|
||||||
<span v-if="appearNote.isHidden" style="opacity: 0.5">({{ i18n.ts.private }})</span>
|
<span v-if="appearNote.isHidden" style="opacity: 0.5">({{ i18n.ts.private }})</span>
|
||||||
<MkA v-if="appearNote.replyId" class="reply" :to="`/notes/${appearNote.replyId}`"><i class="ti ti-arrow-back-up"></i></MkA>
|
<MkA v-if="appearNote.replyId" class="reply" :to="`/notes/${appearNote.replyId}`"><i class="ti ti-arrow-back-up"></i></MkA>
|
||||||
<Mfm v-if="appearNote.text" :text="appearNote.text" :author="appearNote.user" :i="$i"/>
|
<Mfm v-if="appearNote.text" v-once :text="appearNote.text" :author="appearNote.user" :i="$i"/>
|
||||||
<a v-if="appearNote.renote != null" class="rp">RN:</a>
|
<a v-if="appearNote.renote != null" class="rp">RN:</a>
|
||||||
<div v-if="translating || translation" class="translation">
|
<div v-if="translating || translation" class="translation">
|
||||||
<MkLoading v-if="translating" mini/>
|
<MkLoading v-if="translating" mini/>
|
||||||
@@ -75,14 +80,25 @@
|
|||||||
<i class="ti ti-arrow-back-up"></i>
|
<i class="ti ti-arrow-back-up"></i>
|
||||||
<p v-if="appearNote.repliesCount > 0" class="count">{{ appearNote.repliesCount }}</p>
|
<p v-if="appearNote.repliesCount > 0" class="count">{{ appearNote.repliesCount }}</p>
|
||||||
</button>
|
</button>
|
||||||
<MkRenoteButton ref="renoteButton" class="button" :note="appearNote" :count="appearNote.renoteCount"/>
|
<button
|
||||||
<button v-if="appearNote.myReaction == null" ref="reactButton" class="button _button" @click="react()">
|
v-if="canRenote"
|
||||||
|
ref="renoteButton"
|
||||||
|
class="button _button"
|
||||||
|
@mousedown="renote()"
|
||||||
|
>
|
||||||
|
<i class="ti ti-repeat"></i>
|
||||||
|
<p v-if="appearNote.renoteCount > 0" class="count">{{ appearNote.renoteCount }}</p>
|
||||||
|
</button>
|
||||||
|
<button v-else class="button _button" disabled>
|
||||||
|
<i class="ti ti-ban"></i>
|
||||||
|
</button>
|
||||||
|
<button v-if="appearNote.myReaction == null" ref="reactButton" class="button _button" @mousedown="react()">
|
||||||
<i class="ti ti-plus"></i>
|
<i class="ti ti-plus"></i>
|
||||||
</button>
|
</button>
|
||||||
<button v-if="appearNote.myReaction != null" ref="reactButton" class="button _button reacted" @click="undoReact(appearNote)">
|
<button v-if="appearNote.myReaction != null" ref="reactButton" class="button _button reacted" @click="undoReact(appearNote)">
|
||||||
<i class="ti ti-minus"></i>
|
<i class="ti ti-minus"></i>
|
||||||
</button>
|
</button>
|
||||||
<button ref="menuButton" class="button _button" @click="menu()">
|
<button ref="menuButton" class="button _button" @mousedown="menu()">
|
||||||
<i class="ti ti-dots"></i>
|
<i class="ti ti-dots"></i>
|
||||||
</button>
|
</button>
|
||||||
</footer>
|
</footer>
|
||||||
@@ -111,10 +127,9 @@ import MkReactionsViewer from '@/components/MkReactionsViewer.vue';
|
|||||||
import MkMediaList from '@/components/MkMediaList.vue';
|
import MkMediaList from '@/components/MkMediaList.vue';
|
||||||
import MkCwButton from '@/components/MkCwButton.vue';
|
import MkCwButton from '@/components/MkCwButton.vue';
|
||||||
import MkPoll from '@/components/MkPoll.vue';
|
import MkPoll from '@/components/MkPoll.vue';
|
||||||
import MkRenoteButton from '@/components/MkRenoteButton.vue';
|
import MkUsersTooltip from '@/components/MkUsersTooltip.vue';
|
||||||
import MkUrlPreview from '@/components/MkUrlPreview.vue';
|
import MkUrlPreview from '@/components/MkUrlPreview.vue';
|
||||||
import MkInstanceTicker from '@/components/MkInstanceTicker.vue';
|
import MkInstanceTicker from '@/components/MkInstanceTicker.vue';
|
||||||
import MkVisibility from '@/components/MkVisibility.vue';
|
|
||||||
import { pleaseLogin } from '@/scripts/please-login';
|
import { pleaseLogin } from '@/scripts/please-login';
|
||||||
import { focusPrev, focusNext } from '@/scripts/focus';
|
import { focusPrev, focusNext } from '@/scripts/focus';
|
||||||
import { checkWordMute } from '@/scripts/check-word-mute';
|
import { checkWordMute } from '@/scripts/check-word-mute';
|
||||||
@@ -128,6 +143,7 @@ import { i18n } from '@/i18n';
|
|||||||
import { getNoteMenu } from '@/scripts/get-note-menu';
|
import { getNoteMenu } from '@/scripts/get-note-menu';
|
||||||
import { useNoteCapture } from '@/scripts/use-note-capture';
|
import { useNoteCapture } from '@/scripts/use-note-capture';
|
||||||
import { deepClone } from '@/scripts/clone';
|
import { deepClone } from '@/scripts/clone';
|
||||||
|
import { useTooltip } from '@/scripts/use-tooltip';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
note: misskey.entities.Note;
|
note: misskey.entities.Note;
|
||||||
@@ -158,7 +174,7 @@ const isRenote = (
|
|||||||
|
|
||||||
const el = shallowRef<HTMLElement>();
|
const el = shallowRef<HTMLElement>();
|
||||||
const menuButton = shallowRef<HTMLElement>();
|
const menuButton = shallowRef<HTMLElement>();
|
||||||
const renoteButton = shallowRef<InstanceType<typeof MkRenoteButton>>();
|
const renoteButton = shallowRef<HTMLElement>();
|
||||||
const renoteTime = shallowRef<HTMLElement>();
|
const renoteTime = shallowRef<HTMLElement>();
|
||||||
const reactButton = shallowRef<HTMLElement>();
|
const reactButton = shallowRef<HTMLElement>();
|
||||||
let appearNote = $computed(() => isRenote ? note.renote as misskey.entities.Note : note);
|
let appearNote = $computed(() => isRenote ? note.renote as misskey.entities.Note : note);
|
||||||
@@ -175,6 +191,7 @@ const translation = ref(null);
|
|||||||
const translating = ref(false);
|
const translating = ref(false);
|
||||||
const urls = appearNote.text ? extractUrlFromMfm(mfm.parse(appearNote.text)) : null;
|
const urls = appearNote.text ? extractUrlFromMfm(mfm.parse(appearNote.text)) : null;
|
||||||
const showTicker = (defaultStore.state.instanceTicker === 'always') || (defaultStore.state.instanceTicker === 'remote' && appearNote.user.instance);
|
const showTicker = (defaultStore.state.instanceTicker === 'always') || (defaultStore.state.instanceTicker === 'remote' && appearNote.user.instance);
|
||||||
|
const canRenote = computed(() => ['public', 'home'].includes(appearNote.visibility) || appearNote.userId === $i.id);
|
||||||
|
|
||||||
const keymap = {
|
const keymap = {
|
||||||
'r': () => reply(true),
|
'r': () => reply(true),
|
||||||
@@ -193,6 +210,47 @@ useNoteCapture({
|
|||||||
isDeletedRef: isDeleted,
|
isDeletedRef: isDeleted,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
useTooltip(renoteButton, async (showing) => {
|
||||||
|
const renotes = await os.api('notes/renotes', {
|
||||||
|
noteId: appearNote.id,
|
||||||
|
limit: 11,
|
||||||
|
});
|
||||||
|
|
||||||
|
const users = renotes.map(x => x.user);
|
||||||
|
|
||||||
|
if (users.length < 1) return;
|
||||||
|
|
||||||
|
os.popup(MkUsersTooltip, {
|
||||||
|
showing,
|
||||||
|
users,
|
||||||
|
count: appearNote.renoteCount,
|
||||||
|
targetElement: renoteButton.value,
|
||||||
|
}, {}, 'closed');
|
||||||
|
});
|
||||||
|
|
||||||
|
function renote(viaKeyboard = false) {
|
||||||
|
pleaseLogin();
|
||||||
|
os.popupMenu([{
|
||||||
|
text: i18n.ts.renote,
|
||||||
|
icon: 'ti ti-repeat',
|
||||||
|
action: () => {
|
||||||
|
os.api('notes/create', {
|
||||||
|
renoteId: appearNote.id,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
text: i18n.ts.quote,
|
||||||
|
icon: 'ti ti-quote',
|
||||||
|
action: () => {
|
||||||
|
os.post({
|
||||||
|
renote: appearNote,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
}], renoteButton.value, {
|
||||||
|
viaKeyboard,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function reply(viaKeyboard = false): void {
|
function reply(viaKeyboard = false): void {
|
||||||
pleaseLogin();
|
pleaseLogin();
|
||||||
os.post({
|
os.post({
|
||||||
|
@@ -25,7 +25,12 @@
|
|||||||
<i v-if="isMyRenote" class="ti ti-dots dropdownIcon"></i>
|
<i v-if="isMyRenote" class="ti ti-dots dropdownIcon"></i>
|
||||||
<MkTime :time="note.createdAt"/>
|
<MkTime :time="note.createdAt"/>
|
||||||
</button>
|
</button>
|
||||||
<MkVisibility :note="note"/>
|
<span v-if="note.visibility !== 'public'" style="margin-left: 0.5em;" :title="i18n.ts._visibility[note.visibility]">
|
||||||
|
<i v-if="note.visibility === 'home'" class="ti ti-home"></i>
|
||||||
|
<i v-else-if="note.visibility === 'followers'" class="ti ti-lock"></i>
|
||||||
|
<i v-else-if="note.visibility === 'specified'" ref="specified" class="ti ti-mail"></i>
|
||||||
|
</span>
|
||||||
|
<span v-if="note.localOnly" style="margin-left: 0.5em;" :title="i18n.ts._visibility['localOnly']"><i class="ti ti-world-off"></i></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<article class="article" @contextmenu.stop="onContextmenu">
|
<article class="article" @contextmenu.stop="onContextmenu">
|
||||||
@@ -38,7 +43,12 @@
|
|||||||
</MkA>
|
</MkA>
|
||||||
<span v-if="appearNote.user.isBot" class="is-bot">bot</span>
|
<span v-if="appearNote.user.isBot" class="is-bot">bot</span>
|
||||||
<div class="info">
|
<div class="info">
|
||||||
<MkVisibility :note="appearNote"/>
|
<span v-if="appearNote.visibility !== 'public'" style="margin-left: 0.5em;" :title="i18n.ts._visibility[appearNote.visibility]">
|
||||||
|
<i v-if="appearNote.visibility === 'home'" class="ti ti-home"></i>
|
||||||
|
<i v-else-if="appearNote.visibility === 'followers'" class="ti ti-lock"></i>
|
||||||
|
<i v-else-if="appearNote.visibility === 'specified'" ref="specified" class="ti ti-mail"></i>
|
||||||
|
</span>
|
||||||
|
<span v-if="appearNote.localOnly" style="margin-left: 0.5em;" :title="i18n.ts._visibility['localOnly']"><i class="ti ti-world-off"></i></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="username"><MkAcct :user="appearNote.user"/></div>
|
<div class="username"><MkAcct :user="appearNote.user"/></div>
|
||||||
@@ -85,14 +95,25 @@
|
|||||||
<i class="ti ti-arrow-back-up"></i>
|
<i class="ti ti-arrow-back-up"></i>
|
||||||
<p v-if="appearNote.repliesCount > 0" class="count">{{ appearNote.repliesCount }}</p>
|
<p v-if="appearNote.repliesCount > 0" class="count">{{ appearNote.repliesCount }}</p>
|
||||||
</button>
|
</button>
|
||||||
<MkRenoteButton ref="renoteButton" class="button" :note="appearNote" :count="appearNote.renoteCount"/>
|
<button
|
||||||
<button v-if="appearNote.myReaction == null" ref="reactButton" class="button _button" @click="react()">
|
v-if="canRenote"
|
||||||
|
ref="renoteButton"
|
||||||
|
class="button _button"
|
||||||
|
@mousedown="renote()"
|
||||||
|
>
|
||||||
|
<i class="ti ti-repeat"></i>
|
||||||
|
<p v-if="appearNote.renoteCount > 0" class="count">{{ appearNote.renoteCount }}</p>
|
||||||
|
</button>
|
||||||
|
<button v-else class="button _button" disabled>
|
||||||
|
<i class="ti ti-ban"></i>
|
||||||
|
</button>
|
||||||
|
<button v-if="appearNote.myReaction == null" ref="reactButton" class="button _button" @mousedown="react()">
|
||||||
<i class="ti ti-plus"></i>
|
<i class="ti ti-plus"></i>
|
||||||
</button>
|
</button>
|
||||||
<button v-if="appearNote.myReaction != null" ref="reactButton" class="button _button reacted" @click="undoReact(appearNote)">
|
<button v-if="appearNote.myReaction != null" ref="reactButton" class="button _button reacted" @click="undoReact(appearNote)">
|
||||||
<i class="ti ti-minus"></i>
|
<i class="ti ti-minus"></i>
|
||||||
</button>
|
</button>
|
||||||
<button ref="menuButton" class="button _button" @click="menu()">
|
<button ref="menuButton" class="button _button" @mousedown="menu()">
|
||||||
<i class="ti ti-dots"></i>
|
<i class="ti ti-dots"></i>
|
||||||
</button>
|
</button>
|
||||||
</footer>
|
</footer>
|
||||||
@@ -121,10 +142,9 @@ import MkReactionsViewer from '@/components/MkReactionsViewer.vue';
|
|||||||
import MkMediaList from '@/components/MkMediaList.vue';
|
import MkMediaList from '@/components/MkMediaList.vue';
|
||||||
import MkCwButton from '@/components/MkCwButton.vue';
|
import MkCwButton from '@/components/MkCwButton.vue';
|
||||||
import MkPoll from '@/components/MkPoll.vue';
|
import MkPoll from '@/components/MkPoll.vue';
|
||||||
import MkRenoteButton from '@/components/MkRenoteButton.vue';
|
import MkUsersTooltip from '@/components/MkUsersTooltip.vue';
|
||||||
import MkUrlPreview from '@/components/MkUrlPreview.vue';
|
import MkUrlPreview from '@/components/MkUrlPreview.vue';
|
||||||
import MkInstanceTicker from '@/components/MkInstanceTicker.vue';
|
import MkInstanceTicker from '@/components/MkInstanceTicker.vue';
|
||||||
import MkVisibility from '@/components/MkVisibility.vue';
|
|
||||||
import { pleaseLogin } from '@/scripts/please-login';
|
import { pleaseLogin } from '@/scripts/please-login';
|
||||||
import { checkWordMute } from '@/scripts/check-word-mute';
|
import { checkWordMute } from '@/scripts/check-word-mute';
|
||||||
import { userPage } from '@/filters/user';
|
import { userPage } from '@/filters/user';
|
||||||
@@ -138,6 +158,7 @@ import { i18n } from '@/i18n';
|
|||||||
import { getNoteMenu } from '@/scripts/get-note-menu';
|
import { getNoteMenu } from '@/scripts/get-note-menu';
|
||||||
import { useNoteCapture } from '@/scripts/use-note-capture';
|
import { useNoteCapture } from '@/scripts/use-note-capture';
|
||||||
import { deepClone } from '@/scripts/clone';
|
import { deepClone } from '@/scripts/clone';
|
||||||
|
import { useTooltip } from '@/scripts/use-tooltip';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
note: misskey.entities.Note;
|
note: misskey.entities.Note;
|
||||||
@@ -168,7 +189,7 @@ const isRenote = (
|
|||||||
|
|
||||||
const el = shallowRef<HTMLElement>();
|
const el = shallowRef<HTMLElement>();
|
||||||
const menuButton = shallowRef<HTMLElement>();
|
const menuButton = shallowRef<HTMLElement>();
|
||||||
const renoteButton = shallowRef<InstanceType<typeof MkRenoteButton>>();
|
const renoteButton = shallowRef<HTMLElement>();
|
||||||
const renoteTime = shallowRef<HTMLElement>();
|
const renoteTime = shallowRef<HTMLElement>();
|
||||||
const reactButton = shallowRef<HTMLElement>();
|
const reactButton = shallowRef<HTMLElement>();
|
||||||
let appearNote = $computed(() => isRenote ? note.renote as misskey.entities.Note : note);
|
let appearNote = $computed(() => isRenote ? note.renote as misskey.entities.Note : note);
|
||||||
@@ -182,6 +203,7 @@ const urls = appearNote.text ? extractUrlFromMfm(mfm.parse(appearNote.text)) : n
|
|||||||
const showTicker = (defaultStore.state.instanceTicker === 'always') || (defaultStore.state.instanceTicker === 'remote' && appearNote.user.instance);
|
const showTicker = (defaultStore.state.instanceTicker === 'always') || (defaultStore.state.instanceTicker === 'remote' && appearNote.user.instance);
|
||||||
const conversation = ref<misskey.entities.Note[]>([]);
|
const conversation = ref<misskey.entities.Note[]>([]);
|
||||||
const replies = ref<misskey.entities.Note[]>([]);
|
const replies = ref<misskey.entities.Note[]>([]);
|
||||||
|
const canRenote = computed(() => ['public', 'home'].includes(appearNote.visibility) || appearNote.userId === $i.id);
|
||||||
|
|
||||||
const keymap = {
|
const keymap = {
|
||||||
'r': () => reply(true),
|
'r': () => reply(true),
|
||||||
@@ -198,6 +220,47 @@ useNoteCapture({
|
|||||||
isDeletedRef: isDeleted,
|
isDeletedRef: isDeleted,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
useTooltip(renoteButton, async (showing) => {
|
||||||
|
const renotes = await os.api('notes/renotes', {
|
||||||
|
noteId: appearNote.id,
|
||||||
|
limit: 11,
|
||||||
|
});
|
||||||
|
|
||||||
|
const users = renotes.map(x => x.user);
|
||||||
|
|
||||||
|
if (users.length < 1) return;
|
||||||
|
|
||||||
|
os.popup(MkUsersTooltip, {
|
||||||
|
showing,
|
||||||
|
users,
|
||||||
|
count: appearNote.renoteCount,
|
||||||
|
targetElement: renoteButton.value,
|
||||||
|
}, {}, 'closed');
|
||||||
|
});
|
||||||
|
|
||||||
|
function renote(viaKeyboard = false) {
|
||||||
|
pleaseLogin();
|
||||||
|
os.popupMenu([{
|
||||||
|
text: i18n.ts.renote,
|
||||||
|
icon: 'ti ti-repeat',
|
||||||
|
action: () => {
|
||||||
|
os.api('notes/create', {
|
||||||
|
renoteId: appearNote.id,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
text: i18n.ts.quote,
|
||||||
|
icon: 'ti ti-quote',
|
||||||
|
action: () => {
|
||||||
|
os.post({
|
||||||
|
renote: appearNote,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
}], renoteButton.value, {
|
||||||
|
viaKeyboard,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function reply(viaKeyboard = false): void {
|
function reply(viaKeyboard = false): void {
|
||||||
pleaseLogin();
|
pleaseLogin();
|
||||||
os.post({
|
os.post({
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<header class="kkwtjztg">
|
<header class="kkwtjztg">
|
||||||
<MkA v-user-preview="note.user.id" class="name" :to="userPage(note.user)">
|
<MkA v-once v-user-preview="note.user.id" class="name" :to="userPage(note.user)">
|
||||||
<MkUserName :user="note.user"/>
|
<MkUserName :user="note.user"/>
|
||||||
</MkA>
|
</MkA>
|
||||||
<div v-if="note.user.isBot" class="is-bot">bot</div>
|
<div v-if="note.user.isBot" class="is-bot">bot</div>
|
||||||
@@ -9,7 +9,12 @@
|
|||||||
<MkA class="created-at" :to="notePage(note)">
|
<MkA class="created-at" :to="notePage(note)">
|
||||||
<MkTime :time="note.createdAt"/>
|
<MkTime :time="note.createdAt"/>
|
||||||
</MkA>
|
</MkA>
|
||||||
<MkVisibility :note="note"/>
|
<span v-if="note.visibility !== 'public'" style="margin-left: 0.5em;" :title="i18n.ts._visibility[note.visibility]">
|
||||||
|
<i v-if="note.visibility === 'home'" class="ti ti-home"></i>
|
||||||
|
<i v-else-if="note.visibility === 'followers'" class="ti ti-lock"></i>
|
||||||
|
<i v-else-if="note.visibility === 'specified'" ref="specified" class="ti ti-mail"></i>
|
||||||
|
</span>
|
||||||
|
<span v-if="note.localOnly" style="margin-left: 0.5em;" :title="i18n.ts._visibility['localOnly']"><i class="ti ti-world-off"></i></span>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
</template>
|
</template>
|
||||||
@@ -17,7 +22,7 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { } from 'vue';
|
import { } from 'vue';
|
||||||
import * as misskey from 'misskey-js';
|
import * as misskey from 'misskey-js';
|
||||||
import MkVisibility from '@/components/MkVisibility.vue';
|
import { i18n } from '@/i18n';
|
||||||
import { notePage } from '@/filters/note';
|
import { notePage } from '@/filters/note';
|
||||||
import { userPage } from '@/filters/user';
|
import { userPage } from '@/filters/user';
|
||||||
|
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div ref="elRef" class="qglefbjs" :class="notification.type">
|
<div ref="elRef" class="qglefbjs" :class="notification.type">
|
||||||
<div class="head">
|
<div v-once class="head">
|
||||||
<MkAvatar v-if="notification.type === 'pollEnded'" class="icon" :user="notification.note.user"/>
|
<MkAvatar v-if="notification.type === 'pollEnded'" class="icon" :user="notification.note.user"/>
|
||||||
<MkAvatar v-else-if="notification.user" class="icon" :user="notification.user"/>
|
<MkAvatar v-else-if="notification.user" class="icon" :user="notification.user"/>
|
||||||
<img v-else-if="notification.icon" class="icon" :src="notification.icon" alt=""/>
|
<img v-else-if="notification.icon" class="icon" :src="notification.icon" alt=""/>
|
||||||
@@ -13,10 +13,9 @@
|
|||||||
<i v-else-if="notification.type === 'reply'" class="ti ti-arrow-back-up"></i>
|
<i v-else-if="notification.type === 'reply'" class="ti ti-arrow-back-up"></i>
|
||||||
<i v-else-if="notification.type === 'mention'" class="ti ti-at"></i>
|
<i v-else-if="notification.type === 'mention'" class="ti ti-at"></i>
|
||||||
<i v-else-if="notification.type === 'quote'" class="ti ti-quote"></i>
|
<i v-else-if="notification.type === 'quote'" class="ti ti-quote"></i>
|
||||||
<i v-else-if="notification.type === 'pollVote'" class="ti ti-chart-arrows"></i>
|
|
||||||
<i v-else-if="notification.type === 'pollEnded'" class="ti ti-chart-arrows"></i>
|
<i v-else-if="notification.type === 'pollEnded'" class="ti ti-chart-arrows"></i>
|
||||||
<!-- notification.reaction が null になることはまずないが、ここでoptional chaining使うと一部ブラウザで刺さるので念の為 -->
|
<!-- notification.reaction が null になることはまずないが、ここでoptional chaining使うと一部ブラウザで刺さるので念の為 -->
|
||||||
<XReactionIcon
|
<MkReactionIcon
|
||||||
v-else-if="notification.type === 'reaction'"
|
v-else-if="notification.type === 'reaction'"
|
||||||
ref="reactionRef"
|
ref="reactionRef"
|
||||||
:reaction="notification.reaction ? notification.reaction.replace(/^:(\w+):$/, ':$1@.:') : notification.reaction"
|
:reaction="notification.reaction ? notification.reaction.replace(/^:(\w+):$/, ':$1@.:') : notification.reaction"
|
||||||
@@ -32,42 +31,39 @@
|
|||||||
<span v-else>{{ notification.header }}</span>
|
<span v-else>{{ notification.header }}</span>
|
||||||
<MkTime v-if="withTime" :time="notification.createdAt" class="time"/>
|
<MkTime v-if="withTime" :time="notification.createdAt" class="time"/>
|
||||||
</header>
|
</header>
|
||||||
<MkA v-if="notification.type === 'reaction'" class="text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)">
|
<div v-once class="content">
|
||||||
<i class="ti ti-quote"></i>
|
<MkA v-if="notification.type === 'reaction'" class="text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)">
|
||||||
<Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="!full"/>
|
<i class="ti ti-quote"></i>
|
||||||
<i class="ti ti-quote"></i>
|
<Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="!full"/>
|
||||||
</MkA>
|
<i class="ti ti-quote"></i>
|
||||||
<MkA v-if="notification.type === 'renote'" class="text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note.renote)">
|
</MkA>
|
||||||
<i class="ti ti-quote"></i>
|
<MkA v-else-if="notification.type === 'renote'" class="text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note.renote)">
|
||||||
<Mfm :text="getNoteSummary(notification.note.renote)" :plain="true" :nowrap="!full"/>
|
<i class="ti ti-quote"></i>
|
||||||
<i class="ti ti-quote"></i>
|
<Mfm :text="getNoteSummary(notification.note.renote)" :plain="true" :nowrap="!full"/>
|
||||||
</MkA>
|
<i class="ti ti-quote"></i>
|
||||||
<MkA v-if="notification.type === 'reply'" class="text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)">
|
</MkA>
|
||||||
<Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="!full"/>
|
<MkA v-else-if="notification.type === 'reply'" class="text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)">
|
||||||
</MkA>
|
<Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="!full"/>
|
||||||
<MkA v-if="notification.type === 'mention'" class="text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)">
|
</MkA>
|
||||||
<Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="!full"/>
|
<MkA v-else-if="notification.type === 'mention'" class="text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)">
|
||||||
</MkA>
|
<Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="!full"/>
|
||||||
<MkA v-if="notification.type === 'quote'" class="text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)">
|
</MkA>
|
||||||
<Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="!full"/>
|
<MkA v-else-if="notification.type === 'quote'" class="text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)">
|
||||||
</MkA>
|
<Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="!full"/>
|
||||||
<MkA v-if="notification.type === 'pollVote'" class="text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)">
|
</MkA>
|
||||||
<i class="ti ti-quote"></i>
|
<MkA v-else-if="notification.type === 'pollEnded'" class="text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)">
|
||||||
<Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="!full"/>
|
<i class="ti ti-quote"></i>
|
||||||
<i class="ti ti-quote"></i>
|
<Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="!full"/>
|
||||||
</MkA>
|
<i class="ti ti-quote"></i>
|
||||||
<MkA v-if="notification.type === 'pollEnded'" class="text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)">
|
</MkA>
|
||||||
<i class="ti ti-quote"></i>
|
<span v-else-if="notification.type === 'follow'" class="text" style="opacity: 0.6;">{{ i18n.ts.youGotNewFollower }}<div v-if="full"><MkFollowButton :user="notification.user" :full="true"/></div></span>
|
||||||
<Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="!full"/>
|
<span v-else-if="notification.type === 'followRequestAccepted'" class="text" style="opacity: 0.6;">{{ i18n.ts.followRequestAccepted }}</span>
|
||||||
<i class="ti ti-quote"></i>
|
<span v-else-if="notification.type === 'receiveFollowRequest'" class="text" style="opacity: 0.6;">{{ i18n.ts.receiveFollowRequest }}<div v-if="full && !followRequestDone"><button class="_textButton" @click="acceptFollowRequest()">{{ i18n.ts.accept }}</button> | <button class="_textButton" @click="rejectFollowRequest()">{{ i18n.ts.reject }}</button></div></span>
|
||||||
</MkA>
|
<span v-else-if="notification.type === 'groupInvited'" class="text" style="opacity: 0.6;">{{ i18n.ts.groupInvited }}: <b>{{ notification.invitation.group.name }}</b><div v-if="full && !groupInviteDone"><button class="_textButton" @click="acceptGroupInvitation()">{{ i18n.ts.accept }}</button> | <button class="_textButton" @click="rejectGroupInvitation()">{{ i18n.ts.reject }}</button></div></span>
|
||||||
<span v-if="notification.type === 'follow'" class="text" style="opacity: 0.6;">{{ i18n.ts.youGotNewFollower }}<div v-if="full"><MkFollowButton :user="notification.user" :full="true"/></div></span>
|
<span v-else-if="notification.type === 'app'" class="text">
|
||||||
<span v-if="notification.type === 'followRequestAccepted'" class="text" style="opacity: 0.6;">{{ i18n.ts.followRequestAccepted }}</span>
|
<Mfm :text="notification.body" :nowrap="!full"/>
|
||||||
<span v-if="notification.type === 'receiveFollowRequest'" class="text" style="opacity: 0.6;">{{ i18n.ts.receiveFollowRequest }}<div v-if="full && !followRequestDone"><button class="_textButton" @click="acceptFollowRequest()">{{ i18n.ts.accept }}</button> | <button class="_textButton" @click="rejectFollowRequest()">{{ i18n.ts.reject }}</button></div></span>
|
</span>
|
||||||
<span v-if="notification.type === 'groupInvited'" class="text" style="opacity: 0.6;">{{ i18n.ts.groupInvited }}: <b>{{ notification.invitation.group.name }}</b><div v-if="full && !groupInviteDone"><button class="_textButton" @click="acceptGroupInvitation()">{{ i18n.ts.accept }}</button> | <button class="_textButton" @click="rejectGroupInvitation()">{{ i18n.ts.reject }}</button></div></span>
|
</div>
|
||||||
<span v-if="notification.type === 'app'" class="text">
|
|
||||||
<Mfm :text="notification.body" :nowrap="!full"/>
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -75,7 +71,7 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { ref, shallowRef, onMounted, onUnmounted, watch } from 'vue';
|
import { ref, shallowRef, onMounted, onUnmounted, watch } from 'vue';
|
||||||
import * as misskey from 'misskey-js';
|
import * as misskey from 'misskey-js';
|
||||||
import XReactionIcon from '@/components/MkReactionIcon.vue';
|
import MkReactionIcon from '@/components/MkReactionIcon.vue';
|
||||||
import MkFollowButton from '@/components/MkFollowButton.vue';
|
import MkFollowButton from '@/components/MkFollowButton.vue';
|
||||||
import XReactionTooltip from '@/components/MkReactionTooltip.vue';
|
import XReactionTooltip from '@/components/MkReactionTooltip.vue';
|
||||||
import { getNoteSummary } from '@/scripts/get-note-summary';
|
import { getNoteSummary } from '@/scripts/get-note-summary';
|
||||||
@@ -239,12 +235,6 @@ useTooltip(reactionRef, (showing) => {
|
|||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.pollVote {
|
|
||||||
padding: 3px;
|
|
||||||
background: #88a6b7;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.pollEnded {
|
&.pollEnded {
|
||||||
padding: 3px;
|
padding: 3px;
|
||||||
background: #88a6b7;
|
background: #88a6b7;
|
||||||
@@ -275,23 +265,25 @@ useTooltip(reactionRef, (showing) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
> .text {
|
> .content {
|
||||||
white-space: nowrap;
|
> .text {
|
||||||
overflow: hidden;
|
white-space: nowrap;
|
||||||
text-overflow: ellipsis;
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
|
||||||
> i {
|
> i {
|
||||||
vertical-align: super;
|
vertical-align: super;
|
||||||
font-size: 50%;
|
font-size: 50%;
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
> i:first-child {
|
> i:first-child {
|
||||||
margin-right: 4px;
|
margin-right: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
> i:last-child {
|
> i:last-child {
|
||||||
margin-left: 4px;
|
margin-left: 4px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -35,7 +35,7 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { } from 'vue';
|
import { } from 'vue';
|
||||||
import { notificationTypes } from 'misskey-js';
|
import { notificationTypes } from 'misskey-js';
|
||||||
import MkSwitch from './form/switch.vue';
|
import MkSwitch from './MkSwitch.vue';
|
||||||
import MkInfo from './MkInfo.vue';
|
import MkInfo from './MkInfo.vue';
|
||||||
import MkButton from './MkButton.vue';
|
import MkButton from './MkButton.vue';
|
||||||
import MkModalWindow from '@/components/MkModalWindow.vue';
|
import MkModalWindow from '@/components/MkModalWindow.vue';
|
||||||
|
@@ -84,6 +84,7 @@ provideMetadataReceiver((info) => {
|
|||||||
});
|
});
|
||||||
provide('shouldOmitHeaderTitle', true);
|
provide('shouldOmitHeaderTitle', true);
|
||||||
provide('shouldHeaderThin', true);
|
provide('shouldHeaderThin', true);
|
||||||
|
provide('forceSpacerMin', true);
|
||||||
|
|
||||||
const contextmenu = $computed(() => ([{
|
const contextmenu = $computed(() => ([{
|
||||||
icon: 'ti ti-player-eject',
|
icon: 'ti ti-player-eject',
|
||||||
@@ -136,5 +137,7 @@ defineExpose({
|
|||||||
.yrolvcoq {
|
.yrolvcoq {
|
||||||
min-height: 100%;
|
min-height: 100%;
|
||||||
background: var(--bg);
|
background: var(--bg);
|
||||||
|
|
||||||
|
--margin: var(--marginHalf);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@@ -19,7 +19,8 @@ const emit = defineEmits<{
|
|||||||
}>();
|
}>();
|
||||||
|
|
||||||
let up = $ref(false);
|
let up = $ref(false);
|
||||||
const zIndex = os.claimZIndex('veryLow');
|
const zIndex = os.claimZIndex('middle');
|
||||||
|
const angle = (45 - (Math.random() * 90)) + 'deg';
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
window.setTimeout(() => {
|
window.setTimeout(() => {
|
||||||
@@ -50,15 +51,17 @@ onMounted(() => {
|
|||||||
right: 0;
|
right: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
margin: auto;
|
margin: auto;
|
||||||
color: var(--accent);
|
color: #fff;
|
||||||
|
text-shadow: 0 0 6px #000;
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
transform: translateY(-30px);
|
transform: translateY(0px);
|
||||||
transition: transform 1s cubic-bezier(0,.5,0,1), opacity 1s cubic-bezier(.5,0,1,.5);
|
transition: transform 1s cubic-bezier(0,.5,0,1), opacity 1s cubic-bezier(.5,0,1,.5);
|
||||||
|
will-change: opacity, transform;
|
||||||
|
|
||||||
&.up {
|
&.up {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transform: translateY(-50px);
|
transform: translateY(-50px) rotateZ(v-bind(angle));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -49,9 +49,9 @@
|
|||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { ref, watch } from 'vue';
|
import { ref, watch } from 'vue';
|
||||||
import MkInput from './form/input.vue';
|
import MkInput from './MkInput.vue';
|
||||||
import MkSelect from './form/select.vue';
|
import MkSelect from './MkSelect.vue';
|
||||||
import MkSwitch from './form/switch.vue';
|
import MkSwitch from './MkSwitch.vue';
|
||||||
import MkButton from './MkButton.vue';
|
import MkButton from './MkButton.vue';
|
||||||
import { formatDateTimeString } from '@/scripts/format-time-string';
|
import { formatDateTimeString } from '@/scripts/format-time-string';
|
||||||
import { addTime } from '@/scripts/time';
|
import { addTime } from '@/scripts/time';
|
||||||
|