Compare commits

...

96 Commits

Author SHA1 Message Date
syuilo
8cc80faf20 13.0.0-beta.35 2023-01-09 16:27:27 +09:00
syuilo
4d66077f85 Update MkEmojiPicker.vue 2023-01-09 16:27:09 +09:00
syuilo
3ece2dc990 13.0.0-beta.34 2023-01-09 16:16:25 +09:00
syuilo
6071e962f4 Revert "Update vite.config.ts"
This reverts commit c438bd2e27.
2023-01-09 16:16:00 +09:00
syuilo
ed43369797 Update CHANGELOG.md 2023-01-09 16:09:40 +09:00
syuilo
c65957853b 13.0.0-beta.33 2023-01-09 16:08:25 +09:00
syuilo
6a18360269 update mfm-js 2023-01-09 16:07:14 +09:00
syuilo
c438bd2e27 Update vite.config.ts 2023-01-09 15:51:36 +09:00
syuilo
462acc9eee カスタム絵文字一覧情報をmetaから分離 2023-01-09 15:50:25 +09:00
syuilo
e4144a17a4 fix(server): アンテナタイムライン(ストリーミング)が、フォローしていないユーザーの鍵投稿も拾ってしまう問題を修正
Fix #9025
2023-01-09 14:12:42 +09:00
syuilo
3cfd017538 fix(server): 特定のPNG画像のアップロードに失敗する問題を修正
Co-Authored-By: haru <64310155+usbharu@users.noreply.github.com>
2023-01-09 14:03:22 +09:00
syuilo
403849805a enhance(client): force lazy load some images 2023-01-09 14:00:04 +09:00
syuilo
402b234d15 🎨 2023-01-09 13:56:30 +09:00
syuilo
eba6b326fa 13.0.0-beta.32 2023-01-09 13:45:04 +09:00
syuilo
4c9b93a12f 🎨 2023-01-09 13:39:16 +09:00
syuilo
dfee79f841 🎨
Resolve #9498
2023-01-09 13:32:48 +09:00
syuilo
962373cf06 fix(server): 非公開のクリップのURLでOGPレンダリングされる問題を修正
Fix #9129
2023-01-09 13:26:42 +09:00
syuilo
13aa4b64b4 tweak client 2023-01-09 10:07:37 +09:00
syuilo
5ce56886a1 fix 2023-01-09 09:43:28 +09:00
syuilo
2817ca03f5 Update queue.chart.vue 2023-01-09 09:42:11 +09:00
syuilo
e633c3b84b refactor 2023-01-09 09:41:25 +09:00
syuilo
8524e9d735 tweak client 2023-01-09 09:04:35 +09:00
syuilo
91ced90fb2 fix imports 2023-01-09 08:58:16 +09:00
syuilo
2acb3917ba Merge branch 'develop' of https://github.com/misskey-dev/misskey into develop 2023-01-09 08:46:10 +09:00
syuilo
dd78ac089c 🍪 2023-01-08 20:42:45 +09:00
MeiMei
10e526ba56 fix: Escape SQL LIKE (#9493)
* SQL LIKE escape

* CHANGELOG
2023-01-08 20:32:17 +09:00
syuilo
7ed905f76b 🍪 cps 2023-01-08 20:30:19 +09:00
syuilo
5d13e2744f 🎨 2023-01-08 20:21:32 +09:00
syuilo
1d7e0293a8 fix following chart 2023-01-08 20:02:07 +09:00
marihachi
8977d87021 fix typo (#9492) 2023-01-08 19:59:05 +09:00
syuilo
809400ff23 Update CHANGELOG.md 2023-01-08 18:08:43 +09:00
syuilo
4c8dbcc20d 13.0.0-beta.31 2023-01-08 17:44:24 +09:00
syuilo
416dcf884d 🎨 2023-01-08 17:42:51 +09:00
syuilo
09d3ce444a Merge branch 'develop' of https://github.com/misskey-dev/misskey into develop 2023-01-08 17:41:12 +09:00
syuilo
27c2ca5048 feat(client): 🍪👈 2023-01-08 17:41:09 +09:00
syuilo
fceeb1b108 🎨 2023-01-08 17:38:33 +09:00
tamaina
b442c38f41 enhance: Push Notification badges to Tabler Icons (#9406)
* enhance: Push Notification badges to Tabler Icons

* add receiveFollowRequest icon
2023-01-08 16:47:57 +09:00
syuilo
7c2d2676f7 refactor 2023-01-08 16:17:42 +09:00
syuilo
1f6a41cea7 13.0.0-beta.30 2023-01-08 14:30:00 +09:00
syuilo
0d7ee20a77 🎨 2023-01-08 14:29:22 +09:00
syuilo
dcca2350dd 🎨 2023-01-08 14:28:14 +09:00
syuilo
1cfdd4c41a refactor 2023-01-08 14:22:04 +09:00
syuilo
25f4ee7030 Update CHANGELOG.md 2023-01-08 14:19:32 +09:00
syuilo
5320f23017 enhance(client): improve user activity page 2023-01-08 14:17:56 +09:00
syuilo
4ffbbbe6d8 🎨 2023-01-08 13:10:01 +09:00
syuilo
132e45dff4 13.0.0-beta.29 2023-01-08 11:58:00 +09:00
syuilo
01652b72b3 🎨 2023-01-08 11:57:34 +09:00
syuilo
8b1fdb5a3b enhance(client): add theme 2023-01-08 11:55:37 +09:00
syuilo
192add376c fix MkModal animation 2023-01-08 11:47:16 +09:00
syuilo
244ea9593a tweak components 2023-01-08 11:30:40 +09:00
syuilo
f20d7cba74 13.0.0-beta.28 2023-01-08 11:17:02 +09:00
syuilo
a3e282bc75 New Crowdin updates (#9478)
* New translations ja-JP.yml (Thai)

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

* New translations ja-JP.yml (Italian)

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

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (German)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (Korean)

* New translations ja-JP.yml (Korean)

* New translations ja-JP.yml (Slovak)
2023-01-08 11:16:36 +09:00
syuilo
49a95c34bf Update CHANGELOG.md 2023-01-08 11:16:26 +09:00
Soni L
ecbefce2aa Support remote objects in search (#9479)
* Support remote objects in search

Closes #9428

* Use account instead of localStorage

* Use useRouter instead of mainRouter

Co-authored-by: Chaos <chaoticryptidz@owo.monster>
2023-01-08 11:15:54 +09:00
syuilo
91356b1805 tweak 2023-01-08 11:12:11 +09:00
syuilo
2e2ed1385f delete pollVote notification 2023-01-08 10:54:45 +09:00
syuilo
49f3090edd tweak note componsnt 2023-01-08 10:48:44 +09:00
syuilo
4594fb11de 🎨 2023-01-08 10:32:37 +09:00
syuilo
b93e56d2e5 🎨 2023-01-08 10:24:30 +09:00
syuilo
c550dafb81 tweak note component 2023-01-08 10:20:28 +09:00
syuilo
8709574f3d 🎨 2023-01-08 09:58:35 +09:00
syuilo
1b7043fa79 🎨 2023-01-07 19:57:48 +09:00
syuilo
55ef2393fb 13.0.0-beta.27 2023-01-07 17:44:15 +09:00
syuilo
7769095efb 🎨 2023-01-07 17:39:24 +09:00
syuilo
b8248bdd65 🎨 2023-01-07 17:37:30 +09:00
syuilo
6f4ad581dc 🎨 2023-01-07 17:27:09 +09:00
syuilo
aec94920ab 🎨 2023-01-07 17:20:50 +09:00
syuilo
155ca39063 update aiscript 2023-01-07 17:18:30 +09:00
syuilo
58bfb4dca4 refactor 2023-01-07 15:09:46 +09:00
syuilo
49a0b6c48b fix typo 2023-01-07 15:00:29 +09:00
syuilo
799a653b44 🎨 2023-01-07 15:00:00 +09:00
syuilo
d09e1f4925 refactor 2023-01-07 14:59:54 +09:00
syuilo
cac784af8a fix #9483 2023-01-07 14:44:50 +09:00
syuilo
d7e0ddcbca Merge branch 'develop' of https://github.com/misskey-dev/misskey into develop 2023-01-07 14:33:36 +09:00
syuilo
8c0811a442 🎨 2023-01-07 14:33:33 +09:00
syuilo
bab6f75260 Update CHANGELOG.md 2023-01-07 14:24:33 +09:00
syuilo
54e3fccd87 enhance(server): refactor and tweak emoji proxy 2023-01-07 14:19:25 +09:00
syuilo
6a992b6982 fix chart rendering 2023-01-07 14:10:01 +09:00
syuilo
ecd6fc1db8 🎨 2023-01-07 11:49:04 +09:00
syuilo
d99be6697e enhance(client): donation dialog 2023-01-07 11:49:00 +09:00
syuilo
d2d77b5dc1 Merge branch 'develop' of https://github.com/misskey-dev/misskey into develop 2023-01-07 10:13:07 +09:00
syuilo
91503405b4 refactor(client): typed localStorage 2023-01-07 10:13:02 +09:00
tamaina
c336201084 Merge branch 'develop' of https://github.com/misskey-dev/misskey into develop 2023-01-06 14:01:02 +00:00
tamaina
0f3399753d chore: remove Search from the name of OpenSearch 2023-01-06 14:00:54 +00:00
tamaina
5ec89ea0c3 カスタム絵文字にプロキシを復活 (#9481)
* wip

* Revert "Update ClientServerService.ts"

This reverts commit 88c64ece78.

* Revert "disable custom emoji proxy temporary"

This reverts commit 495d513efd.

* ✌️
2023-01-06 22:34:50 +09:00
syuilo
a42b03c154 13.0.0-beta.26 2023-01-06 20:29:48 +09:00
syuilo
4b181a30da tweak confetti 2023-01-06 20:25:08 +09:00
syuilo
70805e00eb 🎨 2023-01-06 20:19:27 +09:00
syuilo
3551ac328e tweak retention heatmap 2023-01-06 20:01:32 +09:00
syuilo
e36e5df635 chore(client): tweak charts 2023-01-06 20:00:06 +09:00
syuilo
3e7d8b5f17 update deps 2023-01-06 19:59:31 +09:00
syuilo
5846198eee enhance(client): provide THIS_ID and THIS_URL vars to aiscript of flash 2023-01-06 17:19:25 +09:00
syuilo
c14063a921 🎨 2023-01-06 17:07:32 +09:00
syuilo
457670e730 Update MkTime.vue 2023-01-06 16:58:43 +09:00
syuilo
513cef50a2 tweak federation widget 2023-01-06 16:32:34 +09:00
syuilo
88c64ece78 Update ClientServerService.ts 2023-01-06 14:07:34 +09:00
257 changed files with 3297 additions and 2219 deletions

View File

@@ -12,7 +12,7 @@ You should also include the user name that made the change.
## 13.0.0 (unreleased)
### TL;DR
- New features (Play, new widgets, new charts, etc)
- New features (Play, new widgets, new charts, 🍪👈, etc)
- Rewriten backend
- Better performance (backend and frontend)
- Various usability improvements
@@ -30,15 +30,18 @@ You should also include the user name that made the change.
#### For users
- ノートのウォッチ機能が削除されました
- アンケートに投票された際に通知が作成されなくなりました
- 新たに動的なPagesを作ることはできなくなりました
- 代わりに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.1未満のプラグインは読み込むことはできません
- 0.12.x未満のプラグインは読み込むことはできません
- iOS15以下のデバイスはサポートされなくなりました
- Firefox109以下はサポートされなくなりました
#### For app developers
- API: metaのレスポンスに`emojis`プロパティが含まれなくなりました
- カスタム絵文字一覧情報を取得するには、`emojis`エンドポイントにリクエストします
- API: カスタム絵文字エンティティに`url`プロパティが含まれなくなりました
- 絵文字画像を表示するには、`<instance host>/emoji/<emoji name>.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: show Unicode emoji tooltip with its name in MkReactionsViewer.reaction @saschanaz
- 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 heatmap of daily active users to about page @syuilo
- Client: introduce fluent emoji @syuilo
- Client: add new theme @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: improve overall performance of client @syuilo
- Client: ui tweaks @syuilo
- Client: clicker game @syuilo
### Bugfixes
- Server: 引用内の文章がnyaizeされてしまう問題を修正 @kabo2468
@@ -94,10 +101,16 @@ You should also include the user name that made the change.
- Server: アンテナの作成数上限を追加 @syuilo
- Server: pages/likeのエラーIDが重複しているのを修正 @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: InAppウィンドウが操作できなくなることがあるのを修正 @tamaina
- Client: use proxied image for instance icon @syuilo
- Client: Webhookの編集画面で、内容を保存することができない問題を修正 @m-hayabusa
- Client: Page編集でブロックの移動が行えない問題を修正 @syuilo
- Client: update emoji picker immediately on all input @saschanaz
- Client: チャートのツールチップが画面に残ることがあるのを修正 @syuilo
- Client: fix wrong link in tutorial @syuilo

View File

@@ -920,6 +920,10 @@ like: "Gefällt mir"
unlike: "\"Gefällt mir\" entfernen"
numberOfLikes: "\"Gefällt mir\"-Anzahl"
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:
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"

View File

@@ -920,6 +920,10 @@ like: "Like"
unlike: "Unlike"
numberOfLikes: "Likes"
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:
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"

View File

@@ -28,7 +28,7 @@ timeline: "Timeline"
noAccountDescription: "L'utente non ha ancora scritto niente nella biografia di profilo."
login: "Accedi"
loggingIn: "Accesso in corso..."
logout: "Esci"
logout: "Uscita"
signup: "Iscriviti"
uploading: "Caricamento..."
save: "Salva"

View File

@@ -920,6 +920,10 @@ like: "いいね!"
unlike: "いいねを解除"
numberOfLikes: "いいね数"
show: "表示"
neverShow: "今後表示しない"
remindMeLater: "また後で"
didYouLikeMisskey: "Misskeyを気に入っていただけましたか"
pleaseDonate: "Misskeyは{host}が使用している無料のソフトウェアです。これからも開発を続けられるように、ぜひ寄付をお願いします!"
_sensitiveMediaDetection:
description: "機械学習を使って自動でセンシティブなメディアを検出し、モデレーションに役立てることができます。サーバーの負荷が少し増えます。"
@@ -1357,6 +1361,7 @@ _widgets:
userList: "ユーザーリスト"
_userList:
chooseList: "リストを選択"
clicker: "クリッカー"
_cw:
hide: "隠す"
@@ -1546,7 +1551,6 @@ _notification:
youGotReply: "{name}からのリプライ"
youGotQuote: "{name}による引用"
youRenoted: "{name}がRenoteしました"
youGotPoll: "{name}が投票しました"
youGotMessagingMessageFromUser: "{name}からのチャットがあります"
youGotMessagingMessageFromGroup: "{name}のチャットがあります"
youWereFollowed: "フォローされました"
@@ -1565,7 +1569,6 @@ _notification:
renote: "Renote"
quote: "引用"
reaction: "リアクション"
pollVote: "アンケートに投票された"
pollEnded: "アンケートが終了"
receiveFollowRequest: "フォロー申請を受け取った"
followRequestAccepted: "フォローが受理された"

View File

@@ -920,6 +920,10 @@ like: "좋아요!"
unlike: "좋아요 취소"
numberOfLikes: "좋아요 수"
show: "표시"
neverShow: "다시 보지 않기"
remindMeLater: "나중에 알림"
didYouLikeMisskey: "Misskey가 마음에 드시나요?"
pleaseDonate: "{host}은(는) 무료 소프트웨어 Misskey를 사용합니다. 후원을 통해 저희의 개발이 이어질 수 있게 도와주세요!"
_sensitiveMediaDetection:
description: "기계학습을 통해 자동으로 민감한 미디어를 탐지하여, 모더레이션에 참고할 수 있도록 합니다. 서버의 부하를 약간 증가시킵니다."
sensitivity: "탐지 민감도"

View File

@@ -913,6 +913,10 @@ tools: "Nástroje"
cannotLoad: "Nedá sa načítať."
like: "Páči sa mi"
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:
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"

View File

@@ -917,6 +917,8 @@ tools: "เครื่องมือ"
cannotLoad: "ไม่สามารถโหลดได้"
numberOfProfileView: "มุมมองโปรไฟล์"
like: "ชื่นชอบ"
unlike: "ไม่ชอบ"
numberOfLikes: "จำนวนไลค์"
show: "แสดงผล"
_sensitiveMediaDetection:
description: "ลดความพยายามในการดูแลเซิร์ฟเวอร์ผ่านการจดจำสื่อ NSFW โดยอัตโนมัติผ่านการเรียนรู้ของเครื่อง การทำสิ่งนี้อาจจะเพิ่มภาระบนเซิร์ฟเวอร์เล็กน้อย"
@@ -1317,6 +1319,7 @@ _widgets:
jobQueue: "คิวงาน"
serverMetric: "ตัวชี้วัดเซิร์ฟเวอร์"
aiscript: "AiScript คอนโซล"
aiscriptApp: "AiScript แอพ"
aichan: "เอไอ"
userList: "รายชื่อผู้ใช้"
_userList:
@@ -1423,7 +1426,16 @@ _timelines:
social: "โซเชี่ยล"
global: "ทั่วโลก"
_play:
new: "สร้างการเล่น"
edit: "แก้ไขเล่น"
created: "สร้างการเล่นแล้ว"
updated: "แก้ไขการเล่นแล้ว"
deleted: "ลบการเล่นแล้ว"
pageSetting: "ตั้งค่าการเล่น"
editThisPage: "แก้ไข Play นี้"
viewSource: "ดูต้นฉบับ"
my: "มาย เพลย์"
liked: "ไลค์ เพลย์"
featured: "เป็นที่นิยม"
title: "หัวข้อ"
script: "สคริปต์"

View File

@@ -918,7 +918,11 @@ cannotLoad: "無法載入"
numberOfProfileView: "個人檔案檢視次數"
like: "讚"
unlike: "收回讚"
numberOfLikes: "讚數"
show: "檢視"
neverShow: "不再顯示"
remindMeLater: "以後再說"
didYouLikeMisskey: "您是否喜愛Misskey呢"
_sensitiveMediaDetection:
description: "您可以使用機器學習自動檢測敏感媒體並將其用於審核。 伺服器的負荷會稍微增加。"
sensitivity: "檢測敏感度"
@@ -1318,6 +1322,7 @@ _widgets:
jobQueue: "佇列"
serverMetric: "服務器指標 "
aiscript: "AiScript控制台"
aiscriptApp: "AiScript App"
aichan: "小藍"
userList: "使用者列表"
_userList:
@@ -1424,7 +1429,16 @@ _timelines:
social: "社群"
global: "公開"
_play:
new: "新增Play"
edit: "編輯Play"
created: "已新增Play"
updated: "已更新Play"
deleted: "已刪除Play"
pageSetting: "Play設定"
editThisPage: "編輯這個Play"
viewSource: "檢視原始碼"
my: "自己的Play"
liked: "按了讚的Play"
featured: "人氣"
title: "標題"
script: "腳本"

View File

@@ -1,6 +1,6 @@
{
"name": "misskey",
"version": "13.0.0-beta.25",
"version": "13.0.0-beta.35",
"codename": "indigo",
"repository": {
"type": "git",
@@ -53,10 +53,10 @@
"devDependencies": {
"@types/gulp": "4.0.10",
"@types/gulp-rename": "2.0.1",
"@typescript-eslint/eslint-plugin": "5.47.1",
"@typescript-eslint/parser": "5.47.1",
"@typescript-eslint/eslint-plugin": "5.48.0",
"@typescript-eslint/parser": "5.48.0",
"cross-env": "7.0.3",
"cypress": "12.2.0",
"cypress": "12.3.0",
"eslint": "^8.31.0",
"start-server-and-test": "1.15.2",
"typescript": "4.9.4"

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

View File

@@ -1,5 +0,0 @@
Font Awesome Icons
-------------------------
Ⓒ Font Awesome
CC BY 4.0 (https://creativecommons.org/licenses/by/4.0/)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 577 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 844 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 507 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 689 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 772 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 930 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 798 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 991 B

View 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.

Binary file not shown.

After

Width:  |  Height:  |  Size: 516 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 952 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 829 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

Before

Width:  |  Height:  |  Size: 174 B

After

Width:  |  Height:  |  Size: 174 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 414 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1011 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@@ -21,9 +21,9 @@
"@tensorflow/tfjs-node": "4.1.0"
},
"dependencies": {
"@bull-board/api": "^4.10.0",
"@bull-board/fastify": "^4.10.0",
"@bull-board/ui": "^4.10.0",
"@bull-board/api": "^4.10.1",
"@bull-board/fastify": "^4.10.1",
"@bull-board/ui": "^4.10.1",
"@discordapp/twemoji": "14.0.2",
"@fastify/accepts": "4.1.0",
"@fastify/cookie": "^8.3.0",
@@ -38,10 +38,10 @@
"@peertube/http-signature": "1.7.0",
"@sinonjs/fake-timers": "10.0.2",
"accepts": "^1.3.8",
"ajv": "8.11.2",
"ajv": "8.12.0",
"archiver": "5.3.1",
"autwh": "0.1.0",
"aws-sdk": "2.1286.0",
"aws-sdk": "2.1289.0",
"bcryptjs": "2.4.3",
"blurhash": "2.0.4",
"bull": "4.10.2",
@@ -72,7 +72,7 @@
"json5-loader": "4.0.1",
"jsonld": "8.1.0",
"jsrsasign": "10.6.1",
"mfm-js": "0.23.0",
"mfm-js": "0.23.1",
"mime-types": "2.1.35",
"misskey-js": "0.0.14",
"ms": "3.0.0-canary.1",
@@ -110,8 +110,8 @@
"stringz": "2.1.0",
"summaly": "2.7.0",
"syslog-pro": "git+https://github.com/misskey-dev/SyslogPro#0.2.9-misskey.2",
"systeminformation": "5.16.9",
"tinycolor2": "1.5.1",
"systeminformation": "5.17.1",
"tinycolor2": "1.5.2",
"tmp": "0.2.1",
"tsc-alias": "1.8.2",
"tsconfig-paths": "4.1.2",
@@ -128,7 +128,7 @@
},
"devDependencies": {
"@redocly/openapi-core": "1.0.0-beta.117",
"@swc/core": "1.3.24",
"@swc/core": "1.3.25",
"@swc/jest": "0.2.24",
"@types/accepts": "1.3.5",
"@types/archiver": "5.3.1",
@@ -172,8 +172,8 @@
"@types/web-push": "3.3.2",
"@types/websocket": "1.0.5",
"@types/ws": "8.5.4",
"@typescript-eslint/eslint-plugin": "5.47.1",
"@typescript-eslint/parser": "5.47.1",
"@typescript-eslint/eslint-plugin": "5.48.0",
"@typescript-eslint/parser": "5.48.0",
"cross-env": "7.0.3",
"eslint": "8.31.0",
"eslint-plugin-import": "2.26.0",

View File

@@ -15,8 +15,8 @@ import type { Packed } from '@/misc/schema.js';
import { DI } from '@/di-symbols.js';
import type { MutingsRepository, BlockingsRepository, NotesRepository, AntennaNotesRepository, AntennasRepository, UserGroupJoiningsRepository, UserListJoiningsRepository } from '@/models/index.js';
import { UtilityService } from '@/core/UtilityService.js';
import type { OnApplicationShutdown } from '@nestjs/common';
import { bindThis } from '@/decorators.js';
import type { OnApplicationShutdown } from '@nestjs/common';
@Injectable()
export class AntennaService implements OnApplicationShutdown {
@@ -135,7 +135,7 @@ export class AntennaService implements OnApplicationShutdown {
this.globalEventServie.publishMainStream(antenna.userId, 'unreadAntenna', antenna);
this.pushNotificationService.pushNotification(antenna.userId, 'unreadAntennaNote', {
antenna: { id: antenna.id, name: antenna.name },
note: await this.noteEntityService.pack(note)
note: await this.noteEntityService.pack(note),
});
}
}, 2000);
@@ -144,27 +144,19 @@ export class AntennaService implements OnApplicationShutdown {
// NOTE: フォローしているユーザーのノート、リストのユーザーのノート、グループのユーザーのノート指定はパフォーマンス上の理由で無効になっている
/**
* noteUserFollowers / antennaUserFollowing はどちらか一方が指定されていればよい
*/
@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 === 'followers') return false;
// アンテナ作成者がノート作成者にブロックされていたらスキップ
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 (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.src === 'home') {
if (noteUserFollowers && !noteUserFollowers.includes(antenna.userId)) return false;
if (antennaUserFollowing && !antennaUserFollowing.includes(note.userId)) return false;
// TODO
} else if (antenna.src === 'list') {
const listUsers = (await this.userListJoiningsRepository.findBy({
userListId: antenna.userListId!,

View File

@@ -398,13 +398,13 @@ export class FileInfoService {
.raw()
.ensureAlpha()
.resize(64, 64, { fit: 'inside' })
.toBuffer((err, buffer, { width, height }) => {
.toBuffer((err, buffer, info) => {
if (err) return reject(err);
let hash;
try {
hash = encode(new Uint8ClampedArray(buffer), width, height, 5, 5);
hash = encode(new Uint8ClampedArray(buffer), info.width, info.height, 5, 5);
} catch (e) {
return reject(e);
}

View File

@@ -92,13 +92,6 @@ export class PollService {
choice: choice,
userId: user.id,
});
// Notify
this.createNotificationService.createNotification(note.userId, 'pollVote', {
notifierId: user.id,
noteId: note.id,
choice: choice,
});
}
@bindThis

View File

@@ -22,23 +22,25 @@ export class EmojiEntityService {
@bindThis
public async pack(
src: Emoji['id'] | Emoji,
opts: { omitHost?: boolean; omitId?: boolean; } = {},
): Promise<Packed<'Emoji'>> {
const emoji = typeof src === 'object' ? src : await this.emojisRepository.findOneByOrFail({ id: src });
return {
id: emoji.id,
id: opts.omitId ? undefined : emoji.id,
aliases: emoji.aliases,
name: emoji.name,
category: emoji.category,
host: emoji.host,
host: opts.omitHost ? undefined : emoji.host,
};
}
@bindThis
public packMany(
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)));
}
}

View File

@@ -98,7 +98,7 @@ export class NotificationEntityService implements OnModuleInit {
}),
reaction: notification.reaction,
} : {}),
...(notification.type === 'pollVote' ? {
...(notification.type === 'pollVote' ? { // TODO: そのうち消す
note: this.noteEntityService.pack(notification.note ?? notification.noteId!, { id: notification.notifieeId }, {
detail: true,
_hint_: options._hintForEachNotes_,

View File

@@ -3,6 +3,7 @@ import { FILE_TYPE_BROWSERSAFE } from '@/const.js';
const dictionary = {
'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-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);

View File

@@ -0,0 +1,3 @@
export function sqlLikeEscape(s: string) {
return s.replace(/([%_])/g, '\\$1');
}

View File

@@ -55,11 +55,11 @@ export class Notification {
* 通知の種類。
* follow - フォローされた
* mention - 投稿で自分が言及された
* reply - (自分または自分がWatchしている)投稿返信された
* renote - (自分または自分がWatchしている)投稿がRenoteされた
* quote - (自分または自分がWatchしている)投稿が引用Renoteされた
* reaction - (自分または自分がWatchしている)投稿にリアクションされた
* pollVote - (自分または自分がWatchしている)投稿のアンケートに投票された
* reply - 投稿返信された
* renote - 投稿がRenoteされた
* quote - 投稿が引用Renoteされた
* reaction - 投稿にリアクションされた
* pollVote - 投稿のアンケートに投票された (廃止)
* pollEnded - 自分のアンケートもしくは自分が投票したアンケートが終了した
* receiveFollowRequest - フォローリクエストされた
* followRequestAccepted - 自分の送ったフォローリクエストが承認された

View File

@@ -3,7 +3,7 @@ export const packedEmojiSchema = {
properties: {
id: {
type: 'string',
optional: false, nullable: false,
optional: true, nullable: false,
format: 'id',
example: 'xxxxxxxxxx',
},
@@ -26,12 +26,8 @@ export const packedEmojiSchema = {
},
host: {
type: 'string',
optional: false, nullable: true,
optional: true, nullable: true,
description: 'The local host is represented with `null`.',
},
url: {
type: 'string',
optional: true, nullable: false,
},
},
} as const;

View File

@@ -79,9 +79,17 @@ export class MediaProxyServerService {
const { mime, ext } = await this.fileInfoService.detectType(path);
const isConvertibleImage = isMimeImage(mime, 'sharp-convertible-image');
const isAnimationConvertibleImage = isMimeImage(mime, 'sharp-animation-convertible-image');
let image: IImage;
if ('emoji' in request.query && isConvertibleImage) {
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({
height: 128,
@@ -95,6 +103,7 @@ export class MediaProxyServerService {
ext: 'webp',
type: 'image/webp',
};
}
} else if ('static' in request.query && isConvertibleImage) {
image = await this.imageProcessingService.convertToWebp(path, 498, 280);
} else if ('preview' in request.query && isConvertibleImage) {

View File

@@ -1,12 +1,11 @@
import cluster from 'node:cluster';
import * as fs from 'node:fs';
import * as http from 'node:http';
import { Inject, Injectable } from '@nestjs/common';
import Fastify from 'fastify';
import { IsNull } from 'typeorm';
import { GlobalEventService } from '@/core/GlobalEventService.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 type Logger from '@/logger.js';
import { envOption } from '@/env.js';
@@ -39,6 +38,9 @@ export class ServerService {
@Inject(DI.userProfilesRepository)
private userProfilesRepository: UserProfilesRepository,
@Inject(DI.emojisRepository)
private emojisRepository: EmojisRepository,
private userEntityService: UserEntityService,
private apiServerService: ApiServerService,
private streamingApiServerService: StreamingApiServerService,
@@ -77,6 +79,43 @@ export class ServerService {
fastify.register(this.nodeinfoServerService.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) => {
const { username, host } = Acct.parse(request.params.acct);
const user = await this.usersRepository.findOne({

View File

@@ -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_read from './endpoints/messaging/messages/read.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___mute_create from './endpoints/mute/create.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_read: Provider = { provide: 'ep:messaging/messages/read', useClass: ep___messaging_messages_read.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 $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 };
@@ -884,6 +886,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
$messaging_messages_delete,
$messaging_messages_read,
$meta,
$emojis,
$miauth_genToken,
$mute_create,
$mute_delete,
@@ -1212,6 +1215,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
$messaging_messages_delete,
$messaging_messages_read,
$meta,
$emojis,
$miauth_genToken,
$mute_create,
$mute_delete,

View File

@@ -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_read from './endpoints/messaging/messages/read.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___mute_create from './endpoints/mute/create.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/read', ep___messaging_messages_read],
['meta', ep___meta],
['emojis', ep___emojis],
['miauth/gen-token', ep___miauth_genToken],
['mute/create', ep___mute_create],
['mute/delete', ep___mute_delete],

View File

@@ -5,6 +5,7 @@ import { QueryService } from '@/core/QueryService.js';
import { UtilityService } from '@/core/UtilityService.js';
import { EmojiEntityService } from '@/core/entities/EmojiEntityService.js';
import { DI } from '@/di-symbols.js';
import { sqlLikeEscape } from '@/misc/sql-like-escape.js';
export const meta = {
tags: ['admin'],
@@ -92,7 +93,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
}
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

View File

@@ -5,6 +5,7 @@ import type { Emoji } from '@/models/entities/Emoji.js';
import { QueryService } from '@/core/QueryService.js';
import { DI } from '@/di-symbols.js';
import { EmojiEntityService } from '@/core/entities/EmojiEntityService.js';
//import { sqlLikeEscape } from '@/misc/sql-like-escape.js';
export const meta = {
tags: ['admin'],
@@ -82,7 +83,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
let emojis: Emoji[];
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();
emojis = await q.getMany();

View File

@@ -3,6 +3,7 @@ import type { UsersRepository } from '@/models/index.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { DI } from '@/di-symbols.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { sqlLikeEscape } from '@/misc/sql-like-escape.js';
export const meta = {
tags: ['admin'],
@@ -68,7 +69,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
}
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) {

View 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,
}),
};
});
}
}

View File

@@ -4,6 +4,7 @@ import type { InstancesRepository } from '@/models/index.js';
import { InstanceEntityService } from '@/core/entities/InstanceEntityService.js';
import { MetaService } from '@/core/MetaService.js';
import { DI } from '@/di-symbols.js';
import { sqlLikeEscape } from '@/misc/sql-like-escape.js';
export const meta = {
tags: ['federation'],
@@ -120,7 +121,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
}
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();

View File

@@ -2,6 +2,7 @@ import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import type { HashtagsRepository } from '@/models/index.js';
import { DI } from '@/di-symbols.js';
import { sqlLikeEscape } from '@/misc/sql-like-escape.js';
export const meta = {
tags: ['hashtags'],
@@ -37,7 +38,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
) {
super(meta, paramDef, async (ps, me) => {
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')
.groupBy('tag.id')
.take(ps.limit)

View File

@@ -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 { Endpoint } from '@/server/api/endpoint-base.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { EmojiEntityService } from '@/core/entities/EmojiEntityService.js';
import { MetaService } from '@/core/MetaService.js';
import type { Config } from '@/config.js';
import { DI } from '@/di-symbols.js';
@@ -152,43 +151,6 @@ export const meta = {
type: 'number',
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: {
type: 'array',
optional: false, nullable: false,
@@ -326,30 +288,12 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
@Inject(DI.adsRepository)
private adsRepository: AdsRepository,
@Inject(DI.emojisRepository)
private emojisRepository: EmojisRepository,
private userEntityService: UserEntityService,
private emojiEntityService: EmojiEntityService,
private metaService: MetaService,
) {
super(meta, paramDef, async (ps, me) => {
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({
where: {
expiresAt: MoreThan(new Date()),
@@ -390,7 +334,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
backgroundImageUrl: instance.backgroundImageUrl,
logoImageUrl: instance.logoImageUrl,
maxNoteTextLength: MAX_NOTE_TEXT_LENGTH, // 後方互換性のため
emojis: await this.emojiEntityService.packMany(emojis),
defaultLightTheme: instance.defaultLightTheme,
defaultDarkTheme: instance.defaultDarkTheme,
ads: ads.map(ad => ({

View File

@@ -162,13 +162,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
userId: me.id,
});
// Notify
this.createNotificationService.createNotification(note.userId, 'pollVote', {
notifierId: me.id,
noteId: note.id,
choice: ps.choice,
});
// リモート投票の場合リプライ送信
if (note.userHost != null) {
const pollOwner = await this.usersRepository.findOneByOrFail({ id: note.userId }) as IRemoteUser;

View File

@@ -6,6 +6,7 @@ import { QueryService } from '@/core/QueryService.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import type { Config } from '@/config.js';
import { DI } from '@/di-symbols.js';
import { sqlLikeEscape } from '@/misc/sql-like-escape.js';
export const meta = {
tags: ['notes'],
@@ -70,7 +71,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
}
query
.andWhere('note.text ILIKE :q', { q: `%${ps.query}%` })
.andWhere('note.text ILIKE :q', { q: `%${ sqlLikeEscape(ps.query) }%` })
.innerJoinAndSelect('note.user', 'user')
.leftJoinAndSelect('user.avatar', 'avatar')
.leftJoinAndSelect('user.banner', 'banner')

View File

@@ -6,6 +6,7 @@ import type { User } from '@/models/entities/User.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { DI } from '@/di-symbols.js';
import { sqlLikeEscape } from '@/misc/sql-like-escape.js';
export const meta = {
tags: ['users'],
@@ -59,10 +60,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
if (ps.host) {
const q = this.usersRepository.createQueryBuilder('user')
.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) {
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');
@@ -83,7 +84,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
.where(`user.id IN (${ followingQuery.getQuery() })`)
.andWhere('user.id != :meId', { meId: me.id })
.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
.where('user.updatedAt IS NULL')
.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() })`)
.andWhere('user.id != :meId', { meId: me.id })
.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');
otherQuery.setParameters(followingQuery.getParameters());
@@ -116,7 +117,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
} else {
users = await this.usersRepository.createQueryBuilder('user')
.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')
.orderBy('user.updatedAt', 'DESC')
.take(ps.limit - users.length)

View File

@@ -5,6 +5,7 @@ import type { User } from '@/models/entities/User.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { DI } from '@/di-symbols.js';
import { sqlLikeEscape } from '@/misc/sql-like-escape.js';
export const meta = {
tags: ['users'],
@@ -57,7 +58,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
if (isUsername) {
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
.where('user.updatedAt IS NULL')
.orWhere('user.updatedAt > :activeThreshold', { activeThreshold: activeThreshold });
@@ -78,11 +79,11 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
} else {
const nameQuery = this.usersRepository.createQueryBuilder('user')
.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
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
@@ -106,7 +107,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
if (users.length < ps.limit) {
const profQuery = this.userProfilesRepository.createQueryBuilder('prof')
.select('prof.userId')
.where('prof.description ILIKE :query', { query: '%' + ps.query + '%' });
.where('prof.description ILIKE :query', { query: '%' + sqlLikeEscape(ps.query) + '%' });
if (ps.origin === 'local') {
profQuery.andWhere('prof.userHost IS NULL');

View File

@@ -1,6 +1,5 @@
import { dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
import { PathOrFileDescriptor, readFileSync } from 'node:fs';
import { Inject, Injectable } from '@nestjs/common';
import { createBullBoard } from '@bull-board/api';
import { BullAdapter } from '@bull-board/api/bullAdapter.js';
@@ -71,9 +70,6 @@ export class ClientServerService {
@Inject(DI.pagesRepository)
private pagesRepository: PagesRepository,
@Inject(DI.emojisRepository)
private emojisRepository: EmojisRepository,
@Inject(DI.flashsRepository)
private flashsRepository: FlashsRepository,
@@ -225,49 +221,6 @@ export class ClientServerService {
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) => {
const path = request.params.path;
@@ -359,10 +312,10 @@ export class ClientServerService {
fastify.get('/opensearch.xml', async (request, reply) => {
const meta = await this.metaService.fetch();
const name = meta.name || 'Misskey';
const name = meta.name ?? 'Misskey';
let content = '';
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 += '<InputEncoding>UTF-8</InputEncoding>';
content += `<Image width="16" height="16" type="image/x-icon">${this.config.url}/favicon.ico</Image>`;
@@ -580,13 +533,12 @@ export class ClientServerService {
});
// Clip
// TODO: 非publicなclipのハンドリング
fastify.get<{ Params: { clip: string; } }>('/clips/:clip', async (request, reply) => {
const clip = await this.clipsRepository.findOneBy({
id: request.params.clip,
});
if (clip) {
if (clip && clip.isPublic) {
const _clip = await this.clipEntityService.pack(clip);
const profile = await this.userProfilesRepository.findOneByOrFail({ userId: clip.userId });
const meta = await this.metaService.fetch();

View File

@@ -31,7 +31,7 @@ html
link(rel='icon' href= icon || '/favicon.ico')
link(rel='apple-touch-icon' href= icon || '/apple-touch-icon.png')
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/not-found.jpg')
link(rel='prefetch' href='https://xn--931a.moe/assets/error.jpg')

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

View File

@@ -11,7 +11,7 @@
"@rollup/plugin-alias": "4.0.2",
"@rollup/plugin-json": "6.0.0",
"@rollup/pluginutils": "5.0.2",
"@syuilo/aiscript": "0.12.1",
"@syuilo/aiscript": "0.12.2",
"@tabler/icons": "^1.118.0",
"@vitejs/plugin-vue": "4.0.0",
"@vue/compiler-sfc": "3.2.45",
@@ -21,7 +21,7 @@
"broadcast-channel": "4.19.1",
"browser-image-resizer": "git+https://github.com/misskey-dev/browser-image-resizer#v2.2.1-misskey.3",
"canvas-confetti": "^1.6.0",
"chart.js": "4.1.1",
"chart.js": "4.1.2",
"chartjs-adapter-date-fns": "3.0.0",
"chartjs-chart-matrix": "^1.3.0",
"chartjs-plugin-gradient": "0.6.1",
@@ -36,16 +36,16 @@
"insert-text-at-cursor": "0.3.0",
"is-file-animated": "1.0.2",
"json5": "2.2.3",
"katex": "0.15.6",
"katex": "0.16.4",
"matter-js": "0.18.0",
"mfm-js": "0.23.0",
"mfm-js": "0.23.1",
"misskey-js": "0.0.14",
"photoswipe": "5.3.4",
"prismjs": "1.29.0",
"punycode": "2.1.1",
"querystring": "0.2.1",
"rndstr": "1.0.0",
"rollup": "3.9.0",
"rollup": "3.9.1",
"s-age": "1.1.2",
"sanitize-html": "^2.8.1",
"sass": "1.57.1",
@@ -56,14 +56,14 @@
"textarea-caret": "3.1.0",
"three": "0.148.0",
"throttle-debounce": "5.0.0",
"tinycolor2": "1.5.1",
"tinycolor2": "1.5.2",
"tsc-alias": "1.8.2",
"tsconfig-paths": "4.1.2",
"twemoji-parser": "14.0.0",
"typescript": "4.9.4",
"uuid": "9.0.0",
"vanilla-tilt": "1.8.0",
"vite": "4.0.3",
"vite": "4.0.4",
"vue": "3.2.45",
"vue-prism-editor": "2.0.0-alpha.2",
"vuedraggable": "next"
@@ -73,7 +73,7 @@
"@types/glob": "8.0.0",
"@types/gulp": "4.0.10",
"@types/gulp-rename": "2.0.1",
"@types/katex": "0.14.0",
"@types/katex": "0.16.0",
"@types/matter-js": "0.18.2",
"@types/punycode": "2.1.0",
"@types/sanitize-html": "^2.8.0",
@@ -83,16 +83,16 @@
"@types/uuid": "9.0.0",
"@types/websocket": "1.0.5",
"@types/ws": "8.5.4",
"@typescript-eslint/eslint-plugin": "5.47.1",
"@typescript-eslint/parser": "5.47.1",
"@typescript-eslint/eslint-plugin": "5.48.0",
"@typescript-eslint/parser": "5.48.0",
"@vue/runtime-core": "3.2.45",
"cross-env": "7.0.3",
"cypress": "12.2.0",
"cypress": "12.3.0",
"eslint": "8.31.0",
"eslint-plugin-import": "2.26.0",
"eslint-plugin-vue": "9.8.0",
"start-server-and-test": "1.15.2",
"vue-eslint-parser": "^9.1.0",
"vue-tsc": "^1.0.19"
"vue-tsc": "^1.0.22"
}
}

View File

@@ -6,12 +6,13 @@ import { del, get, set } from '@/scripts/idb-proxy';
import { apiUrl } from '@/config';
import { waiting, api, popup, popupMenu, success, alert } from '@/os';
import { unisonReload, reloadChannel } from '@/scripts/unison-reload';
import { miLocalStorage } from './local-storage';
// TODO: 他のタブと永続化されたstateを同期
type Account = misskey.entities.MeDetailed;
const accountData = localStorage.getItem('account');
const accountData = miLocalStorage.getItem('account');
// TODO: 外部からはreadonlyに
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() {
waiting();
localStorage.removeItem('account');
miLocalStorage.removeItem('account');
await removeAccount($i.id);
@@ -119,7 +120,7 @@ export function updateAccount(accountData) {
for (const [key, value] of Object.entries(accountData)) {
$i[key] = value;
}
localStorage.setItem('account', JSON.stringify($i));
miLocalStorage.setItem('account', JSON.stringify($i));
}
export function refreshAccount() {
@@ -130,7 +131,7 @@ export async function login(token: Account['token'], redirect?: string) {
waiting();
if (_DEV_) console.log('logging as token ', 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の認証とかで使う
await addAccount(me.id, token);

View File

@@ -37,7 +37,7 @@
<script lang="ts" setup>
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 { acct, userPage } from '@/filters/user';
import * as os from '@/os';

View File

@@ -28,7 +28,7 @@
import { ref, shallowRef } from 'vue';
import * as Misskey from 'misskey-js';
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 * as os from '@/os';
import { i18n } from '@/i18n';

View File

@@ -33,12 +33,12 @@
<option v-for="item in c.items" :key="item.value" :value="item.value">{{ item.text }}</option>
</MkSelect>
<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 v-for="child in c.children" :key="child">
<MkAsUi v-if="!g(child).hidden" :component="g(child)" :components="props.components" :size="size"/>
</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 }">
<template v-for="child in c.children" :key="child">
<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 * as os from '@/os';
import MkButton from '@/components/MkButton.vue';
import MkInput from '@/components/form/input.vue';
import MkSwitch from '@/components/form/switch.vue';
import MkTextarea from '@/components/form/textarea.vue';
import MkSelect from '@/components/form/select.vue';
import MkInput from '@/components/MkInput.vue';
import MkSwitch from '@/components/MkSwitch.vue';
import MkTextarea from '@/components/MkTextarea.vue';
import MkSelect from '@/components/MkSelect.vue';
import { AsUiComponent } from '@/scripts/aiscript/ui';
import FormFolder from '@/components/form/folder.vue';
import MkFolder from '@/components/MkFolder.vue';
const props = withDefaults(defineProps<{
component: AsUiComponent;

View File

@@ -46,6 +46,10 @@ import { defaultStore } from '@/store';
import { emojilist } from '@/scripts/emojilist';
import { instance } from '@/instance';
import { i18n } from '@/i18n';
import { miLocalStorage } from '@/local-storage';
import { getCustomEmojis } from '@/custom-emojis';
const customEmojis = await getCustomEmojis();
type EmojiDef = {
emoji: string;
@@ -85,7 +89,6 @@ for (const x of lib) {
emjdb.sort((a, b) => a.name.length - b.name.length);
//#region Construct Emoji DB
const customEmojis = instance.emojis;
const emojiDefinitions: EmojiDef[] = [];
for (const x of customEmojis) {
@@ -116,7 +119,6 @@ export default {
emojiDb,
emojiDefinitions,
emojilist,
customEmojis,
};
</script>
@@ -208,7 +210,7 @@ function exec() {
}
} else if (props.type === 'hashtag') {
if (!props.q || props.q === '') {
hashtags.value = JSON.parse(localStorage.getItem('hashtags') || '[]');
hashtags.value = JSON.parse(miLocalStorage.getItem('hashtags') || '[]');
fetching.value = false;
} else {
const cacheKey = `autocomplete:hashtag:${props.q}`;

View File

@@ -16,7 +16,6 @@
*/
import { onMounted, ref, shallowRef, watch, PropType, onUnmounted } from 'vue';
import { Chart } from 'chart.js';
import { enUS } from 'date-fns/locale';
import gradient from 'chartjs-plugin-gradient';
import * as os from '@/os';
import { defaultStore } from '@/store';
@@ -186,6 +185,10 @@ const render = () => {
time: {
stepSize: 1,
unit: props.span === 'day' ? 'month' : 'day',
displayFormats: {
day: 'M/d',
month: 'Y/M',
},
},
grid: {
},
@@ -194,11 +197,6 @@ const render = () => {
maxRotation: 0,
autoSkipPadding: 16,
},
adapters: {
date: {
locale: enUS,
},
},
min: getDate(props.limit).getTime(),
},
y: {

View File

@@ -72,4 +72,11 @@ defineExpose({
}
}
}
@container (max-width: 500px) {
.root {
font-size: 90%;
gap: 6px;
}
}
</style>

View 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>

View File

@@ -74,7 +74,7 @@ function onMousedown(evt: Event) {
}
.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;
}

View File

@@ -42,8 +42,8 @@
import { onBeforeUnmount, onMounted, ref, shallowRef } from 'vue';
import MkModal from '@/components/MkModal.vue';
import MkButton from '@/components/MkButton.vue';
import MkInput from '@/components/form/input.vue';
import MkSelect from '@/components/form/select.vue';
import MkInput from '@/components/MkInput.vue';
import MkSelect from '@/components/MkSelect.vue';
import { i18n } from '@/i18n';
type Input = {

View 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>

View File

@@ -1,12 +1,12 @@
<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()">
<div ref="emojis" class="emojis">
<div v-if="customEmojis != null && customEmojiCategories != null" ref="emojisEl" class="emojis">
<section class="result">
<div v-if="searchResultCustom.length > 0" class="body">
<button
v-for="emoji in searchResultCustom"
:key="emoji.id"
:key="emoji.name"
class="_button item"
:title="emoji.name"
tabindex="0"
@@ -85,15 +85,17 @@ import MkRippleEffect from '@/components/MkRippleEffect.vue';
import * as os from '@/os';
import { isTouchUsing } from '@/scripts/touch';
import { deviceKind } from '@/scripts/device-kind';
import { emojiCategories, instance } from '@/instance';
import { instance } from '@/instance';
import { i18n } from '@/i18n';
import { defaultStore } from '@/store';
import { getCustomEmojiCategories, getCustomEmojis } from '@/custom-emojis';
const props = withDefaults(defineProps<{
showPinned?: boolean;
asReactionPicker?: boolean;
maxHeight?: number;
asDrawer?: boolean;
asWindow?: boolean;
}>(), {
showPinned: true,
});
@@ -102,8 +104,17 @@ const emit = defineEmits<{
(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 emojis = shallowRef<HTMLDivElement>();
const emojisEl = shallowRef<HTMLDivElement>();
const {
reactions: pinned,
@@ -117,15 +128,13 @@ const {
const size = computed(() => props.asReactionPicker ? reactionPickerSize.value : 1);
const width = computed(() => props.asReactionPicker ? reactionPickerWidth.value : 3);
const height = computed(() => props.asReactionPicker ? reactionPickerHeight.value : 2);
const customEmojiCategories = emojiCategories;
const customEmojis = instance.emojis;
const q = ref<string>('');
const searchResultCustom = ref<Misskey.entities.CustomEmoji[]>([]);
const searchResultUnicode = ref<UnicodeEmojiDef[]>([]);
const tab = ref<'index' | 'custom' | 'unicode' | 'tags'>('index');
watch(q, () => {
if (emojis.value) emojis.value.scrollTop = 0;
if (emojisEl.value) emojisEl.value.scrollTop = 0;
if (q.value === '') {
searchResultCustom.value = [];
@@ -274,7 +283,7 @@ function focus() {
}
function reset() {
if (emojis.value) emojis.value.scrollTop = 0;
if (emojisEl.value) emojisEl.value.scrollTop = 0;
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 {
width: 100%;
padding: 12px;

View File

@@ -1,13 +1,13 @@
<template>
<MkWindow ref="window"
:initial-width="null"
:initial-height="null"
:can-resize="false"
:initial-width="300"
:initial-height="290"
:can-resize="true"
:mini="true"
:front="true"
@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>
</template>
@@ -34,147 +34,8 @@ function chosen(emoji: any) {
}
</script>
<style lang="scss" scoped>
.omfetrab {
$pad: 8px;
--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 lang="scss" module>
.picker {
height: 100%;
}
</style>

View File

@@ -23,7 +23,7 @@
import { } from 'vue';
import * as Misskey from 'misskey-js';
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 { i18n } from '@/i18n';

View 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>

View File

@@ -1,13 +1,15 @@
<template>
<div class="ssazuxis">
<header class="_button" :style="{ background: bg }" @click="showBody = !showBody">
<div class="title"><slot name="header"></slot></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>
<div ref="rootEl" class="dwzlatin" :class="{ opened }">
<div class="header _button" @click="toggle">
<span class="icon"><slot name="icon"></slot></span>
<span class="text"><slot name="label"></slot></span>
<span class="right">
<span class="text"><slot name="suffix"></slot></span>
<i v-if="opened" class="ti ti-chevron-up icon"></i>
<i v-else class="ti ti-chevron-down icon"></i>
</span>
</div>
<div v-if="openedAtLeastOnce" class="body" :class="{ bgSame }" :style="{ maxHeight: maxHeight ? `${maxHeight}px` : null }">
<Transition
:name="$store.state.animation ? 'folder-toggle' : ''"
@enter="enter"
@@ -15,147 +17,162 @@
@leave="leave"
@after-leave="afterLeave"
>
<div v-show="showBody">
<KeepAlive>
<div v-show="opened">
<MkSpacer :margin-min="14" :margin-max="22">
<slot></slot>
</MkSpacer>
</div>
</KeepAlive>
</Transition>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import tinycolor from 'tinycolor2';
<script lang="ts" setup>
import { nextTick, onMounted } from 'vue';
const localStoragePrefix = 'ui:folder:';
const props = withDefaults(defineProps<{
defaultOpen: boolean;
maxHeight: number | null;
}>(), {
defaultOpen: false,
maxHeight: null,
});
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 && 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;
const getBgColor = (el: HTMLElement) => {
const style = window.getComputedStyle(el);
if (style.backgroundColor && !['rgba(0, 0, 0, 0)', 'rgba(0,0,0,0)', 'transparent'].includes(style.backgroundColor)) {
return style.backgroundColor;
} else {
return getParentBg(el.parentElement);
return el.parentElement ? getBgColor(el.parentElement) : 'transparent';
}
}
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>();
let bgSame = $ref(false);
let opened = $ref(props.defaultOpen);
let openedAtLeastOnce = $ref(props.defaultOpen);
function enter(el) {
const elementHeight = el.getBoundingClientRect().height;
el.style.height = 0;
el.offsetHeight; // reflow
el.style.height = elementHeight + 'px';
},
afterEnter(el) {
el.style.height = Math.min(elementHeight, props.maxHeight ?? Infinity) + 'px';
}
function afterEnter(el) {
el.style.height = null;
},
leave(el) {
}
function leave(el) {
const elementHeight = el.getBoundingClientRect().height;
el.style.height = elementHeight + 'px';
el.offsetHeight; // reflow
el.style.height = 0;
},
afterLeave(el) {
}
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>
<style lang="scss" scoped>
.folder-toggle-enter-active, .folder-toggle-leave-active {
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 {
opacity: 0;
}
.folder-toggle-leave-to {
.folder-toggle-enter-from, .folder-toggle-leave-to {
opacity: 0;
}
.ssazuxis {
position: relative;
.dwzlatin {
display: block;
> header {
> .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));
align-items: center;
width: 100%;
box-sizing: border-box;
padding: 10px 14px 10px 14px;
background: var(--buttonBg);
border-radius: 6px;
> .title {
display: grid;
place-content: center;
margin: 0;
padding: 12px 16px 12px 0;
> i {
margin-right: 6px;
&:hover {
text-decoration: none;
background: var(--buttonHoverBg);
}
&.active {
color: var(--accent);
background: var(--buttonHoverBg);
}
> .icon {
margin-right: 0.75em;
flex-shrink: 0;
text-align: center;
opacity: 0.8;
&:empty {
display: none;
& + .text {
padding-left: 4px;
}
}
}
> .divider {
flex: 1;
margin: auto;
height: 1px;
background: var(--divider);
> .text {
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
padding-right: 12px;
}
> button {
padding: 12px 0 12px 16px;
}
}
}
> .right {
margin-left: auto;
opacity: 0.7;
white-space: nowrap;
@container (max-width: 500px) {
.ssazuxis {
> header {
> .title {
padding: 8px 10px 8px 0;
> .text:not(:empty) {
margin-right: 0.75em;
}
}
}
> .body {
background: var(--panel);
border-radius: 0 0 6px 6px;
container-type: inline-size;
overflow: auto;
&.bgSame {
background: var(--bg);
}
}
&.opened {
> .header {
border-radius: 6px 6px 0 0;
}
}
}
</style>

View File

@@ -36,7 +36,7 @@
import { } from 'vue';
import MkModalWindow from '@/components/MkModalWindow.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 { instance } from '@/instance';
import { i18n } from '@/i18n';

View File

@@ -17,34 +17,34 @@
<MkSpacer :margin-min="20" :margin-max="32">
<div class="xkpnjxcv _gaps_m">
<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 v-if="form[item].description" #caption>{{ form[item].description }}</template>
</FormInput>
<FormInput v-else-if="form[item].type === 'string' && !form[item].multiline" v-model="values[item]" type="text">
</MkInput>
<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 v-if="form[item].description" #caption>{{ form[item].description }}</template>
</FormInput>
<FormTextarea v-else-if="form[item].type === 'string' && form[item].multiline" v-model="values[item]">
</MkInput>
<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 v-if="form[item].description" #caption>{{ form[item].description }}</template>
</FormTextarea>
<FormSwitch v-else-if="form[item].type === 'boolean'" v-model="values[item]">
</MkTextarea>
<MkSwitch v-else-if="form[item].type === 'boolean'" v-model="values[item]">
<span v-text="form[item].label || item"></span>
<template v-if="form[item].description" #caption>{{ form[item].description }}</template>
</FormSwitch>
<FormSelect v-else-if="form[item].type === 'enum'" v-model="values[item]">
</MkSwitch>
<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>
<option v-for="item in form[item].enum" :key="item.value" :value="item.value">{{ item.label }}</option>
</FormSelect>
<FormRadios v-else-if="form[item].type === 'radio'" v-model="values[item]">
</MkSelect>
<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>
<option v-for="item in form[item].options" :key="item.value" :value="item.value">{{ item.label }}</option>
</FormRadios>
<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">
</MkRadios>
<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 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)">
<span v-text="form[item].content || item"></span>
</MkButton>
@@ -56,25 +56,25 @@
<script lang="ts">
import { defineComponent } from 'vue';
import FormInput from './form/input.vue';
import FormTextarea from './form/textarea.vue';
import FormSwitch from './form/switch.vue';
import FormSelect from './form/select.vue';
import FormRange from './form/range.vue';
import MkInput from './MkInput.vue';
import MkTextarea from './MkTextarea.vue';
import MkSwitch from './MkSwitch.vue';
import MkSelect from './MkSelect.vue';
import MkRange from './MkRange.vue';
import MkButton from './MkButton.vue';
import FormRadios from './form/radios.vue';
import MkRadios from './MkRadios.vue';
import MkModalWindow from '@/components/MkModalWindow.vue';
export default defineComponent({
components: {
MkModalWindow,
FormInput,
FormTextarea,
FormSwitch,
FormSelect,
FormRange,
MkInput,
MkTextarea,
MkSwitch,
MkSelect,
MkRange,
MkButton,
FormRadios,
MkRadios,
},
props: {

View File

@@ -10,7 +10,6 @@
<script lang="ts" setup>
import { markRaw, version as vueVersion, onMounted, onBeforeUnmount, nextTick, watch } from 'vue';
import { Chart } from 'chart.js';
import { enUS } from 'date-fns/locale';
import tinycolor from 'tinycolor2';
import { MatrixController, MatrixElement } from 'chartjs-chart-matrix';
import * as os from '@/os';
@@ -149,7 +148,9 @@ async function renderChart() {
round: 'week',
isoWeekday: 0,
displayFormats: {
week: 'MMM dd',
day: 'M/d',
month: 'Y/M',
week: 'M/d',
},
},
grid: {

View File

@@ -1,6 +1,6 @@
<template>
<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">
<span class="host">{{ instance.name ?? instance.host }}</span>
<span class="sub _monospace"><b>{{ instance.host }}</b> / {{ instance.softwareName || '?' }} {{ instance.softwareVersion }}</span>

View File

@@ -1,6 +1,6 @@
<template>
<div :class="$style.root">
<MkFolder class="item">
<MkFoldableSection class="item">
<template #header>Chart</template>
<div :class="$style.chart">
<div class="selects">
@@ -34,9 +34,9 @@
<MkChart :src="chartSrc" :span="chartSpan" :limit="chartLimit" :detailed="true"></MkChart>
</div>
</div>
</MkFolder>
</MkFoldableSection>
<MkFolder class="item">
<MkFoldableSection class="item">
<template #header>Active users heatmap</template>
<MkSelect v-model="heatmapSrc" style="margin: 0 0 12px 0;">
<option value="active-users">Active users</option>
@@ -48,16 +48,16 @@
<div class="_panel" :class="$style.heatmap">
<MkHeatmap :src="heatmapSrc"/>
</div>
</MkFolder>
</MkFoldableSection>
<MkFolder class="item">
<MkFoldableSection class="item">
<template #header>Retention rate</template>
<div class="_panel" :class="$style.retention">
<MkRetentionHeatmap/>
</div>
</MkFolder>
</MkFoldableSection>
<MkFolder class="item">
<MkFoldableSection class="item">
<template #header>Federation</template>
<div :class="$style.federation">
<div class="pies">
@@ -71,20 +71,20 @@
</div>
</div>
</div>
</MkFolder>
</MkFoldableSection>
</div>
</template>
<script lang="ts" setup>
import { onMounted } from 'vue';
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 { useChartTooltip } from '@/scripts/use-chart-tooltip';
import * as os from '@/os';
import { i18n } from '@/i18n';
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 { initChart } from '@/scripts/init-chart';

View File

@@ -31,7 +31,7 @@
<span v-if="item.indicate" class="indicator"><i class="_indicatorCircle"></i></span>
</button>
<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>
<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>
@@ -58,7 +58,7 @@
<script lang="ts" setup>
import { defineAsyncComponent, nextTick, onBeforeUnmount, onMounted, onUnmounted, Ref, ref, watch } from 'vue';
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 * as os from '@/os';
import { i18n } from '@/i18n';
@@ -251,17 +251,18 @@ onBeforeUnmount(() => {
color: #fff;
&:before {
background: #d42e2e;
background: #d42e2e !important;
}
}
}
&:active,
&.active {
color: var(--fgOnAccent);
color: var(--fgOnAccent) !important;
opacity: 1;
&:before {
background: var(--accent);
background: var(--accent) !important;
}
}

View File

@@ -1,7 +1,7 @@
<template>
<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 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">
<slot :max-height="maxHeight" :type="type"></slot>
</div>
@@ -63,6 +63,7 @@ let transformOrigin = $ref('center');
let showing = $ref(true);
let content = $shallowRef<HTMLElement>();
const zIndex = os.claimZIndex(props.zPriority);
let useSendAnime = $ref(false);
const type = $computed<ModalTypes>(() => {
if (props.preferType === 'auto') {
if (!defaultStore.state.disableDrawer && isTouchUsing && deviceKind === 'smartphone') {
@@ -74,15 +75,34 @@ const type = $computed<ModalTypes>(() => {
return props.preferType!;
}
});
let transitionName = $ref(defaultStore.state.animation ? (type === 'drawer') ? 'modal-drawer' : (type === 'popup') ? 'modal-popup' : 'modal' : '');
let transitionDuration = $ref(defaultStore.state.animation ? 200 : 0);
let transitionName = $computed((() =>
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;
function close(opts: { useSendAnimation?: boolean } = {}) {
if (opts.useSendAnimation) {
transitionName = 'send';
transitionDuration = 400;
useSendAnime = true;
}
// eslint-disable-next-line vue/no-mutating-props
@@ -267,9 +287,8 @@ defineExpose({
}
> .content {
transform-style: preserve-3d;
transform: perspective(50cm) translateZ(0px) translateY(0px) rotateX(0deg);
transition: opacity 0.4s cubic-bezier(.5,-0.5,.75,1), transform 0.4s cubic-bezier(.5,-0.5,.75,1) !important;
transform: translateY(0px);
transition: opacity 0.3s ease-in, transform 0.3s cubic-bezier(.5,-0.5,1,.5) !important;
}
}
.send-enter-from, .send-leave-to {
@@ -280,8 +299,7 @@ defineExpose({
> .content {
pointer-events: none;
opacity: 0;
transform-style: preserve-3d;
transform: perspective(50cm) translateZ(-300px) translateY(-200px) rotateX(40deg);
transform: translateY(-300px);
}
}
@@ -310,12 +328,12 @@ defineExpose({
.modal-popup-enter-active, .modal-popup-leave-active {
> .bg {
transition: opacity 0.2s !important;
transition: opacity 0.1s !important;
}
> .content {
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 {

View File

@@ -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._featuredId_" class="info"><i class="ti ti-bolt"></i> {{ i18n.ts.featured }}</div>
<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>
<I18n :src="i18n.ts.renotedBy" tag="span">
<template #user>
@@ -27,11 +27,16 @@
<i v-if="isMyRenote" class="ti ti-dots dropdownIcon"></i>
<MkTime :time="note.createdAt"/>
</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>
<article class="article" @contextmenu.stop="onContextmenu">
<MkAvatar class="avatar" :user="appearNote.user"/>
<MkAvatar v-once class="avatar" :user="appearNote.user"/>
<div class="main">
<MkNoteHeader class="header" :note="appearNote" :mini="true"/>
<MkInstanceTicker v-if="showTicker" class="ticker" :instance="appearNote.user.instance"/>
@@ -44,7 +49,7 @@
<div class="text">
<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>
<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>
<div v-if="translating || translation" class="translation">
<MkLoading v-if="translating" mini/>
@@ -75,14 +80,25 @@
<i class="ti ti-arrow-back-up"></i>
<p v-if="appearNote.repliesCount > 0" class="count">{{ appearNote.repliesCount }}</p>
</button>
<MkRenoteButton ref="renoteButton" class="button" :note="appearNote" :count="appearNote.renoteCount"/>
<button v-if="appearNote.myReaction == null" ref="reactButton" class="button _button" @click="react()">
<button
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>
</button>
<button v-if="appearNote.myReaction != null" ref="reactButton" class="button _button reacted" @click="undoReact(appearNote)">
<i class="ti ti-minus"></i>
</button>
<button ref="menuButton" class="button _button" @click="menu()">
<button ref="menuButton" class="button _button" @mousedown="menu()">
<i class="ti ti-dots"></i>
</button>
</footer>
@@ -111,10 +127,9 @@ import MkReactionsViewer from '@/components/MkReactionsViewer.vue';
import MkMediaList from '@/components/MkMediaList.vue';
import MkCwButton from '@/components/MkCwButton.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 MkInstanceTicker from '@/components/MkInstanceTicker.vue';
import MkVisibility from '@/components/MkVisibility.vue';
import { pleaseLogin } from '@/scripts/please-login';
import { focusPrev, focusNext } from '@/scripts/focus';
import { checkWordMute } from '@/scripts/check-word-mute';
@@ -128,6 +143,7 @@ import { i18n } from '@/i18n';
import { getNoteMenu } from '@/scripts/get-note-menu';
import { useNoteCapture } from '@/scripts/use-note-capture';
import { deepClone } from '@/scripts/clone';
import { useTooltip } from '@/scripts/use-tooltip';
const props = defineProps<{
note: misskey.entities.Note;
@@ -158,7 +174,7 @@ const isRenote = (
const el = shallowRef<HTMLElement>();
const menuButton = shallowRef<HTMLElement>();
const renoteButton = shallowRef<InstanceType<typeof MkRenoteButton>>();
const renoteButton = shallowRef<HTMLElement>();
const renoteTime = shallowRef<HTMLElement>();
const reactButton = shallowRef<HTMLElement>();
let appearNote = $computed(() => isRenote ? note.renote as misskey.entities.Note : note);
@@ -175,6 +191,7 @@ const translation = ref(null);
const translating = ref(false);
const urls = appearNote.text ? extractUrlFromMfm(mfm.parse(appearNote.text)) : null;
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 = {
'r': () => reply(true),
@@ -193,6 +210,47 @@ useNoteCapture({
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 {
pleaseLogin();
os.post({

View File

@@ -25,7 +25,12 @@
<i v-if="isMyRenote" class="ti ti-dots dropdownIcon"></i>
<MkTime :time="note.createdAt"/>
</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>
<article class="article" @contextmenu.stop="onContextmenu">
@@ -38,7 +43,12 @@
</MkA>
<span v-if="appearNote.user.isBot" class="is-bot">bot</span>
<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 class="username"><MkAcct :user="appearNote.user"/></div>
@@ -85,14 +95,25 @@
<i class="ti ti-arrow-back-up"></i>
<p v-if="appearNote.repliesCount > 0" class="count">{{ appearNote.repliesCount }}</p>
</button>
<MkRenoteButton ref="renoteButton" class="button" :note="appearNote" :count="appearNote.renoteCount"/>
<button v-if="appearNote.myReaction == null" ref="reactButton" class="button _button" @click="react()">
<button
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>
</button>
<button v-if="appearNote.myReaction != null" ref="reactButton" class="button _button reacted" @click="undoReact(appearNote)">
<i class="ti ti-minus"></i>
</button>
<button ref="menuButton" class="button _button" @click="menu()">
<button ref="menuButton" class="button _button" @mousedown="menu()">
<i class="ti ti-dots"></i>
</button>
</footer>
@@ -121,10 +142,9 @@ import MkReactionsViewer from '@/components/MkReactionsViewer.vue';
import MkMediaList from '@/components/MkMediaList.vue';
import MkCwButton from '@/components/MkCwButton.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 MkInstanceTicker from '@/components/MkInstanceTicker.vue';
import MkVisibility from '@/components/MkVisibility.vue';
import { pleaseLogin } from '@/scripts/please-login';
import { checkWordMute } from '@/scripts/check-word-mute';
import { userPage } from '@/filters/user';
@@ -138,6 +158,7 @@ import { i18n } from '@/i18n';
import { getNoteMenu } from '@/scripts/get-note-menu';
import { useNoteCapture } from '@/scripts/use-note-capture';
import { deepClone } from '@/scripts/clone';
import { useTooltip } from '@/scripts/use-tooltip';
const props = defineProps<{
note: misskey.entities.Note;
@@ -168,7 +189,7 @@ const isRenote = (
const el = shallowRef<HTMLElement>();
const menuButton = shallowRef<HTMLElement>();
const renoteButton = shallowRef<InstanceType<typeof MkRenoteButton>>();
const renoteButton = shallowRef<HTMLElement>();
const renoteTime = shallowRef<HTMLElement>();
const reactButton = shallowRef<HTMLElement>();
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 conversation = ref<misskey.entities.Note[]>([]);
const replies = ref<misskey.entities.Note[]>([]);
const canRenote = computed(() => ['public', 'home'].includes(appearNote.visibility) || appearNote.userId === $i.id);
const keymap = {
'r': () => reply(true),
@@ -198,6 +220,47 @@ useNoteCapture({
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 {
pleaseLogin();
os.post({

View File

@@ -1,6 +1,6 @@
<template>
<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"/>
</MkA>
<div v-if="note.user.isBot" class="is-bot">bot</div>
@@ -9,7 +9,12 @@
<MkA class="created-at" :to="notePage(note)">
<MkTime :time="note.createdAt"/>
</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>
</header>
</template>
@@ -17,7 +22,7 @@
<script lang="ts" setup>
import { } from 'vue';
import * as misskey from 'misskey-js';
import MkVisibility from '@/components/MkVisibility.vue';
import { i18n } from '@/i18n';
import { notePage } from '@/filters/note';
import { userPage } from '@/filters/user';

View File

@@ -1,6 +1,6 @@
<template>
<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-else-if="notification.user" class="icon" :user="notification.user"/>
<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 === 'mention'" class="ti ti-at"></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>
<!-- notification.reaction null になることはまずないがここでoptional chaining使うと一部ブラウザで刺さるので念の為 -->
<XReactionIcon
<MkReactionIcon
v-else-if="notification.type === 'reaction'"
ref="reactionRef"
:reaction="notification.reaction ? notification.reaction.replace(/^:(\w+):$/, ':$1@.:') : notification.reaction"
@@ -32,50 +31,47 @@
<span v-else>{{ notification.header }}</span>
<MkTime v-if="withTime" :time="notification.createdAt" class="time"/>
</header>
<div v-once class="content">
<MkA v-if="notification.type === 'reaction'" class="text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)">
<i class="ti ti-quote"></i>
<Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="!full"/>
<i class="ti ti-quote"></i>
</MkA>
<MkA v-if="notification.type === 'renote'" class="text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note.renote)">
<MkA v-else-if="notification.type === 'renote'" class="text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note.renote)">
<i class="ti ti-quote"></i>
<Mfm :text="getNoteSummary(notification.note.renote)" :plain="true" :nowrap="!full"/>
<i class="ti ti-quote"></i>
</MkA>
<MkA v-if="notification.type === 'reply'" class="text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)">
<MkA v-else-if="notification.type === 'reply'" class="text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)">
<Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="!full"/>
</MkA>
<MkA v-if="notification.type === 'mention'" class="text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)">
<MkA v-else-if="notification.type === 'mention'" class="text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)">
<Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="!full"/>
</MkA>
<MkA v-if="notification.type === 'quote'" class="text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)">
<MkA v-else-if="notification.type === 'quote'" class="text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)">
<Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="!full"/>
</MkA>
<MkA v-if="notification.type === 'pollVote'" class="text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)">
<MkA v-else-if="notification.type === 'pollEnded'" class="text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)">
<i class="ti ti-quote"></i>
<Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="!full"/>
<i class="ti ti-quote"></i>
</MkA>
<MkA v-if="notification.type === 'pollEnded'" class="text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)">
<i class="ti ti-quote"></i>
<Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="!full"/>
<i class="ti ti-quote"></i>
</MkA>
<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-if="notification.type === 'followRequestAccepted'" class="text" style="opacity: 0.6;">{{ i18n.ts.followRequestAccepted }}</span>
<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 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>
<span v-if="notification.type === 'app'" class="text">
<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>
<span v-else-if="notification.type === 'followRequestAccepted'" class="text" style="opacity: 0.6;">{{ i18n.ts.followRequestAccepted }}</span>
<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>
<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-else-if="notification.type === 'app'" class="text">
<Mfm :text="notification.body" :nowrap="!full"/>
</span>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { ref, shallowRef, onMounted, onUnmounted, watch } from 'vue';
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 XReactionTooltip from '@/components/MkReactionTooltip.vue';
import { getNoteSummary } from '@/scripts/get-note-summary';
@@ -239,12 +235,6 @@ useTooltip(reactionRef, (showing) => {
pointer-events: none;
}
&.pollVote {
padding: 3px;
background: #88a6b7;
pointer-events: none;
}
&.pollEnded {
padding: 3px;
background: #88a6b7;
@@ -275,6 +265,7 @@ useTooltip(reactionRef, (showing) => {
}
}
> .content {
> .text {
white-space: nowrap;
overflow: hidden;
@@ -295,6 +286,7 @@ useTooltip(reactionRef, (showing) => {
}
}
}
}
}
@container (max-width: 600px) {

View File

@@ -35,7 +35,7 @@
<script lang="ts" setup>
import { } from 'vue';
import { notificationTypes } from 'misskey-js';
import MkSwitch from './form/switch.vue';
import MkSwitch from './MkSwitch.vue';
import MkInfo from './MkInfo.vue';
import MkButton from './MkButton.vue';
import MkModalWindow from '@/components/MkModalWindow.vue';

View File

@@ -84,6 +84,7 @@ provideMetadataReceiver((info) => {
});
provide('shouldOmitHeaderTitle', true);
provide('shouldHeaderThin', true);
provide('forceSpacerMin', true);
const contextmenu = $computed(() => ([{
icon: 'ti ti-player-eject',
@@ -136,5 +137,7 @@ defineExpose({
.yrolvcoq {
min-height: 100%;
background: var(--bg);
--margin: var(--marginHalf);
}
</style>

View File

@@ -19,7 +19,8 @@ const emit = defineEmits<{
}>();
let up = $ref(false);
const zIndex = os.claimZIndex('veryLow');
const zIndex = os.claimZIndex('middle');
const angle = (45 - (Math.random() * 90)) + 'deg';
onMounted(() => {
window.setTimeout(() => {
@@ -50,15 +51,17 @@ onMounted(() => {
right: 0;
bottom: 0;
margin: auto;
color: var(--accent);
color: #fff;
text-shadow: 0 0 6px #000;
font-size: 18px;
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);
will-change: opacity, transform;
&.up {
opacity: 0;
transform: translateY(-50px);
transform: translateY(-50px) rotateZ(v-bind(angle));
}
}
}

View File

@@ -49,9 +49,9 @@
<script lang="ts" setup>
import { ref, watch } from 'vue';
import MkInput from './form/input.vue';
import MkSelect from './form/select.vue';
import MkSwitch from './form/switch.vue';
import MkInput from './MkInput.vue';
import MkSelect from './MkSelect.vue';
import MkSwitch from './MkSwitch.vue';
import MkButton from './MkButton.vue';
import { formatDateTimeString } from '@/scripts/format-time-string';
import { addTime } from '@/scripts/time';

Some files were not shown because too many files have changed in this diff Show More