Merge branch 'develop'

This commit is contained in:
syuilo 2021-12-03 22:30:10 +09:00
commit 83a77f1064
165 changed files with 3765 additions and 2704 deletions

View File

@ -7,6 +7,30 @@
--> -->
## 12.98.0 (2021/12/03)
### Improvements
- API: /antennas/notes API で日付による絞り込みができるように
- クライアント: アンケートに投票する際に確認ダイアログを出すように
- クライアント: Renoteなート詳細ページから元のートページに遷移できるように
- クライアント: 画像ポップアップでクリックで閉じられるように
- クライアント: デザインの調整
- フォロワーを解除できる機能
### Bugfixes
- クライアント: LTLやGTLが無効になっている場合でもUI上にタブが表示される問題を修正
- クライアント: ログインにおいてパスワードが誤っている際のエラーメッセージが正しく表示されない問題を修正
- クライアント: リアクションツールチップ、Renoteツールチップのユーザーの並び順を修正
- クライアント: サウンドのマスターボリュームが正しく保存されない問題を修正
- クライアント: 一部環境において通知が表示されると操作不能になる問題を修正
- クライアント: モバイルでタップしたときにツールチップが表示される問題を修正
- クライアント: リモートインスタンスのノートに返信するとき、対象のノートにそのリモートインスタンス内のユーザーへのメンションが含まれていると、返信テキスト内にローカルユーザーへのメンションとして引き継がれてしまう場合がある問題を修正
- クライアント: 画像ビューワーで全体表示した時に上側の一部しか表示されない画像がある問題を修正
- API: ユーザーを取得時に条件によっては内部エラーになる問題を修正
### Changes
- クライアント: ノートにモデレーターバッジを表示するのを廃止
## 12.97.0 (2021/11/19) ## 12.97.0 (2021/11/19)
### Improvements ### Improvements

129
README.md
View File

