Merge branch 'develop' into fetch

This commit is contained in:
tamaina 2023-01-09 09:23:42 +00:00
commit 5fa3415971
157 changed files with 2213 additions and 1185 deletions

View file

@ -12,7 +12,7 @@ You should also include the user name that made the change.
## 13.0.0 (unreleased) ## 13.0.0 (unreleased)
### TL;DR ### TL;DR
- New features (Play, new widgets, new charts, etc) - New features (Play, new widgets, new charts, 🍪👈, etc)
- Rewriten backend - Rewriten backend
- Better performance (backend and frontend) - Better performance (backend and frontend)
- Various usability improvements - Various usability improvements
@ -30,6 +30,7 @@ You should also include the user name that made the change.
#### For users #### For users
- ノートのウォッチ機能が削除されました - ノートのウォッチ機能が削除されました
- アンケートに投票された際に通知が作成されなくなりました
- 新たに動的なPagesを作ることはできなくなりました - 新たに動的なPagesを作ることはできなくなりました
- 代わりにAiScriptを用いてより柔軟に動的なコンテンツを作成できるMisskey Play機能が実装されています。 - 代わりにAiScriptを用いてより柔軟に動的なコンテンツを作成できるMisskey Play機能が実装されています。
- AiScriptが0.12.2にアップデートされました - AiScriptが0.12.2にアップデートされました
@ -39,6 +40,8 @@ You should also include the user name that made the change.
- Firefox109以下はサポートされなくなりました - Firefox109以下はサポートされなくなりました
#### For app developers #### For app developers
- API: metaのレスポンスに`emojis`プロパティが含まれなくなりました
- カスタム絵文字一覧情報を取得するには、`emojis`エンドポイントにリクエストします
- API: カスタム絵文字エンティティに`url`プロパティが含まれなくなりました - API: カスタム絵文字エンティティに`url`プロパティが含まれなくなりました
- 絵文字画像を表示するには、`<instance host>/emoji/<emoji name>.webp`にリクエストすると画像が返ります。 - 絵文字画像を表示するには、`<instance host>/emoji/<emoji name>.webp`にリクエストすると画像が返ります。
- e.g. `https://p1.a9z.dev/emoji/misskey.webp` - e.g. `https://p1.a9z.dev/emoji/misskey.webp`
@ -77,13 +80,17 @@ You should also include the user name that made the change.
- Client: Improve RSS widget @tamaina - Client: Improve RSS widget @tamaina
- Client: show Unicode emoji tooltip with its name in MkReactionsViewer.reaction @saschanaz - Client: show Unicode emoji tooltip with its name in MkReactionsViewer.reaction @saschanaz
- Client: OpenSearch support @SoniEx2 @chaoticryptidz - Client: OpenSearch support @SoniEx2 @chaoticryptidz
- Client: Support remote objects in search @SoniEx2
- Client: user activity page @syuilo
- Client: add user list widget @syuilo - Client: add user list widget @syuilo
- Client: add heatmap of daily active users to about page @syuilo - Client: add heatmap of daily active users to about page @syuilo
- Client: introduce fluent emoji @syuilo - Client: introduce fluent emoji @syuilo
- Client: add new theme @syuilo
- Client: show fireworks when visit user who today is birthday @syuilo - Client: show fireworks when visit user who today is birthday @syuilo
- Client: show bot warning on screen when logged in as bot account @syuilo - Client: show bot warning on screen when logged in as bot account @syuilo
- Client: improve overall performance of client @syuilo - Client: improve overall performance of client @syuilo
- Client: ui tweaks @syuilo - Client: ui tweaks @syuilo
- Client: clicker game @syuilo
### Bugfixes ### Bugfixes
- Server: 引用内の文章がnyaizeされてしまう問題を修正 @kabo2468 - Server: 引用内の文章がnyaizeされてしまう問題を修正 @kabo2468
@ -94,6 +101,11 @@ You should also include the user name that made the change.
- Server: アンテナの作成数上限を追加 @syuilo - Server: アンテナの作成数上限を追加 @syuilo
- Server: pages/likeのエラーIDが重複しているのを修正 @syuilo - Server: pages/likeのエラーIDが重複しているのを修正 @syuilo
- Server: pages/updateのパラメータによってはsummaryの値が更新されないのを修正 @syuilo - Server: pages/updateのパラメータによってはsummaryの値が更新されないのを修正 @syuilo
- Server: Escape SQL LIKE @mei23
- Server: 特定のPNG画像のアップロードに失敗する問題を修正 @usbharu
- Server: 非公開のクリップのURLでOGPレンダリングされる問題を修正 @syuilo
- Server: アンテナタイムライン(ストリーミング)が、フォローしていないユーザーの鍵投稿も拾ってしまう @syuilo
- Client: 日付形式の文字列などがカスタム絵文字として表示されるのを修正 @syuilo
- Client: case insensitive emoji search @saschanaz - Client: case insensitive emoji search @saschanaz
- Client: InAppウィンドウが操作できなくなることがあるのを修正 @tamaina - Client: InAppウィンドウが操作できなくなることがあるのを修正 @tamaina
- Client: use proxied image for instance icon @syuilo - Client: use proxied image for instance icon @syuilo

View file

@ -1294,7 +1294,6 @@ _notification:
youGotReply: "ردّ عليك {name}" youGotReply: "ردّ عليك {name}"
youGotQuote: "اقتبس منك {name}" youGotQuote: "اقتبس منك {name}"
youRenoted: "إعادت نشر من {name}" youRenoted: "إعادت نشر من {name}"
youGotPoll: "شارك {name} في استطلاع الرأي"
youGotMessagingMessageFromUser: "لقد تلقيت رسالة مِن {name}" youGotMessagingMessageFromUser: "لقد تلقيت رسالة مِن {name}"
youGotMessagingMessageFromGroup: "لقد أرسِلَت رسالة إلى الفريق {name}" youGotMessagingMessageFromGroup: "لقد أرسِلَت رسالة إلى الفريق {name}"
youWereFollowed: "يتابعك" youWereFollowed: "يتابعك"
@ -1311,7 +1310,6 @@ _notification:
renote: "أعد النشر" renote: "أعد النشر"
quote: "الاقتباسات" quote: "الاقتباسات"
reaction: "التفاعلات" reaction: "التفاعلات"
pollVote: "مصوِت شارك في الاستطلاع"
receiveFollowRequest: "طلبات المتابعة المتلقاة" receiveFollowRequest: "طلبات المتابعة المتلقاة"
followRequestAccepted: "طلبات المتابعة المقبولة" followRequestAccepted: "طلبات المتابعة المقبولة"
groupInvited: "دعوات الفريق" groupInvited: "دعوات الفريق"

View file

@ -1386,7 +1386,6 @@ _notification:
youGotReply: "{name} আপনাকে জবাব দিয়েছে" youGotReply: "{name} আপনাকে জবাব দিয়েছে"
youGotQuote: "{name} আপনাকে উদ্ধৃত করেছে" youGotQuote: "{name} আপনাকে উদ্ধৃত করেছে"
youRenoted: "{name} এর Renote" youRenoted: "{name} এর Renote"
youGotPoll: "{name} আপনার পোলে ভোট দিয়েছে"
youGotMessagingMessageFromUser: "{name} আপনাকে মেসেজ করেছে" youGotMessagingMessageFromUser: "{name} আপনাকে মেসেজ করেছে"
youGotMessagingMessageFromGroup: "{name} গ্রুপে একটি নতুন মেসেজ আছে" youGotMessagingMessageFromGroup: "{name} গ্রুপে একটি নতুন মেসেজ আছে"
youWereFollowed: "আপনাকে অনুসরণ করছে" youWereFollowed: "আপনাকে অনুসরণ করছে"
@ -1403,7 +1402,6 @@ _notification:
renote: "রিনোট" renote: "রিনোট"
quote: "উদ্ধৃতি" quote: "উদ্ধৃতি"
reaction: "প্রতিক্রিয়া" reaction: "প্রতিক্রিয়া"
pollVote: "পোলে ভোট আছে"
pollEnded: "পোল শেষ" pollEnded: "পোল শেষ"
receiveFollowRequest: "প্রাপ্ত অনুসরণের অনুরোধসমূহ" receiveFollowRequest: "প্রাপ্ত অনুসরণের অনুরোধসমূহ"
followRequestAccepted: "গৃহীত অনুসরণের অনুরোধসমূহ" followRequestAccepted: "গৃহীত অনুসরণের অনুরোধসমূহ"

View file

@ -920,6 +920,10 @@ like: "Gefällt mir"
unlike: "\"Gefällt mir\" entfernen" unlike: "\"Gefällt mir\" entfernen"
numberOfLikes: "\"Gefällt mir\"-Anzahl" numberOfLikes: "\"Gefällt mir\"-Anzahl"
show: "Anzeigen" show: "Anzeigen"
neverShow: "Nicht wieder anzeigen"
remindMeLater: "Vielleicht später"
didYouLikeMisskey: "Gefällt dir Misskey?"
pleaseDonate: "Misskey ist die kostenlose Software, die von {host} verwendet wird. Wir würden uns über Spenden freuen, damit dessen Entwicklung weitergeführt werden kann!"
_sensitiveMediaDetection: _sensitiveMediaDetection:
description: "Ermöglicht eine Erleichterung der Servermoderation durch die automatische Erkennungen von NSFW-Medien unter Verwendung von Machine Learning. Hierdurch wird die Serverlast etwas erhöht." description: "Ermöglicht eine Erleichterung der Servermoderation durch die automatische Erkennungen von NSFW-Medien unter Verwendung von Machine Learning. Hierdurch wird die Serverlast etwas erhöht."
sensitivity: "Erkennungssensitivität" sensitivity: "Erkennungssensitivität"
@ -1324,6 +1328,7 @@ _widgets:
userList: "Benutzerliste" userList: "Benutzerliste"
_userList: _userList:
chooseList: "Liste auswählen" chooseList: "Liste auswählen"
clicker: "Klickzähler"
_cw: _cw:
hide: "Inhalt verbergen" hide: "Inhalt verbergen"
show: "Inhalt anzeigen" show: "Inhalt anzeigen"
@ -1499,7 +1504,6 @@ _notification:
youGotReply: "{name} hat dir geantwortet" youGotReply: "{name} hat dir geantwortet"
youGotQuote: "{name} hat dich zitiert" youGotQuote: "{name} hat dich zitiert"
youRenoted: "Renote deiner Notiz von {name}" youRenoted: "Renote deiner Notiz von {name}"
youGotPoll: "{name} hat in deiner Umfrage abgestimmt"
youGotMessagingMessageFromUser: "{name} hat dir eine Chatnachricht gesendet" youGotMessagingMessageFromUser: "{name} hat dir eine Chatnachricht gesendet"
youGotMessagingMessageFromGroup: "In die Gruppe {name} wurde eine Chatnachricht gesendet" youGotMessagingMessageFromGroup: "In die Gruppe {name} wurde eine Chatnachricht gesendet"
youWereFollowed: "ist dir gefolgt" youWereFollowed: "ist dir gefolgt"
@ -1517,7 +1521,6 @@ _notification:
renote: "Renotes" renote: "Renotes"
quote: "Zitationen" quote: "Zitationen"
reaction: "Reaktionen" reaction: "Reaktionen"
pollVote: "Antworten auf Umfragen"
pollEnded: "Ende von Umfragen" pollEnded: "Ende von Umfragen"
receiveFollowRequest: "Erhaltene Follow-Anfragen" receiveFollowRequest: "Erhaltene Follow-Anfragen"
followRequestAccepted: "Akzeptierte Follow-Anfragen" followRequestAccepted: "Akzeptierte Follow-Anfragen"

View file

@ -920,6 +920,10 @@ like: "Like"
unlike: "Unlike" unlike: "Unlike"
numberOfLikes: "Likes" numberOfLikes: "Likes"
show: "Show" show: "Show"
neverShow: "Don't show again"
remindMeLater: "Maybe later"
didYouLikeMisskey: "Have you taken a liking to Misskey?"
pleaseDonate: "{host} uses the free software, Misskey. We would highly appreciate your donations so development of Misskey can continue!"
_sensitiveMediaDetection: _sensitiveMediaDetection:
description: "Reduces the effort of server moderation through automatically recognizing NSFW media via Machine Learning. This will slightly increase the load on the server." description: "Reduces the effort of server moderation through automatically recognizing NSFW media via Machine Learning. This will slightly increase the load on the server."
sensitivity: "Detection sensitivity" sensitivity: "Detection sensitivity"
@ -1324,6 +1328,7 @@ _widgets:
userList: "User list" userList: "User list"
_userList: _userList:
chooseList: "Select a list" chooseList: "Select a list"
clicker: "Clicker"
_cw: _cw:
hide: "Hide" hide: "Hide"
show: "Show content" show: "Show content"
@ -1499,7 +1504,6 @@ _notification:
youGotReply: "{name} replied to you" youGotReply: "{name} replied to you"
youGotQuote: "{name} quoted you" youGotQuote: "{name} quoted you"
youRenoted: "Renote from {name}" youRenoted: "Renote from {name}"
youGotPoll: "{name} voted on your poll"
youGotMessagingMessageFromUser: "{name} sent you a chat message" youGotMessagingMessageFromUser: "{name} sent you a chat message"
youGotMessagingMessageFromGroup: "A chat message was sent to the {name} group" youGotMessagingMessageFromGroup: "A chat message was sent to the {name} group"
youWereFollowed: "followed you" youWereFollowed: "followed you"
@ -1517,7 +1521,6 @@ _notification:
renote: "Renotes" renote: "Renotes"
quote: "Quotes" quote: "Quotes"
reaction: "Reactions" reaction: "Reactions"
pollVote: "Votes on polls"
pollEnded: "Polls ending" pollEnded: "Polls ending"
receiveFollowRequest: "Received follow requests" receiveFollowRequest: "Received follow requests"
followRequestAccepted: "Accepted follow requests" followRequestAccepted: "Accepted follow requests"

View file

@ -1487,7 +1487,6 @@ _notification:
youGotReply: "Respuesta de {name}" youGotReply: "Respuesta de {name}"
youGotQuote: "Citado por {name}" youGotQuote: "Citado por {name}"
youRenoted: "Renotado por {name}" youRenoted: "Renotado por {name}"
youGotPoll: "Encuestado por {name}"
youGotMessagingMessageFromUser: "{name} comenzó un chat contigo" youGotMessagingMessageFromUser: "{name} comenzó un chat contigo"
youGotMessagingMessageFromGroup: "Tienes un chat de {name}" youGotMessagingMessageFromGroup: "Tienes un chat de {name}"
youWereFollowed: "te ha seguido" youWereFollowed: "te ha seguido"
@ -1505,7 +1504,6 @@ _notification:
renote: "Renotar" renote: "Renotar"
quote: "Citar" quote: "Citar"
reaction: "Reacción" reaction: "Reacción"
pollVote: "Votado en la encuesta"
pollEnded: "La encuesta terminó" pollEnded: "La encuesta terminó"
receiveFollowRequest: "Recibió una solicitud de seguimiento" receiveFollowRequest: "Recibió una solicitud de seguimiento"
followRequestAccepted: "El seguimiento fue aceptado" followRequestAccepted: "El seguimiento fue aceptado"

View file

@ -1478,7 +1478,6 @@ _notification:
youGotReply: "Réponse de {name}" youGotReply: "Réponse de {name}"
youGotQuote: "Cité·e par {name}" youGotQuote: "Cité·e par {name}"
youRenoted: "{name} vous a Renoté" youRenoted: "{name} vous a Renoté"
youGotPoll: "{name} a participé à votre sondage"
youGotMessagingMessageFromUser: "{name} vous envoyé un message" youGotMessagingMessageFromUser: "{name} vous envoyé un message"
youGotMessagingMessageFromGroup: "Un message a été envoyé au groupe {name}" youGotMessagingMessageFromGroup: "Un message a été envoyé au groupe {name}"
youWereFollowed: "Vous suit" youWereFollowed: "Vous suit"
@ -1496,7 +1495,6 @@ _notification:
renote: "Renotes" renote: "Renotes"
quote: "Citations" quote: "Citations"
reaction: "Réactions" reaction: "Réactions"
pollVote: "Votes dans des sondages"
pollEnded: "Sondages se cloturant" pollEnded: "Sondages se cloturant"
receiveFollowRequest: "Demande d'abonnement reçue" receiveFollowRequest: "Demande d'abonnement reçue"
followRequestAccepted: "Demande d'abonnement acceptée" followRequestAccepted: "Demande d'abonnement acceptée"

View file

@ -1402,7 +1402,6 @@ _notification:
youGotReply: "{name} membalas kamu" youGotReply: "{name} membalas kamu"
youGotQuote: "{name} mengutip kamu" youGotQuote: "{name} mengutip kamu"
youRenoted: "{name} me-renote kamu" youRenoted: "{name} me-renote kamu"
youGotPoll: "{name} memilih di angket kamu"
youGotMessagingMessageFromUser: "{name} mengirimi kamu pesan" youGotMessagingMessageFromUser: "{name} mengirimi kamu pesan"
youGotMessagingMessageFromGroup: "Sebuah pesan telah dikirim ke grup {name}" youGotMessagingMessageFromGroup: "Sebuah pesan telah dikirim ke grup {name}"
youWereFollowed: "Mengikuti kamu" youWereFollowed: "Mengikuti kamu"
@ -1419,7 +1418,6 @@ _notification:
renote: "Renote" renote: "Renote"
quote: "Kutip" quote: "Kutip"
reaction: "Reaksi" reaction: "Reaksi"
pollVote: "Memilih di angket"
pollEnded: "Jajak pendapat berakhir" pollEnded: "Jajak pendapat berakhir"
receiveFollowRequest: "Permintaan mengikuti diterima" receiveFollowRequest: "Permintaan mengikuti diterima"
followRequestAccepted: "Permintaan mengikuti disetujui" followRequestAccepted: "Permintaan mengikuti disetujui"

View file