@ -1,116 +1,57 @@
[![Misskey](/assets/about/banner.svg)](https://join.misskey.page/) [![Misskey](https://github.com/misskey-dev/assets/blob/main/banner.png?raw=true)](https://join.misskey.page/)
<h1 align="center">Misskey</h1>
<div align="center"> <div align="center">
[![Dependencies](https://img.shields.io/david/misskey-dev/misskey.svg?style=for-the-badge&logo=npm)](https://david-dm.org/misskey-dev/misskey) **🌎 A forever evolving, interplanetary microblogging platform. 🚀**
[![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=for-the-badge&logo=github)](http://makeapullrequest.com)
[![Awesome Humane Tech](https://raw.githubusercontent.com/humanetech-community/awesome-humane-tech/main/humane-tech-badge.svg?sanitize=true)](https://github.com/humanetech-community/awesome-humane-tech)
**A forever evolving, interplanetary microblogging platform.** **Misskey** is a distributed microblogging platform with advanced features such as Reactions and a highly customizable UI.
<a href="https://join.misskey.page/">Misskey</a> is a decentralized microblogging platform born on Earth. [Learn more](https://misskey-hub.net/)
Since it exists within the Fediverse (a universe where various social media platforms are organized),
it is mutually linked with other social media platforms. ---
Why don't you take a short break from the hustle and bustle of the city, and dive into a new Internet? <a href="https://join.misskey.page/">Find an instance!</a>
[✨ Find an instance](https://misskey-hub.net/instances.html)
[📦 Create your own instance](https://misskey-hub.net/docs/install.html)
[🛠️ Contribute](./CONTRIBUTING.md)
[🚀 Join the community](https://discord.gg/Wp8gVStHW3)
---
<a href="https://www.patreon.com/syuilo"><img src="https://c5.patreon.com/external/logo/become_a_patron_button@2x.png" alt="Become a Patron!" width="160" /></a> <a href="https://www.patreon.com/syuilo"><img src="https://c5.patreon.com/external/logo/become_a_patron_button@2x.png" alt="Become a Patron!" width="160" /></a>
</div> </div>
--- <div>
Do you have a question? Or are you experiencing trouble? <a href="https://xn--931a.moe/"><img src="https://github.com/misskey-dev/misskey/blob/develop/assets/ai.png?raw=true" align="right" height="320px"/></a>
Visit [our forum](https://forum.misskey.io/)!
--- ## ✨ Features
- **ActivityPub support**\
It is possible to interact with other software.
- **Reactions**\
You can add "reactions" to each post, making it easy for you to express your feelings.
- **Drive**\
An interface to manage uploaded files such as images, videos, sounds, etc.
You can also organize your favorite content into folders, making it easy to share again.
- **Rich Web UI**\
Misskey has a rich WebUI by default.
It is highly customizable by flexibly changing the layout and installing various widgets and themes.
Furthermore, plug-ins can be created using AiScript, a original programming language.
- and more...
![](https://ja.mstdn.wiki/images/e/ed/Deck.jpg) </div>
:sparkles: Features <div style="clear: both;"></div>
----------------------------------------------------------------
<a href="https://xn--931a.moe/"><img src="https://github.com/misskey-dev/misskey/blob/develop/assets/ai-orig.png?raw=true" align="right" height="320px"/></a>
<h3>Posting</h3> ## Sponsors
<p>
Post your ideas, discussion topics, fun moments, or anything else you want to share! Misskey supports text, emoji, pictures, videos, and polls!
</p>
---
<h3 >Reactions</h3>
<p>
Reactions are the simplest way to respond to others' posts. Simply pick a reaction emote from the list! Reactions on Misskey are much more expressive than other social media services which only allow “liking”.
</p>
---
<h3>Interface</h3>
<p>
Customize the UI to your own tastes! No UI will work for everyone, so Misskey is completely customizable. Make Misskey *yours* by editing the style, adjusting timeline layouts, and placing widgets.
</p>
---
<h3>Misskey Drive</h3>
<p>
Organize and store your files! Want to post a picture you have already uploaded? Wish you could organize your files into folders? Misskey Drive is a solution!
</p>
---
...and more!
:package: Create your own instance
----------------------------------------------------------------
Please see the [Setup and Installation Guide](https://misskey-hub.net/docs/install/install.html).
:wrench: Contribution
----------------------------------------------------------------
Please see the [Contribution Guide](./CONTRIBUTING.md).
### Collaborators
<table>
<tr>
<td><img src="https://avatars3.githubusercontent.com/u/4439005?s=460&v=4" alt="syuilo" width="100"></td>
<td><img src="https://avatars0.githubusercontent.com/u/10798641?s=460&v=4" alt="AyaMorisawa" width="100"></td>
<td><img src="https://avatars1.githubusercontent.com/u/30769358?s=460&v=4" alt="mei23" width="100"></td>
<td><img src="https://avatars2.githubusercontent.com/u/20679825?s=460&v=4" alt="acid-chicken" width="100"></td>
<td><img src="https://avatars2.githubusercontent.com/u/6533808?s=460&v=4" alt="rinsuki" width="100"></td>
<td><img src="https://avatars0.githubusercontent.com/u/7973572?s=460&v=4" alt="tamaina" width="100"></td>
<td><img src="https://avatars1.githubusercontent.com/u/7106976?s=460&v=4" alt="Xeltica" width="100"></td>
<td><img src="https://avatars1.githubusercontent.com/u/17376330?s=460&v=4" alt="u1-liquid" width="100"></td>
</tr>
<tr>
<td align="center"><a href="https://github.com/syuilo">@syuilo</a></td>
<td align="center"><a href="https://github.com/AyaMorisawa">@AyaMorisawa</a></td>
<td align="center"><a href="https://github.com/mei23">@mei23</a></td>
<td align="center"><a href="https://github.com/acid-chicken">@acid-chicken</a></td>
<td align="center"><a href="https://github.com/rinsuki">@rinsuki</a></td>
<td align="center"><a href="https://github.com/tamaina">@tamaina</a></td>
<td align="center"><a href="https://github.com/Xeltica">@Xeltica</a></td>
<td align="center"><a href="https://github.com/u1-liquid">@u1-liquid</a></td>
</tr>
</table>
---
To receive updates of this repo, follow [@repo@misskey.io](https://misskey.io/@repo) on fediverse.
Related projects
----------------------------------------------------------------
- [misskey.js](https://github.com/misskey-dev/misskey.js) - Misskey SDK for JavaScript
- [mfm.js](https://github.com/misskey-dev/mfm.js) - MFM parser
Sponsors
----------------------------------------------------------------
<div align="center"> <div align="center">
<a class="rss3" title="RSS3" href="https://rss3.io/" target="_blank" style="display: inline-block;"><img src="https://rss3.io/assets/images/Logo.svg" alt="RSS3" style="display: inline-block; height: 60px;"></a> <a class="rss3" title="RSS3" href="https://rss3.io/" target="_blank" style="display: inline-block;"><img src="https://rss3.io/assets/images/Logo.svg" alt="RSS3" style="display: inline-block; height: 60px;"></a>
</div> </div>
:heart: Backers ## Backers
----------------------------------------------------------------
<!-- PATREON_START --> <!-- PATREON_START -->
<table><tr> <table><tr>
<td><img src="https://c8.patreon.com/2/200/20832595" alt="Roujo " width="100"></td> <td><img src="https://c8.patreon.com/2/200/20832595" alt="Roujo " width="100"></td>

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 38 KiB

View File

@ -734,7 +734,10 @@ translate: "ترجم"
translatedFrom: "تُرجم من {x}" translatedFrom: "تُرجم من {x}"
accountDeletionInProgress: "حذف الحساب جارٍ" accountDeletionInProgress: "حذف الحساب جارٍ"
usernameInfo: "الاسم الذي يميزك عن بافي مستخدمي هذا الخادم، يمكنك استخدام الحروف اللاتينية (a~z, A~Z) والأرقام (0~9) والشرطة السفلية (_). لا يمكنك تغييره بعد تسجيله." usernameInfo: "الاسم الذي يميزك عن بافي مستخدمي هذا الخادم، يمكنك استخدام الحروف اللاتينية (a~z, A~Z) والأرقام (0~9) والشرطة السفلية (_). لا يمكنك تغييره بعد تسجيله."
keepCw: "أبقِ على تحذيرات المحتوى"
lastCommunication: "آخر تواصل" lastCommunication: "آخر تواصل"
resolved: "عولج"
unresolved: "لم يعالج"
itsOn: "مفعّل" itsOn: "مفعّل"
itsOff: "معطّل" itsOff: "معطّل"
emailRequiredForSignup: "عنوان البريد الإلكتروني إلزامي للتسجيل" emailRequiredForSignup: "عنوان البريد الإلكتروني إلزامي للتسجيل"
@ -747,6 +750,16 @@ makeReactionsPublicDescription: "هذا سيجعل قائمة تفاعلاتك
classic: "تقليدي" classic: "تقليدي"
muteThread: "اكتم النقاش" muteThread: "اكتم النقاش"
unmuteThread: "ارفع الكتم عن النقاش" unmuteThread: "ارفع الكتم عن النقاش"
deleteAccountConfirm: "سيحذف حسابك نهائيًا، أتريد المتابعة؟"
incorrectPassword: "كلمة السر خاطئة."
_emailUnavailable:
used: "هذا البريد الإلكتروني مستخدم"
format: "صيغة البريد الإلكتروني غير صالحة"
mx: "خادم البريد الإلكتروني غير صالح"
smtp: "خادم البريد الإلكتروتي لا يستجيب"
_ffVisibility:
public: "علني"
private: "خاص"
_signup: _signup:
almostThere: "كدت تنتهي" almostThere: "كدت تنتهي"
emailAddressInfo: "رجاءً أدخل بريدك الإلكتروني." emailAddressInfo: "رجاءً أدخل بريدك الإلكتروني."
@ -829,6 +842,7 @@ _mfm:
font: "الخط" font: "الخط"
rainbow: "قوس قزح" rainbow: "قوس قزح"
rainbowDescription: "اجعل المحتوى يظهر بألوان الطيف" rainbowDescription: "اجعل المحتوى يظهر بألوان الطيف"
rotate: "تدوير"
_reversi: _reversi:
gameSettings: "إعدادات اللعبة" gameSettings: "إعدادات اللعبة"
chooseBoard: "اختر اللوح" chooseBoard: "اختر اللوح"
@ -980,9 +994,13 @@ _tutorial:
step7_2: "إذا أردت معرفة المزيد عن ميسكي زر {help}." step7_2: "إذا أردت معرفة المزيد عن ميسكي زر {help}."
step7_3: "حظًا سعيدًا واستمتع بوقتك مع ميسكي! 🚀" step7_3: "حظًا سعيدًا واستمتع بوقتك مع ميسكي! 🚀"
_2fa: _2fa:
alreadyRegistered: "سجلت سلفًا جهازًا للاستيثاق بعاملين."
registerDevice: "سجّل جهازًا جديدًا" registerDevice: "سجّل جهازًا جديدًا"
registerKey: "تسجيل مفتاح أمان جديد" registerKey: "تسجيل مفتاح أمان جديد"
step1: "أولًا ثبّت تطبيق استيثاق على جهازك (مثل {a} و{b})." step1: "أولًا ثبّت تطبيق استيثاق على جهازك (مثل {a} و{b})."
step2: "امسح رمز الاستجابة السريعة الموجد على الشاشة."
step3: "أدخل الرمز الموجود في تطبيقك لإكمال التثبيت."
step4: "من هذه اللحظة أثناء ولوجك سيُطلب منك الرمز."
_permissions: _permissions:
"read:account": "اعرض معلومات حسابك" "read:account": "اعرض معلومات حسابك"
"write:account": "تعديل معلومات حسابك" "write:account": "تعديل معلومات حسابك"
@ -993,6 +1011,7 @@ _permissions:
"read:favorites": "اعرض المفضلة" "read:favorites": "اعرض المفضلة"
"write:favorites": "عدّل المفضلة" "write:favorites": "عدّل المفضلة"
"read:following": "اعرض معلومات متابَعيك" "read:following": "اعرض معلومات متابَعيك"
"write:following": "تابع أو ألغ متابعة حسابات"
"read:messaging": "اعرض المحادثات" "read:messaging": "اعرض المحادثات"
"write:messaging": "اكتب أو احذف رسائل محادثة" "write:messaging": "اكتب أو احذف رسائل محادثة"
"read:mutes": "اعرض قائمة المستخدمين المكتومين" "read:mutes": "اعرض قائمة المستخدمين المكتومين"
@ -1005,11 +1024,14 @@ _permissions:
"write:votes": "صوّت" "write:votes": "صوّت"
"read:pages": "اعرض صفحاتك" "read:pages": "اعرض صفحاتك"
"write:pages": "عدّل أو احذف صفحاتك" "write:pages": "عدّل أو احذف صفحاتك"
"read:page-likes": "يعرض ما أعجبك من ملاحظات في صفحات"
"read:user-groups": "اعرض فِرق المستخدمين" "read:user-groups": "اعرض فِرق المستخدمين"
"write:user-groups": "عدّل أو احذف فِرق المستخدمين" "write:user-groups": "عدّل أو احذف فِرق المستخدمين"
"read:channels": "طالع قنواتك"
"write:channels": "عدّل القنوات" "write:channels": "عدّل القنوات"
"read:gallery": "اعرض المعرض" "read:gallery": "اعرض المعرض"
"write:gallery": "عدّل المعرض" "write:gallery": "عدّل المعرض"
"read:gallery-likes": "يعرض ما أعجبك من مشاركات المعرض"
_auth: _auth:
shareAccess: "أتريد التفويض لـ \"{name}\" بالوصول لحسابك؟" shareAccess: "أتريد التفويض لـ \"{name}\" بالوصول لحسابك؟"
shareAccessAsk: "هل تخول لهذا التطبيق الوصول لحسابك؟" shareAccessAsk: "هل تخول لهذا التطبيق الوصول لحسابك؟"
@ -1173,6 +1195,7 @@ _rooms:
tv: "تلفاز" tv: "تلفاز"
pinguin: "بطريق" pinguin: "بطريق"
sofa: "أريكة" sofa: "أريكة"
bin: "سلة مهملات"
banknote: "أوراق نقدية" banknote: "أوراق نقدية"
_pages: _pages:
newPage: "أنشئ صفحة جديدة" newPage: "أنشئ صفحة جديدة"
@ -1212,6 +1235,7 @@ _pages:
name: "اسم المتغير" name: "اسم المتغير"
text: "العنوان" text: "العنوان"
default: "القيمة الافتراضية" default: "القيمة الافتراضية"
textareaInput: "مدخل نصي متعدد الأسطر"
_textareaInput: _textareaInput:
name: "اسم المتغير" name: "اسم المتغير"
text: "العنوان" text: "العنوان"
@ -1227,6 +1251,7 @@ _pages:
note: "ملاحظة مضمّنة" note: "ملاحظة مضمّنة"
_note: _note:
id: "معرّف الملاحظة" id: "معرّف الملاحظة"
idDescription: "كبديل يمكنك إدخال رابك الملاحظة هنا"
detailed: "عرض مفصّل" detailed: "عرض مفصّل"
switch: "بدّل" switch: "بدّل"
_switch: _switch:

View File

@ -792,6 +792,7 @@ pubSub: "Pub/Sub Benutzerkonten"
lastCommunication: "Letzte Kommunikation" lastCommunication: "Letzte Kommunikation"
resolved: "Gelöst" resolved: "Gelöst"
unresolved: "Ungelöst" unresolved: "Ungelöst"
breakFollow: "Follower entfernen"
itsOn: "Eingeschaltet" itsOn: "Eingeschaltet"
itsOff: "Ausgeschaltet" itsOff: "Ausgeschaltet"
emailRequiredForSignup: "Angaben einer Email-Adresse als benötigt markieren" emailRequiredForSignup: "Angaben einer Email-Adresse als benötigt markieren"
@ -808,6 +809,8 @@ ffVisibility: "Sichtbarkeit von Gefolgten/Followern"
ffVisibilityDescription: "Konfiguriere wer sehen kann, wem du folgst sowie wer dir folgt." ffVisibilityDescription: "Konfiguriere wer sehen kann, wem du folgst sowie wer dir folgt."
continueThread: "Weiteren Threadverlauf anzeigen" continueThread: "Weiteren Threadverlauf anzeigen"
deleteAccountConfirm: "Dein Benutzerkonto wird unwiderruflich gelöscht. Trotzdem fortfahren?" deleteAccountConfirm: "Dein Benutzerkonto wird unwiderruflich gelöscht. Trotzdem fortfahren?"
incorrectPassword: "Falsches Passwort."
voteConfirm: "Wirklich für \"{choice}\" abstimmen?"
_emailUnavailable: _emailUnavailable:
used: "Diese Email-Adresse wird bereits verwendet" used: "Diese Email-Adresse wird bereits verwendet"
format: "Das Format dieser Email-Adresse ist ungültig" format: "Das Format dieser Email-Adresse ist ungültig"
@ -931,6 +934,8 @@ _mfm:
rainbowDescription: "Lässt den Inhalt in Regenbogenfarben erscheinen." rainbowDescription: "Lässt den Inhalt in Regenbogenfarben erscheinen."
sparkle: "Glitzer" sparkle: "Glitzer"
sparkleDescription: "Verleiht Inhalt einen glitzernden Partikeleffekt." sparkleDescription: "Verleiht Inhalt einen glitzernden Partikeleffekt."
rotate: "Drehen"
rotateDescription: "Dreht den Inhalt um einen angegebenen Winkel"
_reversi: _reversi:
reversi: "Reversi" reversi: "Reversi"
gameSettings: "Spieleinstellungen" gameSettings: "Spieleinstellungen"

View File

@ -808,6 +808,8 @@ ffVisibility: "Follows/Followers Visibility"
ffVisibilityDescription: "Allows you to configure who can see who you follow and who follows you." ffVisibilityDescription: "Allows you to configure who can see who you follow and who follows you."
continueThread: "View thread continuation" continueThread: "View thread continuation"
deleteAccountConfirm: "This will irreversibly delete your account. Proceed?" deleteAccountConfirm: "This will irreversibly delete your account. Proceed?"
incorrectPassword: "Incorrect password."
voteConfirm: "Confirm your vote for \"{choice}\"?"
_emailUnavailable: _emailUnavailable:
used: "This email address is already being used" used: "This email address is already being used"
format: "The format of this email address is invalid" format: "The format of this email address is invalid"
@ -931,6 +933,8 @@ _mfm:
rainbowDescription: "Makes the content appear in rainbow colors." rainbowDescription: "Makes the content appear in rainbow colors."
sparkle: "Sparkle" sparkle: "Sparkle"
sparkleDescription: "Gives content a sparkling particle effect." sparkleDescription: "Gives content a sparkling particle effect."
rotate: "Rotate"
rotateDescription: "Turns content by a specified angle."
_reversi: _reversi:
reversi: "Reversi" reversi: "Reversi"
gameSettings: "Game settings" gameSettings: "Game settings"

View File

@ -2,18 +2,18 @@
_lang_: "Esperanto" _lang_: "Esperanto"
headlineMisskey: "Jen la reto konektata de notoj" headlineMisskey: "Jen la reto konektata de notoj"
introMisskey: "Bonvenon! Misskey estas malfermitkoda malcentraliza etbloga servo.\nKreu \"noto\"n por paroli vian penson al iuj ĉirkaŭ vi. 📡\nLa funkcion \"reago\" ebligas esprimi rapide vian senton pri ies noto en Fediverso. 👍\nBonvole esploru novan mondon. 🚀" introMisskey: "Bonvenon! Misskey estas malfermitkoda malcentraliza etbloga servo.\nKreu \"noto\"n por paroli vian penson al iuj ĉirkaŭ vi. 📡\nLa funkcion \"reago\" ebligas esprimi rapide vian senton pri ies noto en Fediverso. 👍\nBonvole esploru novan mondon. 🚀"
monthAndDay: "La {day}a de la {month}a" monthAndDay: "la {day}a de la {month}a"
search: "Serĉi" search: "Serĉi"
notifications: "Sciigoj" notifications: "Sciigoj"
username: "Uzantnomo" username: "Uzantnomo"
password: "Pasvorto" password: "Pasvorto"
forgotPassword: "Ĉu vi forgesis pasvorton?" forgotPassword: "Ĉu vi forgesis pasvorton?"
fetchingAsApObject: "Informpetado de kunfederaĵo…" fetchingAsApObject: "Informpetado de la Fediverso…"
ok: "Akcepteble" ok: "OK"
gotIt: "Mi komprenas" gotIt: "Kompreni"
cancel: "Nuligi" cancel: "Nuligi"
enterUsername: "Entajpu uzantnomon" enterUsername: "Entajpu uzantnomon"
renotedBy: "Noto plusendita de {user}" renotedBy: "Plusendita de {user}"
noNotes: "Neniu noto!" noNotes: "Neniu noto!"
noNotifications: "Vi ne havas sciigojn." noNotifications: "Vi ne havas sciigojn."
instance: "Nodo" instance: "Nodo"
@ -35,22 +35,22 @@ addUser: "Aldoni uzanton"
favorite: "Preferi" favorite: "Preferi"
favorites: "Preferaĵoj" favorites: "Preferaĵoj"
unfavorite: "Malpreferi" unfavorite: "Malpreferi"
favorited: "Aldonita al via listo de preferaĵoj." favorited: "Aldonita al viaj preferaĵoj."
alreadyFavorited: "Jam aldonita al via listo de preferaĵoj." alreadyFavorited: "Jam aldonita al viaj preferaĵoj."
cantFavorite: "Ĝi ne povis esti aldonita al via listo de preferaĵoj." cantFavorite: "Oni ne povis aldoni al viaj preferaĵoj."
pin: "Alpingli" pin: "Alpingli"
unpin: "Depingli" unpin: "Depingli"
copyContent: "Kopii enhavon" copyContent: "Kopii enhavon"
copyLink: "Kopii ligilon" copyLink: "Kopii ligilon"
delete: "Forviŝi" delete: "Forviŝi"
deleteAndEdit: "Forviŝi kaj redakti" deleteAndEdit: "Forviŝi kaj redakti"
deleteAndEditConfirm: "Ĉu vi certas ke vi volas redakti forviŝinte la noton? Tio forviŝos ankaŭ ĉiujn reagojn, plusendojn, kaj respondojn apartenantajn al ĝi." deleteAndEditConfirm: "Ĉu vi certas ke vi volas redakti foriginte la noton? Tio forviŝos reagojn, plusendojn, kaj respondojn ĉiujn apartenantajn al ĝi."
addToList: "Aldoni al listo" addToList: "Aldoni al listo"
sendMessage: "Sendi mesaĝon" sendMessage: "Sendi mesaĝon"
copyUsername: "Kopii uzantnomon" copyUsername: "Kopii uzantnomon"
searchUser: "Serĉi uzanton" searchUser: "Serĉi uzanton"
reply: "Respondi" reply: "Respondi"
loadMore: "Vidu pli" loadMore: "Vidi pli"
showMore: "Vidi pli" showMore: "Vidi pli"
youGotNewFollower: "eksekvis vin" youGotNewFollower: "eksekvis vin"
receiveFollowRequest: "Peto de sekvado estas ricevita" receiveFollowRequest: "Peto de sekvado estas ricevita"
@ -77,10 +77,11 @@ manageLists: "Administri liston"
error: "Eraro" error: "Eraro"
somethingHappened: "Problemo okazis" somethingHappened: "Problemo okazis"
retry: "Provi denove" retry: "Provi denove"
serverIsDead: "La servilo ne respondas. Vole atendu iom kaj penu denove."
enterListName: "Entajpu nomon de la listo" enterListName: "Entajpu nomon de la listo"
privacy: "Privateco" privacy: "Privateco"
makeFollowManuallyApprove: "Eksekvi vin devas peti al vi" makeFollowManuallyApprove: "Eksekvi vin devas peti al vi"
defaultNoteVisibility: "Implicitaĵo de videbleco" defaultNoteVisibility: "Implicita videbleco de la noto"
follow: "Sekvi" follow: "Sekvi"
followRequest: "Peti de sekvado" followRequest: "Peti de sekvado"
followRequests: "Petoj de sekvado" followRequests: "Petoj de sekvado"
@ -88,10 +89,10 @@ unfollow: "Ne plu sekvi"
followRequestPending: "Atendado akcepti vian peton de eksekvado" followRequestPending: "Atendado akcepti vian peton de eksekvado"
enterEmoji: "Entajpu emoĵion" enterEmoji: "Entajpu emoĵion"
renote: "Plusendi la noton" renote: "Plusendi la noton"
unrenote: "Malfari plusendadon" unrenote: "Malfari plusendon"
renoted: "Sukcese plusendita" renoted: "Sukcese plusendita"
cantRenote: "Oni ne povas plusendi la noton." cantRenote: "Oni ne povas plusendi la noton."
cantReRenote: "Plusendo de noto ne estas plusendebla." cantReRenote: "Plusendo ne estas plusendebla."
quote: "Citi" quote: "Citi"
pinnedNote: "Alpinglita noto" pinnedNote: "Alpinglita noto"
pinned: "Alpingli" pinned: "Alpingli"
@ -101,7 +102,7 @@ sensitive: "Enhavo ne estas deca por laborejo (NSFW)"
add: "Aldoni" add: "Aldoni"
reaction: "Reagoj" reaction: "Reagoj"
reactionSettingDescription: "Agordi la reagojn kiujn vi volas prefere montrigi ĉe la elektilo de reagoj" reactionSettingDescription: "Agordi la reagojn kiujn vi volas prefere montrigi ĉe la elektilo de reagoj"
rememberNoteVisibility: "Rememori la agordon de videbleco de la noto laste sendita " rememberNoteVisibility: "Rememori la agordon de videbleco de la laste sendita"
attachCancel: "Deigi aldonaĵon" attachCancel: "Deigi aldonaĵon"
markAsSensitive: "Troviĝi NSFW" markAsSensitive: "Troviĝi NSFW"
unmarkAsSensitive: "Ne troviĝi NSFW" unmarkAsSensitive: "Ne troviĝi NSFW"
@ -121,16 +122,16 @@ selectAntenna: "Elekti antenon"
selectWidget: "Elekti enestraĵon" selectWidget: "Elekti enestraĵon"
editWidgets: "Redakti fenestraĵon" editWidgets: "Redakti fenestraĵon"
editWidgetsExit: "Fini la redaktadon" editWidgetsExit: "Fini la redaktadon"
customEmojis: "Personecigitaj emoĵioj"
emoji: "Emoĵio" emoji: "Emoĵio"
emojis: "Emoĵio" emojis: "Emoĵio"
emojiName: "Nomo de emoĵio" emojiName: "Nomo de la emoĵio"
emojiUrl: "URL de la emoĵio" emojiUrl: "URL de la emoĵio"
addEmoji: "Aldoni emoĵion" addEmoji: "Aldoni emoĵion"
settingGuide: "Agordaj rekomendoj" settingGuide: "Agordaj rekomendoj"
cacheRemoteFiles: "Stapli transajn dosierojn" cacheRemoteFiles: "Stapli forajn dosierojn"
flagAsBot: "Agordo por robota uzanto" flagAsBot: "Fari la flagon por robota uzanto"
flagAsCat: "Agi kat-iĝon" flagAsCat: "Fari la flagon por kat-iĝi"
autoAcceptFollowed: "Aŭtomate akcepti la peton de sekvado far uzantoj kiujn vi sekvas"
addAccount: "Aldoni konton" addAccount: "Aldoni konton"
showOnRemote: "Vidi ĉe la surloka nodo" showOnRemote: "Vidi ĉe la surloka nodo"
general: "Ĝenerala" general: "Ĝenerala"
@ -140,7 +141,7 @@ removeWallpaper: "Forviŝi ekranfonon. "
searchWith: "Serĉi: {q}" searchWith: "Serĉi: {q}"
youHaveNoLists: "Vi ne havas listojn." youHaveNoLists: "Vi ne havas listojn."
followConfirm: "Ĉu vi certas ke vi volas sekvi {name}'(o)n?" followConfirm: "Ĉu vi certas ke vi volas sekvi {name}'(o)n?"
host: "Gastigo" host: "Nodo"
selectUser: "Elekti uzanton" selectUser: "Elekti uzanton"
recipient: "Ricevonto" recipient: "Ricevonto"
annotation: "Komentarioj" annotation: "Komentarioj"
@ -164,8 +165,9 @@ disk: "Disko"
instanceInfo: "Informoj pri la nodo" instanceInfo: "Informoj pri la nodo"
statistics: "Statistikoj" statistics: "Statistikoj"
clearCachedFiles: "Malplenigi la staplon" clearCachedFiles: "Malplenigi la staplon"
clearCachedFilesConfirm: "Ĉu vi certas, ke vi volas forviŝi ĉiujn transajn dosierojn en la staplo?" clearCachedFilesConfirm: "Ĉu vi certas, ke vi volas forviŝi ĉiujn forajn dosierojn en la staplo?"
blockedInstances: "Blokitaj nodoj" blockedInstances: "Blokitaj nodoj"
muteAndBlock: "Silentigi / Bloki"
mutedUsers: "Silentigitaj uzantoj" mutedUsers: "Silentigitaj uzantoj"
blockedUsers: "Blokitaj uzantoj" blockedUsers: "Blokitaj uzantoj"
noUsers: "Neniu uzanto" noUsers: "Neniu uzanto"
@ -175,7 +177,7 @@ pinLimitExceeded: "Vi ne povas alpingli pli"
done: "Fini" done: "Fini"
processing: "Prilaborado…" processing: "Prilaborado…"
preview: "Antaŭmontro" preview: "Antaŭmontro"
default: "Defaŭlta" default: "Implicitaĵo"
noCustomEmojis: "Neniu emoĵio" noCustomEmojis: "Neniu emoĵio"
noJobs: "Neniu laboro" noJobs: "Neniu laboro"
federating: "Federantaj" federating: "Federantaj"
@ -195,7 +197,7 @@ currentPassword: "Aktuala pasvorto"
newPassword: "Nova pasvorto" newPassword: "Nova pasvorto"
newPasswordRetype: "Reentajpu la novan pasvorton" newPasswordRetype: "Reentajpu la novan pasvorton"
attachFile: "Aldoni dosieron" attachFile: "Aldoni dosieron"
more: "Plu!" more: "Pli!"
featured: "Maksimumi" featured: "Maksimumi"
usernameOrUserId: "Uzantnomo aŭ identigilo de uzanto" usernameOrUserId: "Uzantnomo aŭ identigilo de uzanto"
noSuchUser: "Neniuj uzantoj trovitaj" noSuchUser: "Neniuj uzantoj trovitaj"
@ -204,8 +206,8 @@ announcements: "Novaĵoj"
imageUrl: "URL de la bildo" imageUrl: "URL de la bildo"
remove: "Forigi" remove: "Forigi"
removed: "Forigita" removed: "Forigita"
removeAreYouSure: "Ĉu vi certas ke vi volas forigi \"{x}\"'(o)n?" removeAreYouSure: "Ĉu vi certas ke vi volas forigi \"{x}\"n?"
deleteAreYouSure: "Ĉu vi certas ke vi volas forviŝi \"{x}\"'(o)n?" deleteAreYouSure: "Ĉu vi certas ke vi volas forviŝi \"{x}\"'?"
resetAreYouSure: "Ĉu vi certas restarigi?" resetAreYouSure: "Ĉu vi certas restarigi?"
saved: "Konservita" saved: "Konservita"
messaging: "Retbabili" messaging: "Retbabili"
@ -225,13 +227,13 @@ agreeTo: "Mi akceptas {0}'(o)n"
tos: "Kondiĉoj de uzado" tos: "Kondiĉoj de uzado"
start: "Komenciĝi" start: "Komenciĝi"
home: "Hejma" home: "Hejma"
remoteUserCaution: "Ĉi tiuj infomoj estas ne tute ekzaktaj pro transa uzanto." remoteUserCaution: "Ĉi tiuj infomoj de la uzanto el fora nodo, ne estas tute ekzaktaj."
activity: "Aktiveco" activity: "Aktiveco"
images: "Bildoj" images: "Bildoj"
birthday: "Naskiĝdato" birthday: "Naskiĝdato"
yearsOld: "{age} jaroj aĝa" yearsOld: "{age} jaroj aĝa"
registeredDate: "Dato de registriĝo" registeredDate: "Dato de registriĝo"
location: "Loko" location: "Kie"
theme: "Koloraro" theme: "Koloraro"
themeForLightMode: "Luma kolararo en la luma modo" themeForLightMode: "Luma kolararo en la luma modo"
themeForDarkMode: "Malluma kolararo en la malluma modo" themeForDarkMode: "Malluma kolararo en la malluma modo"
@ -253,7 +255,7 @@ deleteFolder: "Forviŝi dosierujon"
addFile: "Aldoni dosieron" addFile: "Aldoni dosieron"
emptyDrive: "La disko malplenas" emptyDrive: "La disko malplenas"
emptyFolder: "La dosierujo malplenas" emptyFolder: "La dosierujo malplenas"
unableToDelete: "Ne forigebla" unableToDelete: "Ne forviŝebla"
inputNewFileName: "Entajpu novan nomon de la dosiero" inputNewFileName: "Entajpu novan nomon de la dosiero"
inputNewDescription: "Entajpu novan priskribon" inputNewDescription: "Entajpu novan priskribon"
inputNewFolderName: "Entajpu novan nomon de la dosierujo" inputNewFolderName: "Entajpu novan nomon de la dosierujo"
@ -266,9 +268,11 @@ nsfw: "Enhavo ne estas deca por laborejo (NSFW)"
disconnectedFromServer: "Malkonektita de servilo" disconnectedFromServer: "Malkonektita de servilo"
reload: "Reŝargi" reload: "Reŝargi"
doNothing: "Ignori" doNothing: "Ignori"
reloadConfirm: "Ĉu vi volas reŝargi?"
watch: "Observi" watch: "Observi"
unwatch: "Malobservi" unwatch: "Malobservi"
accept: "Permesi" accept: "Permesi"
reject: "Malakcepti"
normal: "Normala" normal: "Normala"
instanceName: "Nomo de la nodo" instanceName: "Nomo de la nodo"
instanceDescription: "Priskribo de la nodo " instanceDescription: "Priskribo de la nodo "
@ -291,20 +295,22 @@ registration: "Registri"
enableRegistration: "Ebligi novan uzanton registriĝon" enableRegistration: "Ebligi novan uzanton registriĝon"
invite: "Inviti" invite: "Inviti"
driveCapacityPerLocalAccount: "Volumo de disko po unu loka uzanto" driveCapacityPerLocalAccount: "Volumo de disko po unu loka uzanto"
driveCapacityPerRemoteAccount: "Volumo de disko po unu transa uzanto" driveCapacityPerRemoteAccount: "Volumo de disko po unu fora uzanto"
iconUrl: "URL de la ikono (retpaĝsimbolo, ktp)" iconUrl: "URL de la ikono (retpaĝsimbolo, ktp)"
bannerUrl: "URL de standardo" bannerUrl: "URL de standardo"
backgroundImageUrl: "URL de fona bildo" backgroundImageUrl: "URL de fona bildo"
basicInfo: "Baza informo" basicInfo: "Baza informo"
pinnedUsers: "Alpinglita uzanto" pinnedUsers: "Alpinglita uzanto"
pinnedUsersDescription: "Listigu uzantnomojn apartige en ĉiu linio por alpingli al la paĝoj ekz \"Esplori\"."
pinnedPages: "Alpinglitaj paĝoj" pinnedPages: "Alpinglitaj paĝoj"
pinnedPagesDescription: "Listigu dosierindiko apartige en ĉiu linio por alpingli al la ĉefpaĝo de la nodo."
pinnedNotes: "Alpinglita noto" pinnedNotes: "Alpinglita noto"
hcaptcha: "hCaptcha" hcaptcha: "hCaptcha"
enableHcaptcha: "Ebligi hCaptcha" enableHcaptcha: "Ebligi hCaptcha"
hcaptchaSiteKey: "Reteja ŝlosilo" hcaptchaSiteKey: "Reteja ŝlosilo"
hcaptchaSecretKey: "Sekreta ŝlosilo" hcaptchaSecretKey: "Sekreta ŝlosilo"
recaptcha: "reCAPTCHA" recaptcha: "reCAPTCHA"
enableRecaptcha: "Ebligi reCAPTCHA'on" enableRecaptcha: "Ebligi reCAPTCHA"
recaptchaSiteKey: "Reteja ŝlosilo" recaptchaSiteKey: "Reteja ŝlosilo"
recaptchaSecretKey: "Sekreta ŝlosilo" recaptchaSecretKey: "Sekreta ŝlosilo"
antennas: "Antenoj" antennas: "Antenoj"
@ -338,15 +344,17 @@ moderator: "Kontrolisto"
nUsersMentioned: "{n} uzanto(j) menciis" nUsersMentioned: "{n} uzanto(j) menciis"
securityKey: "Sekureca ŝlosilo" securityKey: "Sekureca ŝlosilo"
securityKeyName: "Nomo de la ŝlosilo" securityKeyName: "Nomo de la ŝlosilo"
registerSecurityKey: "Registri ŝlosilon de sekureco"
lastUsed: "Plej malnove uzita" lastUsed: "Plej malnove uzita"
unregister: "Malregistriĝi" unregister: "Malregistriĝi"
passwordLessLogin: "Ensaluti sen pasvorto" passwordLessLogin: "Ensaluti sen pasvorto"
resetPassword: "Restarigi pasvorton" resetPassword: "Restarigi pasvorton"
newPasswordIs: "La nova pasvorto estas {password}." newPasswordIs: "La nova pasvorto estas {password}."
share: "Diskonigi" reduceUiAnimation: "Redukti la animacioj de la fasado"
share: "Kundividi"
notFound: "Ne trovita" notFound: "Ne trovita"
cacheClear: "Malplenigi staplon" cacheClear: "Malplenigi staplon"
markAsReadAllNotifications: "Marki ĉiujn sciigojn kiel legito" markAsReadAllNotifications: "Marki ĉiujn sciigojn kiel legita"
help: "Manlibro de uzado" help: "Manlibro de uzado"
inputMessageHere: "Entajpu masaĝo tie ĉi" inputMessageHere: "Entajpu masaĝo tie ĉi"
close: "Fermi" close: "Fermi"
@ -354,10 +362,11 @@ group: "Grupo"
groups: "Grupoj" groups: "Grupoj"
createGroup: "Krei grupon" createGroup: "Krei grupon"
ownedGroups: "Administrataj grupoj" ownedGroups: "Administrataj grupoj"
joinedGroups: "La grupoj kiujn la uzanto aliĝis" joinedGroups: "Al grupoj kiuj vi aliĝis"
invites: "Inviti" invites: "Inviti"
groupName: "Grupa nomo" groupName: "Grupa nomo"
members: "Membroj" members: "Membroj"
transfer: "Movi"
messagingWithUser: "Babili private" messagingWithUser: "Babili private"
messagingWithGroup: "Babili grupe" messagingWithGroup: "Babili grupe"
title: "Titolo" title: "Titolo"
@ -366,6 +375,7 @@ enable: "Ebligi"
next: "Sekve" next: "Sekve"
retype: "Retajpu" retype: "Retajpu"
noteOf: "Noto de {user}" noteOf: "Noto de {user}"
inviteToGroup: "Inviti al grupo"
quoteAttached: "Kun citaĵo" quoteAttached: "Kun citaĵo"
quoteQuestion: "Ĉu vi aldonas citaĵon?" quoteQuestion: "Ĉu vi aldonas citaĵon?"
noMessagesYet: "Ankoraŭ neniu mesaĝo" noMessagesYet: "Ankoraŭ neniu mesaĝo"
@ -374,27 +384,38 @@ onlyOneFileCanBeAttached: "Oni povas aldoni nur unu dosieron po mesaĝo."
signinRequired: "Bonvolu ensaluti" signinRequired: "Bonvolu ensaluti"
invitations: "Inviti" invitations: "Inviti"
invitationCode: "Invita kodo" invitationCode: "Invita kodo"
available: "Disposabla"
unavailable: "Ne disponebla" unavailable: "Ne disponebla"
usernameInvalidFormat: "La uzantnomo povas enhavi minusklajn kaj majusklajn literojn, numerojn, nur kaj '_'."
tooShort: "Tro mallonga"
tooLong: "Tro longa"
weakPassword: "Malforta pasvorto"
normalPassword: "Normala pasvorto"
strongPassword: "Forta pasvorto"
passwordMatched: "Konforma" passwordMatched: "Konforma"
passwordNotMatched: "Nekonforma" passwordNotMatched: "Nekonforma"
signinWith: "Ensaluti kun {x}"
or: "Aŭ" or: "Aŭ"
language: "Lingvo" language: "Lingvo"
uiLanguage: "Lingvo de fasado" uiLanguage: "Lingvo de fasado"
aboutX: "Pri {x}" aboutX: "Pri {x}"
useOsNativeEmojis: "Oni uzas la emoĵioj de la denaska sistemo" useOsNativeEmojis: "Uzi la emoĵiojn implicitan de la operaciumo"
youHaveNoGroups: "Neniuj grupoj" youHaveNoGroups: "Neniuj grupoj"
noHistory: "Neniom historio"
signinHistory: "Historio de aliroj al la konto"
doing: "Traktado..." doing: "Traktado..."
category: "Kategorio" category: "Kategorio"
tags: "Etikedoj" tags: "Etikedoj"
docSource: "Fonto de la dokumento"
createAccount: "Krei konton" createAccount: "Krei konton"
existingAccount: "Ekzista konto" existingAccount: "Ekzista konto"
regenerate: "Regeneri" regenerate: "Regeneri"
fontSize: "Tipara grando" fontSize: "Tipara grando"
noFollowRequests: "Vi ne havas peto de sekvado" noFollowRequests: "Vi ne havas peto de sekvado"
openImageInNewTab: "Fermi la bildon en nova tablo" openImageInNewTab: "Malfermi la bildojn en nova tablo"
dashboard: "Stirpanelo" dashboard: "Stirpanelo"
local: "Loka" local: "Loka"
remote: "Transa" remote: "Fora"
total: "Entute" total: "Entute"
appearance: "Eksteraĵo" appearance: "Eksteraĵo"
clientSettings: "Agordoj de kliento" clientSettings: "Agordoj de kliento"
@ -402,6 +423,7 @@ accountSettings: "Agordoj de konto"
numberOfDays: "Nombro de tagoj" numberOfDays: "Nombro de tagoj"
hideThisNote: "Kaŝi la noton" hideThisNote: "Kaŝi la noton"
objectStorageBaseUrl: "Baza URL" objectStorageBaseUrl: "Baza URL"
objectStoragePrefix: "Prefix"
objectStorageRegion: "Regiono" objectStorageRegion: "Regiono"
objectStorageUseSSL: "Oni uzas SSL" objectStorageUseSSL: "Oni uzas SSL"
serverLogs: "Servila protokolo" serverLogs: "Servila protokolo"
@ -416,7 +438,7 @@ volume: "Laŭteco"
masterVolume: "Baza laŭteco" masterVolume: "Baza laŭteco"
details: "Detaloj" details: "Detaloj"
chooseEmoji: "Elekti emoĵion" chooseEmoji: "Elekti emoĵion"
recentUsed: "Lastatempaj uzitaj" recentUsed: "Lastatempe uzitaj"
install: "Instali" install: "Instali"
uninstall: "Malinstali" uninstall: "Malinstali"
installedApps: "Instalita programo" installedApps: "Instalita programo"
@ -425,10 +447,12 @@ installedDate: "Dato de instalado"
lastUsedDate: "Lastfoje uzita je" lastUsedDate: "Lastfoje uzita je"
state: "Stato" state: "Stato"
sort: "Ordigado" sort: "Ordigado"
ascendingOrder: "Kreski"
descendingOrder: "Malkreski"
scratchpad: "Malneta redaktilo" scratchpad: "Malneta redaktilo"
output: "Elmeto" output: "Elmeto"
script: "Skripto" script: "Skripto"
disablePagesScript: "Malebligi AiScripto en la paĝoj" disablePagesScript: "Malebligi AiScript en la paĝoj"
deleteAllFiles: "Forviŝi ĉiujn dosierojn" deleteAllFiles: "Forviŝi ĉiujn dosierojn"
deleteAllFilesConfirm: "Ĉu vi certas, ke vi volas forviŝi ĉiujn dosierojn?" deleteAllFilesConfirm: "Ĉu vi certas, ke vi volas forviŝi ĉiujn dosierojn?"
removeAllFollowing: "Ĉesi sekvi ĉiujn sekvatojn" removeAllFollowing: "Ĉesi sekvi ĉiujn sekvatojn"
@ -438,7 +462,8 @@ menu: "Menuo"
addItem: "Aldoni novaĵon" addItem: "Aldoni novaĵon"
rooms: "Ĉambro" rooms: "Ĉambro"
deletedNote: "Forviŝita noto" deletedNote: "Forviŝita noto"
invisibleNote: "Malpublika noto" invisibleNote: "Malpublikigita noto"
enableInfiniteScroll: "Ebligi infinitan rulumon"
visibility: "Videbleco" visibility: "Videbleco"
poll: "Balotujo" poll: "Balotujo"
useCw: "Kaŝi enhavo" useCw: "Kaŝi enhavo"
@ -453,16 +478,23 @@ author: "Aŭtoro"
manage: "Administro" manage: "Administro"
plugins: "Kromaĵoj" plugins: "Kromaĵoj"
deck: "Kartaro" deck: "Kartaro"
useFullReactionPicker: "Uzi la tuton de la elektilon de reagoj"
width: "Larĝeco" width: "Larĝeco"
height: "Alteco" height: "Alteco"
large: "Granda"
medium: "Meza" medium: "Meza"
small: "Malgranda" small: "Malgranda"
generateAccessToken: "Generi ĵetonon de aliro"
permission: "Permesoj"
enableAll: "Ebligi ĉiujn"
disableAll: "Malebligi ĉiujn"
notificationType: "Tipo de sciigoj"
edit: "Redakti" edit: "Redakti"
emailServer: "Retpoŝta servilo" emailServer: "Retpoŝta servilo"
email: "Retpoŝto" email: "Retpoŝto"
emailAddress: "Retpoŝta adreso" emailAddress: "Retpoŝta adreso"
smtpConfig: "Agordoj de SMTP servilo" smtpConfig: "Agordoj de SMTP servilo"
smtpHost: "Gastigo" smtpHost: "Transa servilo"
smtpPort: "Pordo" smtpPort: "Pordo"
smtpUser: "Uzantnomo" smtpUser: "Uzantnomo"
smtpPass: "Pasvorto" smtpPass: "Pasvorto"
@ -471,13 +503,20 @@ userSaysSomething: "{name} parolis ion"
makeActive: "Aktivigi" makeActive: "Aktivigi"
display: "Vidi" display: "Vidi"
copy: "Kopii" copy: "Kopii"
metrics: "mezurciferoj"
overview: "Resumo" overview: "Resumo"
logs: "Protokoloj"
delayed: "Prokrasto "
database: "Datumbazo" database: "Datumbazo"
channel: "Kanalo" channel: "Kanalo"
create: "Krei" create: "Krei"
notificationSetting: "Agordoj de sciigoj" notificationSetting: "Agordoj de sciigoj"
useGlobalSetting: "Oni uzas malloka agordo" useGlobalSetting: "Oni uzas malloka agordo"
other: "Aliaj"
regenerateLoginToken: "Regeneri la ĵetonon de aliro"
fileIdOrUrl: "Dosiera identigilo aŭ URL" fileIdOrUrl: "Dosiera identigilo aŭ URL"
chatOpenBehavior: "Konduto por malfermi la fenestron de babilejo"
behavior: "Konduto"
sample: "Ekzemplo" sample: "Ekzemplo"
abuseReports: "Signaloj" abuseReports: "Signaloj"
reportAbuse: "Signalo" reportAbuse: "Signalo"
@ -485,20 +524,21 @@ reportAbuseOf: "Signali kontraŭ {name}'(o)"
send: "Sendi" send: "Sendi"
openInNewTab: "Malfermi en nova langeto" openInNewTab: "Malfermi en nova langeto"
editTheseSettingsMayBreakAccount: "Redakti ĉi tiujn agordojn povas damaĝi vian konton." editTheseSettingsMayBreakAccount: "Redakti ĉi tiujn agordojn povas damaĝi vian konton."
instanceTicker: "Informoj pri la nodo kiu dissendas la noton" instanceTicker: "Nomo de la nodo sendinta notojn"
waitingFor: "Atendado pro {x}"
random: "Hazarde" random: "Hazarde"
system: "Sistemo" system: "Sistemo"
desktop: "Labortablo" desktop: "Labortablo"
createNew: "Krei novan" createNew: "Krei novan"
optional: "Opciaj" optional: "Opciaj"
public: "Publika" public: "Publika"
i18nInfo: "Misskey estas tradukata en diversaj lingvoj far volontuloj. Oni povas kontribui por la tradukado ĉe {link}." i18nInfo: "Misskey estas tradukata en diversaj lingvoj de volontuloj. Oni povas kontribui ĉe {link}."
accountInfo: "Kontaj Informoj" accountInfo: "Kontaj Informoj"
notesCount: "La nombro de notoj" notesCount: "La nombro de notoj"
repliesCount: "La nombro de respondoj senditaj" repliesCount: "La nombro de respondoj senditaj"
renotesCount: "La nombro de notoj kiujn la uzanto plusendis" renotesCount: "La nombro de notoj plusenditaj de la uzanto"
repliedCount: "La nombro de respondoj ricevitaj" repliedCount: "La nombro de respondoj ricevitaj"
renotedCount: "La nombro de uzantulaj notoj plusenditaj" renotedCount: "La nombro de plusendoj de la notoj skribitaj de la uzanto"
followingCount: "La nombro de sekvatoj" followingCount: "La nombro de sekvatoj"
followersCount: "La nombro de sekvantoj" followersCount: "La nombro de sekvantoj"
sentReactionsCount: "La nombro de la reagoj senditaj" sentReactionsCount: "La nombro de la reagoj senditaj"
@ -512,10 +552,15 @@ noteFavoritesCount: "La nombro de notoj preferataj"
pageLikesCount: "La nombro de paĝoj kiun la uzanto preferas" pageLikesCount: "La nombro de paĝoj kiun la uzanto preferas"
pageLikedCount: "La nombro de uzantoj, kiuj preferas paĝon de ĉi tiu uzanto" pageLikedCount: "La nombro de uzantoj, kiuj preferas paĝon de ĉi tiu uzanto"
contact: "Kontakto" contact: "Kontakto"
useSystemFont: "Uzi la tiparon implicitan de la sistemo"
developer: "Evoluiganto"
makeExplorable: "Videbligi konton sur la paĝo \"Esplori\"" makeExplorable: "Videbligi konton sur la paĝo \"Esplori\""
makeExplorableDescription: "Se vi elŝaltas tiun, via konto ne montros en la paĝo \"Esplori\"."
duplicate: "Duobligi" duplicate: "Duobligi"
left: "Maldekstra" left: "Maldekstra"
center: "Centra" center: "Centra"
wide: "Vasta"
narrow: "Malvasta"
showTitlebar: "Videbligi titolan stangon" showTitlebar: "Videbligi titolan stangon"
clearCache: "Malplenigi staplon" clearCache: "Malplenigi staplon"
onlineUsersCount: "{n} uzanto(j) estas surlinea" onlineUsersCount: "{n} uzanto(j) estas surlinea"
@ -525,9 +570,11 @@ myTheme: "Miaj koloraroj"
backgroundColor: "Fona koloro" backgroundColor: "Fona koloro"
textColor: "Teksto" textColor: "Teksto"
saveAs: "Konservi kiel…" saveAs: "Konservi kiel…"
advanced: "Altnivela"
value: "Valoro" value: "Valoro"
createdAt: "Kreita je" createdAt: "Kreita je"
updatedAt: "Laste ĝisdatigita" updatedAt: "Laste ĝisdatigita"
saveConfirm: "Ĉu vi konservas la ŝanĝon?"
deleteConfirm: "Ĉu certas forviŝi?" deleteConfirm: "Ĉu certas forviŝi?"
closeAccount: "Forigi konton" closeAccount: "Forigi konton"
currentVersion: "Nuna versio" currentVersion: "Nuna versio"
@ -538,9 +585,10 @@ inUse: "Uzata"
editCode: "Redakti kodon" editCode: "Redakti kodon"
emailNotification: "Sciigoj per retpoŝto" emailNotification: "Sciigoj per retpoŝto"
inChannelSearch: "Serĉi en kanalo" inChannelSearch: "Serĉi en kanalo"
useReactionPickerForContextMenu: "Malfermi reago-elektilon per dekstro-klaki" useReactionPickerForContextMenu: "Dekstre-klaki por malfermi la elektilon de reagoj"
typingUsers: "{users} nun skribas…" typingUsers: "{users} nun skribas…"
clear: "Vakigi" clear: "Vakigi"
markAllAsRead: "Marki ĉiujn kiel legito"
goBack: "Reiri antaŭ" goBack: "Reiri antaŭ"
addDescription: "Priskribi" addDescription: "Priskribi"
info: "Informoj" info: "Informoj"
@ -559,7 +607,7 @@ memo: "Memorigilo"
high: "Alta" high: "Alta"
middle: "Meza" middle: "Meza"
low: "Malalta" low: "Malalta"
customCss: "Uzantula CSS" customCss: "Personecigita CSS"
global: "Malloka" global: "Malloka"
sent: "Sendi" sent: "Sendi"
received: "Ricevita" received: "Ricevita"
@ -569,10 +617,27 @@ troubleshooting: "Problemsolvi"
learnMore: "Lernu pli" learnMore: "Lernu pli"
translate: "Traduki" translate: "Traduki"
translatedFrom: "Tradukita el {x}" translatedFrom: "Tradukita el {x}"
itsOn: "Ŝaltita"
unread: "Nelegita"
controlPanel: "Ŝaltpodio" controlPanel: "Ŝaltpodio"
classic: "Klasika" classic: "Klasika"
ffVisibility: "Videbleco pri viaj sekvataro/sekvantaro\n"
ffVisibilityDescription: "Agordi la videblecon kiu povas vidi tiujn kiujn vi sekvas kaj tiujn kiuj sekvas vin."
continueThread: "Vidi pli mesaĝarojn"
incorrectPassword: "Nevalida pasvorto"
_emailUnavailable:
used: "La retpoŝto jam estas uzita."
format: "Nevalida formato."
disposable: "Dumtempa retpoŝto ne estas uzebla."
smtp: "Tiu retpoŝta servilo ne respondas"
_ffVisibility:
public: "Publika"
followers: "Afiŝi nur al sekvantoj"
private: "Malpublikigita"
_signup: _signup:
emailAddressInfo: "Entajpu vian retpoŝton" emailAddressInfo: "Entajpu vian retpoŝton"
_accountDelete:
accountDelete: "Forigi konton"
_ad: _ad:
back: "Nuligi" back: "Nuligi"
_forgotPassword: _forgotPassword:
@ -598,7 +663,7 @@ _aboutMisskey:
contributors: "Precipaj kontribuantoj" contributors: "Precipaj kontribuantoj"
allContributors: "Ĉiuj kontribuantoj" allContributors: "Ĉiuj kontribuantoj"
source: "Fontkodo" source: "Fontkodo"
translation: "Traduki Misskey'on" translation: "Traduki Misskey"
patrons: "Mecenatoj" patrons: "Mecenatoj"
_mfm: _mfm:
dummy: "Misskey evoluigas la mondon de Fediverso" dummy: "Misskey evoluigas la mondon de Fediverso"
@ -614,19 +679,21 @@ _mfm:
inlineMath: "Formulo (en linio)" inlineMath: "Formulo (en linio)"
blockMath: "Formulo (bloko)" blockMath: "Formulo (bloko)"
quote: "Citi" quote: "Citi"
emoji: "Personecigitaj emoĵioj"
search: "Serĉi" search: "Serĉi"
flip: "Inversa" flip: "Inversa"
x2: "Granda" x2: "Granda"
x3: "Grandega" x3: "Grandega"
x4: "Pli grandega" x4: "Pli grandega"
font: "Presliteraro" font: "Presliteraro"
rotate: "Orientiĝo"
_reversi: _reversi:
total: "Entute" total: "Entute"
_instanceTicker: _instanceTicker:
none: "Ne montri" none: "Ne montri"
remote: "Montri al transaj uzantoj" remote: "Montri al foraj uzantoj"
always: "Ĉiam montri" always: "Ĉiam montri"
_serverDisconnectedBehavior:
reload: "Aŭtomate reŝargi"
_channel: _channel:
create: "Krei kanalon" create: "Krei kanalon"
edit: "Redakti kanalon" edit: "Redakti kanalon"
@ -640,13 +707,14 @@ _menuDisplay:
hide: "Kaŝi" hide: "Kaŝi"
_wordMute: _wordMute:
muteWords: "Silentigitaj vortoj" muteWords: "Silentigitaj vortoj"
soft: "En kliento" soft: "Per la kliento"
hard: "En servilo" hard: "Per la servilo"
mutedNotes: "Silentigitaj notoj" mutedNotes: "Silentigitaj notoj"
_theme: _theme:
manage: "Administri kolorarojn" manage: "Administri kolorarojn"
code: "Kolorara kodo" code: "Kolorara kodo"
description: "Priskribo" description: "Priskribo"
defaultValue: "Implicitaĵa valoro"
color: "Koloro" color: "Koloro"
darken: "Malbrileco" darken: "Malbrileco"
lighten: "Brileco" lighten: "Brileco"
@ -657,7 +725,7 @@ _theme:
hashtag: "Kradvorto" hashtag: "Kradvorto"
mention: "Mencioj" mention: "Mencioj"
mentionMe: "Mencio al vi" mentionMe: "Mencio al vi"
renote: "Noto plusendita" renote: "Plusendita"
buttonBg: "Fono de butono" buttonBg: "Fono de butono"
driveFolderBg: "Fono de dosierujo de la disko" driveFolderBg: "Fono de dosierujo de la disko"
messageBg: "Fono de retbabilejo" messageBg: "Fono de retbabilejo"
@ -688,7 +756,7 @@ _tutorial:
title: "Uzado de Misskey" title: "Uzado de Misskey"
step1_1: "Bonvenon." step1_1: "Bonvenon."
step7_2: "Se vi volas scii pli pri Misskey, rigardu la fakon {help}." step7_2: "Se vi volas scii pli pri Misskey, rigardu la fakon {help}."
step7_3: "Do, bonvolu amuziĝi Misskey'on🚀" step7_3: "Do, bonvolu amuziĝi sur Misskey🚀"
_2fa: _2fa:
registerKey: "Nove registri ŝlosilon" registerKey: "Nove registri ŝlosilon"
_permissions: _permissions:
@ -732,10 +800,10 @@ _widgets:
federation: "Federaĵo" federation: "Federaĵo"
slideshow: "Bildoprezento" slideshow: "Bildoprezento"
button: "Butono" button: "Butono"
onlineUsers: "Surkonektita uzanto" onlineUsers: "Surkonektitaj uzantoj"
aichan: "Ai" aichan: "Ai"
_cw: _cw:
show: "Vidu pli" show: "Vidi pli"
files: "{count} dosiero(j)" files: "{count} dosiero(j)"
_poll: _poll:
choiceN: "Balotilo {n}" choiceN: "Balotilo {n}"
@ -747,15 +815,15 @@ _poll:
closed: "Oni jam balotis ĝin" closed: "Oni jam balotis ĝin"
_visibility: _visibility:
public: "Publika" public: "Publika"
publicDescription: "Via noto estos videbla de ĉiuj uzantoj" publicDescription: "Afiŝi al ĉiuj en la Fediverso"
home: "Hejma" home: "Hejma"
homeDescription: "Dissendi nur sur hejma templinio" homeDescription: "Dissendi nur sur hejma templinio"
followers: "Nur al sekvantoj" followers: "Nur al sekvantoj"
followersDescription: "Publiki nur al viaj sekvantoj" followersDescription: "Afiŝi nur al sekvantoj"
specified: "Rekte" specified: "Rekte"
specifiedDescription: "Montri nur al specifaj uzantoj" specifiedDescription: "Afiŝi nur al specifaj uzantoj"
localOnly: "Nur loka" localOnly: "Nur loka"
localOnlyDescription: "Ne montri al transaj uzantoj" localOnlyDescription: "Ne afiŝi al foraj uzantoj"
_postForm: _postForm:
replyPlaceholder: "Respondi la noton…" replyPlaceholder: "Respondi la noton…"
quotePlaceholder: "Citi la noton…" quotePlaceholder: "Citi la noton…"
@ -789,7 +857,7 @@ _rooms:
translate: "Movi" translate: "Movi"
chooseImage: "Elekti bildon" chooseImage: "Elekti bildon"
_roomType: _roomType:
default: "Defaŭlta" default: "Implicitaĵo"
_furnitures: _furnitures:
bed: "Lito" bed: "Lito"
low-table: "Malaltotablo" low-table: "Malaltotablo"
@ -835,18 +903,22 @@ _pages:
textInput: "Enmeto el teksto" textInput: "Enmeto el teksto"
_textInput: _textInput:
text: "Titolo" text: "Titolo"
default: "Implicitaĵa valoro"
textareaInput: "Enmeto el teksto en multaj linioj" textareaInput: "Enmeto el teksto en multaj linioj"
_textareaInput: _textareaInput:
text: "Titolo" text: "Titolo"
default: "Implicitaĵa valoro"
numberInput: "Nombra enmeto" numberInput: "Nombra enmeto"
_numberInput: _numberInput:
text: "Titolo" text: "Titolo"
default: "Implicitaĵa valoro"
_canvas: _canvas:
id: "Kanvasa identigilo" id: "Kanvasa identigilo"
_note: _note:
id: "Identigilo de noto" id: "Identigilo de noto"
_switch: _switch:
text: "Titolo" text: "Titolo"
default: "Implicitaĵa valoro"
_counter: _counter:
text: "Titolo" text: "Titolo"
_button: _button:
@ -856,6 +928,7 @@ _pages:
event: "Nomo de la evento" event: "Nomo de la evento"
_radioButton: _radioButton:
title: "Titolo" title: "Titolo"
default: "Implicitaĵa valoro"
script: script:
categories: categories:
text: "Manipulo de teksto" text: "Manipulo de teksto"
@ -874,6 +947,7 @@ _pages:
arg1: "Teksto" arg1: "Teksto"
_join: _join:
arg1: "Listoj" arg1: "Listoj"
arg2: "apartigilo"
_randomPick: _randomPick:
arg1: "Listoj" arg1: "Listoj"
_dailyRandomPick: _dailyRandomPick:
@ -904,6 +978,7 @@ _pages:
_relayStatus: _relayStatus:
requesting: "Atendado de aprobon" requesting: "Atendado de aprobon"
accepted: "Konfirmita" accepted: "Konfirmita"
rejected: "Malakceptita"
_notification: _notification:
fileUploaded: "La dosiero sukcese alŝutiĝis." fileUploaded: "La dosiero sukcese alŝutiĝis."
youGotMention: "{name} mencis" youGotMention: "{name} mencis"
@ -918,13 +993,13 @@ _notification:
yourFollowRequestAccepted: "Via peto de sekvado estis akceptita." yourFollowRequestAccepted: "Via peto de sekvado estis akceptita."
_types: _types:
all: "Ĉio" all: "Ĉio"
follow: "Nova sekvatoj" follow: "Novaj sekvatoj"
mention: "Mencioj" mention: "Mencioj"
reply: "Respondoj" reply: "Respondoj"
renote: "Notoj plusenditaj" renote: "Plusendoj"
quote: "Citi" quote: "Citi"
reaction: "Reagoj" reaction: "Reagoj"
receiveFollowRequest: "Ricevita peton de sekvado" receiveFollowRequest: "Ricevi peton de sekvado"
followRequestAccepted: "Akceptita peto por sekvado" followRequestAccepted: "Akceptita peto por sekvado"
_deck: _deck:
profile: "Agordaro" profile: "Agordaro"

View File

@ -737,6 +737,7 @@ pubSub: "Cuentas Pub/Sub"
lastCommunication: "Última comunicación" lastCommunication: "Última comunicación"
resolved: "Resuelto" resolved: "Resuelto"
unresolved: "Sin resolver" unresolved: "Sin resolver"
controlPanel: "Panel de control"
_accountDelete: _accountDelete:
accountDelete: "Eliminar Cuenta" accountDelete: "Eliminar Cuenta"
_ad: _ad:
@ -767,6 +768,7 @@ _mfm:
flip: "Echar de un capirotazo" flip: "Echar de un capirotazo"
flipDescription: "Voltea el contenido hacia arriba / abajo o hacia la izquierda / derecha." flipDescription: "Voltea el contenido hacia arriba / abajo o hacia la izquierda / derecha."
font: "Fuente" font: "Fuente"
rotate: "Rotar"
_reversi: _reversi:
reversi: "Reversi" reversi: "Reversi"
gameSettings: "Configuración del juego" gameSettings: "Configuración del juego"

View File

@ -919,6 +919,7 @@ _mfm:
rainbowDescription: "Permet d'afficher le contenu en couleurs arc-en-ciel." rainbowDescription: "Permet d'afficher le contenu en couleurs arc-en-ciel."
sparkle: "Paillettes" sparkle: "Paillettes"
sparkleDescription: "Ajoute un effet scintillant au contenu." sparkleDescription: "Ajoute un effet scintillant au contenu."
rotate: "Pivoter"
_reversi: _reversi:
reversi: "Reversi" reversi: "Reversi"
gameSettings: "Réglages de la partie" gameSettings: "Réglages de la partie"

View File

@ -806,6 +806,10 @@ muteThread: "Bisukan thread"
unmuteThread: "Suarakan thread" unmuteThread: "Suarakan thread"
ffVisibility: "Visibilitas Mengikuti/Pengikut" ffVisibility: "Visibilitas Mengikuti/Pengikut"
ffVisibilityDescription: "Mengatur siapa yang dapat melihat pengikutmu dan yang kamu ikuti." ffVisibilityDescription: "Mengatur siapa yang dapat melihat pengikutmu dan yang kamu ikuti."
continueThread: "Lihat lanjutan thread"
deleteAccountConfirm: "Akun akan dihapus. Apakah kamu yakin?"
incorrectPassword: "Kata sandi salah."
voteConfirm: "Konfirmasi suara kamu untuk ({choice})"
_emailUnavailable: _emailUnavailable:
used: "Alamat surel ini telah digunakan" used: "Alamat surel ini telah digunakan"
format: "Format tidak valid." format: "Format tidak valid."
@ -929,6 +933,8 @@ _mfm:
rainbowDescription: "Membuat konten muncul dalam warna pelangi." rainbowDescription: "Membuat konten muncul dalam warna pelangi."
sparkle: "Kelap-kelip" sparkle: "Kelap-kelip"
sparkleDescription: "Memberikan konten efek partikel kelap-kelip." sparkleDescription: "Memberikan konten efek partikel kelap-kelip."
rotate: "Putar"
rotateDescription: "Putar konten sesuai sudut yang ditentukan."
_reversi: _reversi:
reversi: "Reversi" reversi: "Reversi"
gameSettings: "Pengaturan permainan" gameSettings: "Pengaturan permainan"

View File

@ -806,6 +806,7 @@ _mfm:
font: "Tipo di carattere" font: "Tipo di carattere"
fontDescription: "Puoi scegliere il tipo di carattere per il contenuto." fontDescription: "Puoi scegliere il tipo di carattere per il contenuto."
rainbow: "Arcobaleno" rainbow: "Arcobaleno"
rotate: "Ruota"
_reversi: _reversi:
reversi: "Reversi" reversi: "Reversi"
gameSettings: "Impostazioni di gioco" gameSettings: "Impostazioni di gioco"

View File

@ -792,6 +792,7 @@ pubSub: "Pub/Subのアカウント"
lastCommunication: "直近の通信" lastCommunication: "直近の通信"
resolved: "解決済み" resolved: "解決済み"
unresolved: "未解決" unresolved: "未解決"
breakFollow: "フォロワーを解除"
itsOn: "オンになっています" itsOn: "オンになっています"
itsOff: "オフになっています" itsOff: "オフになっています"
emailRequiredForSignup: "アカウント登録にメールアドレスを必須にする" emailRequiredForSignup: "アカウント登録にメールアドレスを必須にする"
@ -808,6 +809,8 @@ ffVisibility: "つながりの公開範囲"
ffVisibilityDescription: "自分のフォロー/フォロワー情報の公開範囲を設定できます。" ffVisibilityDescription: "自分のフォロー/フォロワー情報の公開範囲を設定できます。"
continueThread: "さらにスレッドを見る" continueThread: "さらにスレッドを見る"
deleteAccountConfirm: "アカウントが削除されます。よろしいですか?" deleteAccountConfirm: "アカウントが削除されます。よろしいですか?"
incorrectPassword: "パスワードが間違っています。"
voteConfirm: "「{choice}」に投票しますか?"
_emailUnavailable: _emailUnavailable:
used: "既に使用されています" used: "既に使用されています"
@ -823,7 +826,7 @@ _ffVisibility:
_signup: _signup:
almostThere: "ほとんど完了です" almostThere: "ほとんど完了です"
emailAddressInfo: "あなたが使っているメールアドレスを入力してください。" emailAddressInfo: "あなたが使っているメールアドレスを入力してください。メールアドレスが公開されることはありません。"
emailSent: "入力されたメールアドレス({email})宛に確認のメールが送信されました。メールに記載されたリンクにアクセスすると、アカウントの作成が完了します。" emailSent: "入力されたメールアドレス({email})宛に確認のメールが送信されました。メールに記載されたリンクにアクセスすると、アカウントの作成が完了します。"
_accountDelete: _accountDelete:
@ -944,6 +947,8 @@ _mfm:
rainbowDescription: "内容をレインボーにします。" rainbowDescription: "内容をレインボーにします。"
sparkle: "キラキラ" sparkle: "キラキラ"
sparkleDescription: "キラキラしたパーティクルのエフェクトを追加します。" sparkleDescription: "キラキラしたパーティクルのエフェクトを追加します。"
rotate: "回転"
rotateDescription: "指定した角度で回転させます。"
_reversi: _reversi:
reversi: "リバーシ" reversi: "リバーシ"

View File

@ -700,6 +700,7 @@ _mfm:
spin: "アニメーション(回転)" spin: "アニメーション(回転)"
blur: "ぼかし" blur: "ぼかし"
font: "フォント" font: "フォント"
rotate: "回転"
_reversi: _reversi:
reversi: "リバーシ" reversi: "リバーシ"
gameSettings: "対局の設定" gameSettings: "対局の設定"

View File

@ -899,6 +899,7 @@ _mfm:
rainbowDescription: "내용을 무지개로 표시합니다." rainbowDescription: "내용을 무지개로 표시합니다."
sparkle: "반짝반짝" sparkle: "반짝반짝"
sparkleDescription: "반짝이는 파티클 효과를 추가합니다." sparkleDescription: "반짝이는 파티클 효과를 추가합니다."
rotate: "회전"
_reversi: _reversi:
reversi: "리버시" reversi: "리버시"
gameSettings: "대국 설정" gameSettings: "대국 설정"

View File

@ -1,5 +1,193 @@
--- ---
_lang_: "Nederlands" _lang_: "Nederlands"
headlineMisskey: "Netwerk verbonden door notities" headlineMisskey: "Netwerk verbonden door notities"
introMisskey: "Welkom! Misskey is een open source, gedecentraliseerde microblogdienst.\nMaak \"notities\" om je gedachten te delen met iedereen om je heen. 📡\nMet \"reacties\" kun je ook snel je mening geven over berichten van anderen. 👍\nLaten we een nieuwe wereld verkennen! 🚀"
monthAndDay: "{day} {month}"
search: "Zoeken"
notifications: "Meldingen"
username: "Gebruikersnaam"
password: "Wachtwoord"
forgotPassword: "Wachtwoord vergeten"
fetchingAsApObject: "Ophalen vanuit de Fediverse"
ok: "Ok"
gotIt: "Begrepen"
cancel: "Annuleren"
enterUsername: "Voer een gebruikersnaam in"
renotedBy: "Hergedeeld door {user}"
noNotes: "Geen notities"
noNotifications: "Geen meldingen"
instance: "Server"
settings: "Instellingen"
basicSettings: "Basisinstellingen"
otherSettings: "Overige instellingen"
openInWindow: "In een venster openen"
profile: "Profiel"
timeline: "Tijdlijn"
noAccountDescription: "Deze gebruiker heeft nog geen bio geschreven"
login: "Inloggen"
loggingIn: "Aan het inloggen"
logout: "Afmelden"
signup: "Registreren"
uploading: "Bezig met uploaden"
save: "Opslaan"
users: "Gebruikers"
addUser: "Toevoegen gebruiker"
favorite: "Favorieten"
favorites: "Toevoegen aan favorieten"
unfavorite: "Verwijderen uit favorieten"
favorited: "Toegevoegd aan favorieten."
alreadyFavorited: "Al toegevoegd aan favorieten"
cantFavorite: "Kon niet toevoegen aan favorieten"
pin: "Vastmaken aan profielpagina"
unpin: "Losmaken van profielpagina"
copyContent: "Kopiëren inhoud"
copyLink: "Kopiëren link"
delete: "Verwijderen"
deleteAndEdit: "Verwijderen en bewerken"
deleteAndEditConfirm: "Weet je zeker dat je deze notitie wilt verwijderen en dan bewerken? Je verliest alle reacties, herdelingen en antwoorden erop."
addToList: "Aan lijst toevoegen"
sendMessage: "Verstuur bericht"
copyUsername: "Kopiëren gebruikersnaam "
searchUser: "Zoeken een gebruiker"
reply: "Antwoord"
loadMore: "Laad meer"
showMore: "Toon meer"
youGotNewFollower: "volgde jou"
receiveFollowRequest: "Volgverzoek ontvangen"
followRequestAccepted: "Volgverzoek geaccepteerd"
mention: "Vermelding"
mentions: "Vermeldingen"
directNotes: "Directe notities"
importAndExport: "Import / export"
import: "Import"
export: "Export"
files: "Bestanden"
download: "Downloaden"
driveFileDeleteConfirm: "Weet je zeker dat je het bestand \"{name}\" wilt verwijderen? Notities met dit bestand als bijlage worden ook verwijderd."
unfollowConfirm: "Weet je zeker dat je {name} wilt ontvolgen?"
exportRequested: "Je hebt een export aangevraagd. Dit kan een tijdje duren. Het wordt toegevoegd aan je Drive zodra het is voltooid."
importRequested: "Je hebt een import aangevraagd. Dit kan even duren."
lists: "Lijsten"
noLists: "Je hebt geen lijsten"
note: "Notitie"
notes: "Notities"
following: "Volgend"
followers: "Volgers"
followsYou: "Volgt jou"
createList: "Creëer lijst"
manageLists: "Beheren lijsten"
error: "Fout"
somethingHappened: "Er is iets misgegaan."
retry: "Probeer opnieuw"
pageLoadError: "Pagina laden mislukt"
pageLoadErrorDescription: "Dit wordt normaal gesproken veroorzaakt door netwerkfouten of door de cache van de browser. Probeer de cache te wissen en probeer het na een tijdje wachten opnieuw."
serverIsDead: "De server reageert niet. Wacht even en probeer het opnieuw."
youShouldUpgradeClient: "Werk je client bij om deze pagina te zien."
enterListName: "Voer de naam van de lijst in"
privacy: "Privacy"
makeFollowManuallyApprove: "Volgverzoeken vergen een goedkeuring"
defaultNoteVisibility: "Standaard zichtbaarheid"
follow: "Volgen"
followRequest: "Verzoek om te mogen volgen"
followRequests: "Volgverzoeken"
unfollow: "Ontvolgen"
followRequestPending: "Wachten op goedkeuring volgverzoek"
enterEmoji: "Voer een emoji in"
renote: "Herdelen"
unrenote: "Stop herdelen"
renoted: "Herdeeld"
cantRenote: "Dit bericht kan niet worden herdeeld"
cantReRenote: "Een herdeling kan niet worden herdeeld"
quote: "Quote"
pinnedNote: "Vastgemaakte notitie"
pinned: "Vastmaken aan profielpagina"
you: "Jij"
clickToShow: "Klik om te bekijken"
sensitive: "NSFW"
add: "Toevoegen"
reaction: "Reacties"
reactionSettingDescription: "Configureer welke reacties je wilt weergeven in de reactiekiezer."
reactionSettingDescription2: "Sleep om opnieuw te ordenen, Klik om te verwijderen, Druk op \"+\" om toe te voegen"
rememberNoteVisibility: "Vergeet niet de notitie zichtbaarheidsinstellingen"
attachCancel: "Verwijder bijlage"
markAsSensitive: "Markeren als NSFW"
unmarkAsSensitive: "Geen NSFW"
enterFileName: "Invoeren bestandsnaam"
mute: "Dempen"
unmute: "Stop dempen"
block: "Blokkeren"
unblock: "Deblokkeren"
suspend: "Opschorten"
unsuspend: "Heractiveren"
blockConfirm: "Weet je zeker dat je dit account wil blokkeren?"
instances: "Server"
remove: "Verwijderen"
nsfw: "NSFW"
pinnedNotes: "Vastgemaakte notitie"
userList: "Lijsten"
smtpUser: "Gebruikersnaam"
smtpPass: "Wachtwoord"
user: "Gebruikers"
muteThread: "Discussies dempen " muteThread: "Discussies dempen "
unmuteThread: "Dempen van discussie ongedaan maken" unmuteThread: "Dempen van discussie ongedaan maken"
_email:
_follow:
title: "volgde jou"
_mfm:
mention: "Vermelding"
quote: "Quote"
search: "Zoeken"
_theme:
keys:
mention: "Vermelding"
renote: "Herdelen"
_sfx:
note: "Notities"
notification: "Meldingen"
_widgets:
notifications: "Meldingen"
timeline: "Tijdlijn"
_cw:
show: "Laad meer"
_visibility:
followers: "Volgers"
_profile:
username: "Gebruikersnaam"
_exportOrImport:
followingList: "Volgend"
muteList: "Dempen"
blockingList: "Blokkeren"
userLists: "Lijsten"
_pages:
script:
categories:
list: "Lijsten"
blocks:
_join:
arg1: "Lijsten"
_randomPick:
arg1: "Lijsten"
_dailyRandomPick:
arg1: "Lijsten"
_seedRandomPick:
arg2: "Lijsten"
_pick:
arg1: "Lijsten"
_listLen:
arg1: "Lijsten"
types:
array: "Lijsten"
_notification:
youWereFollowed: "volgde jou"
_types:
follow: "Volgend"
mention: "Vermelding"
renote: "Herdelen"
quote: "Quote"
reaction: "Reacties"
_deck:
_columns:
notifications: "Meldingen"
tl: "Tijdlijn"
list: "Lijsten"
mentions: "Vermeldingen"

View File

@ -815,6 +815,7 @@ _mfm:
blur: "Rozmycie" blur: "Rozmycie"
font: "Czcionka" font: "Czcionka"
fontDescription: "Wybiera czcionkę do wyświetlania treści." fontDescription: "Wybiera czcionkę do wyświetlania treści."
rotate: "Obróć"
_reversi: _reversi:
reversi: "Reversi" reversi: "Reversi"
gameSettings: "Ustawienia gry" gameSettings: "Ustawienia gry"

View File

@ -1,22 +1,33 @@
--- ---
_lang_: "Português" _lang_: "Português"
headlineMisskey: "Rede conectada por notas"
monthAndDay: "{day}/{month}" monthAndDay: "{day}/{month}"
search: "Pesquisar" search: "Pesquisar"
notifications: "Notificações" notifications: "Notificações"
username: "Nome de usuário" username: "Nome de usuário"
password: "Senha" password: "Senha"
forgotPassword: "Esqueci a senha"
fetchingAsApObject: "Buscando no Fediverso"
ok: "OK" ok: "OK"
gotIt: "Entendi" gotIt: "Entendi"
cancel: "Cancelar" cancel: "Cancelar"
enterUsername: "Digite o nome de usuário" enterUsername: "Digite o nome de usuário"
renotedBy: "Repostado por {user}" renotedBy: "Repostado por {user}"
noNotes: "Sem posts" noNotes: "Sem posts"
noNotifications: "Sem notificações"
instance: "Instância"
settings: "Configurações" settings: "Configurações"
basicSettings: "Configurações básicas" basicSettings: "Configurações básicas"
otherSettings: "Outras configurações" otherSettings: "Outras configurações"
openInWindow: "Abrir numa janela"
profile: "Perfil" profile: "Perfil"
timeline: "Timeline" timeline: "Timeline"
login: "Iniciar sessão"
loggingIn: "Iniciando sessão…"
logout: "Sair" logout: "Sair"
signup: "Registrar-se"
uploading: "Enviando…"
save: "Guardar"
users: "Usuários" users: "Usuários"
favorite: "Favoritar" favorite: "Favoritar"
favorites: "Favoritar" favorites: "Favoritar"

View File

@ -922,6 +922,7 @@ _mfm:
rainbowDescription: "Заставлять содержимое отображаться в цветах радуги." rainbowDescription: "Заставлять содержимое отображаться в цветах радуги."
sparkle: "Блеск" sparkle: "Блеск"
sparkleDescription: "Добавьте эффект искрящихся частиц." sparkleDescription: "Добавьте эффект искрящихся частиц."
rotate: "Повернуть"
_reversi: _reversi:
reversi: "Реверси" reversi: "Реверси"
gameSettings: "Настройки игры" gameSettings: "Настройки игры"

View File

@ -771,6 +771,7 @@ _mfm:
blurDescription: "Цей ефект зробить контент розмитим. Контент можна зробити чітким, якщо навести на нього вказівник миші." blurDescription: "Цей ефект зробить контент розмитим. Контент можна зробити чітким, якщо навести на нього вказівник миші."
font: "Шрифт" font: "Шрифт"
fontDescription: "Встановлює шрифт для контенту." fontDescription: "Встановлює шрифт для контенту."
rotate: "Обертати"
_reversi: _reversi:
reversi: "Реверсі" reversi: "Реверсі"
gameSettings: "Налаштування гри" gameSettings: "Налаштування гри"

View File

@ -792,6 +792,7 @@ pubSub: "Pub/Sub账户"
lastCommunication: "最近通信" lastCommunication: "最近通信"
resolved: "已解决" resolved: "已解决"
unresolved: "未解决" unresolved: "未解决"
breakFollow: "移除关注者"
itsOn: "已开启" itsOn: "已开启"
itsOff: "已关闭" itsOff: "已关闭"
emailRequiredForSignup: "注册账户需要电子邮件地址" emailRequiredForSignup: "注册账户需要电子邮件地址"
@ -808,6 +809,8 @@ ffVisibility: "连接的可见范围"
ffVisibilityDescription: "您可以设置您的关注/关注者信息的公开范围" ffVisibilityDescription: "您可以设置您的关注/关注者信息的公开范围"
continueThread: "查看更多帖子" continueThread: "查看更多帖子"
deleteAccountConfirm: "将要删除账户。是否确认?" deleteAccountConfirm: "将要删除账户。是否确认?"
incorrectPassword: "密码错误"
voteConfirm: "确定投给“{choice}” "
_emailUnavailable: _emailUnavailable:
used: "已经被使用过" used: "已经被使用过"
format: "无效的格式" format: "无效的格式"
@ -931,6 +934,8 @@ _mfm:
rainbowDescription: "用彩虹色来显示内容。" rainbowDescription: "用彩虹色来显示内容。"
sparkle: "闪光" sparkle: "闪光"
sparkleDescription: "添加发光粒子效果。" sparkleDescription: "添加发光粒子效果。"
rotate: "旋转"
rotateDescription: "旋转指定的角度。"
_reversi: _reversi:
reversi: "黑白棋" reversi: "黑白棋"
gameSettings: "对局设置" gameSettings: "对局设置"

View File

@ -840,6 +840,7 @@ _mfm:
blur: "模糊" blur: "模糊"
font: "字型" font: "字型"
fontDescription: "您可以設定顯示內容的字型" fontDescription: "您可以設定顯示內容的字型"
rotate: "旋轉"
_reversi: _reversi:
reversi: "黑白棋" reversi: "黑白棋"
gameSettings: "對弈設定" gameSettings: "對弈設定"

View File

@ -1,6 +1,6 @@
{ {
"name": "misskey", "name": "misskey",
"version": "12.97.1", "version": "12.98.0",
"codename": "indigo", "codename": "indigo",
"repository": { "repository": {
"type": "git", "type": "git",
@ -46,7 +46,7 @@
"@types/fluent-ffmpeg": "2.1.17", "@types/fluent-ffmpeg": "2.1.17",
"@typescript-eslint/parser": "5.4.0", "@typescript-eslint/parser": "5.4.0",
"cross-env": "7.0.3", "cross-env": "7.0.3",
"cypress": "9.0.0", "cypress": "9.1.0",
"start-server-and-test": "1.14.0", "start-server-and-test": "1.14.0",
"typescript": "4.5.2" "typescript": "4.5.2"
} }

View File

@ -19,6 +19,7 @@ export type FileInfo = {
}; };
width?: number; width?: number;
height?: number; height?: number;
orientation?: number;
blurhash?: string; blurhash?: string;
warnings: string[]; warnings: string[];
}; };
@ -47,6 +48,7 @@ export async function getFileInfo(path: string): Promise<FileInfo> {
// image dimensions // image dimensions
let width: number | undefined; let width: number | undefined;
let height: number | undefined; let height: number | undefined;
let orientation: number | undefined;
if (['image/jpeg', 'image/gif', 'image/png', 'image/apng', 'image/webp', 'image/bmp', 'image/tiff', 'image/svg+xml', 'image/vnd.adobe.photoshop'].includes(type.mime)) { if (['image/jpeg', 'image/gif', 'image/png', 'image/apng', 'image/webp', 'image/bmp', 'image/tiff', 'image/svg+xml', 'image/vnd.adobe.photoshop'].includes(type.mime)) {
const imageSize = await detectImageSize(path).catch(e => { const imageSize = await detectImageSize(path).catch(e => {
@ -61,6 +63,7 @@ export async function getFileInfo(path: string): Promise<FileInfo> {
} else if (imageSize.wUnits === 'px') { } else if (imageSize.wUnits === 'px') {
width = imageSize.width; width = imageSize.width;
height = imageSize.height; height = imageSize.height;
orientation = imageSize.orientation;
// 制限を超えている画像は octet-stream にする // 制限を超えている画像は octet-stream にする
if (imageSize.width > 16383 || imageSize.height > 16383) { if (imageSize.width > 16383 || imageSize.height > 16383) {
@ -87,6 +90,7 @@ export async function getFileInfo(path: string): Promise<FileInfo> {
type, type,
width, width,
height, height,
orientation,
blurhash, blurhash,
warnings, warnings,
}; };
@ -163,6 +167,7 @@ async function detectImageSize(path: string): Promise<{
height: number; height: number;
wUnits: string; wUnits: string;
hUnits: string; hUnits: string;
orientation?: number;
}> { }> {
const readable = fs.createReadStream(path); const readable = fs.createReadStream(path);
const imageSize = await probeImageSize(readable); const imageSize = await probeImageSize(readable);

View File

@ -77,7 +77,7 @@ export class DriveFile {
default: {}, default: {},
comment: 'The any properties of the DriveFile. For example, it includes image width/height.' comment: 'The any properties of the DriveFile. For example, it includes image width/height.'
}) })
public properties: { width?: number; height?: number; avgColor?: string }; public properties: { width?: number; height?: number; orientation?: number; avgColor?: string };
@Index() @Index()
@Column('boolean') @Column('boolean')

View File

@ -28,6 +28,19 @@ export class DriveFileRepository extends Repository<DriveFile> {
); );
} }
public getPublicProperties(file: DriveFile): DriveFile['properties'] {
if (file.properties.orientation != null) {
const properties = JSON.parse(JSON.stringify(file.properties));
if (file.properties.orientation >= 5) {
[properties.width, properties.height] = [properties.height, properties.width];
}
properties.orientation = undefined;
return properties;
}
return file.properties;
}
public getPublicUrl(file: DriveFile, thumbnail = false, meta?: Meta): string | null { public getPublicUrl(file: DriveFile, thumbnail = false, meta?: Meta): string | null {
// リモートかつメディアプロキシ // リモートかつメディアプロキシ
if (file.uri != null && file.userHost != null && config.mediaProxy != null) { if (file.uri != null && file.userHost != null && config.mediaProxy != null) {
@ -122,7 +135,7 @@ export class DriveFileRepository extends Repository<DriveFile> {
size: file.size, size: file.size,
isSensitive: file.isSensitive, isSensitive: file.isSensitive,
blurhash: file.blurhash, blurhash: file.blurhash,
properties: file.properties, properties: opts.self ? file.properties : this.getPublicProperties(file),
url: opts.self ? file.url : this.getPublicUrl(file, false, meta), url: opts.self ? file.url : this.getPublicUrl(file, false, meta),
thumbnailUrl: this.getPublicUrl(file, true, meta), thumbnailUrl: this.getPublicUrl(file, true, meta),
comment: file.comment, comment: file.comment,
@ -202,6 +215,11 @@ export const packedDriveFileSchema = {
optional: true as const, nullable: false as const, optional: true as const, nullable: false as const,
example: 720 example: 720
}, },
orientation: {
type: 'number' as const,
optional: true as const, nullable: false as const,
example: 8
},
avgColor: { avgColor: {
type: 'string' as const, type: 'string' as const,
optional: true as const, nullable: false as const, optional: true as const, nullable: false as const,

View File

@ -189,12 +189,12 @@ export class UserRepository extends Repository<User> {
const followingCount = profile == null ? null : const followingCount = profile == null ? null :
(profile.ffVisibility === 'public') || (meId === user.id) ? user.followingCount : (profile.ffVisibility === 'public') || (meId === user.id) ? user.followingCount :
(profile.ffVisibility === 'followers') && (relation!.isFollowing) ? user.followingCount : (profile.ffVisibility === 'followers') && (relation && relation.isFollowing) ? user.followingCount :
null; null;
const followersCount = profile == null ? null : const followersCount = profile == null ? null :
(profile.ffVisibility === 'public') || (meId === user.id) ? user.followersCount : (profile.ffVisibility === 'public') || (meId === user.id) ? user.followersCount :
(profile.ffVisibility === 'followers') && (relation!.isFollowing) ? user.followersCount : (profile.ffVisibility === 'followers') && (relation && relation.isFollowing) ? user.followersCount :
null; null;
const falsy = opts.detail ? false : undefined; const falsy = opts.detail ? false : undefined;

View File

@ -1,8 +1,9 @@
import { IRemoteUser } from '@/models/entities/user'; import { IRemoteUser } from '@/models/entities/user';
import reject from '@/services/following/requests/reject'; import { remoteReject } from '@/services/following/reject';
import { IFollow } from '../../type'; import { IFollow } from '../../type';
import DbResolver from '../../db-resolver'; import DbResolver from '../../db-resolver';
import { relayRejected } from '@/services/relay'; import { relayRejected } from '@/services/relay';
import { Users } from '@/models';
export default async (actor: IRemoteUser, activity: IFollow): Promise<string> => { export default async (actor: IRemoteUser, activity: IFollow): Promise<string> => {
// ※ activityはこっちから投げたフォローリクエストなので、activity.actorは存在するローカルユーザーである必要がある // ※ activityはこっちから投げたフォローリクエストなので、activity.actorは存在するローカルユーザーである必要がある
@ -14,7 +15,7 @@ export default async (actor: IRemoteUser, activity: IFollow): Promise<string> =>
return `skip: follower not found`; return `skip: follower not found`;
} }
if (follower.host != null) { if (!Users.isLocalUser(follower)) {
return `skip: follower is not a local user`; return `skip: follower is not a local user`;
} }
@ -24,6 +25,6 @@ export default async (actor: IRemoteUser, activity: IFollow): Promise<string> =>
return await relayRejected(match[1]); return await relayRejected(match[1]);
} }
await reject(actor, follower); await remoteReject(actor, follower);
return `ok`; return `ok`;
}; };

View File

@ -0,0 +1,27 @@
import unfollow from '@/services/following/delete';
import cancelRequest from '@/services/following/requests/cancel';
import {IAccept} from '../../type';
import { IRemoteUser } from '@/models/entities/user';
import { Followings } from '@/models/index';
import DbResolver from '../../db-resolver';
export default async (actor: IRemoteUser, activity: IAccept): Promise<string> => {
const dbResolver = new DbResolver();
const follower = await dbResolver.getUserFromApId(activity.object);
if (follower == null) {
return `skip: follower not found`;
}
const following = await Followings.findOne({
followerId: follower.id,
followeeId: actor.id
});
if (following) {
await unfollow(follower, actor);
return `ok: unfollowed`;
}
return `skip: フォローされていない`;
};

View File

@ -1,8 +1,9 @@
import { IRemoteUser } from '@/models/entities/user'; import { IRemoteUser } from '@/models/entities/user';
import { IUndo, isFollow, isBlock, isLike, isAnnounce, getApType } from '../../type'; import {IUndo, isFollow, isBlock, isLike, isAnnounce, getApType, isAccept} from '../../type';
import unfollow from './follow'; import unfollow from './follow';
import unblock from './block'; import unblock from './block';
import undoLike from './like'; import undoLike from './like';
import undoAccept from './accept';
import { undoAnnounce } from './announce'; import { undoAnnounce } from './announce';
import Resolver from '../../resolver'; import Resolver from '../../resolver';
import { apLogger } from '../../logger'; import { apLogger } from '../../logger';
@ -29,6 +30,7 @@ export default async (actor: IRemoteUser, activity: IUndo): Promise<string> => {
if (isBlock(object)) return await unblock(actor, object); if (isBlock(object)) return await unblock(actor, object);
if (isLike(object)) return await undoLike(actor, object); if (isLike(object)) return await undoLike(actor, object);
if (isAnnounce(object)) return await undoAnnounce(actor, object); if (isAnnounce(object)) return await undoAnnounce(actor, object);
if (isAccept(object)) return await undoAccept(actor, object);
return `skip: unknown object type ${getApType(object)}`; return `skip: unknown object type ${getApType(object)}`;
}; };

View File

@ -33,6 +33,14 @@ export const meta = {
untilId: { untilId: {
validator: $.optional.type(ID), validator: $.optional.type(ID),
}, },
sinceDate: {
validator: $.optional.num,
},
untilDate: {
validator: $.optional.num,
},
}, },
errors: { errors: {
@ -68,7 +76,8 @@ export default define(meta, async (ps, user) => {
.select('joining.noteId') .select('joining.noteId')
.where('joining.antennaId = :antennaId', { antennaId: antenna.id }); .where('joining.antennaId = :antennaId', { antennaId: antenna.id });
const query = makePaginationQuery(Notes.createQueryBuilder('note'), ps.sinceId, ps.untilId) const query = makePaginationQuery(Notes.createQueryBuilder('note'),
ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate)
.andWhere(`note.id IN (${ antennaQuery.getQuery() })`) .andWhere(`note.id IN (${ antennaQuery.getQuery() })`)
.innerJoinAndSelect('note.user', 'user') .innerJoinAndSelect('note.user', 'user')
.leftJoinAndSelect('note.reply', 'reply') .leftJoinAndSelect('note.reply', 'reply')

View File

@ -0,0 +1,82 @@
import $ from 'cafy';
import { ID } from '@/misc/cafy-id';
import * as ms from 'ms';
import deleteFollowing from '@/services/following/delete';
import define from '../../define';
import { ApiError } from '../../error';
import { getUser } from '../../common/getters';
import { Followings, Users } from '@/models/index';
export const meta = {
tags: ['following', 'users'],
limit: {
duration: ms('1hour'),
max: 100
},
requireCredential: true as const,
kind: 'write:following',
params: {
userId: {
validator: $.type(ID),
}
},
errors: {
noSuchUser: {
message: 'No such user.',
code: 'NO_SUCH_USER',
id: '5b12c78d-2b28-4dca-99d2-f56139b42ff8'
},
followerIsYourself: {
message: 'Follower is yourself.',
code: 'FOLLOWER_IS_YOURSELF',
id: '07dc03b9-03da-422d-885b-438313707662'
},
notFollowing: {
message: 'The other use is not following you.',
code: 'NOT_FOLLOWING',
id: '5dbf82f5-c92b-40b1-87d1-6c8c0741fd09'
},
},
res: {
type: 'object' as const,
optional: false as const, nullable: false as const,
ref: 'User'
}
};
export default define(meta, async (ps, user) => {
const followee = user;
// Check if the follower is yourself
if (user.id === ps.userId) {
throw new ApiError(meta.errors.followerIsYourself);
}
// Get follower
const follower = await getUser(ps.userId).catch(e => {
if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser);
throw e;
});
// Check not following
const exist = await Followings.findOne({
followerId: follower.id,
followeeId: followee.id
});
if (exist == null) {
throw new ApiError(meta.errors.notFollowing);
}
await deleteFollowing(follower, followee);
return await Users.pack(followee.id, user);
});

View File

@ -1,6 +1,6 @@
import $ from 'cafy'; import $ from 'cafy';
import { ID } from '@/misc/cafy-id'; import { ID } from '@/misc/cafy-id';
import rejectFollowRequest from '@/services/following/requests/reject'; import { rejectFollowRequest } from '@/services/following/reject';
import define from '../../../define'; import define from '../../../define';
import { ApiError } from '../../../error'; import { ApiError } from '../../../error';
import { getUser } from '../../../common/getters'; import { getUser } from '../../../common/getters';

View File

@ -372,12 +372,16 @@ export default async function(
const properties: { const properties: {
width?: number; width?: number;
height?: number; height?: number;
orientation?: number;
} = {}; } = {};
if (info.width) { if (info.width) {
properties['width'] = info.width; properties['width'] = info.width;
properties['height'] = info.height; properties['height'] = info.height;
} }
if (info.orientation != null) {
properties['orientation'] = info.orientation;
}
const profile = user ? await UserProfiles.findOne(user.id) : null; const profile = user ? await UserProfiles.findOne(user.id) : null;

View File

@ -2,6 +2,7 @@ import { publishMainStream, publishUserEvent } from '@/services/stream';
import { renderActivity } from '@/remote/activitypub/renderer/index'; import { renderActivity } from '@/remote/activitypub/renderer/index';
import renderFollow from '@/remote/activitypub/renderer/follow'; import renderFollow from '@/remote/activitypub/renderer/follow';
import renderUndo from '@/remote/activitypub/renderer/undo'; import renderUndo from '@/remote/activitypub/renderer/undo';
import renderReject from '@/remote/activitypub/renderer/reject';
import { deliver } from '@/queue/index'; import { deliver } from '@/queue/index';
import Logger from '../logger'; import Logger from '../logger';
import { registerOrFetchInstanceDoc } from '../register-or-fetch-instance-doc'; import { registerOrFetchInstanceDoc } from '../register-or-fetch-instance-doc';
@ -40,6 +41,12 @@ export default async function(follower: { id: User['id']; host: User['host']; ur
const content = renderActivity(renderUndo(renderFollow(follower, followee), follower)); const content = renderActivity(renderUndo(renderFollow(follower, followee), follower));
deliver(follower, content, followee.inbox); deliver(follower, content, followee.inbox);
} }
if (Users.isLocalUser(followee) && Users.isRemoteUser(follower)) {
// local user has null host
const content = renderActivity(renderReject(renderFollow(follower, followee), followee));
deliver(followee, content, follower.inbox);
}
} }
export async function decrementFollowing(follower: { id: User['id']; host: User['host']; }, followee: { id: User['id']; host: User['host']; }) { export async function decrementFollowing(follower: { id: User['id']; host: User['host']; }, followee: { id: User['id']; host: User['host']; }) {

View File

@ -0,0 +1,105 @@
import { renderActivity } from '@/remote/activitypub/renderer/index';
import renderFollow from '@/remote/activitypub/renderer/follow';
import renderReject from '@/remote/activitypub/renderer/reject';
import { deliver } from '@/queue/index';
import { publishMainStream, publishUserEvent } from '@/services/stream';
import { User, ILocalUser, IRemoteUser } from '@/models/entities/user';
import { Users, FollowRequests, Followings } from '@/models/index';
import { decrementFollowing } from './delete';
type Local = ILocalUser | { id: User['id']; host: User['host']; uri: User['host'] };
type Remote = IRemoteUser;
type Both = Local | Remote;
/**
* API following/request/reject
*/
export async function rejectFollowRequest(user: Local, follower: Both) {
if (Users.isRemoteUser(follower)) {
deliverReject(user, follower);
}
await removeFollowRequest(user, follower);
if (Users.isLocalUser(follower)) {
publishUnfollow(user, follower);
}
}
/**
* API following/reject
*/
export async function rejectFollow(user: Local, follower: Both) {
if (Users.isRemoteUser(follower)) {
deliverReject(user, follower);
}
await removeFollow(user, follower);
if (Users.isLocalUser(follower)) {
publishUnfollow(user, follower);
}
}
/**
* AP Reject/Follow
*/
export async function remoteReject(actor: Remote, follower: Local) {
await removeFollowRequest(actor, follower);
await removeFollow(actor, follower);
publishUnfollow(actor, follower);
}
/**
* Remove follow request record
*/
async function removeFollowRequest(followee: Both, follower: Both) {
const request = await FollowRequests.findOne({
followeeId: followee.id,
followerId: follower.id
});
if (!request) return;
await FollowRequests.delete(request.id);
}
/**
* Remove follow record
*/
async function removeFollow(followee: Both, follower: Both) {
const following = await Followings.findOne({
followeeId: followee.id,
followerId: follower.id
});
if (!following) return;
await Followings.delete(following.id);
decrementFollowing(follower, followee);
}
/**
* Deliver Reject to remote
*/
async function deliverReject(followee: Local, follower: Remote) {
const request = await FollowRequests.findOne({
followeeId: followee.id,
followerId: follower.id
});
const content = renderActivity(renderReject(renderFollow(follower, followee, request?.requestId || undefined), followee));
deliver(followee, content, follower.inbox);
}
/**
* Publish unfollow to local
*/
async function publishUnfollow(followee: Both, follower: Local) {
const packedFollowee = await Users.pack(followee.id, follower, {
detail: true
});
publishUserEvent(follower.id, 'unfollow', packedFollowee);
publishMainStream(follower.id, 'unfollow', packedFollowee);
}

View File

@ -1,46 +0,0 @@
import { renderActivity } from '@/remote/activitypub/renderer/index';
import renderFollow from '@/remote/activitypub/renderer/follow';
import renderReject from '@/remote/activitypub/renderer/reject';
import { deliver } from '@/queue/index';
import { publishMainStream, publishUserEvent } from '@/services/stream';
import { User, ILocalUser } from '@/models/entities/user';
import { Users, FollowRequests, Followings } from '@/models/index';
import { decrementFollowing } from '../delete';
export default async function(followee: { id: User['id']; host: User['host']; uri: User['host'] }, follower: User) {
if (Users.isRemoteUser(follower) && Users.isLocalUser(followee)) {
const request = await FollowRequests.findOne({
followeeId: followee.id,
followerId: follower.id
});
const content = renderActivity(renderReject(renderFollow(follower, followee, request!.requestId!), followee));
deliver(followee, content, follower.inbox);
}
const request = await FollowRequests.findOne({
followeeId: followee.id,
followerId: follower.id
});
if (request) {
await FollowRequests.delete(request.id);
} else {
const following = await Followings.findOne({
followeeId: followee.id,
followerId: follower.id
});
if (following) {
await Followings.delete(following.id);
decrementFollowing(follower, followee);
}
}
Users.pack(followee.id, follower, {
detail: true
}).then(packed => {
publishUserEvent(follower.id, 'unfollow', packed);
publishMainStream(follower.id, 'unfollow', packed);
});
}

View File

@ -1,7 +1,10 @@
import { Emojis } from '@/models/index'; import { initDb } from '@/db/postgre';
import { genId } from '@/misc/gen-id'; import { genId } from '@/misc/gen-id';
async function main(name: string, url: string, alias?: string): Promise<any> { async function main(name: string, url: string, alias?: string): Promise<any> {
await initDb();
const { Emojis } = await import('@/models/index');
const aliases = alias != null ? [ alias ] : []; const aliases = alias != null ? [ alias ] : [];
await Emojis.save({ await Emojis.save({

View File

@ -1,13 +1,11 @@
import { initDb } from '../db/postgre'; import { initDb } from '../db/postgre';
import { getRepository } from 'typeorm';
import { User } from '@/models/entities/user';
async function main(username: string) { async function main(username: string) {
if (!username) throw `username required`; if (!username) throw `username required`;
username = username.replace(/^@/, ''); username = username.replace(/^@/, '');
await initDb(); await initDb();
const Users = getRepository(User); const { Users } = await import('@/models/index');
const res = await Users.update({ const res = await Users.update({
usernameLower: username.toLowerCase(), usernameLower: username.toLowerCase(),

View File

@ -1,13 +1,11 @@
import { initDb } from '../db/postgre'; import { initDb } from '../db/postgre';
import { getRepository } from 'typeorm';
import { User } from '@/models/entities/user';
async function main(username: string) { async function main(username: string) {
if (!username) throw `username required`; if (!username) throw `username required`;
username = username.replace(/^@/, ''); username = username.replace(/^@/, '');
await initDb(); await initDb();
const Users = getRepository(User); const { Users } = await import('@/models/index');
const res = await Users.update({ const res = await Users.update({
usernameLower: username.toLowerCase(), usernameLower: username.toLowerCase(),

View File

@ -1,6 +1,9 @@
import { updateQuestion } from '@/remote/activitypub/models/question'; import { initDb } from '@/db/postgre';
async function main(uri: string): Promise<any> { async function main(uri: string): Promise<any> {
await initDb();
const { updateQuestion } = await import('@/remote/activitypub/models/question');
return await updateQuestion(uri); return await updateQuestion(uri);
} }

View File

@ -1,4 +1,4 @@
import { Users, Signins } from '@/models/index'; import { initDb } from '@/db/postgre';
// node built/tools/show-signin-history username // node built/tools/show-signin-history username
// => {Success} {Date} {IPAddrsss} // => {Success} {Date} {IPAddrsss}
@ -10,6 +10,9 @@ import { Users, Signins } from '@/models/index';
// with full request headers // with full request headers
async function main(username: string, headers?: string[]) { async function main(username: string, headers?: string[]) {
await initDb();
const { Users, Signins } = await import('@/models/index');
const user = await Users.findOne({ const user = await Users.findOne({
host: null, host: null,
usernameLower: username.toLowerCase(), usernameLower: username.toLowerCase(),

View File

@ -17,6 +17,7 @@ describe('Get file info', () => {
}, },
width: undefined, width: undefined,
height: undefined, height: undefined,
orientation: undefined,
}); });
})); }));
@ -34,6 +35,7 @@ describe('Get file info', () => {
}, },
width: 512, width: 512,
height: 512, height: 512,
orientation: undefined,
}); });
})); }));
@ -51,6 +53,7 @@ describe('Get file info', () => {
}, },
width: 256, width: 256,
height: 256, height: 256,
orientation: undefined,
}); });
})); }));
@ -68,6 +71,7 @@ describe('Get file info', () => {
}, },
width: 256, width: 256,
height: 256, height: 256,
orientation: undefined,
}); });
})); }));
@ -85,6 +89,7 @@ describe('Get file info', () => {
}, },
width: 256, width: 256,
height: 256, height: 256,
orientation: undefined,
}); });
})); }));
@ -102,6 +107,7 @@ describe('Get file info', () => {
}, },
width: 256, width: 256,
height: 256, height: 256,
orientation: undefined,
}); });
})); }));
@ -120,6 +126,7 @@ describe('Get file info', () => {
}, },
width: 256, width: 256,
height: 256, height: 256,
orientation: undefined,
}); });
})); }));
@ -137,6 +144,25 @@ describe('Get file info', () => {
}, },
width: 25000, width: 25000,
height: 25000, height: 25000,
orientation: undefined,
});
}));
it('Rotate JPEG', async (async () => {
const path = `${__dirname}/resources/rotate.jpg`;
const info = await getFileInfo(path) as any;
delete info.warnings;
delete info.blurhash;
assert.deepStrictEqual(info, {
size: 12624,
md5: '68d5b2d8d1d1acbbce99203e3ec3857e',
type: {
mime: 'image/jpeg',
ext: 'jpg'
},
width: 512,
height: 256,
orientation: 8,
}); });
})); }));
}); });

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

@ -1,26 +0,0 @@
"use strict";
// ex) node built/tools/accept-migration Yo 1000000000001
Object.defineProperty(exports, "__esModule", { value: true });
const typeorm_1 = require("typeorm");
const index_1 = require("@/config/index");
(0, typeorm_1.createConnection)({
type: 'postgres',
host: index_1.default.db.host,
port: index_1.default.db.port,
username: index_1.default.db.user,
password: index_1.default.db.pass,
database: index_1.default.db.db,
extra: index_1.default.db.extra,
synchronize: false,
dropSchema: false,
}).then(c => {
c.query(`INSERT INTO migrations(timestamp,name) VALUES (${process.argv[3]}, '${process.argv[2]}${process.argv[3]}');`).then(() => {
console.log('done');
process.exit(0);
}).catch(e => {
console.log('ERROR:');
console.log(e);
process.exit(1);
});
});
//# sourceMappingURL=accept-migration.js.map

View File

@ -1 +0,0 @@
{"version":3,"file":"accept-migration.js","sourceRoot":"","sources":["accept-migration.ts"],"names":[],"mappings":";AAAA,yDAAyD;;AAEzD,qCAA2C;AAC3C,0CAAoC;AAEpC,IAAA,0BAAgB,EAAC;IAChB,IAAI,EAAE,UAAU;IAChB,IAAI,EAAE,eAAM,CAAC,EAAE,CAAC,IAAI;IACpB,IAAI,EAAE,eAAM,CAAC,EAAE,CAAC,IAAI;IACpB,QAAQ,EAAE,eAAM,CAAC,EAAE,CAAC,IAAI;IACxB,QAAQ,EAAE,eAAM,CAAC,EAAE,CAAC,IAAI;IACxB,QAAQ,EAAE,eAAM,CAAC,EAAE,CAAC,EAAE;IACtB,KAAK,EAAE,eAAM,CAAC,EAAE,CAAC,KAAK;IACtB,WAAW,EAAE,KAAK;IAClB,UAAU,EAAE,KAAK;CACjB,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE;IACX,CAAC,CAAC,KAAK,CAAC,kDAAkD,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,GAAG,EAAE;QAChI,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;QACpB,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IACjB,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE;QACZ,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;QACtB,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;QACf,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IACjB,CAAC,CAAC,CAAC;AACJ,CAAC,CAAC,CAAC"}