@ -28,7 +28,7 @@ timeline: "Timeline"
noAccountDescription: "L'utente non ha ancora scritto niente nella biografia di profilo." noAccountDescription: "L'utente non ha ancora scritto niente nella biografia di profilo."
login: "Accedi" login: "Accedi"
loggingIn: "Accesso in corso..." loggingIn: "Accesso in corso..."
logout: "Esci" logout: "Uscita"
signup: "Iscriviti" signup: "Iscriviti"
uploading: "Caricamento..." uploading: "Caricamento..."
save: "Salva" save: "Salva"
@ -1487,7 +1487,6 @@ _notification:
youGotReply: "{name} ti ha risposto" youGotReply: "{name} ti ha risposto"
youGotQuote: "{name} ha citato il tuo Nota e ha detto" youGotQuote: "{name} ha citato il tuo Nota e ha detto"
youRenoted: "{name} ha rinotato" youRenoted: "{name} ha rinotato"
youGotPoll: "{name} ha votato"
youGotMessagingMessageFromUser: "{name} ti ha mandato un messaggio" youGotMessagingMessageFromUser: "{name} ti ha mandato un messaggio"
youGotMessagingMessageFromGroup: "{name} ti ha mandato un messaggio nella chat" youGotMessagingMessageFromGroup: "{name} ti ha mandato un messaggio nella chat"
youWereFollowed: "Ha iniziato a seguirti" youWereFollowed: "Ha iniziato a seguirti"
@ -1505,7 +1504,6 @@ _notification:
renote: "Rinota" renote: "Rinota"
quote: "Cita" quote: "Cita"
reaction: "Reazioni" reaction: "Reazioni"
pollVote: "Voti ricevuti"
pollEnded: "Sondaggio chiuso." pollEnded: "Sondaggio chiuso."
receiveFollowRequest: "Richiesta di follow ricevuta" receiveFollowRequest: "Richiesta di follow ricevuta"
followRequestAccepted: "Richiesta di follow accettata" followRequestAccepted: "Richiesta di follow accettata"

View file

@ -1361,6 +1361,7 @@ _widgets:
userList: "ユーザーリスト" userList: "ユーザーリスト"
_userList: _userList:
chooseList: "リストを選択" chooseList: "リストを選択"
clicker: "クリッカー"
_cw: _cw:
hide: "隠す" hide: "隠す"
@ -1550,7 +1551,6 @@ _notification:
youGotReply: "{name}からのリプライ" youGotReply: "{name}からのリプライ"
youGotQuote: "{name}による引用" youGotQuote: "{name}による引用"
youRenoted: "{name}がRenoteしました" youRenoted: "{name}がRenoteしました"
youGotPoll: "{name}が投票しました"
youGotMessagingMessageFromUser: "{name}からのチャットがあります" youGotMessagingMessageFromUser: "{name}からのチャットがあります"
youGotMessagingMessageFromGroup: "{name}のチャットがあります" youGotMessagingMessageFromGroup: "{name}のチャットがあります"
youWereFollowed: "フォローされました" youWereFollowed: "フォローされました"
@ -1569,7 +1569,6 @@ _notification:
renote: "Renote" renote: "Renote"
quote: "引用" quote: "引用"
reaction: "リアクション" reaction: "リアクション"
pollVote: "アンケートに投票された"
pollEnded: "アンケートが終了" pollEnded: "アンケートが終了"
receiveFollowRequest: "フォロー申請を受け取った" receiveFollowRequest: "フォロー申請を受け取った"
followRequestAccepted: "フォローが受理された" followRequestAccepted: "フォローが受理された"

View file

@ -1485,7 +1485,6 @@ _notification:
youGotReply: "{name}からのリプライ" youGotReply: "{name}からのリプライ"
youGotQuote: "{name}による引用" youGotQuote: "{name}による引用"
youRenoted: "{name}がRenoteしたみたいやで" youRenoted: "{name}がRenoteしたみたいやで"
youGotPoll: "{name}が投票したみたいやで"
youGotMessagingMessageFromUser: "{name}からのチャットがあるで" youGotMessagingMessageFromUser: "{name}からのチャットがあるで"
youGotMessagingMessageFromGroup: "{name}のチャットがあるで" youGotMessagingMessageFromGroup: "{name}のチャットがあるで"
youWereFollowed: "フォローされたで" youWereFollowed: "フォローされたで"
@ -1503,7 +1502,6 @@ _notification:
renote: "Renote" renote: "Renote"
quote: "引用" quote: "引用"
reaction: "リアクション" reaction: "リアクション"
pollVote: "アンケートに投票されたで"
pollEnded: "アンケートが終了したで" pollEnded: "アンケートが終了したで"
receiveFollowRequest: "フォロー許可してほしいみたいやで" receiveFollowRequest: "フォロー許可してほしいみたいやで"
followRequestAccepted: "フォローが受理されたで" followRequestAccepted: "フォローが受理されたで"

View file

@ -920,6 +920,10 @@ like: "좋아요!"
unlike: "좋아요 취소" unlike: "좋아요 취소"
numberOfLikes: "좋아요 수" numberOfLikes: "좋아요 수"
show: "표시" show: "표시"
neverShow: "다시 보지 않기"
remindMeLater: "나중에 알림"
didYouLikeMisskey: "Misskey가 마음에 드시나요?"
pleaseDonate: "{host}은(는) 무료 소프트웨어 Misskey를 사용합니다. 후원을 통해 저희의 개발이 이어질 수 있게 도와주세요!"
_sensitiveMediaDetection: _sensitiveMediaDetection:
description: "기계학습을 통해 자동으로 민감한 미디어를 탐지하여, 모더레이션에 참고할 수 있도록 합니다. 서버의 부하를 약간 증가시킵니다." description: "기계학습을 통해 자동으로 민감한 미디어를 탐지하여, 모더레이션에 참고할 수 있도록 합니다. 서버의 부하를 약간 증가시킵니다."
sensitivity: "탐지 민감도" sensitivity: "탐지 민감도"
@ -1324,6 +1328,7 @@ _widgets:
userList: "사용자 목록" userList: "사용자 목록"
_userList: _userList:
chooseList: "리스트 선택" chooseList: "리스트 선택"
clicker: "클리커"
_cw: _cw:
hide: "숨기기" hide: "숨기기"
show: "더 보기" show: "더 보기"
@ -1499,7 +1504,6 @@ _notification:
youGotReply: "{name}님이 답글함" youGotReply: "{name}님이 답글함"
youGotQuote: "{name}님이 인용함" youGotQuote: "{name}님이 인용함"
youRenoted: "{name}님이 Renote" youRenoted: "{name}님이 Renote"
youGotPoll: "{name}님이 투표함"
youGotMessagingMessageFromUser: "{name} 님이 보낸 채팅이 있어요" youGotMessagingMessageFromUser: "{name} 님이 보낸 채팅이 있어요"
youGotMessagingMessageFromGroup: "{name}에서 보낸 채팅이 있어요" youGotMessagingMessageFromGroup: "{name}에서 보낸 채팅이 있어요"
youWereFollowed: "새로운 팔로워가 있습니다" youWereFollowed: "새로운 팔로워가 있습니다"
@ -1517,7 +1521,6 @@ _notification:
renote: "리노트" renote: "리노트"
quote: "인용" quote: "인용"
reaction: "리액션" reaction: "리액션"
pollVote: "투표 참여"
pollEnded: "투표가 종료됨" pollEnded: "투표가 종료됨"
receiveFollowRequest: "팔로우 요청을 받았을 때" receiveFollowRequest: "팔로우 요청을 받았을 때"
followRequestAccepted: "팔로우 요청이 승인되었을 때" followRequestAccepted: "팔로우 요청이 승인되었을 때"

View file

@ -1380,7 +1380,6 @@ _notification:
youGotReply: "{name} odpowiedział(a) Tobie" youGotReply: "{name} odpowiedział(a) Tobie"
youGotQuote: "{name} zacytował(a) Ciebie" youGotQuote: "{name} zacytował(a) Ciebie"
youRenoted: "{name} udostępnił(a) Twój wpis" youRenoted: "{name} udostępnił(a) Twój wpis"
youGotPoll: "{name} zagłosował(a) w Twojej ankiecie"
youGotMessagingMessageFromUser: "{name} wysłał(a) Ci wiadomość" youGotMessagingMessageFromUser: "{name} wysłał(a) Ci wiadomość"
youGotMessagingMessageFromGroup: "Została wysłana wiadomość do grupy {name}" youGotMessagingMessageFromGroup: "Została wysłana wiadomość do grupy {name}"
youWereFollowed: "Zaobserwował(a) Cię" youWereFollowed: "Zaobserwował(a) Cię"
@ -1398,7 +1397,6 @@ _notification:
renote: "Udostępnij" renote: "Udostępnij"
quote: "Cytuj" quote: "Cytuj"
reaction: "Reakcja" reaction: "Reakcja"
pollVote: "Głosy w ankietach"
receiveFollowRequest: "Otrzymano prośbę o możliwość obserwacji" receiveFollowRequest: "Otrzymano prośbę o możliwość obserwacji"
followRequestAccepted: "Przyjęto prośbę o możliwość obserwacji" followRequestAccepted: "Przyjęto prośbę o możliwość obserwacji"
groupInvited: "Zaproszono do grup" groupInvited: "Zaproszono do grup"

View file

@ -524,7 +524,6 @@ _notification:
youGotMention: "{name} te mencionou" youGotMention: "{name} te mencionou"
youGotReply: "{name} te respondeu" youGotReply: "{name} te respondeu"
youGotQuote: "{name} te citou" youGotQuote: "{name} te citou"
youGotPoll: "{name} votou em sua enquete"
youGotMessagingMessageFromUser: "{name} te mandou uma mensagem de bate-papo" youGotMessagingMessageFromUser: "{name} te mandou uma mensagem de bate-papo"
youGotMessagingMessageFromGroup: "Uma mensagem foi mandada para o grupo {name}" youGotMessagingMessageFromGroup: "Uma mensagem foi mandada para o grupo {name}"
youWereFollowed: "Você tem um novo seguidor" youWereFollowed: "Você tem um novo seguidor"
@ -541,7 +540,6 @@ _notification:
renote: "Repostar" renote: "Repostar"
quote: "Citar" quote: "Citar"
reaction: "Reações" reaction: "Reações"
pollVote: "Votações em enquetes"
pollEnded: "Enquetes terminando" pollEnded: "Enquetes terminando"
receiveFollowRequest: "Recebeu pedidos de seguimento" receiveFollowRequest: "Recebeu pedidos de seguimento"
followRequestAccepted: "Aceitou pedidos de seguimento" followRequestAccepted: "Aceitou pedidos de seguimento"

View file

@ -1399,7 +1399,6 @@ _notification:
youGotReply: "{name} отвечает вам." youGotReply: "{name} отвечает вам."
youGotQuote: "{name} цитирует вас." youGotQuote: "{name} цитирует вас."
youRenoted: "{name} передаёт вашу заметку." youRenoted: "{name} передаёт вашу заметку."
youGotPoll: "{name} участвует в вашем опросе."
youGotMessagingMessageFromUser: "{name} пишет вам." youGotMessagingMessageFromUser: "{name} пишет вам."
youGotMessagingMessageFromGroup: "Новое сообщение в группе «{name}»." youGotMessagingMessageFromGroup: "Новое сообщение в группе «{name}»."
youWereFollowed: "У вас новый подписчик." youWereFollowed: "У вас новый подписчик."
@ -1414,7 +1413,6 @@ _notification:
renote: "Репосты" renote: "Репосты"
quote: "Цитаты" quote: "Цитаты"
reaction: "Реакции" reaction: "Реакции"
pollVote: "Голосования"
receiveFollowRequest: "Получен запрос на подписку" receiveFollowRequest: "Получен запрос на подписку"
followRequestAccepted: "Запрос на подписку одобрен" followRequestAccepted: "Запрос на подписку одобрен"
groupInvited: "Приглашение в группы" groupInvited: "Приглашение в группы"

View file

@ -913,6 +913,10 @@ tools: "Nástroje"
cannotLoad: "Nedá sa načítať." cannotLoad: "Nedá sa načítať."
like: "Páči sa mi" like: "Páči sa mi"
show: "Zobraziť" show: "Zobraziť"
neverShow: "Nabudúce nezobrazovať"
remindMeLater: "Pripomenúť neskôr"
didYouLikeMisskey: "Páči sa vám Misskey?"
pleaseDonate: "Misskey je bezplatný softvér, ktorý používa {host}. Prosím, prispejte, aby sme ho mohli ďalej rozvíjať!"
_sensitiveMediaDetection: _sensitiveMediaDetection:
description: "Strojové učenie sa použije na automatickú detekciu citlivých médií na účely ich moderovania. Mierne sa zvýši zaťaženie servera." description: "Strojové učenie sa použije na automatickú detekciu citlivých médií na účely ich moderovania. Mierne sa zvýši zaťaženie servera."
sensitivity: "Citlivosť detekcie" sensitivity: "Citlivosť detekcie"
@ -1480,7 +1484,6 @@ _notification:
youGotReply: "{name} vám odpovedal/a" youGotReply: "{name} vám odpovedal/a"
youGotQuote: "{name} vás citoval/a" youGotQuote: "{name} vás citoval/a"
youRenoted: "{name} preposlal/a vašu poznámku" youRenoted: "{name} preposlal/a vašu poznámku"
youGotPoll: "{name} hlasoval/a"
youGotMessagingMessageFromUser: "{name} vám poslal/a správu" youGotMessagingMessageFromUser: "{name} vám poslal/a správu"
youGotMessagingMessageFromGroup: "Prišla správa do skupiny {name}" youGotMessagingMessageFromGroup: "Prišla správa do skupiny {name}"
youWereFollowed: "Máte nového sledujúceho" youWereFollowed: "Máte nového sledujúceho"
@ -1498,7 +1501,6 @@ _notification:
renote: "Preposlať" renote: "Preposlať"
quote: "Citovať" quote: "Citovať"
reaction: "Reakcie" reaction: "Reakcie"
pollVote: "Hlasy v hlasovaniach"
pollEnded: "Hlasovanie skončilo" pollEnded: "Hlasovanie skončilo"
receiveFollowRequest: "Doručené žiadosti o sledovanie" receiveFollowRequest: "Doručené žiadosti o sledovanie"
followRequestAccepted: "Schválené žiadosti o sledovanie" followRequestAccepted: "Schválené žiadosti o sledovanie"

View file