View File

@ -1,30 +0,0 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const index_1 = require("@/models/index");
const gen_id_1 = require("@/misc/gen-id");
async function main(name, url, alias) {
const aliases = alias != null ? [alias] : [];
await index_1.Emojis.save({
id: (0, gen_id_1.genId)(),
host: null,
name,
url,
aliases,
updatedAt: new Date()
});
}
const args = process.argv.slice(2);
const name = args[0];
const url = args[1];
if (!name)
throw new Error('require name');
if (!url)
throw new Error('require url');
main(name, url).then(() => {
console.log('success');
process.exit(0);
}).catch(e => {
console.warn(e);
process.exit(1);
});
//# sourceMappingURL=add-emoji.js.map

View File

@ -1 +0,0 @@
{"version":3,"file":"add-emoji.js","sourceRoot":"","sources":["add-emoji.ts"],"names":[],"mappings":";;AAAA,0CAAwC;AACxC,0CAAsC;AAEtC,KAAK,UAAU,IAAI,CAAC,IAAY,EAAE,GAAW,EAAE,KAAc;IAC5D,MAAM,OAAO,GAAG,KAAK,IAAI,IAAI,CAAC,CAAC,CAAC,CAAE,KAAK,CAAE,CAAC,CAAC,CAAC,EAAE,CAAC;IAE/C,MAAM,cAAM,CAAC,IAAI,CAAC;QACjB,EAAE,EAAE,IAAA,cAAK,GAAE;QACX,IAAI,EAAE,IAAI;QACV,IAAI;QACJ,GAAG;QACH,OAAO;QACP,SAAS,EAAE,IAAI,IAAI,EAAE;KACrB,CAAC,CAAC;AACJ,CAAC;AAED,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;AACnC,MAAM,IAAI,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC;AACrB,MAAM,GAAG,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC;AAEpB,IAAI,CAAC,IAAI;IAAE,MAAM,IAAI,KAAK,CAAC,cAAc,CAAC,CAAC;AAC3C,IAAI,CAAC,GAAG;IAAE,MAAM,IAAI,KAAK,CAAC,aAAa,CAAC,CAAC;AAEzC,IAAI,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC,IAAI,CAAC,GAAG,EAAE;IACzB,OAAO,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;IACvB,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AACjB,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE;IACZ,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAChB,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AACjB,CAAC,CAAC,CAAC"}