@ -1,7 +1,7 @@
--- ---
_lang_: "Svenska" _lang_: "Svenska"
headlineMisskey: "Ett nätverk kopplat av noter" headlineMisskey: "Ett nätverk kopplat av noter"
introMisskey: "Välkommen! Misskey är en öppen och decentraliserad mikrobloggningstjänst.\nSkapa en \"not\" och dela dina tankar med alla runtomkring dig. 📡\nMed \"reaktioner\" kan du snabbt uttrycka dina känslor kring andras noter.👍\nLåt oss utforska en nya värld!🚀" introMisskey: "Välkommen! Misskey är en öppen och decentraliserad mikrobloggningstjänst.\nSkapa en \"not\" och dela dina tankar med alla runtomkring dig. 📡\nMed \"reaktioner\" kan du snabbt uttrycka dina känslor kring andras noter. 👍\nLåt oss utforska en ny värld! 🚀"
poweredByMisskeyDescription: "{name} är en tjänst driven av den öppna källkodsplatformen <b>Misskey</b> (benämns \"Misskey instans\")." poweredByMisskeyDescription: "{name} är en tjänst driven av den öppna källkodsplatformen <b>Misskey</b> (benämns \"Misskey instans\")."
monthAndDay: "{day}/{month}" monthAndDay: "{day}/{month}"
search: "Sök" search: "Sök"
@ -17,7 +17,7 @@ noThankYou: "Nej tack"
enterUsername: "Ange användarnamn" enterUsername: "Ange användarnamn"
renotedBy: "Omnoterad av {user}" renotedBy: "Omnoterad av {user}"
noNotes: "Inga noteringar" noNotes: "Inga noteringar"
noNotifications: "Inga aviseringar" noNotifications: "Inga notifikationer"
instance: "Instanser" instance: "Instanser"
settings: "Inställningar" settings: "Inställningar"
basicSettings: "Basinställningar" basicSettings: "Basinställningar"
@ -30,13 +30,13 @@ login: "Logga in"
loggingIn: "Loggar in" loggingIn: "Loggar in"
logout: "Logga ut" logout: "Logga ut"
signup: "Registrera" signup: "Registrera"
uploading: "Uppladdning sker..." uploading: "Laddar upp..."
save: "Spara" save: "Spara"
users: "Användare" users: "Användare"
addUser: "Lägg till användare" addUser: "Lägg till användare"
favorite: "Lägg till i favoriter" favorite: "Lägg till i favoriter"
favorites: "Favoriter" favorites: "Favoriter"
unfavorite: "Avfavorisera" unfavorite: "Ta bort från favoriter"
favorited: "Tillagd i favoriter." favorited: "Tillagd i favoriter."
alreadyFavorited: "Redan tillagd i favoriter." alreadyFavorited: "Redan tillagd i favoriter."
cantFavorite: "Gick inte att lägga till i favoriter." cantFavorite: "Gick inte att lägga till i favoriter."
@ -146,7 +146,7 @@ flagAsBotDescription: "Aktivera det här alternativet om kontot är kontrollerat
flagAsCat: "Markera konto som katt" flagAsCat: "Markera konto som katt"
flagAsCatDescription: "Aktivera denna inställning för att markera kontot som en katt." flagAsCatDescription: "Aktivera denna inställning för att markera kontot som en katt."
flagShowTimelineReplies: "Visa svar i tidslinje" flagShowTimelineReplies: "Visa svar i tidslinje"
flagShowTimelineRepliesDescription: "Visar användarsvar till andra användares noter i tidslinjen om påslagen." flagShowTimelineRepliesDescription: "Visar användarsvar till andra användares noter i tidslinjen om aktiverad."
autoAcceptFollowed: "Godkänn följarförfrågningar från användare du följer automatiskt" autoAcceptFollowed: "Godkänn följarförfrågningar från användare du följer automatiskt"
addAccount: "Lägg till konto" addAccount: "Lägg till konto"
loginFailed: "Inloggningen misslyckades" loginFailed: "Inloggningen misslyckades"
@ -253,16 +253,111 @@ explore: "Utforska"
messageRead: "Läs" messageRead: "Läs"
noMoreHistory: "Det finns ingen mer historik" noMoreHistory: "Det finns ingen mer historik"
startMessaging: "Starta en chatt" startMessaging: "Starta en chatt"
nUsersRead: "läst av {n}"
agreeTo: "Jag accepterar {0}"
tos: "Användarvillkor"
home: "Hem"
remoteUserCaution: "Då denna användaren kommer från en fjärrinstans, kan informationen visad vara ofullständig."
activity: "Aktivitet"
images: "Bilder"
birthday: "Födelsedag"
yearsOld: "{age} år gammal"
registeredDate: "Gick med"
location: "Plats"
theme: "Teman"
themeForLightMode: "Tema att använda i Ljust Läge"
themeForDarkMode: "Tema att använda i Mörkt Läge"
light: "Ljust"
dark: "Mörk"
lightThemes: "Ljusa teman"
darkThemes: "Mörka teman"
syncDeviceDarkMode: "Synka Mörkt Läge med din enhets inställningar"
drive: "Drive"
fileName: "Filnamn"
selectFile: "Välj en fil"
selectFiles: "Välj filer"
selectFolder: "Välj en mapp"
selectFolders: "Välj mappar"
renameFile: "Byt namn på filen"
folderName: "Mappnamn"
createFolder: "Skapa en mapp"
renameFolder: "Byt namn på mappen"
deleteFolder: "Ta bort mappen"
addFile: "Lägg till fil"
emptyDrive: "Din Drive är tom"
emptyFolder: "Denna mappen är tom"
unableToDelete: "Kunde inte ta bort"
inputNewFileName: "Ange nytt filnamn"
inputNewDescription: "Ange ny bildtext"
inputNewFolderName: "Ange nytt mappnamn"
circularReferenceFolder: "Destinationsmappen är en undermapp av mappen du vill flytta."
hasChildFilesOrFolders: "Då denna mappen inte är tom, kan den inte tas bort."
copyUrl: "Kopiera URL"
rename: "Byt namn"
avatar: "Profilbild"
banner: "Banner"
nsfw: "Känsligt innehåll" nsfw: "Känsligt innehåll"
reload: "Ladda om"
doNothing: "Ignorera"
reloadConfirm: "Vill du ladda om tidslinjen?"
accept: "Tillåt"
reject: "Neka"
normal: "Normal"
instanceName: "Instansnamn"
instanceDescription: "Instansbeskrivning"
maintainerEmail: "Administratörens epost"
tosUrl: "URL till användarvillkår"
thisYear: "Detta året"
thisMonth: "Denna månaden"
today: "Idag"
dayX: "{day}"
monthX: "{month}"
yearX: "{year}"
pages: "Sidor"
integration: "Integrationer"
connectService: "Anslut"
disconnectService: "Koppla från"
enableLocalTimeline: "Aktivera lokal tidslinje"
enableGlobalTimeline: "Aktivera global tidslinje"
enableRegistration: "Aktivera registrering av nya användare"
inMb: "I megabyte"
iconUrl: "URL till profilbilden"
bannerUrl: "URL till banner-bilden"
pinnedNotes: "Fästad not" pinnedNotes: "Fästad not"
enableHcaptcha: "Aktivera hCaptcha"
enableRecaptcha: "Aktivera reCAPTCHA"
enableTurnstile: "Aktivera Turnstile"
enableServiceworker: "Aktivera pushnotiser i denna webbläsaren"
recentlyUpdatedUsers: "Nyligen aktiva användare"
recentlyRegisteredUsers: "Nyligen registrerade användare"
userList: "Listor" userList: "Listor"
aboutMisskey: "Om Misskey"
administrator: "Administratör"
newPasswordIs: "Det nya lösenordet är \"{password}\""
share: "Dela"
enable: "Aktivera"
serviceworkerInfo: "Måste vara aktiverad för pushnotiser."
enableInfiniteScroll: "Ladda mer automatiskt"
enablePlayer: "Öppna videospelare"
enableAll: "Aktivera alla"
enableEmail: "Aktivera epost-utskick"
smtpHost: "Värd" smtpHost: "Värd"
smtpUser: "Användarnamn" smtpUser: "Användarnamn"
smtpPass: "Lösenord" smtpPass: "Lösenord"
clearCache: "Rensa cache" clearCache: "Rensa cache"
enabled: "Aktiverad"
user: "Användare" user: "Användare"
global: "Global"
squareAvatars: "Visa fyrkantiga profilbilder"
searchByGoogle: "Sök" searchByGoogle: "Sök"
file: "Filer" file: "Filer"
enableAutoSensitive: "Automatisk NSFW markering"
enableAutoSensitiveDescription: "Tillåter automatiskt detektering och marketing av NSFW media genom Maskininlärning när möjligt. Även om denna inställningen är avaktiverad, kan det vara aktiverat på hela instansen."
pushNotification: "Pushnotiser"
subscribePushNotification: "Aktivera pushnotiser"
unsubscribePushNotification: "Avaktivera pushnotiser"
pushNotificationAlreadySubscribed: "Pushnotiser är redan aktiverade"
pushNotificationNotSupported: "Din webbläsare eller instans har inte stöd för pushnotiser"
_email: _email:
_follow: _follow:
title: "följde dig" title: "följde dig"
@ -271,6 +366,9 @@ _mfm:
quote: "Citat" quote: "Citat"
emoji: "Anpassa emoji" emoji: "Anpassa emoji"
search: "Sök" search: "Sök"
_channel:
setBanner: "Välj banner"
removeBanner: "Ta bort banner"
_theme: _theme:
keys: keys:
mention: "Nämn" mention: "Nämn"
@ -282,6 +380,7 @@ _sfx:
_widgets: _widgets:
notifications: "Notifikationer" notifications: "Notifikationer"
timeline: "Tidslinje" timeline: "Tidslinje"
activity: "Aktivitet"
federation: "Federation" federation: "Federation"
jobQueue: "Jobbkö" jobQueue: "Jobbkö"
_userList: _userList:
@ -289,9 +388,12 @@ _widgets:
_cw: _cw:
show: "Ladda mer" show: "Ladda mer"
_visibility: _visibility:
home: "Hem"
followers: "Följare" followers: "Följare"
_profile: _profile:
username: "Användarnamn" username: "Användarnamn"
changeAvatar: "Ändra profilbild"
changeBanner: "Ändra banner"
_exportOrImport: _exportOrImport:
followingList: "Följer" followingList: "Följer"
muteList: "Tysta" muteList: "Tysta"
@ -299,8 +401,15 @@ _exportOrImport:
userLists: "Listor" userLists: "Listor"
_charts: _charts:
federation: "Federation" federation: "Federation"
_timelines:
home: "Hem"
global: "Global"
_pages:
blocks:
image: "Bilder"
_notification: _notification:
youWereFollowed: "följde dig" youWereFollowed: "följde dig"
unreadAntennaNote: "Antenn {name}"
_types: _types:
follow: "Följer" follow: "Följer"
mention: "Nämn" mention: "Nämn"

View file

@ -8,7 +8,7 @@ search: "ค้นหา"
notifications: "การเเจ้งเตือน" notifications: "การเเจ้งเตือน"
username: "ชื่อผู้ใช้" username: "ชื่อผู้ใช้"
password: "รหัสผ่าน" password: "รหัสผ่าน"
forgotPassword: "ลืมรหัสผ่าน?" forgotPassword: "ลืมรหัสผ่านใช่ไหม"
fetchingAsApObject: "กำลังดึงข้อมูล จาก เฟดิเวิร์ส..." fetchingAsApObject: "กำลังดึงข้อมูล จาก เฟดิเวิร์ส..."
ok: "โอเค" ok: "โอเค"
gotIt: "เข้าใจแล้ว !" gotIt: "เข้าใจแล้ว !"
@ -917,7 +917,13 @@ tools: "เครื่องมือ"
cannotLoad: "ไม่สามารถโหลดได้" cannotLoad: "ไม่สามารถโหลดได้"
numberOfProfileView: "มุมมองโปรไฟล์" numberOfProfileView: "มุมมองโปรไฟล์"
like: "ชื่นชอบ" like: "ชื่นชอบ"
unlike: "ไม่ชอบ"
numberOfLikes: "จำนวนไลค์"
show: "แสดงผล" show: "แสดงผล"
neverShow: "ไม่ต้องแสดงข้อความนี้อีก"
remindMeLater: "ไว้ครั้งหน้าแล้วกัน"
didYouLikeMisskey: "คุณเคยชอบ Misskey ไหม?"
pleaseDonate: "{host} ใช้ซอฟต์แวร์ฟรี Misskey เราขอขอบคุณการบริจาคของคุณอย่างสูงเพื่อให้การพัฒนา Misskey สามารถดำเนินต่อไปได้นะ!"
_sensitiveMediaDetection: _sensitiveMediaDetection:
description: "ลดความพยายามในการดูแลเซิร์ฟเวอร์ผ่านการจดจำสื่อ NSFW โดยอัตโนมัติผ่านการเรียนรู้ของเครื่อง การทำสิ่งนี้อาจจะเพิ่มภาระบนเซิร์ฟเวอร์เล็กน้อย" description: "ลดความพยายามในการดูแลเซิร์ฟเวอร์ผ่านการจดจำสื่อ NSFW โดยอัตโนมัติผ่านการเรียนรู้ของเครื่อง การทำสิ่งนี้อาจจะเพิ่มภาระบนเซิร์ฟเวอร์เล็กน้อย"
sensitivity: "การตรวจจับความไว" sensitivity: "การตรวจจับความไว"
@ -1317,10 +1323,12 @@ _widgets:
jobQueue: "คิวงาน" jobQueue: "คิวงาน"
serverMetric: "ตัวชี้วัดเซิร์ฟเวอร์" serverMetric: "ตัวชี้วัดเซิร์ฟเวอร์"
aiscript: "AiScript คอนโซล" aiscript: "AiScript คอนโซล"
aiscriptApp: "AiScript แอพ"
aichan: "เอไอ" aichan: "เอไอ"
userList: "รายชื่อผู้ใช้" userList: "รายชื่อผู้ใช้"
_userList: _userList:
chooseList: "เลือกรายการ" chooseList: "เลือกรายการ"
clicker: "คลิกเกอร์"
_cw: _cw:
hide: "ซ่อน" hide: "ซ่อน"
show: "โหลดเพิ่มเติม" show: "โหลดเพิ่มเติม"
@ -1423,7 +1431,16 @@ _timelines:
social: "โซเชี่ยล" social: "โซเชี่ยล"
global: "ทั่วโลก" global: "ทั่วโลก"
_play: _play:
new: "สร้างการเล่น"
edit: "แก้ไขเล่น"
created: "สร้างการเล่นแล้ว"
updated: "แก้ไขการเล่นแล้ว"
deleted: "ลบการเล่นแล้ว"
pageSetting: "ตั้งค่าการเล่น"
editThisPage: "แก้ไข Play นี้"
viewSource: "ดูต้นฉบับ" viewSource: "ดูต้นฉบับ"
my: "มาย เพลย์"
liked: "ไลค์ เพลย์"
featured: "เป็นที่นิยม" featured: "เป็นที่นิยม"
title: "หัวข้อ" title: "หัวข้อ"
script: "สคริปต์" script: "สคริปต์"
@ -1487,7 +1504,6 @@ _notification:
youGotReply: "{name} ตอบกลับถึงคุณ" youGotReply: "{name} ตอบกลับถึงคุณ"
youGotQuote: "{name} อ้างถึงคุณ" youGotQuote: "{name} อ้างถึงคุณ"
youRenoted: "รีโน้ตจาก {name}" youRenoted: "รีโน้ตจาก {name}"
youGotPoll: "{name} โหวตบนแบบสำรวจความคิดเห็นของคุณ"
youGotMessagingMessageFromUser: "{name} ได้ส่งข้อความแชทถึงคุณ" youGotMessagingMessageFromUser: "{name} ได้ส่งข้อความแชทถึงคุณ"
youGotMessagingMessageFromGroup: "ข้อความแชทถูกส่งไปยัง {name} กลุ่ม" youGotMessagingMessageFromGroup: "ข้อความแชทถูกส่งไปยัง {name} กลุ่ม"
youWereFollowed: "ได้ติดตามคุณ" youWereFollowed: "ได้ติดตามคุณ"
@ -1505,7 +1521,6 @@ _notification:
renote: "รีโน้ต" renote: "รีโน้ต"
quote: "อ้างคำพูด" quote: "อ้างคำพูด"
reaction: "รีแอคชั่น" reaction: "รีแอคชั่น"
pollVote: "จำนวนโหวตที่ได้รับ"
pollEnded: "โพลนี้สิ้นสุดลงแล้ว" pollEnded: "โพลนี้สิ้นสุดลงแล้ว"
receiveFollowRequest: "ได้รับคำขอติดตาม\n" receiveFollowRequest: "ได้รับคำขอติดตาม\n"
followRequestAccepted: "ยอมรับคำขอติดตาม" followRequestAccepted: "ยอมรับคำขอติดตาม"

View file

@ -1415,7 +1415,6 @@ _notification:
youGotReply: "{name} відповідає" youGotReply: "{name} відповідає"
youGotQuote: "{name} цитує вас" youGotQuote: "{name} цитує вас"
youRenoted: "{name} поширює" youRenoted: "{name} поширює"
youGotPoll: "{name} бере участь в опитуванні"
youGotMessagingMessageFromUser: "Повідомлення від {name}" youGotMessagingMessageFromUser: "Повідомлення від {name}"
youGotMessagingMessageFromGroup: "Нове повідомлення в групі {name}" youGotMessagingMessageFromGroup: "Нове повідомлення в групі {name}"
youWereFollowed: "Новий підписник" youWereFollowed: "Новий підписник"
@ -1430,7 +1429,6 @@ _notification:
renote: "Поширення" renote: "Поширення"
quote: "Цитування" quote: "Цитування"
reaction: "Реакції" reaction: "Реакції"
pollVote: "Опитування"
receiveFollowRequest: "Запити на підписку" receiveFollowRequest: "Запити на підписку"
followRequestAccepted: "Прийняті підписки" followRequestAccepted: "Прийняті підписки"
groupInvited: "Запрошення до груп" groupInvited: "Запрошення до груп"

View file

@ -1460,7 +1460,6 @@ _notification:
youGotReply: "{name} trả lời bạn" youGotReply: "{name} trả lời bạn"
youGotQuote: "{name} trích dẫn tút của bạn" youGotQuote: "{name} trích dẫn tút của bạn"
youRenoted: "{name} đăng lại tút của bạn" youRenoted: "{name} đăng lại tút của bạn"
youGotPoll: "{name} bình chọn tút của bạn"
youGotMessagingMessageFromUser: "{name} nhắn tin cho bạn" youGotMessagingMessageFromUser: "{name} nhắn tin cho bạn"
youGotMessagingMessageFromGroup: "Một tin nhắn trong nhóm {name}" youGotMessagingMessageFromGroup: "Một tin nhắn trong nhóm {name}"
youWereFollowed: "đã theo dõi bạn" youWereFollowed: "đã theo dõi bạn"
@ -1477,7 +1476,6 @@ _notification:
renote: "Đăng lại" renote: "Đăng lại"
quote: "Trích dẫn" quote: "Trích dẫn"
reaction: "Biểu cảm" reaction: "Biểu cảm"
pollVote: "Lượt bình chọn"
pollEnded: "Bình chọn kết thúc" pollEnded: "Bình chọn kết thúc"
receiveFollowRequest: "Yêu cầu theo dõi" receiveFollowRequest: "Yêu cầu theo dõi"
followRequestAccepted: "Yêu cầu theo dõi được chấp nhận" followRequestAccepted: "Yêu cầu theo dõi được chấp nhận"

View file

@ -920,6 +920,9 @@ like: "点赞!"
unlike: "取消赞" unlike: "取消赞"
numberOfLikes: "点赞数" numberOfLikes: "点赞数"
show: "显示" show: "显示"
neverShow: "不再显示"
remindMeLater: "稍后提醒我"
didYouLikeMisskey: "你在Misskey玩得还开心吗"
_sensitiveMediaDetection: _sensitiveMediaDetection:
description: "可以使用机器学习技术自动检测敏感媒体,以便进行审核。服务器负载将略微增加。" description: "可以使用机器学习技术自动检测敏感媒体,以便进行审核。服务器负载将略微增加。"
sensitivity: "检测敏感度" sensitivity: "检测敏感度"
@ -1499,7 +1502,6 @@ _notification:
youGotReply: "来自{name}的回复" youGotReply: "来自{name}的回复"
youGotQuote: "来自{name}的引用" youGotQuote: "来自{name}的引用"
youRenoted: "来自{name}的转发" youRenoted: "来自{name}的转发"
youGotPoll: "来自{name}的投票"
youGotMessagingMessageFromUser: "来自{name}的聊天" youGotMessagingMessageFromUser: "来自{name}的聊天"
youGotMessagingMessageFromGroup: "来自{name}的群聊" youGotMessagingMessageFromGroup: "来自{name}的群聊"
youWereFollowed: "关注了你。" youWereFollowed: "关注了你。"
@ -1517,7 +1519,6 @@ _notification:
renote: "转发" renote: "转发"
quote: "引用" quote: "引用"
reaction: "回应" reaction: "回应"
pollVote: "问卷调查被投票"
pollEnded: "问卷调查结束" pollEnded: "问卷调查结束"
receiveFollowRequest: "收到关注请求" receiveFollowRequest: "收到关注请求"
followRequestAccepted: "关注请求已通过" followRequestAccepted: "关注请求已通过"

View file