View File

@ -1,30 +0,0 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const postgre_1 = require("../db/postgre");
const typeorm_1 = require("typeorm");
const user_1 = require("@/models/entities/user");
async function main(username) {
if (!username)
throw `username required`;
username = username.replace(/^@/, '');
await (0, postgre_1.initDb)();
const Users = (0, typeorm_1.getRepository)(user_1.User);
const res = await Users.update({
usernameLower: username.toLowerCase(),
host: null
}, {
isAdmin: false
});
if (res.affected !== 1) {
throw 'Failed';
}
}
const args = process.argv.slice(2);
main(args[0]).then(() => {
console.log('Success');
process.exit(0);
}).catch(e => {
console.error(`Error: ${e.message || e}`);
process.exit(1);
});
//# sourceMappingURL=demote-admin.js.map

View File

@ -1 +0,0 @@
{"version":3,"file":"demote-admin.js","sourceRoot":"","sources":["demote-admin.ts"],"names":[],"mappings":";;AAAA,2CAAuC;AACvC,qCAAwC;AACxC,iDAA8C;AAE9C,KAAK,UAAU,IAAI,CAAC,QAAgB;IACnC,IAAI,CAAC,QAAQ;QAAE,MAAM,mBAAmB,CAAC;IACzC,QAAQ,GAAG,QAAQ,CAAC,OAAO,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;IAEtC,MAAM,IAAA,gBAAM,GAAE,CAAC;IACf,MAAM,KAAK,GAAG,IAAA,uBAAa,EAAC,WAAI,CAAC,CAAC;IAElC,MAAM,GAAG,GAAG,MAAM,KAAK,CAAC,MAAM,CAAC;QAC9B,aAAa,EAAE,QAAQ,CAAC,WAAW,EAAE;QACrC,IAAI,EAAE,IAAI;KACV,EAAE;QACF,OAAO,EAAE,KAAK;KACd,CAAC,CAAC;IAEH,IAAI,GAAG,CAAC,QAAQ,KAAK,CAAC,EAAE;QACvB,MAAM,QAAQ,CAAC;KACf;AACF,CAAC;AAED,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;AAEnC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,EAAE;IACvB,OAAO,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;IACvB,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AACjB,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE;IACZ,OAAO,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC,OAAO,IAAI,CAAC,EAAE,CAAC,CAAC;IAC1C,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AACjB,CAAC,CAAC,CAAC"}

View File

@ -1,30 +0,0 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const postgre_1 = require("../db/postgre");
const typeorm_1 = require("typeorm");
const user_1 = require("@/models/entities/user");
async function main(username) {
if (!username)
throw `username required`;
username = username.replace(/^@/, '');
await (0, postgre_1.initDb)();
const Users = (0, typeorm_1.getRepository)(user_1.User);
const res = await Users.update({
usernameLower: username.toLowerCase(),
host: null
}, {
isAdmin: true
});
if (res.affected !== 1) {
throw 'Failed';
}
}
const args = process.argv.slice(2);
main(args[0]).then(() => {
console.log('Success');
process.exit(0);
}).catch(e => {
console.error(`Error: ${e.message || e}`);
process.exit(1);
});
//# sourceMappingURL=mark-admin.js.map

View File

@ -1 +0,0 @@
{"version":3,"file":"mark-admin.js","sourceRoot":"","sources":["mark-admin.ts"],"names":[],"mappings":";;AAAA,2CAAuC;AACvC,qCAAwC;AACxC,iDAA8C;AAE9C,KAAK,UAAU,IAAI,CAAC,QAAgB;IACnC,IAAI,CAAC,QAAQ;QAAE,MAAM,mBAAmB,CAAC;IACzC,QAAQ,GAAG,QAAQ,CAAC,OAAO,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;IAEtC,MAAM,IAAA,gBAAM,GAAE,CAAC;IACf,MAAM,KAAK,GAAG,IAAA,uBAAa,EAAC,WAAI,CAAC,CAAC;IAElC,MAAM,GAAG,GAAG,MAAM,KAAK,CAAC,MAAM,CAAC;QAC9B,aAAa,EAAE,QAAQ,CAAC,WAAW,EAAE;QACrC,IAAI,EAAE,IAAI;KACV,EAAE;QACF,OAAO,EAAE,IAAI;KACb,CAAC,CAAC;IAEH,IAAI,GAAG,CAAC,QAAQ,KAAK,CAAC,EAAE;QACvB,MAAM,QAAQ,CAAC;KACf;AACF,CAAC;AAED,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;AAEnC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,EAAE;IACvB,OAAO,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;IACvB,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AACjB,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE;IACZ,OAAO,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC,OAAO,IAAI,CAAC,EAAE,CAAC,CAAC;IAC1C,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AACjB,CAAC,CAAC,CAAC"}

View File

@ -1,14 +0,0 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const question_1 = require("@/remote/activitypub/models/question");
async function main(uri) {
return await (0, question_1.updateQuestion)(uri);
}
const args = process.argv.slice(2);
const uri = args[0];
main(uri).then(result => {
console.log(`Done: ${result}`);
}).catch(e => {
console.warn(e);
});
//# sourceMappingURL=refresh-question.js.map

View File

@ -1 +0,0 @@
{"version":3,"file":"refresh-question.js","sourceRoot":"","sources":["refresh-question.ts"],"names":[],"mappings":";;AAAA,mEAAsE;AAEtE,KAAK,UAAU,IAAI,CAAC,GAAW;IAC9B,OAAO,MAAM,IAAA,yBAAc,EAAC,GAAG,CAAC,CAAC;AAClC,CAAC;AAED,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;AACnC,MAAM,GAAG,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC;AAEpB,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE;IACvB,OAAO,CAAC,GAAG,CAAC,SAAS,MAAM,EAAE,CAAC,CAAC;AAChC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE;IACZ,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AACjB,CAAC,CAAC,CAAC"}

View File

@ -1,26 +0,0 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const postgre_1 = require("@/db/postgre");
const Acct = require("misskey-js/built/acct");
async function main(acct) {
await (0, postgre_1.initDb)();
const { resolveUser } = await Promise.resolve().then(() => require('@/remote/resolve-user'));
const { username, host } = Acct.parse(acct);
await resolveUser(username, host, {}, true);
}
// get args
const args = process.argv.slice(2);
let acct = args[0];
// normalize args
acct = acct.replace(/^@/, '');
// check args
if (!acct.match(/^\w+@\w/)) {
throw `Invalid acct format. Valid format are user@host`;
}
console.log(`resync ${acct}`);
main(acct).then(() => {
console.log('Done');
}).catch(e => {
console.warn(e);
});
//# sourceMappingURL=resync-remote-user.js.map

View File

@ -1 +0,0 @@
{"version":3,"file":"resync-remote-user.js","sourceRoot":"","sources":["resync-remote-user.ts"],"names":[],"mappings":";;AAAA,0CAAsC;AACtC,8CAA8C;AAE9C,KAAK,UAAU,IAAI,CAAC,IAAY;IAC/B,MAAM,IAAA,gBAAM,GAAE,CAAC;IACf,MAAM,EAAE,WAAW,EAAE,GAAG,2CAAa,uBAAuB,EAAC,CAAC;IAE9D,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IAC5C,MAAM,WAAW,CAAC,QAAQ,EAAE,IAAI,EAAE,EAAE,EAAE,IAAI,CAAC,CAAC;AAC7C,CAAC;AAED,WAAW;AACX,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;AACnC,IAAI,IAAI,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC;AAEnB,iBAAiB;AACjB,IAAI,GAAG,IAAI,CAAC,OAAO,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;AAE9B,aAAa;AACb,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,SAAS,CAAC,EAAE;IAC3B,MAAM,iDAAiD,CAAC;CACxD;AAED,OAAO,CAAC,GAAG,CAAC,UAAU,IAAI,EAAE,CAAC,CAAC;AAE9B,IAAI,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,GAAG,EAAE;IACpB,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;AACrB,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE;IACZ,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AACjB,CAAC,CAAC,CAAC"}

View File

@ -1,47 +0,0 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const index_1 = require("@/models/index");
// node built/tools/show-signin-history username
// => {Success} {Date} {IPAddrsss}
// node built/tools/show-signin-history username user-agent,x-forwarded-for
// with user-agent and x-forwarded-for
// node built/tools/show-signin-history username all
// with full request headers
async function main(username, headers) {
const user = await index_1.Users.findOne({
host: null,
usernameLower: username.toLowerCase(),
});
if (user == null)
throw new Error('User not found');
const history = await index_1.Signins.find({
userId: user.id
});
for (const signin of history) {
console.log(`${signin.success ? 'OK' : 'NG'} ${signin.createdAt ? signin.createdAt.toISOString() : 'Unknown'} ${signin.ip}`);
// headers
if (headers != null) {
for (const key of Object.keys(signin.headers)) {
if (headers.includes('all') || headers.includes(key)) {
console.log(` ${key}: ${signin.headers[key]}`);
}
}
}
}
}
// get args
const args = process.argv.slice(2);
let username = args[0];
let headers;
if (args[1] != null) {
headers = args[1].split(/,/).map(header => header.toLowerCase());
}
// normalize args
username = username.replace(/^@/, '');
main(username, headers).then(() => {
process.exit(0);
}).catch(e => {
console.warn(e);
process.exit(1);
});
//# sourceMappingURL=show-signin-history.js.map

View File

@ -1 +0,0 @@
{"version":3,"file":"show-signin-history.js","sourceRoot":"","sources":["show-signin-history.ts"],"names":[],"mappings":";;AAAA,0CAAgD;AAEhD,gDAAgD;AAChD,mCAAmC;AAEnC,2EAA2E;AAC3E,uCAAuC;AAEvC,oDAAoD;AACpD,6BAA6B;AAE7B,KAAK,UAAU,IAAI,CAAC,QAAgB,EAAE,OAAkB;IACvD,MAAM,IAAI,GAAG,MAAM,aAAK,CAAC,OAAO,CAAC;QAChC,IAAI,EAAE,IAAI;QACV,aAAa,EAAE,QAAQ,CAAC,WAAW,EAAE;KACrC,CAAC,CAAC;IAEH,IAAI,IAAI,IAAI,IAAI;QAAE,MAAM,IAAI,KAAK,CAAC,gBAAgB,CAAC,CAAC;IAEpD,MAAM,OAAO,GAAG,MAAM,eAAO,CAAC,IAAI,CAAC;QAClC,MAAM,EAAE,IAAI,CAAC,EAAE;KACf,CAAC,CAAC;IAEH,KAAK,MAAM,MAAM,IAAI,OAAO,EAAE;QAC7B,OAAO,CAAC,GAAG,CAAC,GAAG,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,IAAI,MAAM,CAAC,SAAS,CAAC,CAAC,CAAC,MAAM,CAAC,SAAS,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,SAAS,IAAI,MAAM,CAAC,EAAE,EAAE,CAAC,CAAC;QAE7H,UAAU;QACV,IAAI,OAAO,IAAI,IAAI,EAAE;YACpB,KAAK,MAAM,GAAG,IAAI,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,EAAE;gBAC9C,IAAI,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAE;oBACrD,OAAO,CAAC,GAAG,CAAC,MAAM,GAAG,KAAK,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;iBACjD;aACD;SACD;KACD;AACF,CAAC;AAED,WAAW;AACX,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;AAEnC,IAAI,QAAQ,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC;AACvB,IAAI,OAA6B,CAAC;AAElC,IAAI,IAAI,CAAC,CAAC,CAAC,IAAI,IAAI,EAAE;IACpB,OAAO,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC,MAAM,CAAC,WAAW,EAAE,CAAC,CAAC;CACjE;AAED,iBAAiB;AACjB,QAAQ,GAAG,QAAQ,CAAC,OAAO,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;AAEtC,IAAI,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC,IAAI,CAAC,GAAG,EAAE;IACjC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AACjB,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE;IACZ,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAChB,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AACjB,CAAC,CAAC,CAAC"}

View File