@ -252,7 +252,7 @@ uploadFromUrlMayTakeTime: "還需要一些時間才能完成上傳。"
explore: "探索" explore: "探索"
messageRead: "已讀" messageRead: "已讀"
noMoreHistory: "沒有更多歷史紀錄" noMoreHistory: "沒有更多歷史紀錄"
startMessaging: "開始傳送訊息" startMessaging: "開始聊天"
nUsersRead: "{n}人已讀" nUsersRead: "{n}人已讀"
agreeTo: "我同意{0}" agreeTo: "我同意{0}"
tos: "使用條款" tos: "使用條款"
@ -918,7 +918,12 @@ cannotLoad: "無法載入"
numberOfProfileView: "個人檔案檢視次數" numberOfProfileView: "個人檔案檢視次數"
like: "讚" like: "讚"
unlike: "收回讚" unlike: "收回讚"
numberOfLikes: "讚數"
show: "檢視" show: "檢視"
neverShow: "不再顯示"
remindMeLater: "以後再說"
didYouLikeMisskey: "您是否喜愛Misskey呢"
pleaseDonate: "Misskey是由{host}使用的免費軟體。請贊助我們,讓開發能夠持續!"
_sensitiveMediaDetection: _sensitiveMediaDetection:
description: "您可以使用機器學習自動檢測敏感媒體並將其用於審核。 伺服器的負荷會稍微增加。" description: "您可以使用機器學習自動檢測敏感媒體並將其用於審核。 伺服器的負荷會稍微增加。"
sensitivity: "檢測敏感度" sensitivity: "檢測敏感度"
@ -1187,7 +1192,7 @@ _sfx:
note: "貼文" note: "貼文"
noteMy: "我的貼文" noteMy: "我的貼文"
notification: "通知" notification: "通知"
chat: "傳送訊息" chat: "聊天"
chatBg: "聊天背景" chatBg: "聊天背景"
antenna: "天線接收" antenna: "天線接收"
channel: "頻道通知" channel: "頻道通知"
@ -1318,10 +1323,12 @@ _widgets:
jobQueue: "佇列" jobQueue: "佇列"
serverMetric: "服務器指標 " serverMetric: "服務器指標 "
aiscript: "AiScript控制台" aiscript: "AiScript控制台"
aiscriptApp: "AiScript App"
aichan: "小藍" aichan: "小藍"
userList: "使用者列表" userList: "使用者列表"
_userList: _userList:
chooseList: "選擇清單" chooseList: "選擇清單"
clicker: "點擊器"
_cw: _cw:
hide: "隱藏" hide: "隱藏"
show: "瀏覽更多" show: "瀏覽更多"
@ -1424,7 +1431,16 @@ _timelines:
social: "社群" social: "社群"
global: "公開" global: "公開"
_play: _play:
new: "新增Play"
edit: "編輯Play"
created: "已新增Play"
updated: "已更新Play"
deleted: "已刪除Play"
pageSetting: "Play設定"
editThisPage: "編輯這個Play"
viewSource: "檢視原始碼" viewSource: "檢視原始碼"
my: "自己的Play"
liked: "按了讚的Play"
featured: "人氣" featured: "人氣"
title: "標題" title: "標題"
script: "腳本" script: "腳本"
@ -1488,7 +1504,6 @@ _notification:
youGotReply: "{name}回覆了您" youGotReply: "{name}回覆了您"
youGotQuote: "{name}引用了您" youGotQuote: "{name}引用了您"
youRenoted: "{name} 轉發了你的貼文" youRenoted: "{name} 轉發了你的貼文"
youGotPoll: "{name}已投票"
youGotMessagingMessageFromUser: "{name}發送給您的訊息" youGotMessagingMessageFromUser: "{name}發送給您的訊息"
youGotMessagingMessageFromGroup: "{name}發送給您的訊息" youGotMessagingMessageFromGroup: "{name}發送給您的訊息"
youWereFollowed: "您有新的追隨者" youWereFollowed: "您有新的追隨者"
@ -1506,7 +1521,6 @@ _notification:
renote: "轉發貼文" renote: "轉發貼文"
quote: "引用" quote: "引用"
reaction: "反應" reaction: "反應"
pollVote: "統計已投票數"
pollEnded: "問卷調查結束" pollEnded: "問卷調查結束"
receiveFollowRequest: "已收到追隨請求" receiveFollowRequest: "已收到追隨請求"
followRequestAccepted: "追隨請求已接受" followRequestAccepted: "追隨請求已接受"

View file