@ -14,7 +14,9 @@
</div> </div>
<header v-if="title"><Mfm :text="title"/></header> <header v-if="title"><Mfm :text="title"/></header>
<div v-if="text" class="body"><Mfm :text="text"/></div> <div v-if="text" class="body"><Mfm :text="text"/></div>
<MkInput v-if="input" v-model="inputValue" autofocus :type="input.type || 'text'" :placeholder="input.placeholder" @keydown="onInputKeydown"></MkInput> <MkInput v-if="input" v-model="inputValue" autofocus :type="input.type || 'text'" :placeholder="input.placeholder" @keydown="onInputKeydown">
<template v-if="input.type === 'password'" #prefix><i class="fas fa-lock"></i></template>
</MkInput>
<MkSelect v-if="select" v-model="selectedValue" autofocus> <MkSelect v-if="select" v-model="selectedValue" autofocus>
<template v-if="select.items"> <template v-if="select.items">
<option v-for="item in select.items" :value="item.value">{{ item.text }}</option> <option v-for="item in select.items" :value="item.value">{{ item.text }}</option>
@ -165,6 +167,10 @@ export default defineComponent({
> .icon { > .icon {
font-size: 32px; font-size: 32px;
&.info {
color: #55c4dd;
}
&.success { &.success {
color: var(--success); color: var(--success);
} }

View File

@ -1,5 +1,5 @@
<template> <template>
<MkPopup ref="popup" #default="{point}" :manual-showing="manualShowing" :src="src" :front="true" @click="$refs.popup.close()" @opening="opening" @close="$emit('close')" @closed="$emit('closed')"> <MkPopup ref="popup" v-slot="{ point, close }" :manual-showing="manualShowing" :src="src" :front="true" @click="close()" @opening="opening" @close="$emit('close')" @closed="$emit('closed')">
<MkEmojiPicker ref="picker" class="ryghynhb _popup _shadow" :class="{ pointer: point === 'top' }" :show-pinned="showPinned" :as-reaction-picker="asReactionPicker" @chosen="chosen"/> <MkEmojiPicker ref="picker" class="ryghynhb _popup _shadow" :class="{ pointer: point === 'top' }" :show-pinned="showPinned" :as-reaction-picker="asReactionPicker" @chosen="chosen"/>
</MkPopup> </MkPopup>
</template> </template>

View File

@ -12,66 +12,67 @@
<template #header> <template #header>
{{ title }} {{ title }}
</template> </template>
<FormBase class="xkpnjxcv">
<template v-for="item in Object.keys(form).filter(item => !form[item].hidden)"> <MkSpacer :margin-min="20" :margin-max="32">
<FormInput v-if="form[item].type === 'number'" v-model="values[item]" type="number" :step="form[item].step || 1"> <div class="xkpnjxcv _formRoot">
<span v-text="form[item].label || item"></span><span v-if="form[item].required === false"> ({{ $ts.optional }})</span> <template v-for="item in Object.keys(form).filter(item => !form[item].hidden)">
<template v-if="form[item].description" #desc>{{ form[item].description }}</template> <FormInput v-if="form[item].type === 'number'" v-model="values[item]" type="number" :step="form[item].step || 1" class="_formBlock">
</FormInput> <template #label><span v-text="form[item].label || item"></span><span v-if="form[item].required === false"> ({{ $ts.optional }})</span></template>
<FormInput v-else-if="form[item].type === 'string' && !form[item].multiline" v-model="values[item]" type="text"> <template v-if="form[item].description" #caption>{{ form[item].description }}</template>
<span v-text="form[item].label || item"></span><span v-if="form[item].required === false"> ({{ $ts.optional }})</span> </FormInput>
<template v-if="form[item].description" #desc>{{ form[item].description }}</template> <FormInput v-else-if="form[item].type === 'string' && !form[item].multiline" v-model="values[item]" type="text" class="_formBlock">
</FormInput> <template #label><span v-text="form[item].label || item"></span><span v-if="form[item].required === false"> ({{ $ts.optional }})</span></template>
<FormTextarea v-else-if="form[item].type === 'string' && form[item].multiline" v-model="values[item]"> <template v-if="form[item].description" #caption>{{ form[item].description }}</template>
<span v-text="form[item].label || item"></span><span v-if="form[item].required === false"> ({{ $ts.optional }})</span> </FormInput>
<template v-if="form[item].description" #desc>{{ form[item].description }}</template> <FormTextarea v-else-if="form[item].type === 'string' && form[item].multiline" v-model="values[item]" class="_formBlock">
</FormTextarea> <template #label><span v-text="form[item].label || item"></span><span v-if="form[item].required === false"> ({{ $ts.optional }})</span></template>
<FormSwitch v-else-if="form[item].type === 'boolean'" v-model="values[item]"> <template v-if="form[item].description" #caption>{{ form[item].description }}</template>
<span v-text="form[item].label || item"></span> </FormTextarea>
<template v-if="form[item].description" #desc>{{ form[item].description }}</template> <FormSwitch v-else-if="form[item].type === 'boolean'" v-model="values[item]" class="_formBlock">
</FormSwitch> <span v-text="form[item].label || item"></span>
<FormSelect v-else-if="form[item].type === 'enum'" v-model="values[item]"> <template v-if="form[item].description" #caption>{{ form[item].description }}</template>
<template #label><span v-text="form[item].label || item"></span><span v-if="form[item].required === false"> ({{ $ts.optional }})</span></template> </FormSwitch>
<option v-for="item in form[item].enum" :key="item.value" :value="item.value">{{ item.label }}</option> <FormSelect v-else-if="form[item].type === 'enum'" v-model="values[item]" class="_formBlock">
</FormSelect> <template #label><span v-text="form[item].label || item"></span><span v-if="form[item].required === false"> ({{ $ts.optional }})</span></template>
<FormRadios v-else-if="form[item].type === 'radio'" v-model="values[item]"> <option v-for="item in form[item].enum" :key="item.value" :value="item.value">{{ item.label }}</option>
<template #desc><span v-text="form[item].label || item"></span><span v-if="form[item].required === false"> ({{ $ts.optional }})</span></template> </FormSelect>
<option v-for="item in form[item].options" :key="item.value" :value="item.value">{{ item.label }}</option> <FormRadios v-else-if="form[item].type === 'radio'" v-model="values[item]" class="_formBlock">
</FormRadios> <template #caption><span v-text="form[item].label || item"></span><span v-if="form[item].required === false"> ({{ $ts.optional }})</span></template>
<FormRange v-else-if="form[item].type === 'range'" v-model="values[item]" :min="form[item].mim" :max="form[item].max" :step="form[item].step"> <option v-for="item in form[item].options" :key="item.value" :value="item.value">{{ item.label }}</option>
<template #label><span v-text="form[item].label || item"></span><span v-if="form[item].required === false"> ({{ $ts.optional }})</span></template> </FormRadios>
<template v-if="form[item].description" #desc>{{ form[item].description }}</template> <FormRange v-else-if="form[item].type === 'range'" v-model="values[item]" :min="form[item].mim" :max="form[item].max" :step="form[item].step" class="_formBlock">
</FormRange> <template #label><span v-text="form[item].label || item"></span><span v-if="form[item].required === false"> ({{ $ts.optional }})</span></template>
<FormButton v-else-if="form[item].type === 'button'" @click="form[item].action($event, values)"> <template v-if="form[item].description" #caption>{{ form[item].description }}</template>
<span v-text="form[item].content || item"></span> </FormRange>
</FormButton> <MkButton v-else-if="form[item].type === 'button'" @click="form[item].action($event, values)" class="_formBlock">
</template> <span v-text="form[item].content || item"></span>
</FormBase> </MkButton>
</template>
</div>
</MkSpacer>
</XModalWindow> </XModalWindow>
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
import XModalWindow from '@/components/ui/modal-window.vue'; import XModalWindow from '@/components/ui/modal-window.vue';
import FormBase from './debobigego/base.vue'; import FormInput from './form/input.vue';
import FormInput from './debobigego/input.vue'; import FormTextarea from './form/textarea.vue';
import FormTextarea from './debobigego/textarea.vue'; import FormSwitch from './form/switch.vue';
import FormSwitch from './debobigego/switch.vue'; import FormSelect from './form/select.vue';
import FormSelect from './debobigego/select.vue'; import FormRange from './form/range.vue';
import FormRange from './debobigego/range.vue'; import MkButton from './ui/button.vue';
import FormButton from './debobigego/button.vue'; import FormRadios from './form/radios.vue';
import FormRadios from './debobigego/radios.vue';
export default defineComponent({ export default defineComponent({
components: { components: {
XModalWindow, XModalWindow,
FormBase,
FormInput, FormInput,
FormTextarea, FormTextarea,
FormSwitch, FormSwitch,
FormSelect, FormSelect,
FormRange, FormRange,
FormButton, MkButton,
FormRadios, FormRadios,
}, },

View File

@ -0,0 +1,35 @@
<template>
<div v-sticky-container v-panel class="adfeebaf _formBlock">
<div class="label"><slot name="label"></slot></div>
<div class="main _formRoot">
<slot></slot>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
export default defineComponent({
});
</script>
<style lang="scss" scoped>
.adfeebaf {
padding: 24px 24px;
border-radius: var(--radius);
> .label {
font-weight: bold;
padding: 0 0 16px 0;
&:empty {
display: none;
}
}
> .main {
}
}
</style>

View File

@ -5,6 +5,7 @@
<div ref="prefixEl" class="prefix"><slot name="prefix"></slot></div> <div ref="prefixEl" class="prefix"><slot name="prefix"></slot></div>
<input ref="inputEl" <input ref="inputEl"
v-model="v" v-model="v"
v-panel
:type="type" :type="type"
:disabled="disabled" :disabled="disabled"
:required="required" :required="required"
@ -27,7 +28,7 @@
</div> </div>
<div class="caption"><slot name="caption"></slot></div> <div class="caption"><slot name="caption"></slot></div>
<MkButton v-if="manualSave && changed" primary @click="updated"><i class="fas fa-save"></i> {{ $ts.save }}</MkButton> <MkButton v-if="manualSave && changed" primary class="save" @click="updated"><i class="fas fa-check"></i> {{ $ts.save }}</MkButton>
</div> </div>
</template> </template>
@ -114,9 +115,9 @@ export default defineComponent({
const changed = ref(false); const changed = ref(false);
const invalid = ref(false); const invalid = ref(false);
const filled = computed(() => v.value !== '' && v.value != null); const filled = computed(() => v.value !== '' && v.value != null);
const inputEl = ref(null); const inputEl = ref<HTMLElement>();
const prefixEl = ref(null); const prefixEl = ref<HTMLElement>();
const suffixEl = ref(null); const suffixEl = ref<HTMLElement>();
const focus = () => inputEl.value.focus(); const focus = () => inputEl.value.focus();
const onInput = (ev) => { const onInput = (ev) => {
@ -208,7 +209,7 @@ export default defineComponent({
.matxzzsk { .matxzzsk {
> .label { > .label {
font-size: 0.85em; font-size: 0.85em;
padding: 0 0 8px 12px; padding: 0 0 8px 0;
user-select: none; user-select: none;
&:empty { &:empty {
@ -217,8 +218,8 @@ export default defineComponent({
} }
> .caption { > .caption {
font-size: 0.8em; font-size: 0.85em;
padding: 8px 0 0 12px; padding: 8px 0 0 0;
color: var(--fgTransparentWeak); color: var(--fgTransparentWeak);
&:empty { &:empty {
@ -242,8 +243,7 @@ export default defineComponent({
font-weight: normal; font-weight: normal;
font-size: 1em; font-size: 1em;
color: var(--fg); color: var(--fg);
background: var(--panel); border: solid 0.5px var(--panel);
border: solid 0.5px var(--inputBorder);
border-radius: 6px; border-radius: 6px;
outline: none; outline: none;
box-shadow: none; box-shadow: none;
@ -311,5 +311,9 @@ export default defineComponent({
} }
} }
} }
> .save {
margin: 8px 0 0 0;
}
} }
</style> </style>

View File

@ -0,0 +1,112 @@
<template>
<div class="ffcbddfc" :class="{ inline }">
<a v-if="external" class="main _button" :href="to" target="_blank">
<span class="icon"><slot name="icon"></slot></span>
<span class="text"><slot></slot></span>
<span class="right">
<span class="text"><slot name="suffix"></slot></span>
<i class="fas fa-external-link-alt icon"></i>
</span>
</a>
<MkA v-else class="main _button" :class="{ active }" :to="to" :behavior="behavior">
<span class="icon"><slot name="icon"></slot></span>
<span class="text"><slot></slot></span>
<span class="right">
<span class="text"><slot name="suffix"></slot></span>
<i class="fas fa-chevron-right icon"></i>
</span>
</MkA>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
export default defineComponent({
props: {
to: {
type: String,
required: true
},
active: {
type: Boolean,
required: false
},
external: {
type: Boolean,
required: false
},
behavior: {
type: String,
required: false,
},
inline: {
type: Boolean,
required: false
},
},
});
</script>
<style lang="scss" scoped>
.ffcbddfc {
display: block;
&.inline {
display: inline-block;
}
> .main {
display: flex;
align-items: center;
width: 100%;
box-sizing: border-box;
padding: 12px 14px 12px 14px;
background: var(--buttonBg);
border-radius: 6px;
font-size: 0.9em;
&:hover {
text-decoration: none;
background: var(--buttonHoverBg);
}
&.active {
color: var(--accent);
background: var(--buttonHoverBg);
}
> .icon {
margin-right: 0.75em;
flex-shrink: 0;
text-align: center;
opacity: 0.8;
&:empty {
display: none;
& + .text {
padding-left: 4px;
}
}
}
> .text {
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
padding-right: 12px;
}
> .right {
margin-left: auto;
opacity: 0.7;
white-space: nowrap;
> .text:not(:empty) {
margin-right: 0.75em;
}
}
}
}
</style>

View File

@ -0,0 +1,44 @@
<template>
<FormSlot>
<template #label><slot name="label"></slot></template>
<div class="abcaccfa">
<slot :items="items"></slot>
<div v-if="empty" key="_empty_" class="empty">
<slot name="empty"></slot>
</div>
<MkButton v-show="more" class="button" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }" @click="fetchMore">
<template v-if="!moreFetching">{{ $ts.loadMore }}</template>
<template v-if="moreFetching"><MkLoading inline/></template>
</MkButton>
</div>
</FormSlot>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import MkButton from '@/components/ui/button.vue';
import FormSlot from './slot.vue';
import paging from '@/scripts/paging';
export default defineComponent({
components: {
MkButton,
FormSlot,
},
mixins: [
paging({}),
],
props: {
pagination: {
required: true
},
},
});
</script>
<style lang="scss" scoped>
.abcaccfa {
}
</style>

View File

@ -1,5 +1,6 @@
<template> <template>
<div <div
v-panel
class="novjtctn" class="novjtctn"
:class="{ disabled, checked }" :class="{ disabled, checked }"
:aria-checked="checked" :aria-checked="checked"
@ -50,9 +51,10 @@ export default defineComponent({
.novjtctn { .novjtctn {
position: relative; position: relative;
display: inline-block; display: inline-block;
margin: 8px 20px 0 0;
text-align: left; text-align: left;
cursor: pointer; cursor: pointer;
padding: 11px 14px;
border-radius: 6px;
transition: all 0.3s; transition: all 0.3s;
> * { > * {
@ -68,6 +70,14 @@ export default defineComponent({
} }
&.checked { &.checked {
background: var(--accentedBg) !important;
border-color: var(--accent);
color: var(--accent);
&, * {
cursor: default !important;
}
> .button { > .button {
border-color: var(--accent); border-color: var(--accent);
@ -79,6 +89,11 @@ export default defineComponent({
} }
} }
&:hover {
border-color: var(--inputBorderHover);
color: var(--accent);
}
> input { > input {
position: absolute; position: absolute;
width: 0; width: 0;
@ -89,8 +104,8 @@ export default defineComponent({
> .button { > .button {
position: absolute; position: absolute;
width: 20px; width: 14px;
height: 20px; height: 14px;
background: none; background: none;
border: solid 2px var(--inputBorder); border: solid 2px var(--inputBorder);
border-radius: 100%; border-radius: 100%;
@ -114,7 +129,6 @@ export default defineComponent({
> .label { > .label {
margin-left: 28px; margin-left: 28px;
display: block; display: block;
font-size: 16px;
line-height: 20px; line-height: 20px;
cursor: pointer; cursor: pointer;
} }

View File

@ -23,6 +23,8 @@ export default defineComponent({
}, },
render() { render() {
let options = this.$slots.default(); let options = this.$slots.default();
const label = this.$slots.label && this.$slots.label();
const caption = this.$slots.caption && this.$slots.caption();
// Fragment // Fragment
if (options.length === 1 && options[0].props == null) options = options[0].children; if (options.length === 1 && options[0].props == null) options = options[0].children;
@ -30,12 +32,21 @@ export default defineComponent({
return h('div', { return h('div', {
class: 'novjtcto' class: 'novjtcto'
}, [ }, [
...options.map(option => h(MkRadio, { ...(label ? [h('div', {
key: option.key, class: 'label'
value: option.props.value, }, [label])] : []),
modelValue: this.value, h('div', {
'onUpdate:modelValue': value => this.value = value, class: 'body'
}, option.children)) }, options.map(option => h(MkRadio, {
key: option.key,
value: option.props.value,
modelValue: this.value,
'onUpdate:modelValue': value => this.value = value,
}, option.children)),
),
...(caption ? [h('div', {
class: 'caption'
}, [caption])] : []),
]); ]);
} }
}); });
@ -43,12 +54,30 @@ export default defineComponent({
<style lang="scss"> <style lang="scss">
.novjtcto { .novjtcto {
&:first-child { > .label {
margin-top: 0; font-size: 0.85em;
padding: 0 0 8px 0;
user-select: none;
&:empty {
display: none;
}
} }
&:last-child { > .body {
margin-bottom: 0; display: grid;
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
grid-gap: 12px;
}
> .caption {
font-size: 0.85em;
padding: 8px 0 0 0;
color: var(--fgTransparentWeak);
&:empty {
display: none;
}
} }
} }
</style> </style>

View File

@ -1,29 +1,27 @@
<template> <template>
<div class="timctyfi" :class="{ focused, disabled }"> <div class="timctyfi" :class="{ disabled }">
<div class="icon"><slot name="icon"></slot></div> <div class="label"><slot name="label"></slot></div>
<span class="label"><slot name="label"></slot></span> <div v-panel class="body">
<input <div ref="containerEl" class="container">
ref="input" <div class="track">
v-model="v" <div class="highlight" :style="{ width: (steppedValue * 100) + '%' }"></div>
type="range" </div>
:disabled="disabled" <div v-if="steps" class="ticks">
:min="min" <div v-for="i in (steps + 1)" class="tick" :style="{ left: (((i - 1) / steps) * 100) + '%' }"></div>
:max="max" </div>
:step="step" <div ref="thumbEl" v-tooltip="textConverter(finalValue)" class="thumb" :style="{ left: thumbPosition + 'px' }" @mousedown="onMousedown" @touchstart="onMousedown"></div>
:autofocus="autofocus" </div>
@focus="focused = true" </div>
@blur="focused = false"
@input="$emit('update:value', $event.target.value)"
/>
</div> </div>
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent } from 'vue'; import { computed, defineComponent, ref, watch } from 'vue';
import * as os from '@/os';
export default defineComponent({ export default defineComponent({
props: { props: {
value: { modelValue: {
type: Number, type: Number,
required: false, required: false,
default: 0 default: 0
@ -51,88 +49,198 @@ export default defineComponent({
autofocus: { autofocus: {
type: Boolean, type: Boolean,
required: false required: false
} },
textConverter: {
type: Function,
required: false,
default: (v) => v.toString(),
},
}, },
data() {
setup(props, context) {
const rawValue = ref((props.modelValue - props.min) / (props.max - props.min));
const steppedValue = computed(() => {
if (props.step) {
const step = props.step / (props.max - props.min);
return (step * Math.round(rawValue.value / step));
} else {
return rawValue.value;
}
});
const finalValue = computed(() => {
return (steppedValue.value * (props.max - props.min)) + props.min;
});
watch(finalValue, () => {
context.emit('update:modelValue', finalValue.value);
});
const thumbWidth = computed(() => {
if (thumbEl.value == null) return 0;
return thumbEl.value!.offsetWidth;
});
const thumbPosition = computed(() => {
if (containerEl.value == null) return 0;
return (containerEl.value.offsetWidth - thumbWidth.value) * steppedValue.value;
});
const steps = computed(() => {
if (props.step) {
return (props.max - props.min) / props.step;
} else {
return 0;
}
});
const containerEl = ref<HTMLElement>();
const thumbEl = ref<HTMLElement>();
const onMousedown = (ev: MouseEvent | TouchEvent) => {
ev.preventDefault();
const tooltipShowing = ref(true);
os.popup(import('@/components/ui/tooltip.vue'), {
showing: tooltipShowing,
text: computed(() => {
return props.textConverter(finalValue.value);
}),
source: thumbEl,
}, {}, 'closed');
const style = document.createElement('style');
style.appendChild(document.createTextNode('* { cursor: grabbing !important; } body * { pointer-events: none !important; }'));
document.head.appendChild(style);
const onDrag = (ev: MouseEvent | TouchEvent) => {
ev.preventDefault();
const containerRect = containerEl.value!.getBoundingClientRect();
const pointerX = ev.touches && ev.touches.length > 0 ? ev.touches[0].clientX : ev.clientX;
const pointerPositionOnContainer = pointerX - (containerRect.left + (thumbWidth.value / 2));
rawValue.value = Math.min(1, Math.max(0, pointerPositionOnContainer / (containerEl.value!.offsetWidth - thumbWidth.value)));
};
const onMouseup = () => {
document.head.removeChild(style);
tooltipShowing.value = false;
window.removeEventListener('mousemove', onDrag);
window.removeEventListener('touchmove', onDrag);
window.removeEventListener('mouseup', onMouseup);
window.removeEventListener('touchend', onMouseup);
};
window.addEventListener('mousemove', onDrag);
window.addEventListener('touchmove', onDrag);
window.addEventListener('mouseup', onMouseup, { once: true });
window.addEventListener('touchend', onMouseup, { once: true });
};
return { return {
v: this.value, rawValue,
focused: false finalValue,
steppedValue,
onMousedown,
containerEl,
thumbEl,
thumbPosition,
steps,
}; };
}, },
watch: {
value(v) {
this.v = parseFloat(v);
}
},
mounted() {
if (this.autofocus) {
this.$nextTick(() => {
this.$refs.input.focus();
});
}
}
}); });
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@use "sass:math";
.timctyfi { .timctyfi {
position: relative; position: relative;
margin: 8px;
> .icon { > .label {
display: inline-block; font-size: 0.85em;
width: 24px; padding: 0 0 8px 0;
text-align: center; user-select: none;
&:empty {
display: none;
}
} }
> .title { > .caption {
pointer-events: none; font-size: 0.85em;
font-size: 16px; padding: 8px 0 0 0;
color: var(--inputLabel); color: var(--fgTransparentWeak);
overflow: hidden;
&:empty {
display: none;
}
} }
> input { $thumbHeight: 20px;
-webkit-appearance: none; $thumbWidth: 20px;
-moz-appearance: none;
appearance: none;
background: var(--X10);
height: 7px;
margin: 0 8px;
outline: 0;
border: 0;
border-radius: 7px;
&.disabled { > .body {
opacity: 0.6; padding: 12px;
cursor: not-allowed; border-radius: 6px;
}
&::-webkit-slider-thumb { > .container {
-webkit-appearance: none; position: relative;
appearance: none; height: $thumbHeight;
cursor: pointer;
width: 20px;
height: 20px;
display: block;
border-radius: 50%;
border: none;
background: var(--accent);
box-shadow: 0 0 6px rgba(0, 0, 0, 0.3);
box-sizing: content-box;
}
&::-moz-range-thumb { > .track {
-moz-appearance: none; position: absolute;
appearance: none; top: 0;
cursor: pointer; bottom: 0;
width: 20px; left: 0;
height: 20px; right: 0;
display: block; margin: auto;
border-radius: 50%; width: calc(100% - #{$thumbWidth});
border: none; height: 3px;
background: var(--accent); background: rgba(0, 0, 0, 0.1);
box-shadow: 0 0 6px rgba(0, 0, 0, 0.3); border-radius: 999px;
overflow: clip;
> .highlight {
position: absolute;
top: 0;
left: 0;
height: 100%;
background: var(--accent);
opacity: 0.5;
transition: width 0.2s cubic-bezier(0,0,0,1);
}
}
> .ticks {
$tickWidth: 3px;
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
margin: auto;
width: calc(100% - #{$thumbWidth});
> .tick {
position: absolute;
bottom: 0;
width: $tickWidth;
height: 3px;
margin-left: - math.div($tickWidth, 2);
background: var(--divider);
border-radius: 999px;
}
}
> .thumb {
position: absolute;
width: $thumbWidth;
height: $thumbHeight;
cursor: grab;
background: var(--accent);
border-radius: 999px;
transition: left 0.2s cubic-bezier(0,0,0,1);
&:hover {
background: var(--accentLighten);
}
}
} }
} }
} }

View File

@ -1,7 +1,7 @@
<template> <template>
<div v-size="{ max: [500] }" v-sticky-container class="vrtktovh"> <div v-size="{ max: [500] }" v-sticky-container class="vrtktovh _formBlock">
<div class="label"><slot name="label"></slot></div> <div class="label"><slot name="label"></slot></div>
<div class="main"> <div class="main _formRoot">
<slot></slot> <slot></slot>
</div> </div>
</div> </div>
@ -17,15 +17,33 @@ export default defineComponent({
<style lang="scss" scoped> <style lang="scss" scoped>
.vrtktovh { .vrtktovh {
margin: 0;
border-top: solid 0.5px var(--divider); border-top: solid 0.5px var(--divider);
border-bottom: solid 0.5px var(--divider);
padding: 24px 0;
& + .vrtktovh {
border-top: none;
}
&:first-child {
border-top: none;
}
&:last-child {
border-bottom: none;
}
> .label { > .label {
font-weight: bold; font-weight: bold;
padding: 24px 0 16px 0; padding: 0 0 16px 0;
&:empty {
display: none;
}
} }
> .main { > .main {
margin-bottom: 32px;
} }
} }
</style> </style>

View File

@ -3,7 +3,7 @@
<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 }" @click.prevent="onClick">
<div ref="prefixEl" class="prefix"><slot name="prefix"></slot></div> <div ref="prefixEl" class="prefix"><slot name="prefix"></slot></div>
<select ref="inputEl" v-model="v" <select ref="inputEl" v-model="v" v-panel
class="select" class="select"
:disabled="disabled" :disabled="disabled"
:required="required" :required="required"
@ -201,7 +201,7 @@ export default defineComponent({
.vblkjoeq { .vblkjoeq {
> .label { > .label {
font-size: 0.85em; font-size: 0.85em;
padding: 0 0 8px 12px; padding: 0 0 8px 0;
user-select: none; user-select: none;
&:empty { &:empty {
@ -210,8 +210,8 @@ export default defineComponent({
} }
> .caption { > .caption {
font-size: 0.8em; font-size: 0.85em;
padding: 8px 0 0 12px; padding: 8px 0 0 0;
color: var(--fgTransparentWeak); color: var(--fgTransparentWeak);
&:empty { &:empty {
@ -242,8 +242,7 @@ export default defineComponent({
font-weight: normal; font-weight: normal;
font-size: 1em; font-size: 1em;
color: var(--fg); color: var(--fg);
background: var(--panel); border: solid 1px var(--panel);
border: solid 1px var(--inputBorder);
border-radius: 6px; border-radius: 6px;
outline: none; outline: none;
box-shadow: none; box-shadow: none;

View File

@ -18,11 +18,9 @@ export default defineComponent({
<style lang="scss" scoped> <style lang="scss" scoped>
.adhpbeou { .adhpbeou {
margin: 1.5em 0;
> .label { > .label {
font-size: 0.85em; font-size: 0.85em;
padding: 0 0 8px 12px; padding: 0 0 8px 0;
user-select: none; user-select: none;
&:empty { &:empty {
@ -31,20 +29,13 @@ export default defineComponent({
} }
> .caption { > .caption {
font-size: 0.8em; font-size: 0.85em;
padding: 8px 0 0 12px; padding: 8px 0 0 0;
color: var(--fgTransparentWeak); color: var(--fgTransparentWeak);
&:empty { &:empty {
display: none; display: none;
} }
} }
> .content {
position: relative;
background: var(--panel);
border: solid 0.5px var(--inputBorder);
border-radius: 6px;
}
} }
</style> </style>

View File

@ -0,0 +1,98 @@
<template>
<transition name="fade" mode="out-in">
<div v-if="pending">
<MkLoading/>
</div>
<div v-else-if="resolved">
<slot :result="result"></slot>
</div>
<div v-else>
<div class="wszdbhzo">
<div><i class="fas fa-exclamation-triangle"></i> {{ $ts.somethingHappened }}</div>
<MkButton inline class="retry" @click="retry"><i class="fas fa-redo-alt"></i> {{ $ts.retry }}</MkButton>
</div>
</div>
</transition>
</template>
<script lang="ts">
import { defineComponent, PropType, ref, watch } from 'vue';
import MkButton from '@/components/ui/button.vue';
export default defineComponent({
components: {
MkButton
},
props: {
p: {
type: Function as PropType<() => Promise<any>>,
required: true,
}
},
setup(props, context) {
const pending = ref(true);
const resolved = ref(false);
const rejected = ref(false);
const result = ref(null);
const process = () => {
if (props.p == null) {
return;
}
const promise = props.p();
pending.value = true;
resolved.value = false;
rejected.value = false;
promise.then((_result) => {
pending.value = false;
resolved.value = true;
result.value = _result;
});
promise.catch(() => {
pending.value = false;
rejected.value = true;
});
};
watch(() => props.p, () => {
process();
}, {
immediate: true
});
const retry = () => {
process();
};
return {
pending,
resolved,
rejected,
result,
retry,
};
}
});
</script>
<style lang="scss" scoped>
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.125s ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
.wszdbhzo {
padding: 16px;
text-align: center;
> .retry {
margin-top: 16px;
}
}
</style>

View File

@ -18,7 +18,7 @@
</span> </span>
<span class="label"> <span class="label">
<span><slot></slot></span> <span><slot></slot></span>
<p><slot name="caption"></slot></p> <p class="caption"><slot name="caption"></slot></p>
</span> </span>
</div> </div>
</template> </template>
@ -118,10 +118,14 @@ export default defineComponent({
transition: inherit; transition: inherit;
} }
> p { > .caption {
margin: 0; margin: 8px 0 0 0;
color: var(--fgTransparentWeak); color: var(--fgTransparentWeak);
font-size: 90%; font-size: 0.85em;
&:empty {
display: none;
}
} }
} }

View File

@ -4,6 +4,7 @@
<div class="input" :class="{ disabled, focused, tall, pre }"> <div class="input" :class="{ disabled, focused, tall, pre }">
<textarea ref="inputEl" <textarea ref="inputEl"
v-model="v" v-model="v"
v-panel
:class="{ code, _monospace: code }" :class="{ code, _monospace: code }"
:disabled="disabled" :disabled="disabled"
:required="required" :required="required"
@ -20,7 +21,7 @@
</div> </div>
<div class="caption"><slot name="caption"></slot></div> <div class="caption"><slot name="caption"></slot></div>
<MkButton v-if="manualSave && changed" primary @click="updated"><i class="fas fa-save"></i> {{ $ts.save }}</MkButton> <MkButton v-if="manualSave && changed" primary class="save" @click="updated"><i class="fas fa-save"></i> {{ $ts.save }}</MkButton>
</div> </div>
</template> </template>
@ -174,7 +175,7 @@ export default defineComponent({
.adhpbeos { .adhpbeos {
> .label { > .label {
font-size: 0.85em; font-size: 0.85em;
padding: 0 0 8px 12px; padding: 0 0 8px 0;
user-select: none; user-select: none;
&:empty { &:empty {
@ -183,8 +184,8 @@ export default defineComponent({
} }
> .caption { > .caption {
font-size: 0.8em; font-size: 0.85em;
padding: 8px 0 0 12px; padding: 8px 0 0 0;
color: var(--fgTransparentWeak); color: var(--fgTransparentWeak);
&:empty { &:empty {
@ -209,8 +210,7 @@ export default defineComponent({
font-weight: normal; font-weight: normal;
font-size: 1em; font-size: 1em;
color: var(--fg); color: var(--fg);
background: var(--panel); border: solid 0.5px var(--panel);
border: solid 0.5px var(--inputBorder);
border-radius: 6px; border-radius: 6px;
outline: none; outline: none;
box-shadow: none; box-shadow: none;
@ -248,5 +248,9 @@ export default defineComponent({
} }
} }
} }
> .save {
margin: 8px 0 0 0;
}
} }
</style> </style>

View File

@ -7,7 +7,7 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent, onMounted, onUnmounted, ref } from 'vue'; import { defineComponent, inject, onMounted, onUnmounted, ref } from 'vue';
export default defineComponent({ export default defineComponent({
props: { props: {
@ -15,19 +15,35 @@ export default defineComponent({
type: Number, type: Number,
required: false, required: false,
default: null, default: null,
} },
marginMin: {
type: Number,
required: false,
default: 12,
},
marginMax: {
type: Number,
required: false,
default: 24,
},
}, },
setup(props, context) { setup(props, context) {
let ro: ResizeObserver; let ro: ResizeObserver;
const root = ref<HTMLElement>(null); const root = ref<HTMLElement>();
const content = ref<HTMLElement>(null); const content = ref<HTMLElement>();
const margin = ref(0); const margin = ref(0);
const shouldSpacerMin = inject('shouldSpacerMin', false);
const adjust = (rect: { width: number; height: number; }) => { const adjust = (rect: { width: number; height: number; }) => {
if (rect.width > (props.contentMax || 500)) { if (shouldSpacerMin) {
margin.value = 32; margin.value = props.marginMin;
return;
}
if (rect.width > props.contentMax || rect.width > 500) {
margin.value = props.marginMax;
} else { } else {
margin.value = 12; margin.value = props.marginMin;
} }
}; };
@ -40,14 +56,14 @@ export default defineComponent({
}); });
*/ */
adjust({ adjust({
width: root.value.offsetWidth, width: root.value!.offsetWidth,
height: root.value.offsetHeight, height: root.value!.offsetHeight,
}); });
}); });
ro.observe(root.value); ro.observe(root.value!);
if (props.contentMax) { if (props.contentMax) {
content.value.style.maxWidth = `${props.contentMax}px`; content.value!.style.maxWidth = `${props.contentMax}px`;
} }
}); });

View File

@ -0,0 +1,48 @@
<template>
<div class="alqyeyti">
<div class="key">
<slot name="key"></slot>
</div>
<div class="value">
<slot name="value"></slot>
<button v-if="copy" v-tooltip="$ts.copy" class="_textButton" style="margin-left: 0.5em;" @click="copy_"><i class="far fa-copy"></i></button>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import copyToClipboard from '@/scripts/copy-to-clipboard';
import * as os from '@/os';
export default defineComponent({
props: {
copy: {
type: String,
required: false,
default: null,
},
},
setup(props) {
const copy_ = () => {
copyToClipboard(props.copy);
os.success();
};
return {
copy_
};
},
});
</script>
<style lang="scss" scoped>
.alqyeyti {
> .key {
font-size: 0.85em;
padding: 0 0 0.25em 0;
opacity: 0.75;
}
}
</style>

View File

@ -44,16 +44,36 @@ export default defineComponent({
onMounted(() => { onMounted(() => {
const lightbox = new PhotoSwipeLightbox({ const lightbox = new PhotoSwipeLightbox({
dataSource: props.mediaList.filter(media => media.type.startsWith('image')).map(media => ({ dataSource: props.mediaList.filter(media => media.type.startsWith('image')).map(media => {
src: media.url, const item = {
w: media.properties.width, src: media.url,
h: media.properties.height, w: media.properties.width,
alt: media.name, h: media.properties.height,
})), alt: media.name,
};
if (media.properties.orientation != null && media.properties.orientation >= 5) {
[item.w, item.h] = [item.h, item.w];
}
return item;
}),
gallery: gallery.value, gallery: gallery.value,
children: '.image', children: '.image',
thumbSelector: '.image', thumbSelector: '.image',
pswpModule: PhotoSwipe loop: false,
padding: window.innerWidth > 500 ? {
top: 32,
bottom: 32,
left: 32,
right: 32,
} : {
top: 0,
bottom: 0,
left: 0,
right: 0,
},
imageClickAction: 'close',
tapAction: 'toggle-controls',
pswpModule: PhotoSwipe,
}); });
lightbox.on('itemData', (e) => { lightbox.on('itemData', (e) => {
@ -68,6 +88,9 @@ export default defineComponent({
itemData.src = file.url; itemData.src = file.url;
itemData.w = Number(file.properties.width); itemData.w = Number(file.properties.width);
itemData.h = Number(file.properties.height); itemData.h = Number(file.properties.height);
if (file.properties.orientation != null && file.properties.orientation >= 5) {
[itemData.w, itemData.h] = [itemData.h, itemData.w];
}
itemData.msrc = file.thumbnailUrl; itemData.msrc = file.thumbnailUrl;
itemData.thumbCropped = true; itemData.thumbCropped = true;
}); });

View File

@ -184,6 +184,11 @@ export default defineComponent({
count, speed, count, speed,
}, genEl(token.children)); }, genEl(token.children));
} }
case 'rotate': {
const degrees = parseInt(token.props.args.deg) || '90';
style = `transform: rotate(${degrees}deg); transform-origin: center center;`;
break;
}
} }
if (style == null) { if (style == null) {
return h('span', {}, ['$[', token.props.name, ' ', ...genEl(token.children), ']']); return h('span', {}, ['$[', token.props.name, ' ', ...genEl(token.children), ']']);

View File

@ -42,8 +42,6 @@
<MkUserName :user="appearNote.user"/> <MkUserName :user="appearNote.user"/>
</MkA> </MkA>
<span v-if="appearNote.user.isBot" class="is-bot">bot</span> <span v-if="appearNote.user.isBot" class="is-bot">bot</span>
<span v-if="appearNote.user.isAdmin" class="admin"><i class="fas fa-bookmark"></i></span>
<span v-if="!appearNote.user.isAdmin && appearNote.user.isModerator" class="moderator"><i class="far fa-bookmark"></i></span>
<span v-if="appearNote.visibility !== 'public'" class="visibility"> <span v-if="appearNote.visibility !== 'public'" class="visibility">
<i v-if="appearNote.visibility === 'home'" class="fas fa-home"></i> <i v-if="appearNote.visibility === 'home'" class="fas fa-home"></i>
<i v-else-if="appearNote.visibility === 'followers'" class="fas fa-unlock"></i> <i v-else-if="appearNote.visibility === 'followers'" class="fas fa-unlock"></i>
@ -86,7 +84,9 @@
</div> </div>
<footer class="footer"> <footer class="footer">
<div class="info"> <div class="info">
<MkTime class="created-at" :time="appearNote.createdAt" mode="detail"/> <MkA class="created-at" :to="notePage(appearNote)">
<MkTime :time="appearNote.createdAt" mode="detail"/>
</MkA>
</div> </div>
<XReactionsViewer ref="reactionsViewer" :note="appearNote"/> <XReactionsViewer ref="reactionsViewer" :note="appearNote"/>
<button class="button _button" @click="reply()"> <button class="button _button" @click="reply()">
@ -138,6 +138,7 @@ import { url } from '@/config';
import copyToClipboard from '@/scripts/copy-to-clipboard'; import copyToClipboard from '@/scripts/copy-to-clipboard';
import { checkWordMute } from '@/scripts/check-word-mute'; import { checkWordMute } from '@/scripts/check-word-mute';
import { userPage } from '@/filters/user'; import { userPage } from '@/filters/user';
import { notePage } from '@/filters/note';
import * as os from '@/os'; import * as os from '@/os';
import { noteActions, noteViewInterruptors } from '@/store'; import { noteActions, noteViewInterruptors } from '@/store';
import { reactionPicker } from '@/scripts/reaction-picker'; import { reactionPicker } from '@/scripts/reaction-picker';
@ -183,6 +184,7 @@ export default defineComponent({
muted: false, muted: false,
translation: null, translation: null,
translating: false, translating: false,
notePage,
}; };
}, },
@ -647,7 +649,7 @@ export default defineComponent({
text: this.$ts.pin, text: this.$ts.pin,
action: () => this.togglePin(true) action: () => this.togglePin(true)
} : undefined, } : undefined,
...(this.$i.isModerator || this.$i.isAdmin ? [ /*...(this.$i.isModerator || this.$i.isAdmin ? [
null, null,
{ {
icon: 'fas fa-bullhorn', icon: 'fas fa-bullhorn',
@ -655,7 +657,7 @@ export default defineComponent({
action: this.promote action: this.promote
}] }]
: [] : []
), ),*/
...(this.appearNote.userId != this.$i.id ? [ ...(this.appearNote.userId != this.$i.id ? [
null, null,
{ {
@ -1017,12 +1019,6 @@ export default defineComponent({
border: solid 0.5px var(--divider); border: solid 0.5px var(--divider);
border-radius: 4px; border-radius: 4px;
} }
> .admin,
> .moderator {
margin-right: 0.5em;
color: var(--badge);
}
} }
} }
} }

View File

@ -5,8 +5,6 @@
</MkA> </MkA>
<div v-if="note.user.isBot" class="is-bot">bot</div> <div v-if="note.user.isBot" class="is-bot">bot</div>
<div class="username"><MkAcct :user="note.user"/></div> <div class="username"><MkAcct :user="note.user"/></div>
<div v-if="note.user.isAdmin" class="admin"><i class="fas fa-bookmark"></i></div>
<div v-if="!note.user.isAdmin && note.user.isModerator" class="moderator"><i class="far fa-bookmark"></i></div>
<div class="info"> <div class="info">
<MkA class="created-at" :to="notePage(note)"> <MkA class="created-at" :to="notePage(note)">
<MkTime :time="note.createdAt"/> <MkTime :time="note.createdAt"/>
@ -23,7 +21,7 @@
<script lang="ts"> <script lang="ts">
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
import notePage from '@/filters/note'; import { notePage } from '@/filters/note';
import { userPage } from '@/filters/user'; import { userPage } from '@/filters/user';
import * as os from '@/os'; import * as os from '@/os';
@ -79,13 +77,6 @@ export default defineComponent({
border-radius: 3px; border-radius: 3px;
} }
> .admin,
> .moderator {
flex-shrink: 0;
margin-right: 0.5em;
color: var(--badge);
}
> .username { > .username {
flex-shrink: 9999999; flex-shrink: 9999999;
margin: 0 .5em 0 0; margin: 0 .5em 0 0;

View File

@ -26,7 +26,7 @@
<script lang="ts"> <script lang="ts">
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
import notePage from '@/filters/note'; import { notePage } from '@/filters/note';
import XNoteHeader from './note-header.vue'; import XNoteHeader from './note-header.vue';
import XSubNoteContent from './sub-note-content.vue'; import XSubNoteContent from './sub-note-content.vue';
import XCwButton from './cw-button.vue'; import XCwButton from './cw-button.vue';

View File

@ -623,6 +623,7 @@ export default defineComponent({
text: this.$ts.pin, text: this.$ts.pin,
action: () => this.togglePin(true) action: () => this.togglePin(true)
} : undefined, } : undefined,
/*
...(this.$i.isModerator || this.$i.isAdmin ? [ ...(this.$i.isModerator || this.$i.isAdmin ? [
null, null,
{ {
@ -631,7 +632,7 @@ export default defineComponent({
action: this.promote action: this.promote
}] }]
: [] : []
), ),*/
...(this.appearNote.userId != this.$i.id ? [ ...(this.appearNote.userId != this.$i.id ? [
null, null,
{ {
@ -858,6 +859,7 @@ export default defineComponent({
.tkcbzcuz { .tkcbzcuz {
position: relative; position: relative;
transition: box-shadow 0.1s ease; transition: box-shadow 0.1s ease;
font-size: 1.05em;
overflow: clip; overflow: clip;
contain: content; contain: content;

View File

@ -74,7 +74,7 @@ import { getNoteSummary } from '@/scripts/get-note-summary';
import XReactionIcon from './reaction-icon.vue'; import XReactionIcon from './reaction-icon.vue';
import MkFollowButton from './follow-button.vue'; import MkFollowButton from './follow-button.vue';
import XReactionTooltip from './reaction-tooltip.vue'; import XReactionTooltip from './reaction-tooltip.vue';
import notePage from '@/filters/note'; import { notePage } from '@/filters/note';
import { userPage } from '@/filters/user'; import { userPage } from '@/filters/user';
import { i18n } from '@/i18n'; import { i18n } from '@/i18n';
import * as os from '@/os'; import * as os from '@/os';
@ -107,28 +107,25 @@ export default defineComponent({
const reactionRef = ref(null); const reactionRef = ref(null);
onMounted(() => { onMounted(() => {
let readObserver: IntersectionObserver | null = null;
let connection = null;
if (!props.notification.isRead) { if (!props.notification.isRead) {
readObserver = new IntersectionObserver((entries, observer) => { const readObserver = new IntersectionObserver((entries, observer) => {
if (!entries.some(entry => entry.isIntersecting)) return; if (!entries.some(entry => entry.isIntersecting)) return;
os.stream.send('readNotification', { os.stream.send('readNotification', {
id: props.notification.id id: props.notification.id
}); });
entries.map(({ target }) => observer.unobserve(target)); observer.disconnect();
}); });
readObserver.observe(elRef.value); readObserver.observe(elRef.value);
connection = os.stream.useChannel('main'); const connection = os.stream.useChannel('main');
connection.on('readAllNotifications', () => readObserver.unobserve(elRef.value)); connection.on('readAllNotifications', () => readObserver.disconnect());
}
onUnmounted(() => { onUnmounted(() => {
if (readObserver) readObserver.unobserve(elRef.value); readObserver.disconnect();
if (connection) connection.dispose(); connection.dispose();
}); });
}
}); });
const followRequestDone = ref(false); const followRequestDone = ref(false);

View File

@ -206,8 +206,6 @@ export default defineComponent({
> .input { > .input {
flex: 1; flex: 1;
margin-top: 16px;
margin-bottom: 0;
} }
> button { > button {
@ -223,7 +221,7 @@ export default defineComponent({
} }
> section { > section {
margin: 16px 0 -16px 0; margin: 16px 0 0 0;
> div { > div {
margin: 0 8px; margin: 0 8px;

View File

@ -1,7 +1,7 @@
<template> <template>
<div class="tivcixzd" :class="{ done: closed || isVoted }"> <div class="tivcixzd" :class="{ done: closed || isVoted }">
<ul> <ul>
<li v-for="(choice, i) in poll.choices" :key="i" :class="{ voted: choice.voted }" @click="vote(i)"> <li v-for="(choice, i) in note.poll.choices" :key="i" :class="{ voted: choice.voted }" @click="vote(i)">
<div class="backdrop" :style="{ 'width': `${showResult ? (choice.votes / total * 100) : 0}%` }"></div> <div class="backdrop" :style="{ 'width': `${showResult ? (choice.votes / total * 100) : 0}%` }"></div>
<span> <span>
<template v-if="choice.isVoted"><i class="fas fa-check"></i></template> <template v-if="choice.isVoted"><i class="fas fa-check"></i></template>
@ -13,7 +13,7 @@
<p v-if="!readOnly"> <p v-if="!readOnly">
<span>{{ $t('_poll.totalVotes', { n: total }) }}</span> <span>{{ $t('_poll.totalVotes', { n: total }) }}</span>
<span> · </span> <span> · </span>
<a v-if="!closed && !isVoted" @click="toggleShowResult">{{ showResult ? $ts._poll.vote : $ts._poll.showResult }}</a> <a v-if="!closed && !isVoted" @click="showResult = !showResult">{{ showResult ? $ts._poll.vote : $ts._poll.showResult }}</a>
<span v-if="isVoted">{{ $ts._poll.voted }}</span> <span v-if="isVoted">{{ $ts._poll.voted }}</span>
<span v-else-if="closed">{{ $ts._poll.closed }}</span> <span v-else-if="closed">{{ $ts._poll.closed }}</span>
<span v-if="remaining > 0"> · {{ timer }}</span> <span v-if="remaining > 0"> · {{ timer }}</span>
@ -22,9 +22,10 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent } from 'vue'; import { computed, defineComponent, onUnmounted, ref, toRef } from 'vue';
import { sum } from '@/scripts/array'; import { sum } from '@/scripts/array';
import * as os from '@/os'; import * as os from '@/os';
import { i18n } from '@/i18n';
export default defineComponent({ export default defineComponent({
props: { props: {
@ -38,65 +39,67 @@ export default defineComponent({
default: false, default: false,
} }
}, },
data() {
return {
remaining: -1,
showResult: false,
};
},
computed: {
poll(): any {
return this.note.poll;
},
total(): number {
return sum(this.poll.choices.map(x => x.votes));
},
closed(): boolean {
return !this.remaining;
},
timer(): string {
return this.$t(
this.remaining >= 86400 ? '_poll.remainingDays' :
this.remaining >= 3600 ? '_poll.remainingHours' :
this.remaining >= 60 ? '_poll.remainingMinutes' : '_poll.remainingSeconds', {
s: Math.floor(this.remaining % 60),
m: Math.floor(this.remaining / 60) % 60,
h: Math.floor(this.remaining / 3600) % 24,
d: Math.floor(this.remaining / 86400)
});
},
isVoted(): boolean {
return !this.poll.multiple && this.poll.choices.some(c => c.isVoted);
}
},
created() {
this.showResult = this.readOnly || this.isVoted;
if (this.note.poll.expiresAt) { setup(props) {
const update = () => { const remaining = ref(-1);
if (this.remaining = Math.floor(Math.max(new Date(this.note.poll.expiresAt).getTime() - Date.now(), 0) / 1000))
requestAnimationFrame(update); const total = computed(() => sum(props.note.poll.choices.map(x => x.votes)));
else const closed = computed(() => remaining.value === 0);
this.showResult = true; const isVoted = computed(() => !props.note.poll.multiple && props.note.poll.choices.some(c => c.isVoted));
const timer = computed(() => i18n.t(
remaining.value >= 86400 ? '_poll.remainingDays' :
remaining.value >= 3600 ? '_poll.remainingHours' :
remaining.value >= 60 ? '_poll.remainingMinutes' : '_poll.remainingSeconds', {
s: Math.floor(remaining.value % 60),
m: Math.floor(remaining.value / 60) % 60,
h: Math.floor(remaining.value / 3600) % 24,
d: Math.floor(remaining.value / 86400)
}));
const showResult = ref(props.readOnly || isVoted.value);
//
if (props.note.poll.expiresAt) {
const tick = () => {
remaining.value = Math.floor(Math.max(new Date(props.note.poll.expiresAt).getTime() - Date.now(), 0) / 1000);
if (remaining.value === 0) {
showResult.value = true;
}
}; };
update(); tick();
} const intevalId = window.setInterval(tick, 3000);
}, onUnmounted(() => {
methods: { window.clearInterval(intevalId);
toggleShowResult() {
this.showResult = !this.showResult;
},
vote(id) {
if (this.readOnly || this.closed || !this.poll.multiple && this.poll.choices.some(c => c.isVoted)) return;
os.api('notes/polls/vote', {
noteId: this.note.id,
choice: id
}).then(() => {
if (!this.showResult) this.showResult = !this.poll.multiple;
}); });
} }
}
const vote = async (id) => {
if (props.readOnly || closed.value || isVoted.value) return;
const { canceled } = await os.confirm({
type: 'question',
text: i18n.t('voteConfirm', { choice: props.note.poll.choices[id].text }),
});
if (canceled) return;
await os.api('notes/polls/vote', {
noteId: props.note.id,
choice: id,
});
if (!showResult.value) showResult.value = !props.note.poll.multiple;
};
return {
remaining,
showResult,
total,
isVoted,
closed,
timer,
vote,
};
},
}); });
</script> </script>
@ -112,38 +115,38 @@ export default defineComponent({
display: block; display: block;
position: relative; position: relative;
margin: 4px 0; margin: 4px 0;
padding: 4px 8px; padding: 4px;
border: solid 0.5px var(--divider); //border: solid 0.5px var(--divider);
background: var(--accentedBg);
border-radius: 4px; border-radius: 4px;
overflow: hidden; overflow: hidden;
cursor: pointer; cursor: pointer;
&:hover {
background: rgba(#000, 0.05);
}
&:active {
background: rgba(#000, 0.1);
}
> .backdrop { > .backdrop {
position: absolute; position: absolute;
top: 0; top: 0;
left: 0; left: 0;
height: 100%; height: 100%;
background: var(--accent); background: var(--accent);
background: linear-gradient(90deg,var(--buttonGradateA),var(--buttonGradateB));
transition: width 1s ease; transition: width 1s ease;
} }
> span { > span {
position: relative; position: relative;
display: inline-block;
padding: 3px 5px;
background: var(--panel);
border-radius: 3px;
> i { > i {
margin-right: 4px; margin-right: 4px;
color: var(--accent);
} }
> .votes { > .votes {
margin-left: 4px; margin-left: 4px;
opacity: 0.7;
} }
} }
} }
@ -160,14 +163,6 @@ export default defineComponent({
&.done { &.done {
> ul > li { > ul > li {
cursor: default; cursor: default;
&:hover {
background: transparent;
}
&:active {
background: transparent;
}
} }
} }
} }

View File

@ -289,9 +289,14 @@ export default defineComponent({
if (this.reply && this.reply.text != null) { if (this.reply && this.reply.text != null) {
const ast = mfm.parse(this.reply.text); const ast = mfm.parse(this.reply.text);
const otherHost = this.reply.user.host;
for (const x of extractMentions(ast)) { for (const x of extractMentions(ast)) {
const mention = x.host ? `@${x.username}@${toASCII(x.host)}` : `@${x.username}`; const mention = x.host ?
`@${x.username}@${toASCII(x.host)}` :
(otherHost == null || otherHost == host) ?
`@${x.username}` :
`@${x.username}@${toASCII(otherHost)}`;
// //
if (this.$i.username == x.username && x.host == null) continue; if (this.$i.username == x.username && x.host == null) continue;

View File

@ -41,6 +41,7 @@ export default defineComponent({
> .icon { > .icon {
display: block; display: block;
width: 60px; width: 60px;
font-size: 60px; // unicodewidth
margin: 0 auto; margin: 0 auto;
} }

View File

@ -62,6 +62,7 @@ export default defineComponent({
> .icon { > .icon {
display: block; display: block;
width: 60px; width: 60px;
font-size: 60px; // unicodewidth
margin: 0 auto; margin: 0 auto;
} }

View File

@ -97,9 +97,7 @@ export default defineComponent({
limit: 11 limit: 11
}); });
const users = reactions const users = reactions.map(x => x.user);
.sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime())
.map(x => x.user);
os.popup(XDetails, { os.popup(XDetails, {
showing, showing,

View File

@ -48,9 +48,7 @@ export default defineComponent({
limit: 11 limit: 11
}); });
const users = renotes const users = renotes.map(x => x.user);
.sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime())
.map(x => x.user);
if (users.length < 1) return; if (users.length < 1) return;

View File

@ -197,6 +197,14 @@ export default defineComponent({
}); });
break; break;
} }
case '932c904e-9460-45b7-9ce6-7ed33be7eb2c': {
os.alert({
type: 'error',
title: this.$ts.loginFailed,
text: this.$ts.incorrectPassword,
});
break;
}
case 'e03a5f46-d309-4865-9b69-56282d94e1eb': { case 'e03a5f46-d309-4865-9b69-56282d94e1eb': {
showSuspendedDialog(); showSuspendedDialog();
break; break;

View File

@ -142,12 +142,12 @@ export default defineComponent({
padding: 8px 14px; padding: 8px 14px;
text-align: center; text-align: center;
font-weight: normal; font-weight: normal;
font-size: 0.8em; font-size: 0.9em;
line-height: 22px; line-height: 22px;
box-shadow: none; box-shadow: none;
text-decoration: none; text-decoration: none;
background: var(--buttonBg); background: var(--buttonBg);
border-radius: 4px; border-radius: 5px;
overflow: clip; overflow: clip;
box-sizing: border-box; box-sizing: border-box;
transition: background 0.1s ease; transition: background 0.1s ease;

View File

@ -2,7 +2,7 @@
<div ref="items" v-hotkey="keymap" <div ref="items" v-hotkey="keymap"
class="rrevdjwt" class="rrevdjwt"
:class="{ center: align === 'center' }" :class="{ center: align === 'center' }"
:style="{ width: width ? width + 'px' : null }" :style="{ width: width ? width + 'px' : null, maxHeight: maxHeight ? maxHeight + 'px' : null }"
@contextmenu.self="e => e.preventDefault()" @contextmenu.self="e => e.preventDefault()"
> >
<template v-for="(item, i) in items2"> <template v-for="(item, i) in items2">
@ -64,6 +64,10 @@ export default defineComponent({
type: Number, type: Number,
required: false required: false
}, },
maxHeight: {
type: Number,
required: false
},
}, },
emits: ['close'], emits: ['close'],
data() { data() {
@ -146,9 +150,10 @@ export default defineComponent({
<style lang="scss" scoped> <style lang="scss" scoped>
.rrevdjwt { .rrevdjwt {
padding: 8px 0; padding: 8px 0;
box-sizing: border-box;
min-width: 200px; min-width: 200px;
max-height: 90vh;
overflow: auto; overflow: auto;
overscroll-behavior: contain;
&.center { &.center {
> .item { > .item {

View File

@ -1,6 +1,6 @@
<template> <template>
<MkPopup ref="popup" :src="src" @closed="$emit('closed')"> <MkPopup ref="popup" v-slot="{ maxHeight, close }" :src="src" @closed="$emit('closed')">
<MkMenu :items="items" :align="align" :width="width" class="_popup _shadow" @close="$refs.popup.close()"/> <MkMenu :items="items" :align="align" :width="width" :max-height="maxHeight" class="_popup _shadow" @close="close()"/>
</MkPopup> </MkPopup>
</template> </template>

View File

@ -1,15 +1,15 @@
<template> <template>
<transition :name="$store.state.animation ? 'popup-menu' : ''" appear @after-leave="onClosed" @enter="$emit('opening')" @after-enter="childRendered"> <transition :name="$store.state.animation ? 'popup-menu' : ''" appear @after-leave="$emit('closed')" @enter="$emit('opening')">
<div v-show="manualShowing != null ? manualShowing : showing" ref="content" class="ccczpooj" :class="{ front, fixed, top: position === 'top' }" :style="{ pointerEvents: (manualShowing != null ? manualShowing : showing) ? 'auto' : 'none', '--transformOrigin': transformOrigin }"> <div v-show="manualShowing != null ? manualShowing : showing" ref="content" class="ccczpooj" :class="{ front, fixed, top: position === 'top' }" :style="{ pointerEvents: (manualShowing != null ? manualShowing : showing) ? 'auto' : 'none', '--transformOrigin': transformOrigin }">
<slot></slot> <slot :max-height="maxHeight" :close="close"></slot>
</div> </div>
</transition> </transition>
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent, PropType } from 'vue'; import { defineComponent, nextTick, onMounted, onUnmounted, PropType, ref, watch } from 'vue';
function getFixedContainer(el: Element | null): Element | null { function getFixedContainer(el: Element | null | undefined): Element | null {
if (el == null || el.tagName === 'BODY') return null; if (el == null || el.tagName === 'BODY') return null;
const position = window.getComputedStyle(el).getPropertyValue('position'); const position = window.getComputedStyle(el).getPropertyValue('position');
if (position === 'fixed') { if (position === 'fixed') {
@ -41,55 +41,40 @@ export default defineComponent({
type: Boolean, type: Boolean,
required: false, required: false,
default: false, default: false,
} },
noOverlap: {
type: Boolean,
required: false,
default: true,
},
}, },
emits: ['opening', 'click', 'esc', 'close', 'closed'], emits: ['opening', 'click', 'esc', 'close', 'closed'],
data() { setup(props, context) {
return { const maxHeight = ref<number>();
showing: true, const fixed = ref(false);
fixed: false, const transformOrigin = ref('center');
transformOrigin: 'center', const showing = ref(true);
contentClicking: false, const content = ref<HTMLElement>();
const close = () => {
// eslint-disable-next-line vue/no-mutating-props
if (props.src) props.src.style.pointerEvents = 'auto';
showing.value = false;
context.emit('close');
}; };
},
mounted() { const MARGIN = 16;
this.$watch('src', () => {
if (this.src) {
// eslint-disable-next-line vue/no-mutating-props
this.src.style.pointerEvents = 'none';
}
this.fixed = getFixedContainer(this.src) != null;
this.$nextTick(() => {
this.align();
});
}, { immediate: true });
this.$nextTick(() => { const align = () => {
const popover = this.$refs.content as any; if (props.src == null) return;
new ResizeObserver((entries, observer) => {
this.align();
}).observe(popover);
});
document.addEventListener('mousedown', this.onDocumentClick, { passive: true }); const popover = content.value!;
},
beforeUnmount() {
document.removeEventListener('mousedown', this.onDocumentClick);
},
methods: {
align() {
if (this.src == null) return;
const popover = this.$refs.content as any;
if (popover == null) return; if (popover == null) return;
const rect = this.src.getBoundingClientRect(); const rect = props.src.getBoundingClientRect();
const width = popover.offsetWidth; const width = popover.offsetWidth;
const height = popover.offsetHeight; const height = popover.offsetHeight;
@ -97,81 +82,84 @@ export default defineComponent({
let left; let left;
let top; let top;
if (this.srcCenter) { if (props.srcCenter) {
const x = rect.left + (this.fixed ? 0 : window.pageXOffset) + (this.src.offsetWidth / 2); const x = rect.left + (fixed.value ? 0 : window.pageXOffset) + (props.src.offsetWidth / 2);
const y = rect.top + (this.fixed ? 0 : window.pageYOffset) + (this.src.offsetHeight / 2); const y = rect.top + (fixed.value ? 0 : window.pageYOffset) + (props.src.offsetHeight / 2);
left = (x - (width / 2)); left = (x - (width / 2));
top = (y - (height / 2)); top = (y - (height / 2));
} else { } else {
const x = rect.left + (this.fixed ? 0 : window.pageXOffset) + (this.src.offsetWidth / 2); const x = rect.left + (fixed.value ? 0 : window.pageXOffset) + (props.src.offsetWidth / 2);
const y = rect.top + (this.fixed ? 0 : window.pageYOffset) + this.src.offsetHeight; const y = rect.top + (fixed.value ? 0 : window.pageYOffset) + props.src.offsetHeight;
left = (x - (width / 2)); left = (x - (width / 2));
top = y; top = y;
} }
if (this.fixed) { if (fixed.value) {
//
if (left + width > window.innerWidth) { if (left + width > window.innerWidth) {
left = window.innerWidth - width; left = window.innerWidth - width;
} }
if (top + height > window.innerHeight) { //
top = window.innerHeight - height; if (top + height > (window.innerHeight - MARGIN)) {
if (props.noOverlap) {
const underSpace = (window.innerHeight - MARGIN) - top;
const upperSpace = (rect.top - MARGIN);
if (underSpace >= (upperSpace / 3)) {
maxHeight.value = underSpace;
} else {
maxHeight.value = upperSpace;
top = (upperSpace + MARGIN) - height;
}
} else {
top = (window.innerHeight - MARGIN) - height;
}
} }
} else { } else {
//
if (left + width - window.pageXOffset > window.innerWidth) { if (left + width - window.pageXOffset > window.innerWidth) {
left = window.innerWidth - width + window.pageXOffset - 1; left = window.innerWidth - width + window.pageXOffset - 1;
} }
if (top + height - window.pageYOffset > window.innerHeight) { //
top = window.innerHeight - height + window.pageYOffset - 1; if (top + height - window.pageYOffset > (window.innerHeight - MARGIN)) {
if (props.noOverlap) {
const underSpace = (window.innerHeight - MARGIN) - (top - window.pageYOffset);
const upperSpace = (rect.top - MARGIN);
if (underSpace >= (upperSpace / 3)) {
maxHeight.value = underSpace;
} else {
maxHeight.value = upperSpace;
top = window.pageYOffset + ((upperSpace + MARGIN) - height);
}
} else {
top = (window.innerHeight - MARGIN) - height + window.pageYOffset - 1;
}
} }
} }
if (top < 0) { if (top < 0) {
top = 0; top = MARGIN;
} }
if (left < 0) { if (left < 0) {
left = 0; left = 0;
} }
if (top > rect.top + (this.fixed ? 0 : window.pageYOffset)) { if (top > rect.top + (fixed.value ? 0 : window.pageYOffset)) {
this.transformOrigin = 'center top'; transformOrigin.value = 'center top';
} else if ((top + height) <= rect.top + (fixed.value ? 0 : window.pageYOffset)) {
transformOrigin.value = 'center bottom';
} else { } else {
this.transformOrigin = 'center'; transformOrigin.value = 'center';
} }
popover.style.left = left + 'px'; popover.style.left = left + 'px';
popover.style.top = top + 'px'; popover.style.top = top + 'px';
}, };
childRendered() { const onDocumentClick = (ev: MouseEvent) => {
// const flyoutElement = content.value;
const content = this.$refs.content.children[0];
content.addEventListener('mousedown', e => {
this.contentClicking = true;
window.addEventListener('mouseup', e => {
// click mouseup
setTimeout(() => {
this.contentClicking = false;
}, 100);
}, { passive: true, once: true });
}, { passive: true });
},
close() {
// eslint-disable-next-line vue/no-mutating-props
if (this.src) this.src.style.pointerEvents = 'auto';
this.showing = false;
this.$emit('close');
},
onClosed() {
this.$emit('closed');
},
onDocumentClick(ev) {
const flyoutElement = this.$refs.content;
let targetElement = ev.target; let targetElement = ev.target;
do { do {
if (targetElement === flyoutElement) { if (targetElement === flyoutElement) {
@ -179,9 +167,45 @@ export default defineComponent({
} }
targetElement = targetElement.parentNode; targetElement = targetElement.parentNode;
} while (targetElement); } while (targetElement);
this.close(); close();
} };
}
onMounted(() => {
watch(() => props.src, async () => {
if (props.src) {
// eslint-disable-next-line vue/no-mutating-props
props.src.style.pointerEvents = 'none';
}
fixed.value = getFixedContainer(props.src) != null;
await nextTick()
align();
}, { immediate: true, });
nextTick(() => {
const popover = content.value;
new ResizeObserver((entries, observer) => {
align();
}).observe(popover!);
});
document.addEventListener('mousedown', onDocumentClick, { passive: true });
onUnmounted(() => {
document.removeEventListener('mousedown', onDocumentClick);
});
});
return {
showing,
fixed,
content,
transformOrigin,
maxHeight,
close,
};
},
}); });
</script> </script>

View File

@ -51,9 +51,8 @@ export default defineComponent({
} }
> .title { > .title {
font-size: 0.9em;
opacity: 0.7; opacity: 0.7;
margin: 0 0 8px 12px; margin: 0 0 8px 0;
} }
> .items { > .items {
@ -64,7 +63,6 @@ export default defineComponent({
box-sizing: border-box; box-sizing: border-box;
padding: 10px 16px 10px 8px; padding: 10px 16px 10px 8px;
border-radius: 9px; border-radius: 9px;
font-size: 0.9em;
&:hover { &:hover {
text-decoration: none; text-decoration: none;

View File

@ -1,13 +1,13 @@
<template> <template>
<transition name="tooltip" appear @after-leave="$emit('closed')"> <transition name="tooltip" appear @after-leave="$emit('closed')">
<div v-show="showing" ref="content" class="buebdbiu _acrylic _shadow" :style="{ maxWidth: maxWidth + 'px' }"> <div v-show="showing" ref="el" class="buebdbiu _acrylic _shadow" :style="{ maxWidth: maxWidth + 'px' }">
<slot>{{ text }}</slot> <slot>{{ text }}</slot>
</div> </div>
</transition> </transition>
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent } from 'vue'; import { defineComponent, nextTick, onMounted, onUnmounted, ref } from 'vue';
export default defineComponent({ export default defineComponent({
props: { props: {
@ -31,35 +31,64 @@ export default defineComponent({
emits: ['closed'], emits: ['closed'],
mounted() { setup(props, context) {
this.$nextTick(() => { const el = ref<HTMLElement>();
if (this.source == null) {
this.$emit('closed');
return;
}
const rect = this.source.getBoundingClientRect(); const setPosition = () => {
if (el.value == null) return;
const contentWidth = this.$refs.content.offsetWidth; const rect = props.source.getBoundingClientRect();
const contentHeight = this.$refs.content.offsetHeight;
let left = rect.left + window.pageXOffset + (this.source.offsetWidth / 2); const contentWidth = el.value.offsetWidth;
const contentHeight = el.value.offsetHeight;
let left = rect.left + window.pageXOffset + (props.source.offsetWidth / 2);
let top = rect.top + window.pageYOffset - contentHeight; let top = rect.top + window.pageYOffset - contentHeight;
left -= (this.$el.offsetWidth / 2); left -= (el.value.offsetWidth / 2);
if (left + contentWidth - window.pageXOffset > window.innerWidth) { if (left + contentWidth - window.pageXOffset > window.innerWidth) {
left = window.innerWidth - contentWidth + window.pageXOffset - 1; left = window.innerWidth - contentWidth + window.pageXOffset - 1;
} }
if (top - window.pageYOffset < 0) { if (top - window.pageYOffset < 0) {
top = rect.top + window.pageYOffset + this.source.offsetHeight; top = rect.top + window.pageYOffset + props.source.offsetHeight;
this.$refs.content.style.transformOrigin = 'center top'; el.value.style.transformOrigin = 'center top';
} }
this.$el.style.left = left + 'px'; el.value.style.left = left + 'px';
this.$el.style.top = top + 'px'; el.value.style.top = top + 'px';
};
onMounted(() => {
nextTick(() => {
if (props.source == null) {
context.emit('closed');
return;
}
setPosition();
let loopHandler;
const loop = () => {
loopHandler = window.requestAnimationFrame(() => {
setPosition();
loop();
});
};
loop();
onUnmounted(() => {
window.cancelAnimationFrame(loopHandler);
});
});
}); });
return {
el,
};
}, },
}) })
</script> </script>

View File

@ -10,6 +10,7 @@ import appear from './appear';
import anim from './anim'; import anim from './anim';
import stickyContainer from './sticky-container'; import stickyContainer from './sticky-container';
import clickAnime from './click-anime'; import clickAnime from './click-anime';
import panel from './panel';
export default function(app: App) { export default function(app: App) {
app.directive('userPreview', userPreview); app.directive('userPreview', userPreview);
@ -23,4 +24,5 @@ export default function(app: App) {
app.directive('anim', anim); app.directive('anim', anim);
app.directive('click-anime', clickAnime); app.directive('click-anime', clickAnime);
app.directive('sticky-container', stickyContainer); app.directive('sticky-container', stickyContainer);
app.directive('panel', panel);
} }

View File

@ -0,0 +1,24 @@
import { Directive } from 'vue';
export default {
mounted(src, binding, vn) {
const getBgColor = (el: HTMLElement) => {
const style = window.getComputedStyle(el);
if (style.backgroundColor && !['rgba(0, 0, 0, 0)', 'rgba(0,0,0,0)', 'transparent'].includes(style.backgroundColor)) {
return style.backgroundColor;
} else {
return getBgColor(el.parentElement);
}
}
const parentBg = getBgColor(src.parentElement);
const myBg = getComputedStyle(document.documentElement).getPropertyValue('--panel');
if (parentBg === myBg) {
src.style.backgroundColor = 'var(--bg)';
} else {
src.style.backgroundColor = 'var(--panel)';
}
},
} as Directive;

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