@ -1,6 +1,6 @@
{ {
"name": "misskey", "name": "misskey",
"version": "13.0.0-beta.27", "version": "13.0.0-beta.36",
"codename": "indigo", "codename": "indigo",
"repository": { "repository": {
"type": "git", "type": "git",

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 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: 1,011 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

@ -72,7 +72,7 @@
"json5-loader": "4.0.1", "json5-loader": "4.0.1",
"jsonld": "8.1.0", "jsonld": "8.1.0",
"jsrsasign": "10.6.1", "jsrsasign": "10.6.1",
"mfm-js": "0.23.0", "mfm-js": "0.23.1",
"mime-types": "2.1.35", "mime-types": "2.1.35",
"misskey-js": "0.0.14", "misskey-js": "0.0.14",
"ms": "3.0.0-canary.1", "ms": "3.0.0-canary.1",

View file

@ -15,8 +15,8 @@ import type { Packed } from '@/misc/schema.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import type { MutingsRepository, BlockingsRepository, NotesRepository, AntennaNotesRepository, AntennasRepository, UserGroupJoiningsRepository, UserListJoiningsRepository } from '@/models/index.js'; import type { MutingsRepository, BlockingsRepository, NotesRepository, AntennaNotesRepository, AntennasRepository, UserGroupJoiningsRepository, UserListJoiningsRepository } from '@/models/index.js';
import { UtilityService } from '@/core/UtilityService.js'; import { UtilityService } from '@/core/UtilityService.js';
import type { OnApplicationShutdown } from '@nestjs/common';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import type { OnApplicationShutdown } from '@nestjs/common';
@Injectable() @Injectable()
export class AntennaService implements OnApplicationShutdown { export class AntennaService implements OnApplicationShutdown {
@ -135,7 +135,7 @@ export class AntennaService implements OnApplicationShutdown {
this.globalEventServie.publishMainStream(antenna.userId, 'unreadAntenna', antenna); this.globalEventServie.publishMainStream(antenna.userId, 'unreadAntenna', antenna);
this.pushNotificationService.pushNotification(antenna.userId, 'unreadAntennaNote', { this.pushNotificationService.pushNotification(antenna.userId, 'unreadAntennaNote', {
antenna: { id: antenna.id, name: antenna.name }, antenna: { id: antenna.id, name: antenna.name },
note: await this.noteEntityService.pack(note) note: await this.noteEntityService.pack(note),
}); });
} }
}, 2000); }, 2000);
@ -144,27 +144,19 @@ export class AntennaService implements OnApplicationShutdown {
// NOTE: フォローしているユーザーのノート、リストのユーザーのノート、グループのユーザーのノート指定はパフォーマンス上の理由で無効になっている // NOTE: フォローしているユーザーのノート、リストのユーザーのノート、グループのユーザーのノート指定はパフォーマンス上の理由で無効になっている
/**
* noteUserFollowers / antennaUserFollowing
*/
@bindThis @bindThis
public async checkHitAntenna(antenna: Antenna, note: (Note | Packed<'Note'>), noteUser: { id: User['id']; username: string; host: string | null; }, noteUserFollowers?: User['id'][], antennaUserFollowing?: User['id'][]): Promise<boolean> { public async checkHitAntenna(antenna: Antenna, note: (Note | Packed<'Note'>), noteUser: { id: User['id']; username: string; host: string | null; }): Promise<boolean> {
if (note.visibility === 'specified') return false; if (note.visibility === 'specified') return false;
if (note.visibility === 'followers') return false;
// アンテナ作成者がノート作成者にブロックされていたらスキップ // アンテナ作成者がノート作成者にブロックされていたらスキップ
const blockings = await this.blockingCache.fetch(noteUser.id, () => this.blockingsRepository.findBy({ blockerId: noteUser.id }).then(res => res.map(x => x.blockeeId))); const blockings = await this.blockingCache.fetch(noteUser.id, () => this.blockingsRepository.findBy({ blockerId: noteUser.id }).then(res => res.map(x => x.blockeeId)));
if (blockings.some(blocking => blocking === antenna.userId)) return false; if (blockings.some(blocking => blocking === antenna.userId)) return false;
if (note.visibility === 'followers') {
if (noteUserFollowers && !noteUserFollowers.includes(antenna.userId)) return false;
if (antennaUserFollowing && !antennaUserFollowing.includes(note.userId)) return false;
}
if (!antenna.withReplies && note.replyId != null) return false; if (!antenna.withReplies && note.replyId != null) return false;
if (antenna.src === 'home') { if (antenna.src === 'home') {
if (noteUserFollowers && !noteUserFollowers.includes(antenna.userId)) return false; // TODO
if (antennaUserFollowing && !antennaUserFollowing.includes(note.userId)) return false;
} else if (antenna.src === 'list') { } else if (antenna.src === 'list') {
const listUsers = (await this.userListJoiningsRepository.findBy({ const listUsers = (await this.userListJoiningsRepository.findBy({
userListId: antenna.userListId!, userListId: antenna.userListId!,

View file

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

View file

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

View file

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

View file

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

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 - * follow -
* mention - 稿 * mention - 稿
* reply - (Watchしている)稿 * reply - 稿
* renote - (Watchしている)稿Renoteされた * renote - 稿Renoteされた
* quote - (Watchしている)稿Renoteされた * quote - 稿Renoteされた
* reaction - (Watchしている)稿 * reaction - 稿
* pollVote - (Watchしている)稿 * pollVote - 稿 ()
* pollEnded - * pollEnded -
* receiveFollowRequest - * receiveFollowRequest -
* followRequestAccepted - * followRequestAccepted -

View file

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

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_delete from './endpoints/messaging/messages/delete.js';
import * as ep___messaging_messages_read from './endpoints/messaging/messages/read.js'; import * as ep___messaging_messages_read from './endpoints/messaging/messages/read.js';
import * as ep___meta from './endpoints/meta.js'; import * as ep___meta from './endpoints/meta.js';
import * as ep___emojis from './endpoints/emojis.js';
import * as ep___miauth_genToken from './endpoints/miauth/gen-token.js'; import * as ep___miauth_genToken from './endpoints/miauth/gen-token.js';
import * as ep___mute_create from './endpoints/mute/create.js'; import * as ep___mute_create from './endpoints/mute/create.js';
import * as ep___mute_delete from './endpoints/mute/delete.js'; import * as ep___mute_delete from './endpoints/mute/delete.js';
@ -550,6 +551,7 @@ const $messaging_messages_create: Provider = { provide: 'ep:messaging/messages/c
const $messaging_messages_delete: Provider = { provide: 'ep:messaging/messages/delete', useClass: ep___messaging_messages_delete.default }; const $messaging_messages_delete: Provider = { provide: 'ep:messaging/messages/delete', useClass: ep___messaging_messages_delete.default };
const $messaging_messages_read: Provider = { provide: 'ep:messaging/messages/read', useClass: ep___messaging_messages_read.default }; const $messaging_messages_read: Provider = { provide: 'ep:messaging/messages/read', useClass: ep___messaging_messages_read.default };
const $meta: Provider = { provide: 'ep:meta', useClass: ep___meta.default }; const $meta: Provider = { provide: 'ep:meta', useClass: ep___meta.default };
const $emojis: Provider = { provide: 'ep:emojis', useClass: ep___emojis.default };
const $miauth_genToken: Provider = { provide: 'ep:miauth/gen-token', useClass: ep___miauth_genToken.default }; const $miauth_genToken: Provider = { provide: 'ep:miauth/gen-token', useClass: ep___miauth_genToken.default };
const $mute_create: Provider = { provide: 'ep:mute/create', useClass: ep___mute_create.default }; const $mute_create: Provider = { provide: 'ep:mute/create', useClass: ep___mute_create.default };
const $mute_delete: Provider = { provide: 'ep:mute/delete', useClass: ep___mute_delete.default }; const $mute_delete: Provider = { provide: 'ep:mute/delete', useClass: ep___mute_delete.default };
@ -884,6 +886,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
$messaging_messages_delete, $messaging_messages_delete,
$messaging_messages_read, $messaging_messages_read,
$meta, $meta,
$emojis,
$miauth_genToken, $miauth_genToken,
$mute_create, $mute_create,
$mute_delete, $mute_delete,
@ -1212,6 +1215,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
$messaging_messages_delete, $messaging_messages_delete,
$messaging_messages_read, $messaging_messages_read,
$meta, $meta,
$emojis,
$miauth_genToken, $miauth_genToken,
$mute_create, $mute_create,
$mute_delete, $mute_delete,

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_delete from './endpoints/messaging/messages/delete.js';
import * as ep___messaging_messages_read from './endpoints/messaging/messages/read.js'; import * as ep___messaging_messages_read from './endpoints/messaging/messages/read.js';
import * as ep___meta from './endpoints/meta.js'; import * as ep___meta from './endpoints/meta.js';
import * as ep___emojis from './endpoints/emojis.js';
import * as ep___miauth_genToken from './endpoints/miauth/gen-token.js'; import * as ep___miauth_genToken from './endpoints/miauth/gen-token.js';
import * as ep___mute_create from './endpoints/mute/create.js'; import * as ep___mute_create from './endpoints/mute/create.js';
import * as ep___mute_delete from './endpoints/mute/delete.js'; import * as ep___mute_delete from './endpoints/mute/delete.js';
@ -547,6 +548,7 @@ const eps = [
['messaging/messages/delete', ep___messaging_messages_delete], ['messaging/messages/delete', ep___messaging_messages_delete],
['messaging/messages/read', ep___messaging_messages_read], ['messaging/messages/read', ep___messaging_messages_read],
['meta', ep___meta], ['meta', ep___meta],
['emojis', ep___emojis],
['miauth/gen-token', ep___miauth_genToken], ['miauth/gen-token', ep___miauth_genToken],
['mute/create', ep___mute_create], ['mute/create', ep___mute_create],
['mute/delete', ep___mute_delete], ['mute/delete', ep___mute_delete],

View file

@ -5,6 +5,7 @@ import { QueryService } from '@/core/QueryService.js';
import { UtilityService } from '@/core/UtilityService.js'; import { UtilityService } from '@/core/UtilityService.js';
import { EmojiEntityService } from '@/core/entities/EmojiEntityService.js'; import { EmojiEntityService } from '@/core/entities/EmojiEntityService.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import { sqlLikeEscape } from '@/misc/sql-like-escape.js';
export const meta = { export const meta = {
tags: ['admin'], tags: ['admin'],
@ -92,7 +93,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
} }
if (ps.query) { if (ps.query) {
q.andWhere('emoji.name like :query', { query: '%' + ps.query + '%' }); q.andWhere('emoji.name like :query', { query: '%' + sqlLikeEscape(ps.query) + '%' });
} }
const emojis = await q const emojis = await q

View file

@ -5,6 +5,7 @@ import type { Emoji } from '@/models/entities/Emoji.js';
import { QueryService } from '@/core/QueryService.js'; import { QueryService } from '@/core/QueryService.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import { EmojiEntityService } from '@/core/entities/EmojiEntityService.js'; import { EmojiEntityService } from '@/core/entities/EmojiEntityService.js';
//import { sqlLikeEscape } from '@/misc/sql-like-escape.js';
export const meta = { export const meta = {
tags: ['admin'], tags: ['admin'],
@ -82,7 +83,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
let emojis: Emoji[]; let emojis: Emoji[];
if (ps.query) { if (ps.query) {
//q.andWhere('emoji.name ILIKE :q', { q: `%${ps.query}%` }); //q.andWhere('emoji.name ILIKE :q', { q: `%${ sqlLikeEscape(ps.query) }%` });
//const emojis = await q.take(ps.limit).getMany(); //const emojis = await q.take(ps.limit).getMany();
emojis = await q.getMany(); emojis = await q.getMany();

View file

@ -3,6 +3,7 @@ import type { UsersRepository } from '@/models/index.js';
import { Endpoint } from '@/server/api/endpoint-base.js'; import { Endpoint } from '@/server/api/endpoint-base.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { sqlLikeEscape } from '@/misc/sql-like-escape.js';
export const meta = { export const meta = {
tags: ['admin'], tags: ['admin'],
@ -68,7 +69,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
} }
if (ps.username) { if (ps.username) {
query.andWhere('user.usernameLower like :username', { username: ps.username.toLowerCase() + '%' }); query.andWhere('user.usernameLower like :username', { username: sqlLikeEscape(ps.username.toLowerCase()) + '%' });
} }
if (ps.hostname) { if (ps.hostname) {

View file

@ -0,0 +1,90 @@
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: {
name: {
type: 'string',
optional: false, nullable: false,
},
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 { InstanceEntityService } from '@/core/entities/InstanceEntityService.js';
import { MetaService } from '@/core/MetaService.js'; import { MetaService } from '@/core/MetaService.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import { sqlLikeEscape } from '@/misc/sql-like-escape.js';
export const meta = { export const meta = {
tags: ['federation'], tags: ['federation'],
@ -120,7 +121,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
} }
if (ps.host) { if (ps.host) {
query.andWhere('instance.host like :host', { host: '%' + ps.host.toLowerCase() + '%' }); query.andWhere('instance.host like :host', { host: '%' + sqlLikeEscape(ps.host.toLowerCase()) + '%' });
} }
const instances = await query.take(ps.limit).skip(ps.offset).getMany(); const instances = await query.take(ps.limit).skip(ps.offset).getMany();

View file

@ -2,6 +2,7 @@ import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js'; import { Endpoint } from '@/server/api/endpoint-base.js';
import type { HashtagsRepository } from '@/models/index.js'; import type { HashtagsRepository } from '@/models/index.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import { sqlLikeEscape } from '@/misc/sql-like-escape.js';
export const meta = { export const meta = {
tags: ['hashtags'], tags: ['hashtags'],
@ -37,7 +38,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
) { ) {
super(meta, paramDef, async (ps, me) => { super(meta, paramDef, async (ps, me) => {
const hashtags = await this.hashtagsRepository.createQueryBuilder('tag') const hashtags = await this.hashtagsRepository.createQueryBuilder('tag')
.where('tag.name like :q', { q: ps.query.toLowerCase() + '%' }) .where('tag.name like :q', { q: sqlLikeEscape(ps.query.toLowerCase()) + '%' })
.orderBy('tag.count', 'DESC') .orderBy('tag.count', 'DESC')
.groupBy('tag.id') .groupBy('tag.id')
.take(ps.limit) .take(ps.limit)

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 { MAX_NOTE_TEXT_LENGTH, DB_MAX_NOTE_TEXT_LENGTH } from '@/const.js';
import { Endpoint } from '@/server/api/endpoint-base.js'; import { Endpoint } from '@/server/api/endpoint-base.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { EmojiEntityService } from '@/core/entities/EmojiEntityService.js';
import { MetaService } from '@/core/MetaService.js'; import { MetaService } from '@/core/MetaService.js';
import type { Config } from '@/config.js'; import type { Config } from '@/config.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
@ -152,43 +151,6 @@ export const meta = {
type: 'number', type: 'number',
optional: false, nullable: false, optional: false, nullable: false,
}, },
emojis: {
type: 'array',
optional: false, nullable: false,
items: {
type: 'object',
optional: false, nullable: false,
properties: {
id: {
type: 'string',
optional: false, nullable: false,
format: 'id',
},
aliases: {
type: 'array',
optional: false, nullable: false,
items: {
type: 'string',
optional: false, nullable: false,
},
},
category: {
type: 'string',
optional: false, nullable: true,
},
host: {
type: 'string',
optional: false, nullable: true,
description: 'The local host is represented with `null`.',
},
url: {
type: 'string',
optional: false, nullable: false,
format: 'url',
},
},
},
},
ads: { ads: {
type: 'array', type: 'array',
optional: false, nullable: false, optional: false, nullable: false,
@ -326,30 +288,12 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
@Inject(DI.adsRepository) @Inject(DI.adsRepository)
private adsRepository: AdsRepository, private adsRepository: AdsRepository,
@Inject(DI.emojisRepository)
private emojisRepository: EmojisRepository,
private userEntityService: UserEntityService, private userEntityService: UserEntityService,
private emojiEntityService: EmojiEntityService,
private metaService: MetaService, private metaService: MetaService,
) { ) {
super(meta, paramDef, async (ps, me) => { super(meta, paramDef, async (ps, me) => {
const instance = await this.metaService.fetch(true); const instance = await this.metaService.fetch(true);
const emojis = await this.emojisRepository.find({
where: {
host: IsNull(),
},
order: {
category: 'ASC',
name: 'ASC',
},
cache: {
id: 'meta_emojis',
milliseconds: 3600000, // 1 hour
},
});
const ads = await this.adsRepository.find({ const ads = await this.adsRepository.find({
where: { where: {
expiresAt: MoreThan(new Date()), expiresAt: MoreThan(new Date()),
@ -390,7 +334,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
backgroundImageUrl: instance.backgroundImageUrl, backgroundImageUrl: instance.backgroundImageUrl,
logoImageUrl: instance.logoImageUrl, logoImageUrl: instance.logoImageUrl,
maxNoteTextLength: MAX_NOTE_TEXT_LENGTH, // 後方互換性のため maxNoteTextLength: MAX_NOTE_TEXT_LENGTH, // 後方互換性のため
emojis: await this.emojiEntityService.packMany(emojis),
defaultLightTheme: instance.defaultLightTheme, defaultLightTheme: instance.defaultLightTheme,
defaultDarkTheme: instance.defaultDarkTheme, defaultDarkTheme: instance.defaultDarkTheme,
ads: ads.map(ad => ({ ads: ads.map(ad => ({

View file

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

View file

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

View file

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

View file

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

View file

@ -312,7 +312,7 @@ export class ClientServerService {
fastify.get('/opensearch.xml', async (request, reply) => { fastify.get('/opensearch.xml', async (request, reply) => {
const meta = await this.metaService.fetch(); const meta = await this.metaService.fetch();
const name = meta.name || 'Misskey'; const name = meta.name ?? 'Misskey';
let content = ''; let content = '';
content += '<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/" xmlns:moz="http://www.mozilla.org/2006/browser/search/">'; content += '<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/" xmlns:moz="http://www.mozilla.org/2006/browser/search/">';
content += `<ShortName>${name}</ShortName>`; content += `<ShortName>${name}</ShortName>`;
@ -533,13 +533,12 @@ export class ClientServerService {
}); });
// Clip // Clip
// TODO: 非publicなclipのハンドリング
fastify.get<{ Params: { clip: string; } }>('/clips/:clip', async (request, reply) => { fastify.get<{ Params: { clip: string; } }>('/clips/:clip', async (request, reply) => {
const clip = await this.clipsRepository.findOneBy({ const clip = await this.clipsRepository.findOneBy({
id: request.params.clip, id: request.params.clip,
}); });
if (clip) { if (clip && clip.isPublic) {
const _clip = await this.clipEntityService.pack(clip); const _clip = await this.clipEntityService.pack(clip);
const profile = await this.userProfilesRepository.findOneByOrFail({ userId: clip.userId }); const profile = await this.userProfilesRepository.findOneByOrFail({ userId: clip.userId });
const meta = await this.metaService.fetch(); const meta = await this.metaService.fetch();

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

View file

@ -38,7 +38,7 @@
"json5": "2.2.3", "json5": "2.2.3",
"katex": "0.16.4", "katex": "0.16.4",
"matter-js": "0.18.0", "matter-js": "0.18.0",
"mfm-js": "0.23.0", "mfm-js": "0.23.1",
"misskey-js": "0.0.14", "misskey-js": "0.0.14",
"photoswipe": "5.3.4", "photoswipe": "5.3.4",
"prismjs": "1.29.0", "prismjs": "1.29.0",

View file

@ -33,12 +33,12 @@
<option v-for="item in c.items" :key="item.value" :value="item.value">{{ item.text }}</option> <option v-for="item in c.items" :key="item.value" :value="item.value">{{ item.text }}</option>
</MkSelect> </MkSelect>
<MkButton v-else-if="c.type === 'postFormButton'" :primary="c.primary" :rounded="c.rounded" :small="size === 'small'" @click="openPostForm">{{ c.text }}</MkButton> <MkButton v-else-if="c.type === 'postFormButton'" :primary="c.primary" :rounded="c.rounded" :small="size === 'small'" @click="openPostForm">{{ c.text }}</MkButton>
<FormFolder v-else-if="c.type === 'folder'" :default-open="c.opened"> <MkFolder v-else-if="c.type === 'folder'" :default-open="c.opened">
<template #label>{{ c.title }}</template> <template #label>{{ c.title }}</template>
<template v-for="child in c.children" :key="child"> <template v-for="child in c.children" :key="child">
<MkAsUi v-if="!g(child).hidden" :component="g(child)" :components="props.components" :size="size"/> <MkAsUi v-if="!g(child).hidden" :component="g(child)" :components="props.components" :size="size"/>
</template> </template>
</FormFolder> </MkFolder>
<div v-else-if="c.type === 'container'" :class="[$style.container, { [$style.fontSerif]: c.font === 'serif', [$style.fontMonospace]: c.font === 'monospace', [$style.containerCenter]: c.align === 'center' }]" :style="{ backgroundColor: c.bgColor ?? null, color: c.fgColor ?? null, borderWidth: c.borderWidth ? `${c.borderWidth}px` : 0, borderColor: c.borderColor ?? 'var(--divider)', padding: c.padding ? `${c.padding}px` : 0, borderRadius: c.rounded ? '8px' : 0 }"> <div v-else-if="c.type === 'container'" :class="[$style.container, { [$style.fontSerif]: c.font === 'serif', [$style.fontMonospace]: c.font === 'monospace', [$style.containerCenter]: c.align === 'center' }]" :style="{ backgroundColor: c.bgColor ?? null, color: c.fgColor ?? null, borderWidth: c.borderWidth ? `${c.borderWidth}px` : 0, borderColor: c.borderColor ?? 'var(--divider)', padding: c.padding ? `${c.padding}px` : 0, borderRadius: c.rounded ? '8px' : 0 }">
<template v-for="child in c.children" :key="child"> <template v-for="child in c.children" :key="child">
<MkAsUi v-if="!g(child).hidden" :component="g(child)" :components="props.components" :size="size"/> <MkAsUi v-if="!g(child).hidden" :component="g(child)" :components="props.components" :size="size"/>
@ -56,7 +56,7 @@ import MkSwitch from '@/components/MkSwitch.vue';
import MkTextarea from '@/components/MkTextarea.vue'; import MkTextarea from '@/components/MkTextarea.vue';
import MkSelect from '@/components/MkSelect.vue'; import MkSelect from '@/components/MkSelect.vue';
import { AsUiComponent } from '@/scripts/aiscript/ui'; import { AsUiComponent } from '@/scripts/aiscript/ui';
import FormFolder from '@/components/form/folder.vue'; import MkFolder from '@/components/MkFolder.vue';
const props = withDefaults(defineProps<{ const props = withDefaults(defineProps<{
component: AsUiComponent; component: AsUiComponent;

View file

@ -47,6 +47,7 @@ import { emojilist } from '@/scripts/emojilist';
import { instance } from '@/instance'; import { instance } from '@/instance';
import { i18n } from '@/i18n'; import { i18n } from '@/i18n';
import { miLocalStorage } from '@/local-storage'; import { miLocalStorage } from '@/local-storage';
import { customEmojis } from '@/custom-emojis';
type EmojiDef = { type EmojiDef = {
emoji: string; emoji: string;
@ -86,7 +87,6 @@ for (const x of lib) {
emjdb.sort((a, b) => a.name.length - b.name.length); emjdb.sort((a, b) => a.name.length - b.name.length);
//#region Construct Emoji DB //#region Construct Emoji DB
const customEmojis = instance.emojis;
const emojiDefinitions: EmojiDef[] = []; const emojiDefinitions: EmojiDef[] = [];
for (const x of customEmojis) { for (const x of customEmojis) {
@ -117,7 +117,6 @@ export default {
emojiDb, emojiDb,
emojiDefinitions, emojiDefinitions,
emojilist, emojilist,
customEmojis,
}; };
</script> </script>

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 { .fade-enter-active, .fade-leave-active {
transition: opacity 0.5s cubic-bezier(0.16, 1, 0.3, 1), transform 0.5s cubic-bezier(0.16, 1, 0.3, 1); transition: opacity 0.3s cubic-bezier(0.16, 1, 0.3, 1), transform 0.3s cubic-bezier(0.16, 1, 0.3, 1);
transform-origin: left top; transform-origin: left top;
} }

View file

@ -1,12 +1,12 @@
<template> <template>
<div class="omfetrab" :class="['s' + size, 'w' + width, 'h' + height, { asDrawer, asWindow }]" :style="{ maxHeight: maxHeight ? maxHeight + 'px' : undefined }"> <div class="omfetrab" :class="['s' + size, 'w' + width, 'h' + height, { asDrawer, asWindow }]" :style="{ maxHeight: maxHeight ? maxHeight + 'px' : undefined }">
<input ref="search" :value="q" class="search" data-prevent-emoji-insert :class="{ filled: q != null && q != '' }" :placeholder="i18n.ts.search" type="search" @input="input()" @paste.stop="paste" @keyup.enter="done()"> <input ref="searchEl" :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 ref="emojisEl" class="emojis">
<section class="result"> <section class="result">
<div v-if="searchResultCustom.length > 0" class="body"> <div v-if="searchResultCustom.length > 0" class="body">
<button <button
v-for="emoji in searchResultCustom" v-for="emoji in searchResultCustom"
:key="emoji.id" :key="emoji.name"
class="_button item" class="_button item"
:title="emoji.name" :title="emoji.name"
tabindex="0" tabindex="0"
@ -85,9 +85,10 @@ import MkRippleEffect from '@/components/MkRippleEffect.vue';
import * as os from '@/os'; import * as os from '@/os';
import { isTouchUsing } from '@/scripts/touch'; import { isTouchUsing } from '@/scripts/touch';
import { deviceKind } from '@/scripts/device-kind'; import { deviceKind } from '@/scripts/device-kind';
import { emojiCategories, instance } from '@/instance'; import { instance } from '@/instance';
import { i18n } from '@/i18n'; import { i18n } from '@/i18n';
import { defaultStore } from '@/store'; import { defaultStore } from '@/store';
import { getCustomEmojiCategories, customEmojis } from '@/custom-emojis';
const props = withDefaults(defineProps<{ const props = withDefaults(defineProps<{
showPinned?: boolean; showPinned?: boolean;
@ -103,8 +104,9 @@ const emit = defineEmits<{
(ev: 'chosen', v: string): void; (ev: 'chosen', v: string): void;
}>(); }>();
const search = shallowRef<HTMLInputElement>(); const customEmojiCategories = getCustomEmojiCategories();
const emojis = shallowRef<HTMLDivElement>(); const searchEl = shallowRef<HTMLInputElement>();
const emojisEl = shallowRef<HTMLDivElement>();
const { const {
reactions: pinned, reactions: pinned,
@ -118,15 +120,13 @@ const {
const size = computed(() => props.asReactionPicker ? reactionPickerSize.value : 1); const size = computed(() => props.asReactionPicker ? reactionPickerSize.value : 1);
const width = computed(() => props.asReactionPicker ? reactionPickerWidth.value : 3); const width = computed(() => props.asReactionPicker ? reactionPickerWidth.value : 3);
const height = computed(() => props.asReactionPicker ? reactionPickerHeight.value : 2); const height = computed(() => props.asReactionPicker ? reactionPickerHeight.value : 2);
const customEmojiCategories = emojiCategories;
const customEmojis = instance.emojis;
const q = ref<string>(''); const q = ref<string>('');
const searchResultCustom = ref<Misskey.entities.CustomEmoji[]>([]); const searchResultCustom = ref<Misskey.entities.CustomEmoji[]>([]);
const searchResultUnicode = ref<UnicodeEmojiDef[]>([]); const searchResultUnicode = ref<UnicodeEmojiDef[]>([]);
const tab = ref<'index' | 'custom' | 'unicode' | 'tags'>('index'); const tab = ref<'index' | 'custom' | 'unicode' | 'tags'>('index');
watch(q, () => { watch(q, () => {
if (emojis.value) emojis.value.scrollTop = 0; if (emojisEl.value) emojisEl.value.scrollTop = 0;
if (q.value === '') { if (q.value === '') {
searchResultCustom.value = []; searchResultCustom.value = [];
@ -268,14 +268,14 @@ watch(q, () => {
function focus() { function focus() {
if (!['smartphone', 'tablet'].includes(deviceKind) && !isTouchUsing) { if (!['smartphone', 'tablet'].includes(deviceKind) && !isTouchUsing) {
search.value?.focus({ searchEl.value?.focus({
preventScroll: true, preventScroll: true,
}); });
} }
} }
function reset() { function reset() {
if (emojis.value) emojis.value.scrollTop = 0; if (emojisEl.value) emojisEl.value.scrollTop = 0;
q.value = ''; q.value = '';
} }
@ -308,7 +308,7 @@ function input(): void {
// Using custom input event instead of v-model to respond immediately on // Using custom input event instead of v-model to respond immediately on
// Android, where composition happens on all languages // Android, where composition happens on all languages
// (v-model does not update during composition) // (v-model does not update during composition)
q.value = search.value?.value.trim() ?? ''; q.value = searchEl.value?.value.trim() ?? '';
} }
function paste(event: ClipboardEvent): void { function paste(event: ClipboardEvent): void {

View file

@ -59,6 +59,11 @@ function chosen(emoji: any) {
function opening() { function opening() {
picker.value?.reset(); picker.value?.reset();
picker.value?.focus(); picker.value?.focus();
//
setTimeout(() => {
picker.value?.focus();
}, 10);
} }
</script> </script>

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,161 +1,177 @@
<template> <template>
<div class="ssazuxis"> <div ref="rootEl" class="dwzlatin" :class="{ opened }">
<header class="_button" :style="{ background: bg }" @click="showBody = !showBody"> <div class="header _button" @click="toggle">
<div class="title"><slot name="header"></slot></div> <span class="icon"><slot name="icon"></slot></span>
<div class="divider"></div> <span class="text"><slot name="label"></slot></span>
<button class="_button"> <span class="right">
<template v-if="showBody"><i class="ti ti-chevron-up"></i></template> <span class="text"><slot name="suffix"></slot></span>
<template v-else><i class="ti ti-chevron-down"></i></template> <i v-if="opened" class="ti ti-chevron-up icon"></i>
</button> <i v-else class="ti ti-chevron-down icon"></i>
</header> </span>
<Transition </div>
:name="$store.state.animation ? 'folder-toggle' : ''" <div v-if="openedAtLeastOnce" class="body" :class="{ bgSame }" :style="{ maxHeight: maxHeight ? `${maxHeight}px` : null }">
@enter="enter" <Transition
@after-enter="afterEnter" :name="$store.state.animation ? 'folder-toggle' : ''"
@leave="leave" @enter="enter"
@after-leave="afterLeave" @after-enter="afterEnter"
> @leave="leave"
<div v-show="showBody"> @after-leave="afterLeave"
<slot></slot> >
</div> <KeepAlive>
</Transition> <div v-show="opened">
<MkSpacer :margin-min="14" :margin-max="22">
<slot></slot>
</MkSpacer>
</div>
</KeepAlive>
</Transition>
</div>
</div> </div>
</template> </template>
<script lang="ts"> <script lang="ts" setup>
import { defineComponent } from 'vue'; import { nextTick, onMounted } from 'vue';
import tinycolor from 'tinycolor2';
import { miLocalStorage } from '@/local-storage';
const miLocalStoragePrefix = 'ui:folder:' as const; const props = withDefaults(defineProps<{
defaultOpen: boolean;
maxHeight: number | null;
}>(), {
defaultOpen: false,
maxHeight: null,
});
export default defineComponent({ const getBgColor = (el: HTMLElement) => {
props: { const style = window.getComputedStyle(el);
expanded: { if (style.backgroundColor && !['rgba(0, 0, 0, 0)', 'rgba(0,0,0,0)', 'transparent'].includes(style.backgroundColor)) {
type: Boolean, return style.backgroundColor;
required: false, } else {
default: true, return el.parentElement ? getBgColor(el.parentElement) : 'transparent';
}, }
persistKey: { };
type: String,
required: false,
default: null,
},
},
data() {
return {
bg: null,
showBody: (this.persistKey && 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) { let rootEl = $ref<HTMLElement>();
const elementHeight = el.getBoundingClientRect().height; let bgSame = $ref(false);
el.style.height = 0; let opened = $ref(props.defaultOpen);
el.offsetHeight; // reflow let openedAtLeastOnce = $ref(props.defaultOpen);
el.style.height = elementHeight + 'px';
}, function enter(el) {
afterEnter(el) { const elementHeight = el.getBoundingClientRect().height;
el.style.height = null; el.style.height = 0;
}, el.offsetHeight; // reflow
leave(el) { el.style.height = Math.min(elementHeight, props.maxHeight ?? Infinity) + 'px';
const elementHeight = el.getBoundingClientRect().height; }
el.style.height = elementHeight + 'px';
el.offsetHeight; // reflow function afterEnter(el) {
el.style.height = 0; el.style.height = null;
}, }
afterLeave(el) {
el.style.height = null; function leave(el) {
}, const elementHeight = el.getBoundingClientRect().height;
}, el.style.height = elementHeight + 'px';
el.offsetHeight; // reflow
el.style.height = 0;
}
function afterLeave(el) {
el.style.height = null;
}
function toggle() {
if (!opened) {
openedAtLeastOnce = true;
}
nextTick(() => {
opened = !opened;
});
}
onMounted(() => {
const computedStyle = getComputedStyle(document.documentElement);
const parentBg = getBgColor(rootEl.parentElement);
const myBg = computedStyle.getPropertyValue('--panel');
bgSame = parentBg === myBg;
}); });
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.folder-toggle-enter-active, .folder-toggle-leave-active { .folder-toggle-enter-active, .folder-toggle-leave-active {
overflow-y: clip; overflow-y: clip;
transition: opacity 0.5s, height 0.5s !important; transition: opacity 0.3s, height 0.3s, transform 0.3s !important;
} }
.folder-toggle-enter-from { .folder-toggle-enter-from, .folder-toggle-leave-to {
opacity: 0;
}
.folder-toggle-leave-to {
opacity: 0; opacity: 0;
} }
.ssazuxis { .dwzlatin {
position: relative; display: block;
> header { > .header {
display: flex; display: flex;
position: relative; align-items: center;
z-index: 10; width: 100%;
position: sticky; box-sizing: border-box;
top: var(--stickyTop, 0px); padding: 10px 14px 10px 14px;
padding: var(--x-padding); background: var(--buttonBg);
-webkit-backdrop-filter: var(--blur, blur(8px)); border-radius: 6px;
backdrop-filter: var(--blur, blur(20px));
> .title { &:hover {
display: grid; text-decoration: none;
place-content: center; background: var(--buttonHoverBg);
margin: 0; }
padding: 12px 16px 12px 0;
> i { &.active {
margin-right: 6px; color: var(--accent);
} background: var(--buttonHoverBg);
}
> .icon {
margin-right: 0.75em;
flex-shrink: 0;
text-align: center;
opacity: 0.8;
&:empty { &:empty {
display: none; display: none;
& + .text {
padding-left: 4px;
}
} }
} }
> .divider { > .text {
flex: 1; white-space: nowrap;
margin: auto; text-overflow: ellipsis;
height: 1px; overflow: hidden;
background: var(--divider); padding-right: 12px;
} }
> button { > .right {
padding: 12px 0 12px 16px; margin-left: auto;
opacity: 0.7;
white-space: nowrap;
> .text:not(:empty) {
margin-right: 0.75em;
}
} }
} }
}
@container (max-width: 500px) { > .body {
.ssazuxis { background: var(--panel);
> header { border-radius: 0 0 6px 6px;
> .title { container-type: inline-size;
padding: 8px 10px 8px 0; overflow: auto;
}
&.bgSame {
background: var(--bg);
}
}
&.opened {
> .header {
border-radius: 6px 6px 0 0;
} }
} }
} }

View file

@ -1,6 +1,6 @@
<template> <template>
<div :class="[$style.root, { yellow: instance.isNotResponding, red: instance.isBlocked, gray: instance.isSuspended }]"> <div :class="[$style.root, { yellow: instance.isNotResponding, red: instance.isBlocked, gray: instance.isSuspended }]">
<img class="icon" :src="getInstanceIcon(instance)" alt=""/> <img class="icon" :src="getInstanceIcon(instance)" alt="" loading="lazy"/>
<div class="body"> <div class="body">
<span class="host">{{ instance.name ?? instance.host }}</span> <span class="host">{{ instance.name ?? instance.host }}</span>
<span class="sub _monospace"><b>{{ instance.host }}</b> / {{ instance.softwareName || '?' }} {{ instance.softwareVersion }}</span> <span class="sub _monospace"><b>{{ instance.host }}</b> / {{ instance.softwareName || '?' }} {{ instance.softwareVersion }}</span>

View file

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

View file

@ -251,17 +251,18 @@ onBeforeUnmount(() => {
color: #fff; color: #fff;
&:before { &:before {
background: #d42e2e; background: #d42e2e !important;
} }
} }
} }
&:active,
&.active { &.active {
color: var(--fgOnAccent); color: var(--fgOnAccent) !important;
opacity: 1; opacity: 1;
&:before { &:before {
background: var(--accent); background: var(--accent) !important;
} }
} }

View file

@ -1,7 +1,7 @@
<template> <template>
<Transition :name="transitionName" :duration="transitionDuration" appear @after-leave="emit('closed')" @enter="emit('opening')" @after-enter="onOpened"> <Transition :name="transitionName" :duration="transitionDuration" appear @after-leave="emit('closed')" @enter="emit('opening')" @after-enter="onOpened">
<div v-show="manualShowing != null ? manualShowing : showing" v-hotkey.global="keymap" class="qzhlnise" :class="{ drawer: type === 'drawer', dialog: type === 'dialog' || type === 'dialog:top', popup: type === 'popup' }" :style="{ zIndex, pointerEvents: (manualShowing != null ? manualShowing : showing) ? 'auto' : 'none', '--transformOrigin': transformOrigin }"> <div v-show="manualShowing != null ? manualShowing : showing" v-hotkey.global="keymap" class="qzhlnise" :class="{ drawer: type === 'drawer', dialog: type === 'dialog' || type === 'dialog:top', popup: type === 'popup' }" :style="{ zIndex, pointerEvents: (manualShowing != null ? manualShowing : showing) ? 'auto' : 'none', '--transformOrigin': transformOrigin }">
<div class="bg _modalBg" :class="{ transparent: transparentBg && (type === 'popup') }" :style="{ zIndex }" @click="onBgClick" @contextmenu.prevent.stop="() => {}"></div> <div class="bg _modalBg" :class="{ transparent: transparentBg && (type === 'popup') }" :style="{ zIndex }" @click="onBgClick" @mousedown="onBgClick" @contextmenu.prevent.stop="() => {}"></div>
<div ref="content" class="content" :class="{ fixed, top: type === 'dialog:top' }" :style="{ zIndex }" @click.self="onBgClick"> <div ref="content" class="content" :class="{ fixed, top: type === 'dialog:top' }" :style="{ zIndex }" @click.self="onBgClick">
<slot :max-height="maxHeight" :type="type"></slot> <slot :max-height="maxHeight" :type="type"></slot>
</div> </div>
@ -63,6 +63,7 @@ let transformOrigin = $ref('center');
let showing = $ref(true); let showing = $ref(true);
let content = $shallowRef<HTMLElement>(); let content = $shallowRef<HTMLElement>();
const zIndex = os.claimZIndex(props.zPriority); const zIndex = os.claimZIndex(props.zPriority);
let useSendAnime = $ref(false);
const type = $computed<ModalTypes>(() => { const type = $computed<ModalTypes>(() => {
if (props.preferType === 'auto') { if (props.preferType === 'auto') {
if (!defaultStore.state.disableDrawer && isTouchUsing && deviceKind === 'smartphone') { if (!defaultStore.state.disableDrawer && isTouchUsing && deviceKind === 'smartphone') {
@ -74,15 +75,34 @@ const type = $computed<ModalTypes>(() => {
return props.preferType!; return props.preferType!;
} }
}); });
let transitionName = $ref(defaultStore.state.animation ? (type === 'drawer') ? 'modal-drawer' : (type === 'popup') ? 'modal-popup' : 'modal' : ''); let transitionName = $computed((() =>
let transitionDuration = $ref(defaultStore.state.animation ? 200 : 0); defaultStore.state.animation
? useSendAnime
? 'send'
: type === 'drawer'
? 'modal-drawer'
: type === 'popup'
? 'modal-popup'
: 'modal'
: ''
));
let transitionDuration = $computed((() =>
transitionName === 'send'
? 400
: transitionName === 'modal-popup'
? 100
: transitionName === 'modal'
? 200
: transitionName === 'modal-drawer'
? 200
: 0
));
let contentClicking = false; let contentClicking = false;
function close(opts: { useSendAnimation?: boolean } = {}) { function close(opts: { useSendAnimation?: boolean } = {}) {
if (opts.useSendAnimation) { if (opts.useSendAnimation) {
transitionName = 'send'; useSendAnime = true;
transitionDuration = 400;
} }
// eslint-disable-next-line vue/no-mutating-props // eslint-disable-next-line vue/no-mutating-props
@ -308,12 +328,12 @@ defineExpose({
.modal-popup-enter-active, .modal-popup-leave-active { .modal-popup-enter-active, .modal-popup-leave-active {
> .bg { > .bg {
transition: opacity 0.2s !important; transition: opacity 0.1s !important;
} }
> .content { > .content {
transform-origin: var(--transformOrigin); transform-origin: var(--transformOrigin);
transition: opacity 0.2s cubic-bezier(0, 0, 0.2, 1), transform 0.2s cubic-bezier(0, 0, 0.2, 1) !important; transition: opacity 0.1s cubic-bezier(0, 0, 0.2, 1), transform 0.1s cubic-bezier(0, 0, 0.2, 1) !important;
} }
} }
.modal-popup-enter-from, .modal-popup-leave-to { .modal-popup-enter-from, .modal-popup-leave-to {

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._prId_" class="info"><i class="fas fa-bullhorn"></i> {{ i18n.ts.promotion }}<button class="_textButton hide" @click="readPromo()">{{ i18n.ts.hideThisNote }} <i class="ti ti-x"></i></button></div>
<div v-if="appearNote._featuredId_" class="info"><i class="ti ti-bolt"></i> {{ i18n.ts.featured }}</div> <div v-if="appearNote._featuredId_" class="info"><i class="ti ti-bolt"></i> {{ i18n.ts.featured }}</div>
<div v-if="isRenote" class="renote"> <div v-if="isRenote" class="renote">
<MkAvatar class="avatar" :user="note.user"/> <MkAvatar v-once class="avatar" :user="note.user"/>
<i class="ti ti-repeat"></i> <i class="ti ti-repeat"></i>
<I18n :src="i18n.ts.renotedBy" tag="span"> <I18n :src="i18n.ts.renotedBy" tag="span">
<template #user> <template #user>
@ -27,11 +27,16 @@
<i v-if="isMyRenote" class="ti ti-dots dropdownIcon"></i> <i v-if="isMyRenote" class="ti ti-dots dropdownIcon"></i>
<MkTime :time="note.createdAt"/> <MkTime :time="note.createdAt"/>
</button> </button>
<MkVisibility :note="note"/> <span v-if="note.visibility !== 'public'" style="margin-left: 0.5em;" :title="i18n.ts._visibility[note.visibility]">
<i v-if="note.visibility === 'home'" class="ti ti-home"></i>
<i v-else-if="note.visibility === 'followers'" class="ti ti-lock"></i>
<i v-else-if="note.visibility === 'specified'" ref="specified" class="ti ti-mail"></i>
</span>
<span v-if="note.localOnly" style="margin-left: 0.5em;" :title="i18n.ts._visibility['localOnly']"><i class="ti ti-world-off"></i></span>
</div> </div>
</div> </div>
<article class="article" @contextmenu.stop="onContextmenu"> <article class="article" @contextmenu.stop="onContextmenu">
<MkAvatar class="avatar" :user="appearNote.user"/> <MkAvatar v-once class="avatar" :user="appearNote.user"/>
<div class="main"> <div class="main">
<MkNoteHeader class="header" :note="appearNote" :mini="true"/> <MkNoteHeader class="header" :note="appearNote" :mini="true"/>
<MkInstanceTicker v-if="showTicker" class="ticker" :instance="appearNote.user.instance"/> <MkInstanceTicker v-if="showTicker" class="ticker" :instance="appearNote.user.instance"/>
@ -44,7 +49,7 @@
<div class="text"> <div class="text">
<span v-if="appearNote.isHidden" style="opacity: 0.5">({{ i18n.ts.private }})</span> <span v-if="appearNote.isHidden" style="opacity: 0.5">({{ i18n.ts.private }})</span>
<MkA v-if="appearNote.replyId" class="reply" :to="`/notes/${appearNote.replyId}`"><i class="ti ti-arrow-back-up"></i></MkA> <MkA v-if="appearNote.replyId" class="reply" :to="`/notes/${appearNote.replyId}`"><i class="ti ti-arrow-back-up"></i></MkA>
<Mfm v-if="appearNote.text" :text="appearNote.text" :author="appearNote.user" :i="$i"/> <Mfm v-if="appearNote.text" v-once :text="appearNote.text" :author="appearNote.user" :i="$i"/>
<a v-if="appearNote.renote != null" class="rp">RN:</a> <a v-if="appearNote.renote != null" class="rp">RN:</a>
<div v-if="translating || translation" class="translation"> <div v-if="translating || translation" class="translation">
<MkLoading v-if="translating" mini/> <MkLoading v-if="translating" mini/>
@ -75,14 +80,25 @@
<i class="ti ti-arrow-back-up"></i> <i class="ti ti-arrow-back-up"></i>
<p v-if="appearNote.repliesCount > 0" class="count">{{ appearNote.repliesCount }}</p> <p v-if="appearNote.repliesCount > 0" class="count">{{ appearNote.repliesCount }}</p>
</button> </button>
<MkRenoteButton ref="renoteButton" class="button" :note="appearNote" :count="appearNote.renoteCount"/> <button
<button v-if="appearNote.myReaction == null" ref="reactButton" class="button _button" @click="react()"> v-if="canRenote"
ref="renoteButton"
class="button _button"
@mousedown="renote()"
>
<i class="ti ti-repeat"></i>
<p v-if="appearNote.renoteCount > 0" class="count">{{ appearNote.renoteCount }}</p>
</button>
<button v-else class="button _button" disabled>
<i class="ti ti-ban"></i>
</button>
<button v-if="appearNote.myReaction == null" ref="reactButton" class="button _button" @mousedown="react()">
<i class="ti ti-plus"></i> <i class="ti ti-plus"></i>
</button> </button>
<button v-if="appearNote.myReaction != null" ref="reactButton" class="button _button reacted" @click="undoReact(appearNote)"> <button v-if="appearNote.myReaction != null" ref="reactButton" class="button _button reacted" @click="undoReact(appearNote)">
<i class="ti ti-minus"></i> <i class="ti ti-minus"></i>
</button> </button>
<button ref="menuButton" class="button _button" @click="menu()"> <button ref="menuButton" class="button _button" @mousedown="menu()">
<i class="ti ti-dots"></i> <i class="ti ti-dots"></i>
</button> </button>
</footer> </footer>
@ -111,10 +127,9 @@ import MkReactionsViewer from '@/components/MkReactionsViewer.vue';
import MkMediaList from '@/components/MkMediaList.vue'; import MkMediaList from '@/components/MkMediaList.vue';
import MkCwButton from '@/components/MkCwButton.vue'; import MkCwButton from '@/components/MkCwButton.vue';
import MkPoll from '@/components/MkPoll.vue'; import MkPoll from '@/components/MkPoll.vue';
import MkRenoteButton from '@/components/MkRenoteButton.vue'; import MkUsersTooltip from '@/components/MkUsersTooltip.vue';
import MkUrlPreview from '@/components/MkUrlPreview.vue'; import MkUrlPreview from '@/components/MkUrlPreview.vue';
import MkInstanceTicker from '@/components/MkInstanceTicker.vue'; import MkInstanceTicker from '@/components/MkInstanceTicker.vue';
import MkVisibility from '@/components/MkVisibility.vue';
import { pleaseLogin } from '@/scripts/please-login'; import { pleaseLogin } from '@/scripts/please-login';
import { focusPrev, focusNext } from '@/scripts/focus'; import { focusPrev, focusNext } from '@/scripts/focus';
import { checkWordMute } from '@/scripts/check-word-mute'; import { checkWordMute } from '@/scripts/check-word-mute';
@ -128,6 +143,7 @@ import { i18n } from '@/i18n';
import { getNoteMenu } from '@/scripts/get-note-menu'; import { getNoteMenu } from '@/scripts/get-note-menu';
import { useNoteCapture } from '@/scripts/use-note-capture'; import { useNoteCapture } from '@/scripts/use-note-capture';
import { deepClone } from '@/scripts/clone'; import { deepClone } from '@/scripts/clone';
import { useTooltip } from '@/scripts/use-tooltip';
const props = defineProps<{ const props = defineProps<{
note: misskey.entities.Note; note: misskey.entities.Note;
@ -158,7 +174,7 @@ const isRenote = (
const el = shallowRef<HTMLElement>(); const el = shallowRef<HTMLElement>();
const menuButton = shallowRef<HTMLElement>(); const menuButton = shallowRef<HTMLElement>();
const renoteButton = shallowRef<InstanceType<typeof MkRenoteButton>>(); const renoteButton = shallowRef<HTMLElement>();
const renoteTime = shallowRef<HTMLElement>(); const renoteTime = shallowRef<HTMLElement>();
const reactButton = shallowRef<HTMLElement>(); const reactButton = shallowRef<HTMLElement>();
let appearNote = $computed(() => isRenote ? note.renote as misskey.entities.Note : note); let appearNote = $computed(() => isRenote ? note.renote as misskey.entities.Note : note);
@ -175,6 +191,7 @@ const translation = ref(null);
const translating = ref(false); const translating = ref(false);
const urls = appearNote.text ? extractUrlFromMfm(mfm.parse(appearNote.text)) : null; const urls = appearNote.text ? extractUrlFromMfm(mfm.parse(appearNote.text)) : null;
const showTicker = (defaultStore.state.instanceTicker === 'always') || (defaultStore.state.instanceTicker === 'remote' && appearNote.user.instance); const showTicker = (defaultStore.state.instanceTicker === 'always') || (defaultStore.state.instanceTicker === 'remote' && appearNote.user.instance);
const canRenote = computed(() => ['public', 'home'].includes(appearNote.visibility) || appearNote.userId === $i.id);
const keymap = { const keymap = {
'r': () => reply(true), 'r': () => reply(true),
@ -193,6 +210,47 @@ useNoteCapture({
isDeletedRef: isDeleted, isDeletedRef: isDeleted,
}); });
useTooltip(renoteButton, async (showing) => {
const renotes = await os.api('notes/renotes', {
noteId: appearNote.id,
limit: 11,
});
const users = renotes.map(x => x.user);
if (users.length < 1) return;
os.popup(MkUsersTooltip, {
showing,
users,
count: appearNote.renoteCount,
targetElement: renoteButton.value,
}, {}, 'closed');
});
function renote(viaKeyboard = false) {
pleaseLogin();
os.popupMenu([{
text: i18n.ts.renote,
icon: 'ti ti-repeat',
action: () => {
os.api('notes/create', {
renoteId: appearNote.id,
});
},
}, {
text: i18n.ts.quote,
icon: 'ti ti-quote',
action: () => {
os.post({
renote: appearNote,
});
},
}], renoteButton.value, {
viaKeyboard,
});
}
function reply(viaKeyboard = false): void { function reply(viaKeyboard = false): void {
pleaseLogin(); pleaseLogin();
os.post({ os.post({

View file

@ -25,7 +25,12 @@
<i v-if="isMyRenote" class="ti ti-dots dropdownIcon"></i> <i v-if="isMyRenote" class="ti ti-dots dropdownIcon"></i>
<MkTime :time="note.createdAt"/> <MkTime :time="note.createdAt"/>
</button> </button>
<MkVisibility :note="note"/> <span v-if="note.visibility !== 'public'" style="margin-left: 0.5em;" :title="i18n.ts._visibility[note.visibility]">
<i v-if="note.visibility === 'home'" class="ti ti-home"></i>
<i v-else-if="note.visibility === 'followers'" class="ti ti-lock"></i>
<i v-else-if="note.visibility === 'specified'" ref="specified" class="ti ti-mail"></i>
</span>
<span v-if="note.localOnly" style="margin-left: 0.5em;" :title="i18n.ts._visibility['localOnly']"><i class="ti ti-world-off"></i></span>
</div> </div>
</div> </div>
<article class="article" @contextmenu.stop="onContextmenu"> <article class="article" @contextmenu.stop="onContextmenu">
@ -38,7 +43,12 @@
</MkA> </MkA>
<span v-if="appearNote.user.isBot" class="is-bot">bot</span> <span v-if="appearNote.user.isBot" class="is-bot">bot</span>
<div class="info"> <div class="info">
<MkVisibility :note="appearNote"/> <span v-if="appearNote.visibility !== 'public'" style="margin-left: 0.5em;" :title="i18n.ts._visibility[appearNote.visibility]">
<i v-if="appearNote.visibility === 'home'" class="ti ti-home"></i>
<i v-else-if="appearNote.visibility === 'followers'" class="ti ti-lock"></i>
<i v-else-if="appearNote.visibility === 'specified'" ref="specified" class="ti ti-mail"></i>
</span>
<span v-if="appearNote.localOnly" style="margin-left: 0.5em;" :title="i18n.ts._visibility['localOnly']"><i class="ti ti-world-off"></i></span>
</div> </div>
</div> </div>
<div class="username"><MkAcct :user="appearNote.user"/></div> <div class="username"><MkAcct :user="appearNote.user"/></div>
@ -85,14 +95,25 @@
<i class="ti ti-arrow-back-up"></i> <i class="ti ti-arrow-back-up"></i>
<p v-if="appearNote.repliesCount > 0" class="count">{{ appearNote.repliesCount }}</p> <p v-if="appearNote.repliesCount > 0" class="count">{{ appearNote.repliesCount }}</p>
</button> </button>
<MkRenoteButton ref="renoteButton" class="button" :note="appearNote" :count="appearNote.renoteCount"/> <button
<button v-if="appearNote.myReaction == null" ref="reactButton" class="button _button" @click="react()"> v-if="canRenote"
ref="renoteButton"
class="button _button"
@mousedown="renote()"
>
<i class="ti ti-repeat"></i>
<p v-if="appearNote.renoteCount > 0" class="count">{{ appearNote.renoteCount }}</p>
</button>
<button v-else class="button _button" disabled>
<i class="ti ti-ban"></i>
</button>
<button v-if="appearNote.myReaction == null" ref="reactButton" class="button _button" @mousedown="react()">
<i class="ti ti-plus"></i> <i class="ti ti-plus"></i>
</button> </button>
<button v-if="appearNote.myReaction != null" ref="reactButton" class="button _button reacted" @click="undoReact(appearNote)"> <button v-if="appearNote.myReaction != null" ref="reactButton" class="button _button reacted" @click="undoReact(appearNote)">
<i class="ti ti-minus"></i> <i class="ti ti-minus"></i>
</button> </button>
<button ref="menuButton" class="button _button" @click="menu()"> <button ref="menuButton" class="button _button" @mousedown="menu()">
<i class="ti ti-dots"></i> <i class="ti ti-dots"></i>
</button> </button>
</footer> </footer>
@ -121,10 +142,9 @@ import MkReactionsViewer from '@/components/MkReactionsViewer.vue';
import MkMediaList from '@/components/MkMediaList.vue'; import MkMediaList from '@/components/MkMediaList.vue';
import MkCwButton from '@/components/MkCwButton.vue'; import MkCwButton from '@/components/MkCwButton.vue';
import MkPoll from '@/components/MkPoll.vue'; import MkPoll from '@/components/MkPoll.vue';
import MkRenoteButton from '@/components/MkRenoteButton.vue'; import MkUsersTooltip from '@/components/MkUsersTooltip.vue';
import MkUrlPreview from '@/components/MkUrlPreview.vue'; import MkUrlPreview from '@/components/MkUrlPreview.vue';
import MkInstanceTicker from '@/components/MkInstanceTicker.vue'; import MkInstanceTicker from '@/components/MkInstanceTicker.vue';
import MkVisibility from '@/components/MkVisibility.vue';
import { pleaseLogin } from '@/scripts/please-login'; import { pleaseLogin } from '@/scripts/please-login';
import { checkWordMute } from '@/scripts/check-word-mute'; import { checkWordMute } from '@/scripts/check-word-mute';
import { userPage } from '@/filters/user'; import { userPage } from '@/filters/user';
@ -138,6 +158,7 @@ import { i18n } from '@/i18n';
import { getNoteMenu } from '@/scripts/get-note-menu'; import { getNoteMenu } from '@/scripts/get-note-menu';
import { useNoteCapture } from '@/scripts/use-note-capture'; import { useNoteCapture } from '@/scripts/use-note-capture';
import { deepClone } from '@/scripts/clone'; import { deepClone } from '@/scripts/clone';
import { useTooltip } from '@/scripts/use-tooltip';
const props = defineProps<{ const props = defineProps<{
note: misskey.entities.Note; note: misskey.entities.Note;
@ -168,7 +189,7 @@ const isRenote = (
const el = shallowRef<HTMLElement>(); const el = shallowRef<HTMLElement>();
const menuButton = shallowRef<HTMLElement>(); const menuButton = shallowRef<HTMLElement>();
const renoteButton = shallowRef<InstanceType<typeof MkRenoteButton>>(); const renoteButton = shallowRef<HTMLElement>();
const renoteTime = shallowRef<HTMLElement>(); const renoteTime = shallowRef<HTMLElement>();
const reactButton = shallowRef<HTMLElement>(); const reactButton = shallowRef<HTMLElement>();
let appearNote = $computed(() => isRenote ? note.renote as misskey.entities.Note : note); let appearNote = $computed(() => isRenote ? note.renote as misskey.entities.Note : note);
@ -182,6 +203,7 @@ const urls = appearNote.text ? extractUrlFromMfm(mfm.parse(appearNote.text)) : n
const showTicker = (defaultStore.state.instanceTicker === 'always') || (defaultStore.state.instanceTicker === 'remote' && appearNote.user.instance); const showTicker = (defaultStore.state.instanceTicker === 'always') || (defaultStore.state.instanceTicker === 'remote' && appearNote.user.instance);
const conversation = ref<misskey.entities.Note[]>([]); const conversation = ref<misskey.entities.Note[]>([]);
const replies = ref<misskey.entities.Note[]>([]); const replies = ref<misskey.entities.Note[]>([]);
const canRenote = computed(() => ['public', 'home'].includes(appearNote.visibility) || appearNote.userId === $i.id);
const keymap = { const keymap = {
'r': () => reply(true), 'r': () => reply(true),
@ -198,6 +220,47 @@ useNoteCapture({
isDeletedRef: isDeleted, isDeletedRef: isDeleted,
}); });
useTooltip(renoteButton, async (showing) => {
const renotes = await os.api('notes/renotes', {
noteId: appearNote.id,
limit: 11,
});
const users = renotes.map(x => x.user);
if (users.length < 1) return;
os.popup(MkUsersTooltip, {
showing,
users,
count: appearNote.renoteCount,
targetElement: renoteButton.value,
}, {}, 'closed');
});
function renote(viaKeyboard = false) {
pleaseLogin();
os.popupMenu([{
text: i18n.ts.renote,
icon: 'ti ti-repeat',
action: () => {
os.api('notes/create', {
renoteId: appearNote.id,
});
},
}, {
text: i18n.ts.quote,
icon: 'ti ti-quote',
action: () => {
os.post({
renote: appearNote,
});
},
}], renoteButton.value, {
viaKeyboard,
});
}
function reply(viaKeyboard = false): void { function reply(viaKeyboard = false): void {
pleaseLogin(); pleaseLogin();
os.post({ os.post({

View file

@ -1,6 +1,6 @@
<template> <template>
<header class="kkwtjztg"> <header class="kkwtjztg">
<MkA v-user-preview="note.user.id" class="name" :to="userPage(note.user)"> <MkA v-once v-user-preview="note.user.id" class="name" :to="userPage(note.user)">
<MkUserName :user="note.user"/> <MkUserName :user="note.user"/>
</MkA> </MkA>
<div v-if="note.user.isBot" class="is-bot">bot</div> <div v-if="note.user.isBot" class="is-bot">bot</div>
@ -9,7 +9,12 @@
<MkA class="created-at" :to="notePage(note)"> <MkA class="created-at" :to="notePage(note)">
<MkTime :time="note.createdAt"/> <MkTime :time="note.createdAt"/>
</MkA> </MkA>
<MkVisibility :note="note"/> <span v-if="note.visibility !== 'public'" style="margin-left: 0.5em;" :title="i18n.ts._visibility[note.visibility]">
<i v-if="note.visibility === 'home'" class="ti ti-home"></i>
<i v-else-if="note.visibility === 'followers'" class="ti ti-lock"></i>
<i v-else-if="note.visibility === 'specified'" ref="specified" class="ti ti-mail"></i>
</span>
<span v-if="note.localOnly" style="margin-left: 0.5em;" :title="i18n.ts._visibility['localOnly']"><i class="ti ti-world-off"></i></span>
</div> </div>
</header> </header>
</template> </template>
@ -17,7 +22,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import { } from 'vue'; import { } from 'vue';
import * as misskey from 'misskey-js'; import * as misskey from 'misskey-js';
import MkVisibility from '@/components/MkVisibility.vue'; import { i18n } from '@/i18n';
import { notePage } from '@/filters/note'; import { notePage } from '@/filters/note';
import { userPage } from '@/filters/user'; import { userPage } from '@/filters/user';

View file

@ -1,6 +1,6 @@
<template> <template>
<div ref="elRef" class="qglefbjs" :class="notification.type"> <div ref="elRef" class="qglefbjs" :class="notification.type">
<div class="head"> <div v-once class="head">
<MkAvatar v-if="notification.type === 'pollEnded'" class="icon" :user="notification.note.user"/> <MkAvatar v-if="notification.type === 'pollEnded'" class="icon" :user="notification.note.user"/>
<MkAvatar v-else-if="notification.user" class="icon" :user="notification.user"/> <MkAvatar v-else-if="notification.user" class="icon" :user="notification.user"/>
<img v-else-if="notification.icon" class="icon" :src="notification.icon" alt=""/> <img v-else-if="notification.icon" class="icon" :src="notification.icon" alt=""/>
@ -13,10 +13,9 @@
<i v-else-if="notification.type === 'reply'" class="ti ti-arrow-back-up"></i> <i v-else-if="notification.type === 'reply'" class="ti ti-arrow-back-up"></i>
<i v-else-if="notification.type === 'mention'" class="ti ti-at"></i> <i v-else-if="notification.type === 'mention'" class="ti ti-at"></i>
<i v-else-if="notification.type === 'quote'" class="ti ti-quote"></i> <i v-else-if="notification.type === 'quote'" class="ti ti-quote"></i>
<i v-else-if="notification.type === 'pollVote'" class="ti ti-chart-arrows"></i>
<i v-else-if="notification.type === 'pollEnded'" class="ti ti-chart-arrows"></i> <i v-else-if="notification.type === 'pollEnded'" class="ti ti-chart-arrows"></i>
<!-- notification.reaction null になることはまずないがここでoptional chaining使うと一部ブラウザで刺さるので念の為 --> <!-- notification.reaction null になることはまずないがここでoptional chaining使うと一部ブラウザで刺さるので念の為 -->
<XReactionIcon <MkReactionIcon
v-else-if="notification.type === 'reaction'" v-else-if="notification.type === 'reaction'"
ref="reactionRef" ref="reactionRef"
:reaction="notification.reaction ? notification.reaction.replace(/^:(\w+):$/, ':$1@.:') : notification.reaction" :reaction="notification.reaction ? notification.reaction.replace(/^:(\w+):$/, ':$1@.:') : notification.reaction"
@ -32,42 +31,39 @@
<span v-else>{{ notification.header }}</span> <span v-else>{{ notification.header }}</span>
<MkTime v-if="withTime" :time="notification.createdAt" class="time"/> <MkTime v-if="withTime" :time="notification.createdAt" class="time"/>
</header> </header>
<MkA v-if="notification.type === 'reaction'" class="text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)"> <div v-once class="content">
<i class="ti ti-quote"></i> <MkA v-if="notification.type === 'reaction'" class="text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)">
<Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="!full"/> <i class="ti ti-quote"></i>
<i class="ti ti-quote"></i> <Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="!full"/>
</MkA> <i class="ti ti-quote"></i>
<MkA v-if="notification.type === 'renote'" class="text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note.renote)"> </MkA>
<i class="ti ti-quote"></i> <MkA v-else-if="notification.type === 'renote'" class="text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note.renote)">
<Mfm :text="getNoteSummary(notification.note.renote)" :plain="true" :nowrap="!full"/> <i class="ti ti-quote"></i>
<i class="ti ti-quote"></i> <Mfm :text="getNoteSummary(notification.note.renote)" :plain="true" :nowrap="!full"/>
</MkA> <i class="ti ti-quote"></i>
<MkA v-if="notification.type === 'reply'" class="text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)"> </MkA>
<Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="!full"/> <MkA v-else-if="notification.type === 'reply'" class="text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)">
</MkA> <Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="!full"/>
<MkA v-if="notification.type === 'mention'" class="text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)"> </MkA>
<Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="!full"/> <MkA v-else-if="notification.type === 'mention'" class="text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)">
</MkA> <Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="!full"/>
<MkA v-if="notification.type === 'quote'" class="text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)"> </MkA>
<Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="!full"/> <MkA v-else-if="notification.type === 'quote'" class="text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)">
</MkA> <Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="!full"/>
<MkA v-if="notification.type === 'pollVote'" class="text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)"> </MkA>
<i class="ti ti-quote"></i> <MkA v-else-if="notification.type === 'pollEnded'" class="text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)">
<Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="!full"/> <i class="ti ti-quote"></i>
<i class="ti ti-quote"></i> <Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="!full"/>
</MkA> <i class="ti ti-quote"></i>
<MkA v-if="notification.type === 'pollEnded'" class="text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)"> </MkA>
<i class="ti ti-quote"></i> <span v-else-if="notification.type === 'follow'" class="text" style="opacity: 0.6;">{{ i18n.ts.youGotNewFollower }}<div v-if="full"><MkFollowButton :user="notification.user" :full="true"/></div></span>
<Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="!full"/> <span v-else-if="notification.type === 'followRequestAccepted'" class="text" style="opacity: 0.6;">{{ i18n.ts.followRequestAccepted }}</span>
<i class="ti ti-quote"></i> <span v-else-if="notification.type === 'receiveFollowRequest'" class="text" style="opacity: 0.6;">{{ i18n.ts.receiveFollowRequest }}<div v-if="full && !followRequestDone"><button class="_textButton" @click="acceptFollowRequest()">{{ i18n.ts.accept }}</button> | <button class="_textButton" @click="rejectFollowRequest()">{{ i18n.ts.reject }}</button></div></span>
</MkA> <span v-else-if="notification.type === 'groupInvited'" class="text" style="opacity: 0.6;">{{ i18n.ts.groupInvited }}: <b>{{ notification.invitation.group.name }}</b><div v-if="full && !groupInviteDone"><button class="_textButton" @click="acceptGroupInvitation()">{{ i18n.ts.accept }}</button> | <button class="_textButton" @click="rejectGroupInvitation()">{{ i18n.ts.reject }}</button></div></span>
<span v-if="notification.type === 'follow'" class="text" style="opacity: 0.6;">{{ i18n.ts.youGotNewFollower }}<div v-if="full"><MkFollowButton :user="notification.user" :full="true"/></div></span> <span v-else-if="notification.type === 'app'" class="text">
<span v-if="notification.type === 'followRequestAccepted'" class="text" style="opacity: 0.6;">{{ i18n.ts.followRequestAccepted }}</span> <Mfm :text="notification.body" :nowrap="!full"/>
<span v-if="notification.type === 'receiveFollowRequest'" class="text" style="opacity: 0.6;">{{ i18n.ts.receiveFollowRequest }}<div v-if="full && !followRequestDone"><button class="_textButton" @click="acceptFollowRequest()">{{ i18n.ts.accept }}</button> | <button class="_textButton" @click="rejectFollowRequest()">{{ i18n.ts.reject }}</button></div></span> </span>
<span v-if="notification.type === 'groupInvited'" class="text" style="opacity: 0.6;">{{ i18n.ts.groupInvited }}: <b>{{ notification.invitation.group.name }}</b><div v-if="full && !groupInviteDone"><button class="_textButton" @click="acceptGroupInvitation()">{{ i18n.ts.accept }}</button> | <button class="_textButton" @click="rejectGroupInvitation()">{{ i18n.ts.reject }}</button></div></span> </div>
<span v-if="notification.type === 'app'" class="text">
<Mfm :text="notification.body" :nowrap="!full"/>
</span>
</div> </div>
</div> </div>
</template> </template>
@ -75,7 +71,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import { ref, shallowRef, onMounted, onUnmounted, watch } from 'vue'; import { ref, shallowRef, onMounted, onUnmounted, watch } from 'vue';
import * as misskey from 'misskey-js'; import * as misskey from 'misskey-js';
import XReactionIcon from '@/components/MkReactionIcon.vue'; import MkReactionIcon from '@/components/MkReactionIcon.vue';
import MkFollowButton from '@/components/MkFollowButton.vue'; import MkFollowButton from '@/components/MkFollowButton.vue';
import XReactionTooltip from '@/components/MkReactionTooltip.vue'; import XReactionTooltip from '@/components/MkReactionTooltip.vue';
import { getNoteSummary } from '@/scripts/get-note-summary'; import { getNoteSummary } from '@/scripts/get-note-summary';
@ -239,12 +235,6 @@ useTooltip(reactionRef, (showing) => {
pointer-events: none; pointer-events: none;
} }
&.pollVote {
padding: 3px;
background: #88a6b7;
pointer-events: none;
}
&.pollEnded { &.pollEnded {
padding: 3px; padding: 3px;
background: #88a6b7; background: #88a6b7;
@ -275,23 +265,25 @@ useTooltip(reactionRef, (showing) => {
} }
} }
> .text { > .content {
white-space: nowrap; > .text {
overflow: hidden; white-space: nowrap;
text-overflow: ellipsis; overflow: hidden;
text-overflow: ellipsis;
> i { > i {
vertical-align: super; vertical-align: super;
font-size: 50%; font-size: 50%;
opacity: 0.5; opacity: 0.5;
} }
> i:first-child { > i:first-child {
margin-right: 4px; margin-right: 4px;
} }
> i:last-child { > i:last-child {
margin-left: 4px; margin-left: 4px;
}
} }
} }
} }

View file

@ -1,18 +1,14 @@
<template> <template>
<div :class="$style.root" :style="{ zIndex, top: `${y - 64}px`, left: `${x - 64}px` }"> <div :class="$style.root" :style="{ zIndex, top: `${y - 64}px`, left: `${x - 64}px` }">
<span class="text" :class="{ up }"> <span class="text" :class="{ up }">+1</span>
<XReactionIcon class="icon" :reaction="reaction"/>
</span>
</div> </div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { onMounted } from 'vue'; import { onMounted } from 'vue';
import * as os from '@/os'; import * as os from '@/os';
import XReactionIcon from '@/components/MkReactionIcon.vue';
const props = withDefaults(defineProps<{ const props = withDefaults(defineProps<{
reaction: string;
x: number; x: number;
y: number; y: number;
}>(), { }>(), {
@ -23,8 +19,8 @@ const emit = defineEmits<{
}>(); }>();
let up = $ref(false); let up = $ref(false);
const zIndex = os.claimZIndex('veryLow'); const zIndex = os.claimZIndex('middle');
const angle = (90 - (Math.random() * 180)) + 'deg'; const angle = (45 - (Math.random() * 90)) + 'deg';
onMounted(() => { onMounted(() => {
window.setTimeout(() => { window.setTimeout(() => {
@ -55,10 +51,11 @@ onMounted(() => {
right: 0; right: 0;
bottom: 0; bottom: 0;
margin: auto; margin: auto;
color: var(--accent); color: #fff;
text-shadow: 0 0 6px #000;
font-size: 18px; font-size: 18px;
font-weight: bold; font-weight: bold;
transform: translateY(-30px); transform: translateY(0px);
transition: transform 1s cubic-bezier(0,.5,0,1), opacity 1s cubic-bezier(.5,0,1,.5); transition: transform 1s cubic-bezier(0,.5,0,1), opacity 1s cubic-bezier(.5,0,1,.5);
will-change: opacity, transform; will-change: opacity, transform;

View file

@ -18,7 +18,7 @@
<button ref="visibilityButton" v-tooltip="i18n.ts.visibility" class="_button visibility" :disabled="channel != null" @click="setVisibility"> <button ref="visibilityButton" v-tooltip="i18n.ts.visibility" class="_button visibility" :disabled="channel != null" @click="setVisibility">
<span v-if="visibility === 'public'"><i class="ti ti-world"></i></span> <span v-if="visibility === 'public'"><i class="ti ti-world"></i></span>
<span v-if="visibility === 'home'"><i class="ti ti-home"></i></span> <span v-if="visibility === 'home'"><i class="ti ti-home"></i></span>
<span v-if="visibility === 'followers'"><i class="ti ti-lock-open"></i></span> <span v-if="visibility === 'followers'"><i class="ti ti-lock"></i></span>
<span v-if="visibility === 'specified'"><i class="ti ti-mail"></i></span> <span v-if="visibility === 'specified'"><i class="ti ti-mail"></i></span>
</button> </button>
<button v-tooltip="i18n.ts.previewNoteText" class="_button preview" :class="{ active: showPreview }" @click="showPreview = !showPreview"><i class="ti ti-eye"></i></button> <button v-tooltip="i18n.ts.previewNoteText" class="_button preview" :class="{ active: showPreview }" @click="showPreview = !showPreview"><i class="ti ti-eye"></i></button>

View file

@ -0,0 +1,72 @@
<template>
<div :class="$style.root" :style="{ zIndex, top: `${y - 64}px`, left: `${x - 64}px` }">
<span class="text" :class="{ up }">
<MkReactionIcon class="icon" :reaction="reaction"/>
</span>
</div>
</template>
<script lang="ts" setup>
import { onMounted } from 'vue';
import * as os from '@/os';
import MkReactionIcon from '@/components/MkReactionIcon.vue';
const props = withDefaults(defineProps<{
reaction: string;
x: number;
y: number;
}>(), {
});
const emit = defineEmits<{
(ev: 'end'): void;
}>();
let up = $ref(false);
const zIndex = os.claimZIndex('middle');
const angle = (90 - (Math.random() * 180)) + 'deg';
onMounted(() => {
window.setTimeout(() => {
up = true;
}, 10);
window.setTimeout(() => {
emit('end');
}, 1100);
});
</script>
<style lang="scss" module>
.root {
pointer-events: none;
position: fixed;
width: 128px;
height: 128px;
&:global {
> .text {
display: block;
height: 1em;
text-align: center;
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
margin: auto;
color: var(--accent);
font-size: 18px;
font-weight: bold;
transform: translateY(-30px);
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) rotateZ(v-bind(angle));
}
}
}
}
</style>

View file

@ -1,7 +1,7 @@
<template> <template>
<MkTooltip ref="tooltip" :showing="showing" :target-element="targetElement" :max-width="340" @closed="emit('closed')"> <MkTooltip ref="tooltip" :showing="showing" :target-element="targetElement" :max-width="340" @closed="emit('closed')">
<div class="beeadbfb"> <div class="beeadbfb">
<XReactionIcon :reaction="reaction" class="icon" :no-style="true"/> <MkReactionIcon :reaction="reaction" class="icon" :no-style="true"/>
<div class="name">{{ reaction.replace('@.', '') }}</div> <div class="name">{{ reaction.replace('@.', '') }}</div>
</div> </div>
</MkTooltip> </MkTooltip>
@ -10,7 +10,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import { } from 'vue'; import { } from 'vue';
import MkTooltip from './MkTooltip.vue'; import MkTooltip from './MkTooltip.vue';
import XReactionIcon from '@/components/MkReactionIcon.vue'; import MkReactionIcon from '@/components/MkReactionIcon.vue';
defineProps<{ defineProps<{
showing: boolean; showing: boolean;

View file

@ -2,7 +2,7 @@
<MkTooltip ref="tooltip" :showing="showing" :target-element="targetElement" :max-width="340" @closed="emit('closed')"> <MkTooltip ref="tooltip" :showing="showing" :target-element="targetElement" :max-width="340" @closed="emit('closed')">
<div class="bqxuuuey"> <div class="bqxuuuey">
<div class="reaction"> <div class="reaction">
<XReactionIcon :reaction="reaction" class="icon" :no-style="true"/> <MkReactionIcon :reaction="reaction" class="icon" :no-style="true"/>
<div class="name">{{ getReactionName(reaction) }}</div> <div class="name">{{ getReactionName(reaction) }}</div>
</div> </div>
<div class="users"> <div class="users">
@ -19,7 +19,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import { } from 'vue'; import { } from 'vue';
import MkTooltip from './MkTooltip.vue'; import MkTooltip from './MkTooltip.vue';
import XReactionIcon from '@/components/MkReactionIcon.vue'; import MkReactionIcon from '@/components/MkReactionIcon.vue';
import { getEmojiName } from '@/scripts/emojilist'; import { getEmojiName } from '@/scripts/emojilist';
defineProps<{ defineProps<{

View file

@ -1,12 +1,12 @@
<template> <template>
<button <button
ref="buttonRef" ref="buttonEl"
v-ripple="canToggle" v-ripple="canToggle"
class="hkzvhatu _button" class="hkzvhatu _button"
:class="{ reacted: note.myReaction == reaction, canToggle }" :class="{ reacted: note.myReaction == reaction, canToggle }"
@click="toggleReaction()" @click="toggleReaction()"
> >
<XReactionIcon class="icon" :reaction="reaction"/> <MkReactionIcon class="icon" :reaction="reaction"/>
<span class="count">{{ count }}</span> <span class="count">{{ count }}</span>
</button> </button>
</template> </template>
@ -15,11 +15,11 @@
import { computed, onMounted, ref, shallowRef, watch } from 'vue'; import { computed, onMounted, ref, shallowRef, watch } from 'vue';
import * as misskey from 'misskey-js'; import * as misskey from 'misskey-js';
import XDetails from '@/components/MkReactionsViewer.details.vue'; import XDetails from '@/components/MkReactionsViewer.details.vue';
import XReactionIcon from '@/components/MkReactionIcon.vue'; import MkReactionIcon from '@/components/MkReactionIcon.vue';
import * as os from '@/os'; import * as os from '@/os';
import { useTooltip } from '@/scripts/use-tooltip'; import { useTooltip } from '@/scripts/use-tooltip';
import { $i } from '@/account'; import { $i } from '@/account';
import MkPlusOneEffect from '@/components/MkPlusOneEffect.vue'; import MkReactionEffect from '@/components/MkReactionEffect.vue';
const props = defineProps<{ const props = defineProps<{
reaction: string; reaction: string;
@ -28,7 +28,7 @@ const props = defineProps<{
note: misskey.entities.Note; note: misskey.entities.Note;
}>(); }>();
const buttonRef = shallowRef<HTMLElement>(); const buttonEl = shallowRef<HTMLElement>();
const canToggle = computed(() => !props.reaction.match(/@\w/) && $i); const canToggle = computed(() => !props.reaction.match(/@\w/) && $i);
@ -58,10 +58,10 @@ const toggleReaction = () => {
const anime = () => { const anime = () => {
if (document.hidden) return; if (document.hidden) return;
const rect = buttonRef.value.getBoundingClientRect(); const rect = buttonEl.value.getBoundingClientRect();
const x = rect.left + (buttonRef.value.offsetWidth / 2); const x = rect.left + 16;
const y = rect.top + (buttonRef.value.offsetHeight / 2); const y = rect.top + (buttonEl.value.offsetHeight / 2);
os.popup(MkPlusOneEffect, { reaction: props.reaction, x, y }, {}, 'end'); os.popup(MkReactionEffect, { reaction: props.reaction, x, y }, {}, 'end');
}; };
watch(() => props.count, (newCount, oldCount) => { watch(() => props.count, (newCount, oldCount) => {
@ -72,7 +72,7 @@ onMounted(() => {
if (!props.isInitial) anime(); if (!props.isInitial) anime();
}); });
useTooltip(buttonRef, async (showing) => { useTooltip(buttonEl, async (showing) => {
const reactions = await os.apiGet('notes/reactions', { const reactions = await os.apiGet('notes/reactions', {
noteId: props.note.id, noteId: props.note.id,
type: props.reaction, type: props.reaction,
@ -87,7 +87,7 @@ useTooltip(buttonRef, async (showing) => {
reaction: props.reaction, reaction: props.reaction,
users, users,
count: props.count, count: props.count,
targetElement: buttonRef.value, targetElement: buttonEl.value,
}, {}, 'closed'); }, {}, 'closed');
}, 100); }, 100);
</script> </script>

View file

@ -1,99 +0,0 @@
<template>
<button
v-if="canRenote"
ref="buttonRef"
class="eddddedb _button canRenote"
@click="renote()"
>
<i class="ti ti-repeat"></i>
<p v-if="count > 0" class="count">{{ count }}</p>
</button>
<button v-else class="eddddedb _button">
<i class="ti ti-ban"></i>
</button>
</template>
<script lang="ts" setup>
import { computed, ref, shallowRef } from 'vue';
import * as misskey from 'misskey-js';
import XDetails from '@/components/MkUsersTooltip.vue';
import { pleaseLogin } from '@/scripts/please-login';
import * as os from '@/os';
import { $i } from '@/account';
import { useTooltip } from '@/scripts/use-tooltip';
import { i18n } from '@/i18n';
const props = defineProps<{
note: misskey.entities.Note;
count: number;
}>();
const buttonRef = shallowRef<HTMLElement>();
const canRenote = computed(() => ['public', 'home'].includes(props.note.visibility) || props.note.userId === $i.id);
useTooltip(buttonRef, async (showing) => {
const renotes = await os.api('notes/renotes', {
noteId: props.note.id,
limit: 11,
});
const users = renotes.map(x => x.user);
if (users.length < 1) return;
os.popup(XDetails, {
showing,
users,
count: props.count,
targetElement: buttonRef.value,
}, {}, 'closed');
});
const renote = (viaKeyboard = false) => {
pleaseLogin();
os.popupMenu([{
text: i18n.ts.renote,
icon: 'ti ti-repeat',
action: () => {
os.api('notes/create', {
renoteId: props.note.id,
});
},
}, {
text: i18n.ts.quote,
icon: 'ti ti-quote',
action: () => {
os.post({
renote: props.note,
});
},
}], buttonRef.value, {
viaKeyboard,
});
};
</script>
<style lang="scss" scoped>
.eddddedb {
display: inline-block;
height: 32px;
margin: 2px;
padding: 0 6px;
border-radius: 4px;
&:not(.canRenote) {
cursor: default;
}
&.renoted {
background: var(--accent);
}
> .count {
display: inline;
margin-left: 8px;
opacity: 0.7;
}
}
</style>

View file

@ -1,7 +1,7 @@
<template> <template>
<div class="vblkjoeq"> <div class="vblkjoeq">
<div class="label" @click="focus"><slot name="label"></slot></div> <div class="label" @click="focus"><slot name="label"></slot></div>
<div ref="container" class="input" :class="{ inline, disabled, focused }" @click.prevent="onClick"> <div ref="container" class="input" :class="{ inline, disabled, focused }" @mousedown.prevent="show">
<div ref="prefixEl" class="prefix"><slot name="prefix"></slot></div> <div ref="prefixEl" class="prefix"><slot name="prefix"></slot></div>
<select <select
ref="inputEl" ref="inputEl"
@ -118,7 +118,7 @@ onMounted(() => {
}); });
}); });
const onClick = (ev: MouseEvent) => { function show(ev: MouseEvent) {
focused.value = true; focused.value = true;
opening.value = true; opening.value = true;
@ -166,7 +166,7 @@ const onClick = (ev: MouseEvent) => {
}).then(() => { }).then(() => {
focused.value = false; focused.value = false;
}); });
}; }
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@ -285,7 +285,7 @@ const onClick = (ev: MouseEvent) => {
<style lang="scss" module> <style lang="scss" module>
.chevron { .chevron {
transition: transform 0.5s ease; transition: transform 0.1s ease-out;
} }
.chevronOpening { .chevronOpening {

View file

@ -14,7 +14,7 @@
<template #prefix><i class="ti ti-lock"></i></template> <template #prefix><i class="ti ti-lock"></i></template>
<template #caption><button class="_textButton" type="button" @click="resetPassword">{{ i18n.ts.forgotPassword }}</button></template> <template #caption><button class="_textButton" type="button" @click="resetPassword">{{ i18n.ts.forgotPassword }}</button></template>
</MkInput> </MkInput>
<MkButton type="submit" primary :disabled="signing" style="margin: 0 auto;">{{ signing ? i18n.ts.loggingIn : i18n.ts.login }}</MkButton> <MkButton type="submit" large primary rounded :disabled="signing" style="margin: 0 auto;">{{ signing ? i18n.ts.loggingIn : i18n.ts.login }}</MkButton>
</div> </div>
<div v-if="totpLogin" class="2fa-signin" :class="{ securityKeys: user && user.securityKeys }"> <div v-if="totpLogin" class="2fa-signin" :class="{ securityKeys: user && user.securityKeys }">
<div v-if="user && user.securityKeys" class="twofa-group tap-group"> <div v-if="user && user.securityKeys" class="twofa-group tap-group">
@ -36,7 +36,7 @@
<template #label>{{ i18n.ts.token }}</template> <template #label>{{ i18n.ts.token }}</template>
<template #prefix><i class="ti ti-123"></i></template> <template #prefix><i class="ti ti-123"></i></template>
</MkInput> </MkInput>
<MkButton type="submit" :disabled="signing" primary style="margin: 0 auto;">{{ signing ? i18n.ts.loggingIn : i18n.ts.login }}</MkButton> <MkButton type="submit" :disabled="signing" large primary rounded style="margin: 0 auto;">{{ signing ? i18n.ts.loggingIn : i18n.ts.login }}</MkButton>
</div> </div>
</div> </div>
</div> </div>

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