Merge branch 'develop'

This commit is contained in:
syuilo 2022-02-02 00:40:00 +09:00
commit 9c2f5ee041
138 changed files with 2046 additions and 1511 deletions

View File

@ -7,6 +7,24 @@
--> -->
## 12.103.0 (2022/02/02)
### Improvements
- クライアント: 連合インスタンスページからインスタンス情報再取得を行えるように
### Bugfixes
- クライアント: 投稿のNSFW画像を表示したあとにリアクションが更新されると画像が非表示になる問題を修正
- クライアント: 「クリップ」ページが開かない問題を修正
- クライアント: トレンドウィジェットが動作しないのを修正
- クライアント: フェデレーションウィジェットが動作しないのを修正
- クライアント: リアクション設定で絵文字ピッカーが開かないのを修正
- クライアント: DMページでメンションが含まれる問題を修正
- クライアント: 投稿フォームのハッシュタグ保持フィールドが動作しない問題を修正
- クライアント: サイドビューが動かないのを修正
- クライアント: ensure that specified users does not get duplicates
- Add `img-src` and `media-src` directives to `Content-Security-Policy` for
files and media proxy
## 12.102.1 (2022/01/27) ## 12.102.1 (2022/01/27)
### Bugfixes ### Bugfixes
- チャットが表示できない問題を修正 - チャットが表示できない問題を修正

View File

@ -3,7 +3,7 @@ We're glad you're interested in contributing Misskey! In this document you will
** Important:** This project uses Japanese as its major language, **but you do not need to translate and write the Issues/PRs in Japanese.** ** Important:** This project uses Japanese as its major language, **but you do not need to translate and write the Issues/PRs in Japanese.**
Also, you might receive comments on your Issue/PR in Japanese, but you do not need to reply to them in Japanese as well.\ Also, you might receive comments on your Issue/PR in Japanese, but you do not need to reply to them in Japanese as well.\
The accuracy of translation into Japanese is not high, so it will be easier for us to understand if you write it in the original language. The accuracy of machine translation into Japanese is not high, so it will be easier for us to understand if you write it in the original language.
It will also allow the reader to use the translation tool of their preference if necessary. It will also allow the reader to use the translation tool of their preference if necessary.
## Issues ## Issues

View File

@ -176,3 +176,7 @@ describe('After user singed in', () => {
cy.contains('Hello, Misskey!'); cy.contains('Hello, Misskey!');
}); });
}); });
// TODO: 投稿フォームの公開範囲指定のテスト
// TODO: 投稿フォームのファイル添付のテスト
// TODO: 投稿フォームのハッシュタグ保持フィールドのテスト

View File

@ -1 +1,510 @@
--- ---
_lang_: "বাংলা"
headlineMisskey: "নোট ব্যাবহার করে সংযুক্ত নেটওয়ার্ক"
introMisskey: "স্বাগতম! মিসকি একটি ওপেন সোর্স, ডিসেন্ট্রালাইজড মাইক্রোব্লগিং পরিষেবা। \n\"নোট\" তৈরির মাধ্যমে যা ঘটছে তা সবার সাথে শেয়ার করুন 📡\n\"রিঅ্যাকশন\" গুলির মাধ্যমে যেকোনো নোট সম্পর্কে আপনার অনুভূতি ব্যাক্ত করতে পারেন 👍\nএকটি নতুন দুনিয়া ঘুরে দেখুন 🚀\n"
monthAndDay: "{day}/{month}"
search: "খুঁজুন"
notifications: "বিজ্ঞপ্তি"
username: "ব্যবহারকারীর নাম"
password: "পাসওয়ার্ড"
forgotPassword: "পাসওয়ার্ড ভুলে গেছেন"
fetchingAsApObject: "ফেডিভার্স থেকে খবর আনা হচ্ছে..."
ok: "ঠিক"
gotIt: "বুঝেছি"
cancel: "বাতিল"
enterUsername: "ইউজারনেম লিখুন"
renotedBy: "{user} রিনোট করেছেন"
noNotes: "কোন নোট নেই"
noNotifications: "কোনো বিজ্ঞপ্তি নেই"
instance: "ইন্সট্যান্স"
settings: "সেটিংস"
basicSettings: "সাধারণ সেটিংস"
otherSettings: "অন্যান্য সেটিংস"
openInWindow: "নতুন উইন্ডোতে খুলা"
profile: "প্রোফাইল"
timeline: "টাইমলাইন"
noAccountDescription: "এই ব্যাবহারকারীর কোন বায়ো নেই"
login: "প্রবেশ করুন"
loggingIn: "প্রবেশ করা হচ্ছে..."
logout: "লগআউট"
signup: "নিবন্ধন করুন"
uploading: "আপলোড হচ্ছ …"
save: "সংরক্ষণ"
users: "ব্যবহারকারীগণ"
addUser: "ব্যবহারকারী যোগ করুন"
favorite: "পছন্দ"
favorites: "পছন্দগুলি"
unfavorite: "পছন্দ না"
favorited: "পছন্দ করা হয়েছে"
alreadyFavorited: "ইতিমধ্যে পছন্দ করা হয়েছে"
cantFavorite: "পছন্দ করা যায়নি"
pin: "পিন করা"
unpin: "পিন সরান"
copyContent: "বিষয়বস্তু কপি করুন"
copyLink: "লিঙ্ক কপি করুন"
delete: "মুছুন"
deleteAndEdit: "মুছুন এবং সম্পাদনা করুন"
deleteAndEditConfirm: "আপনি কি এই নোটটি মুছে এটি সম্পাদনা করার বিষয়ে নিশ্চিত? আপনি এটির সমস্ত রিঅ্যাকশন, রিনোট এবং জবাব হারাবেন।"
addToList: "লিস্ট এ যোগ করুন"
sendMessage: "একটি বার্তা পাঠান"
copyUsername: "ব্যবহারকারীর নাম কপি করুন"
searchUser: "ব্যবহারকারী খুঁজুন..."
reply: "জবাব"
loadMore: "আরও দেখুন"
showMore: "আরও দেখুন"
youGotNewFollower: "আপনাকে অনুসরণ করছে"
receiveFollowRequest: "অনুসরণ করার জন্য অনুরোধ পাওয়া গেছে"
followRequestAccepted: "অনুসরণ করার অনুরোধ গৃহীত হয়েছে"
mention: "উল্লেখ"
mentions: "উল্লেখসমূহ"
directNotes: "ডাইরেক্ট নোটগুলি"
importAndExport: "আমদানি এবং রপ্তানি"
import: "আমদানি করুণ"
export: "রপ্তানি"
files: "ফাইলগুলি"
download: "ডাউনলোড"
driveFileDeleteConfirm: "আপনি কি নিশ্চিত যে আপনি \"{name}\" ডিলিট করতে চান? যে সকল নোটের সাথে এই ফাইলটি সংযুক্ত সেগুলোও ডিলিট করা হবে।"
unfollowConfirm: "{name} কে আনফলোও করার ব্যাপারে নিশ্চিত?"
exportRequested: "আপনার তথ্যসমূহ রপ্তানির জন্য অনুরোধ করেছেন। এতে কিছু সময় লাগতে পারে। রপ্তানি সম্পন্ন হলে তা আপনার ড্রাইভে সংরক্ষিত হবে।"
importRequested: "আপনার তথ্যসমূহ আমদানির জন্য অনুরোধ করেছেন। এতে কিছু সময় লাগতে পারে। "
lists: "লিস্ট"
noLists: "কোন লিস্ট নেই"
note: "নোট"
notes: "নোটগুলি"
following: "অনুসরণ করা হচ্ছে"
followers: "অনুসরণকারী"
followsYou: "আপনাকে অনুসরণ করে"
createList: "লিস্ট তৈরি করুন"
manageLists: "লিস্ট ব্যাবস্থাপনা"
error: "সমস্যা"
somethingHappened: "একটি ত্রুটি হয়েছে"
retry: "আবার চেষ্টা করুন"
pageLoadError: "পেজ লোড করা যায়নি"
pageLoadErrorDescription: "এটি সাধারনত নেটওয়ার্কের সমস্যার বা ব্রাউজার ক্যাশের কারণে ঘটে থাকে। ব্রাউজার এর ক্যাশ পরিষ্কার করুন এবং একটু পর আবার চেষ্টা করুন। "
serverIsDead: "এই সার্ভার বর্তমানে সাড়া দিচ্ছে না। একটু পরে আবার চেষ্টা করুন।"
youShouldUpgradeClient: "এই পেজ দেখার জন্য আপনার ব্রাউজার রিফ্রেশ করে ক্লায়েন্ট আপডেট করুন। "
enterListName: "লিস্টের নাম লিখুন"
privacy: "গোপনীয়তা"
makeFollowManuallyApprove: "অনুসরণ করার অনুরোধগুলি গৃহীত হওয়ার জন্য আপনার অনুমতি লাগবে"
defaultNoteVisibility: "ডিফল্ট দৃশ্যমান্যতা"
follow: "অনুসরণ"
followRequest: "অনুসরণ করার অনুরোধ"
followRequests: "অনুসরণ করার অনুরোধসমূহ"
unfollow: "অনুসরণ বাতিল"
followRequestPending: "অনুসরণ করার অনুরোধ বিচারাধীন"
enterEmoji: "ইমোজি প্রবেশ করান"
renote: "রিনোট"
unrenote: "রিনোট সরান "
renoted: "রিনোট করা হয়েছে"
cantRenote: "এই নোটটি রিনোট করা যাবে না।"
cantReRenote: "রিনোটকে রিনোট করা যাবে না।"
quote: "উদ্ধৃতি"
pinnedNote: "পিন করা নোট"
pinned: "পিন করা"
you: "আপনি"
clickToShow: "দেখার জন্য ক্লিক করুন"
sensitive: "সংবেদনশীল বিষয়বস্তু"
add: "যুক্ত করুন"
reaction: "প্রতিক্রিয়া"
reactionSetting: "রিঅ্যাকশন পিকারে যেসকল প্রতিক্রিয়া দেখানো হবে"
reactionSettingDescription2: "পুনরায় সাজাতে টেনে আনুন, মুছতে ক্লিক করুন, যোগ করতে + টিপুন।"
rememberNoteVisibility: "নোটের দৃশ্যমান্যতার সেটিংস মনে রাখুন"
attachCancel: "অ্যাটাচমেন্ট সরান "
markAsSensitive: "সংবেদনশীল হিসাবে চিহ্নিত করুন"
unmarkAsSensitive: "সংবেদনশীল চিহ্ন সরান"
enterFileName: "ফাইলের নাম লিখুন"
mute: "মিউট"
unmute: "আনমিউট"
block: "ব্লক"
unblock: "ব্লক সরান"
suspend: "স্থগিত করা"
unsuspend: "অস্থগিত করা"
blockConfirm: "ব্লক করতে চান?"
unblockConfirm: "ব্লক সরাতে চান?"
suspendConfirm: "স্থগিত করতে চান?"
unsuspendConfirm: "অস্থগিত করতে চান?"
selectList: "লিস্ট নির্বাচন করুন"
selectAntenna: "অ্যান্টেনা নির্বাচন করুন"
selectWidget: "উইজেট নির্বাচন করুন"
editWidgets: "উইজেট সম্পাদনা করুন"
editWidgetsExit: "সম্পাদনা শেষ করুন"
customEmojis: "স্বনির্ধারিত ইমোজিগুলি"
emoji: "ইমোজি"
emojis: "ইমোজিগুলি"
emojiName: "ইমোজির নাম"
emojiUrl: "ইমোজির URL"
addEmoji: "ইমোজি যুক্ত করুন"
settingGuide: "সুপারিশকৃত সেটিংস"
cacheRemoteFiles: "রিমোট ফাইলসমুহ ক্যাশ করুন"
cacheRemoteFilesDescription: "যখন এই অপশনটি বন্ধ থাকে তখন রিমোট ফাইল সমূহ সরাসরি রিমোট ইন্সট্যান্স থেকে লোড করা হয়। এই অপশনটি বন্ধ করলে স্টোরেজ এর ব্যাবহার কমবে তবে থাম্বনেইল তৈরি না করার কারণে নেটওয়ার্ক ব্যান্ডউইথ বেশী লাগবে। "
flagAsBot: "বট হিসাবে চিহ্নিত করুন"
flagAsBotDescription: "এই অ্যাকাউন্টটি যদি একটি প্রোগ্রাম দ্বারা পরিচালিত হয়, তাহলে এই অপশনটি চালু করুন। ইন্টারঅ্যাকশান চেইনিং রোধ করতে, মিস্কির সিস্টেম পরিচালনাকে বট-বান্ধব করতে এবং অন্যান্য ডেভেলপারদের সাহায্য করতে আপনার বট এ এই অপশনটি চালু করুন৷"
flagAsCat: "বিড়াল হিসাবে চিহ্নিত করুন"
flagAsCatDescription: "অ্যাকাউন্টটিকে বিড়াল হিসাবে চিহ্নিত করার জন্য অপশনটি চালু করুন।"
autoAcceptFollowed: "আপনি যেসব অ্যাকাউন্ট অনুসরণ করেন, স্বয়ংক্রিয়ভাবে তাদের অনুসরণের অনুরধ স্বীকার করুন"
addAccount: "অ্যাকাউন্ট যোগ করুন"
loginFailed: "প্রবেশ করা যায়নি"
showOnRemote: "রিমোট সার্ভারে দেখুন"
general: "সাধারণ"
wallpaper: "ওয়ালপেপার"
setWallpaper: "ওয়ালপেপার সেট করুন"
removeWallpaper: "ওয়ালপেপার সরান"
searchWith: "খুঁজুন: {q}"
youHaveNoLists: "আপনার কোন লিস্ট নেই"
followConfirm: "{name} কে ফলোও করার ব্যাপারে নিশ্চিত?"
proxyAccount: "প্রক্সি অ্যাকাউন্ট"
proxyAccountDescription: "একটি প্রক্সি অ্যাকাউন্ট এমন একটি অ্যাকাউন্ট যা নির্দিষ্ট শর্তে ব্যবহারকারীদের জন্য রিমোট অনুসরণকারী হিসাবে কাজ করে। উদাহরণস্বরূপ, যখন একজন ব্যবহারকারী একটি রিমোট ব্যবহারকারীকে তালিকাভুক্ত করে, তখন ক্রিয়াকলাপের দৃষ্টান্তে বিতরণ করা হবে না যদি না কেউ তালিকাভুক্ত ব্যবহারকারীকে অনুসরণ করে, তাই প্রক্সি অ্যাকাউন্ট দ্বারা তাকে অনুসরণ করা হবে।"
host: "হোস্ট"
selectUser: "ব্যবহারকারী নির্বাচন করুন"
recipient: "প্রতি"
annotation: "মন্তব্য"
federation: "ফেডিভার্স"
instances: "ইন্সট্যান্স"
registeredAt: "যোগ দিয়েছেন"
latestRequestSentAt: "শেষ রিকুয়েস্ট পাঠানো হয়েছে"
latestRequestReceivedAt: "শেষ রিকুয়েস্ট গৃহীত হয়েছে"
latestStatus: "সর্বশেষ অবস্থা"
storageUsage: "স্টোরেজের ব্যাবহার"
charts: "চার্ট"
perHour: "ঘন্টা প্রতি"
perDay: "দৈনিক"
stopActivityDelivery: "অ্যাক্টিভিটি পাঠানো বন্ধ করুন"
blockThisInstance: "ইন্সট্যান্স ব্লক করুন"
operations: "ক্রিয়াকলাপ"
software: "সফটওয়্যার"
version: "সংস্করণ"
metadata: "মেটাডাটা"
withNFiles: "{n} টি ফাইল"
monitor: "মনিটর"
jobQueue: "জব কিউ"
cpuAndMemory: "সিপিউ এবং মেমরি"
network: "নেটওয়ার্ক"
disk: "ডিস্ক"
instanceInfo: "ইন্সট্যান্সের তথ্য"
statistics: "পরিসংখ্যান"
clearQueue: "কিউ পরিষ্কার করুন"
clearQueueConfirmTitle: "আপনি কি কিউ পরিষ্কার করার ব্যাপারে নিশ্চিত?"
clearQueueConfirmText: "বিতরণ না করা নোট আর বিতরণ করা হবে না। সাধারণত আপনার এটি করার দরকার নেই।"
clearCachedFiles: "ক্যাশ পরিষ্কার করুন"
clearCachedFilesConfirm: "আপনি কি ক্যাশ পরিষ্কার করার ব্যাপারে নিশ্চিত?"
blockedInstances: "ব্লককৃত ইন্সট্যান্সসমুহ"
blockedInstancesDescription: "আপনি যে ইন্সট্যান্সগুলি ব্লক করতে চান তার হোস্টনেমগুলি প্রত্যেকটি আলাদা লাইনে লিখুন। ব্লককৃত ইন্সট্যান্সগুলি এই ইন্সট্যান্সের সাথে যোগাযোগ করতে পারবেনা৷"
muteAndBlock: "মিউট এবং ব্লকগুলি"
mutedUsers: "নিঃশব্দকৃত ব্যবহারকারী"
blockedUsers: "যাদের ব্লক করা হয়েছে"
noUsers: "কোন ব্যাবহারকারী নেই"
editProfile: "প্রোফাইল সম্পাদনা করুন"
noteDeleteConfirm: "আপনি কি নোট ডিলিট করার ব্যাপারে নিশ্চিত?"
pinLimitExceeded: "আপনি আর কোন নোট পিন করতে পারবেন না"
intro: "Misskey এর ইন্সটলেশন সম্পন্ন হয়েছে!দয়া করে অ্যাডমিন ইউজার তৈরি করুন।"
done: "সম্পন্ন"
processing: "প্রক্রিয়াধীন..."
preview: "পূর্বরূপ দেখুন"
default: "পূর্বনির্ধারিত"
noCustomEmojis: "কোন ইমোজি নাই"
noJobs: "কোন জব নাই"
federating: "ফেডারেট করা হচ্ছে"
blocked: "ব্লক করা হয়েছে"
suspended: "স্থগিত করা হয়েছে"
all: "সবগুলো"
subscribing: "সদস্যতা নেয়া হচ্ছে"
publishing: "প্রকাশ করা হচ্ছে"
notResponding: "সাড়া নেই"
instanceFollowing: "ইন্সট্যান্স অনুসরণ করা হচ্ছে"
instanceFollowers: "ইন্সট্যান্স অনুসরণকারী"
instanceUsers: "ইন্সট্যান্স ব্যাবহারকারী"
changePassword: "পাসওয়ার্ড পরিবর্তন করুন"
security: "নিরাপত্তা"
retypedNotMatch: "ইনপুট মেলে না।"
currentPassword: "বর্তমান পাসওয়ার্ড"
newPassword: "নতুন পাসওয়ার্ড"
newPasswordRetype: "নতুন পাসওয়ার্ড (পুনরায় লিখুন)"
attachFile: "ফাইল সংযুক্ত করুন"
more: "আরও!"
featured: "হাইলাইট"
usernameOrUserId: "ব্যাবহারকারীর নাম বা ব্যাবহারকারী ID"
noSuchUser: "কোন ব্যবহারকারী খুঁজে পাওয়া যায়নি"
lookup: "খুঁজে দেখো"
announcements: "ঘোষণা"
imageUrl: "চিত্রের URL"
remove: "মুছুন"
removed: "সরানো হয়েছে"
removeAreYouSure: "আপনি কি \"{x}\" সরানোর ব্যাপারে নিশ্চিত?"
deleteAreYouSure: "আপনি কি \"{x}\" সরানোর ব্যাপারে নিশ্চিত?"
resetAreYouSure: "রিসেট করার ব্যাপারে নিশ্চিত?"
saved: "সংরক্ষিত হয়েছে"
messaging: "চ্যাট"
upload: "আপলোড"
keepOriginalUploading: "আসল ছবি রাখুন"
keepOriginalUploadingDescription: "ছবিটি আপলোড করার সময় আসল সংস্করণটি রাখুন। অপশনটি বন্ধ থাকলে, আপলোডের সময় ওয়েব প্রকাশনার জন্য ছবি ব্রাউজারে তৈরি করা হবে।"
fromDrive: "ড্রাইভ হতে"
fromUrl: "URL হতে"
uploadFromUrl: "URL হতে আপলোড"
uploadFromUrlDescription: "যে ফাইলটি আপলোড করতে চান, সেটির URL"
uploadFromUrlRequested: "আপলোড অনুরোধ করা হয়েছে"
uploadFromUrlMayTakeTime: "URL হতে আপলোড হতে কিছু সময় লাগতে পারে।"
explore: "ঘুরে দেখুন"
messageRead: "পড়া"
noMoreHistory: "আর কোন ইতিহাস নেই"
startMessaging: "চ্যাট শুরু করুন"
nUsersRead: "{n} জন পড়েছেন"
agreeTo: "{0} এর প্রতি আমি সম্মত"
tos: "পরিষেবার শর্তাদি"
start: "শুরু করুন"
home: "মূল পাতা"
remoteUserCaution: "এই ব্যাবহারকারী রিমোট ইন্সট্যান্সের, নিম্নক্ত তথ্য অসম্পূর্ণ হতে পারে।"
activity: "কার্যকলাপ"
images: "ছবি"
birthday: "জন্মদিন"
yearsOld: "{age} বছর"
registeredDate: "যোগদানের তারিখ"
location: "অবস্থান"
theme: "থিম"
themeForLightMode: "লাইট মোডের থিম"
themeForDarkMode: "ডার্ক মোডের থিম"
light: "আলোকিত"
dark: "অন্ধকার"
lightThemes: "আলোকিত থিম"
darkThemes: "অন্ধকার থিম"
syncDeviceDarkMode: "ডিভাইসের সেটিং অনুযায়ী ডার্ক মোড সেট করুন"
drive: "ড্রাইভ"
fileName: "ফাইলের নাম"
selectFile: "ফাইল নির্বাচন করুন"
selectFiles: "ফাইল নির্বাচন করুন"
selectFolder: "ফোল্ডার নির্বাচন করুন"
selectFolders: "ফোল্ডার নির্বাচন করুন"
renameFile: "ফাইল পুনঃনামকরন"
folderName: "ফোল্ডারের নাম"
createFolder: "ফোল্ডার তৈরি করুন"
renameFolder: "ফোল্ডার পুনঃনামকরন"
deleteFolder: "ফোল্ডার মুছুন"
addFile: "ফাইল যোগ করুন"
emptyDrive: "আপনার ড্রাইভ খালি"
emptyFolder: "এই ফোল্ডার খালি"
unableToDelete: "মুছে ফেলা যায়নি"
inputNewFileName: "ফাইলের নতুন নাম লিখুন"
inputNewDescription: "নতুন ক্যাপশন লিখুন"
inputNewFolderName: "ফোল্ডারের নতুন নাম লিখুন"
circularReferenceFolder: "গন্তব্য ফোল্ডারটি আপনি যে ফোল্ডারটি সরাতে চান তার একটি সাবফোল্ডার।"
hasChildFilesOrFolders: "এই ফোল্ডারটি খালি না হওয়ায় ডিলিট করা যায়নি।"
copyUrl: "URL কপি করুন"
rename: "পুনঃনামকরণ"
avatar: "প্রোফাইল ছবি"
banner: "ব্যানার"
nsfw: "সংবেদনশীল বিষয়বস্তু"
whenServerDisconnected: "সার্ভারের সাথে সংযোগ বিচ্ছিন্ন হয়ে গেলে"
disconnectedFromServer: "সার্ভার থেকে সংযোগ বিচ্ছিন্ন হয়েছে"
reload: "আবার লোড করুন"
doNothing: "কিছু করবেন না"
reloadConfirm: "আপনি কি রিলোড করতে চান?"
watch: "দেখুন"
unwatch: "দেখা বন্ধ করুন "
accept: "অনুমোদন"
reject: "প্রত্যাখ্যান"
normal: "স্বাভাবিক"
instanceName: "ইন্সট্যান্সের নাম"
instanceDescription: "ইন্সট্যান্সের বর্ণনা"
maintainerName: "মেইনটেইনার"
maintainerEmail: "মেইনটেইনারের ইমেইল"
tosUrl: "ব্যবহারের শর্তাবলীর URL"
thisYear: "বছর"
thisMonth: "মাস"
today: "আজ"
dayX: "{day}"
monthX: "{month}"
yearX: "{year}"
pages: "পৃষ্ঠা"
integration: "ইন্টিগ্রেশন"
connectService: "সংযুক্ত করুন"
disconnectService: "সংযোগ বিচ্ছিন্ন করুন"
enableLocalTimeline: "স্থানীয় টাইমলাইন চালু করুন"
enableGlobalTimeline: "গ্লোবাল টাইমলাইন চালু করুন"
disablingTimelinesInfo: "আপনি এই টাইমলাইনগুলি বন্ধ করলেও প্রশাসক এবং মডারেটররা এই টাইমলাইনগুলি ব্যাবহার করতে পারবে"
registration: "নিবন্ধন"
enableRegistration: "নতুন ব্যাবহারকারী নিবন্ধন চালু করুন"
invite: "আমন্ত্রণ"
proxyRemoteFiles: "রিমোট ফাইলসমুহ প্রক্সি করুন"
proxyRemoteFilesDescription: "যখন এই সেটিংটি চালু থাকে, তখন অসংরক্ষিত বা অতিরিক্ত ক্ষমতার কারণে দূরবর্তী ফাইলগুলিকে স্থানীয়ভাবে প্রক্সি করা হবে এবং থাম্বনেলগুলিও তৈরি করা হবে৷ সার্ভার স্টোরেজ ব্যাবহার করে না,"
driveCapacityPerLocalAccount: "প্রত্যেক স্থানীয় ব্যাবহারকারীর জন্য ড্রাইভের জায়গা"
driveCapacityPerRemoteAccount: "প্রত্যেক রিমোট ব্যাবহারকারীর জন্য ড্রাইভের জায়গা"
inMb: "মেগাবাইটে লিখুন"
iconUrl: "আইকনের URL (ফ্যাভিকন, ইত্যাদি)"
bannerUrl: "ব্যানার ছবির URL"
backgroundImageUrl: "পটভূমির চিত্রের URL"
basicInfo: "আপনার ব্যক্তিগত তথ্য"
pinnedUsers: "পিন করা ব্যাবহারকারীগণ"
pinnedUsersDescription: "আপনি যেসব ব্যবহারকারীদের \"ঘুরে দেখুন\" পৃষ্ঠায় পিন করতে চান তাদের বর্ণনা করুন, প্রত্যেকের বর্ণনা আলাদা লাইনে লিখুন"
pinnedPages: "পিন করা পৃষ্ঠাসুমহ"
pinnedPagesDescription: "আপনি যেসকল পৃষ্ঠাসমূহকে \"ঘুরে দেখুন\" পৃষ্ঠায় পিন করতে চান তাদের বর্ণনা করুন, প্রত্যেকের বর্ণনা আলাদা লাইনে লিখুন"
pinnedClipId: "পিনকৃত ক্লিপের ID"
pinnedNotes: "পিন করা নোট"
hcaptcha: "hCaptcha"
enableHcaptcha: "hCaptcha চালু করুন"
hcaptchaSiteKey: "সাইট কী"
hcaptchaSecretKey: "সিক্রেট কী"
recaptcha: "reCAPTCHA"
enableRecaptcha: "reCAPTCHA চালু করুন"
recaptchaSiteKey: "সাইট কী"
antennas: "অ্যান্টেনা"
manageAntennas: "অ্যান্টেনা ব্যবস্থাপনা"
name: "নাম"
antennaSource: "অ্যান্টেনার উৎস"
antennaKeywords: "যেসব কীওয়ার্ড দেখা হবে"
antennaExcludeKeywords: "যেসব কীওয়ার্ড দেখা হবে না"
antennaKeywordsDescription: "স্পেস দিয়ে আলাদা করলে AND শর্ত তৈরি হবে এবং আলাদা লাইনে লিখলে OR শর্ত তৈরি হবে।"
notifyAntenna: "নতুন নোট সম্পর্কে অবহিত করুন"
withFileAntenna: "শুধুমাত্র ফাইলযুক্ত নোট"
enableServiceworker: "ServiceWorker চালু করুন"
antennaUsersDescription: "প্রত্যেক লাইনে একজন ব্যবহারকারীর নাম লিখুন"
caseSensitive: "ছোট হাতের এবং বড় হাতের অক্ষর নির্দিষ্ট করুন"
withReplies: "জবাবসমুহ যুক্ত করুন"
connectedTo: "আপনি নিম্নলিখিত অ্যাকাউন্টের সাথে সংযুক্ত"
notesAndReplies: "নোটসমূহ এবং জবাবগুলি"
withFiles: "ফাইলগুলি যুক্ত করুন"
silence: "নীরব"
silenceConfirm: "আপনি কি এই ব্যাবহারকারীকের নীরব করতে চান?"
unsilence: "সরব"
unsilenceConfirm: "আপনি কি এই ব্যাবহারকারীকের সরব করতে চান?"
popularUsers: "জনপ্রিয় ব্যবহারকারীগন"
recentlyUpdatedUsers: "সম্প্রতি পোস্ট করা ব্যবহারকারীগন"
recentlyRegisteredUsers: "নতুন যোগ দেওয়া ব্যবহারকারীগন"
recentlyDiscoveredUsers: "নতুন খুঁজে পাওয়া ব্যবহারকারীগন"
exploreUsersCount: "{count} জন ব্যাবহারকারী"
exploreFediverse: "Fediverse ঘুরে দেখুন"
popularTags: "জনপ্রিয় ট্যাগগুলি"
userList: "লিস্ট"
about: "আপনার সম্পর্কে"
aboutMisskey: "Misskey সম্পর্কে"
administrator: "প্রশাসক"
token: "টোকেন"
twoStepAuthentication: "২-ধাপ প্রমাণীকরণ"
moderator: "মডারেটর"
nUsersMentioned: "{n} জনকে উল্লেখ করা হয়েছে"
securityKey: "সিকিউরিটি কী"
securityKeyName: "কী'র নাম"
registerSecurityKey: "সিকিউরিটি কী নিবন্ধন করুন"
lastUsed: "শেষ ব্যাবহার করা হয়েছে"
unregister: "নিবন্ধনমুক্ত হন"
passwordLessLogin: "পাসওয়ার্ড-বিহীন লগইন সেট আপ করুন"
resetPassword: "পাসওয়ার্ড রিসেট করুন"
newPasswordIs: "নতুন পাসওয়ার্ড হচ্ছে \"{password}\""
reduceUiAnimation: "UI অ্যানিমেশন কমান"
share: "শেয়ার"
notFound: "পাওয়া যায়নি"
notFoundDescription: "এই URL-এর সাথে সম্পর্কিত কোনো পৃষ্ঠা নেই।"
uploadFolder: "আপলোডের জন্য ডিফল্ট ফোল্ডার"
cacheClear: "ক্যাশ পরিষ্কার করুন"
markAsReadAllNotifications: "সমস্ত বিজ্ঞপ্তিগুলি পঠিত হিসাবে চিহ্নিত করুন"
markAsReadAllUnreadNotes: "সমস্ত নোটগুলি পঠিত হিসাবে চিহ্নিত করুন"
invites: "আমন্ত্রণ"
invitations: "আমন্ত্রণ"
useOsNativeEmojis: "অপারেটিং সিস্টেমের নেটিভ ইমোজি ব্যবহার করুন"
disableDrawer: "ড্রয়ার মেনু প্রদর্শন করবেন না"
youHaveNoGroups: "আপনার কোন গ্রুপ নেই "
joinOrCreateGroup: "একটি বিদ্যমান গ্রুপের আমন্ত্রণ পান বা একটি নতুন গ্রুপ তৈরি করুন৷"
noHistory: "কোনো ইতিহাস নেই"
signinHistory: "প্রবেশ করার ইতিহাস"
disableAnimatedMfm: "অ্যানিমেটেড MFM অক্ষম করুন"
doing: "প্রক্রিয়া করছে..."
category: "বিভাগ"
tags: "ট‍্যাগসমূহ"
docSource: "ডকুমেন্টের উৎস"
createAccount: "অ্যাকাউন্ট তৈরি করুন"
existingAccount: "বিদ্যমান অ্যাকাউন্ট"
regenerate: "আবারও তৈরি করুন"
fontSize: "ফন্টের আকার"
noFollowRequests: "আপনার কোন ফলোও রিকুয়েস্ট নেই"
openImageInNewTab: "ছবি নতুন ট্যাবে খুলুন"
dashboard: "ড্যাশবোর্ড"
local: "স্থানীয়"
remote: "রিমোট"
total: "মোট"
weekOverWeekChanges: "গত সপ্তাহে"
dayOverDayChanges: "গতকাল"
appearance: "অবয়ব"
clientSettings: "ক্লায়েন্ট সেটিংস"
accountSettings: "অ্যাকাউন্ট সেটিংস"
promotion: "প্রমোশন"
promote: "প্রচার করুন"
numberOfDays: "দিনের সংখ্যা"
hideThisNote: "নোটটি লুকান"
smtpHost: "হোস্ট"
smtpUser: "ব্যবহারকারীর নাম"
smtpPass: "পাসওয়ার্ড"
clearCache: "ক্যাশ পরিষ্কার করুন"
info: "আপনার সম্পর্কে"
user: "ব্যবহারকারীগণ"
controlPanel: "নিয়ন্ত্রন কেন্দ্র"
_email:
_follow:
title: "আপনাকে অনুসরণ করছে"
_mfm:
mention: "উল্লেখ"
quote: "উদ্ধৃতি"
emoji: "স্বনির্ধারিত ইমোজিগুলি"
search: "খুঁজুন"
_theme:
keys:
mention: "উল্লেখ"
renote: "রিনোট"
_sfx:
note: "নোটগুলি"
notification: "বিজ্ঞপ্তি"
chat: "চ্যাট"
_widgets:
notifications: "বিজ্ঞপ্তি"
timeline: "টাইমলাইন"
activity: "কার্যকলাপ"
federation: "ফেডিভার্স"
jobQueue: "জব কিউ"
_cw:
show: "আরও দেখুন"
_visibility:
home: "মূল পাতা"
followers: "অনুসরণকারী"
_profile:
name: "নাম"
username: "ব্যবহারকারীর নাম"
_exportOrImport:
followingList: "অনুসরণ করা হচ্ছে"
muteList: "মিউট"
blockingList: "ব্লক"
userLists: "লিস্ট"
_timelines:
home: "মূল পাতা"
_pages:
blocks:
image: "ছবি"
script:
categories:
list: "লিস্ট"
blocks:
_join:
arg1: "লিস্ট"
_randomPick:
arg1: "লিস্ট"
_dailyRandomPick:
arg1: "লিস্ট"
_seedRandomPick:
arg2: "লিস্ট"
_pick:
arg1: "লিস্ট"
_listLen:
arg1: "লিস্ট"
types:
array: "লিস্ট"
_notification:
youWereFollowed: "আপনাকে অনুসরণ করছে"
_types:
follow: "অনুসরণ করা হচ্ছে"
mention: "উল্লেখ"
renote: "রিনোট"
quote: "উদ্ধৃতি"
reaction: "প্রতিক্রিয়া"
_deck:
_columns:
notifications: "বিজ্ঞপ্তি"
tl: "টাইমলাইন"
antenna: "অ্যান্টেনা"
list: "লিস্ট"
mentions: "উল্লেখসমূহ"

View File

@ -235,6 +235,8 @@ resetAreYouSure: "Wirklich zurücksetzen?"
saved: "Gespeichert" saved: "Gespeichert"
messaging: "Chat" messaging: "Chat"
upload: "Hochladen" upload: "Hochladen"
keepOriginalUploading: "Originalbild speichern"
keepOriginalUploadingDescription: "Speichert das Originalbild so, wie es ist. Ist dies deaktiviert, wird eine Version zum Anzeigen im Internet generiert."
fromDrive: "Aus Drive" fromDrive: "Aus Drive"
fromUrl: "Von einer URL" fromUrl: "Von einer URL"
uploadFromUrl: "Von einer URL hochladen" uploadFromUrl: "Von einer URL hochladen"

View File

@ -235,6 +235,8 @@ resetAreYouSure: "Really reset?"
saved: "Saved" saved: "Saved"
messaging: "Chat" messaging: "Chat"
upload: "Upload" upload: "Upload"
keepOriginalUploading: "Keep original image"
keepOriginalUploadingDescription: "Saves the originally uploaded image as-is. If turned off, a version to display on the web will be generated on upload."
fromDrive: "From Drive" fromDrive: "From Drive"
fromUrl: "From URL" fromUrl: "From URL"
uploadFromUrl: "Upload from a URL" uploadFromUrl: "Upload from a URL"

View File

@ -235,6 +235,7 @@ resetAreYouSure: "Voulez-vous réinitialiser ?"
saved: "Enregistré" saved: "Enregistré"
messaging: "Discuter" messaging: "Discuter"
upload: "Téléverser" upload: "Téléverser"
keepOriginalUploading: "Garder limage dorigine"
fromDrive: "Depuis le Drive" fromDrive: "Depuis le Drive"
fromUrl: "Depuis une URL" fromUrl: "Depuis une URL"
uploadFromUrl: "Téléverser via une URL" uploadFromUrl: "Téléverser via une URL"
@ -743,6 +744,7 @@ notRecommended: "Déconseillé"
botProtection: "Protection contre les bots" botProtection: "Protection contre les bots"
instanceBlocking: "Instances bloquées" instanceBlocking: "Instances bloquées"
selectAccount: "Sélectionner un compte" selectAccount: "Sélectionner un compte"
switchAccount: "Changer de compte"
enabled: "Activé" enabled: "Activé"
disabled: "Désactivé" disabled: "Désactivé"
quickAction: "Actions rapides" quickAction: "Actions rapides"
@ -803,6 +805,7 @@ makeReactionsPublic: "Rendre les réactions publiques"
makeReactionsPublicDescription: "Ceci rendra la liste de toutes vos réactions données publique." makeReactionsPublicDescription: "Ceci rendra la liste de toutes vos réactions données publique."
classic: "Classique" classic: "Classique"
muteThread: "Mettre ce thread en sourdine" muteThread: "Mettre ce thread en sourdine"
unmuteThread: "Ne plus masquer le fil"
ffVisibility: "Visibilité des abonnés/abonnements" ffVisibility: "Visibilité des abonnés/abonnements"
ffVisibilityDescription: "Permet de configurer qui peut voir les personnes que tu suis et les personnes qui te suivent." ffVisibilityDescription: "Permet de configurer qui peut voir les personnes que tu suis et les personnes qui te suivent."
continueThread: "Afficher la suite du fil" continueThread: "Afficher la suite du fil"
@ -1241,6 +1244,7 @@ _exportOrImport:
muteList: "Comptes masqués" muteList: "Comptes masqués"
blockingList: "Comptes bloqués" blockingList: "Comptes bloqués"
userLists: "Listes" userLists: "Listes"
excludeMutingUsers: "Exclure les utilisateur·rice·s mis en sourdine"
excludeInactiveUsers: "Exclure les utilisateur·rice·s inactifs" excludeInactiveUsers: "Exclure les utilisateur·rice·s inactifs"
_charts: _charts:
federationInstancesIncDec: "Variation du nombre d'instances fédérées" federationInstancesIncDec: "Variation du nombre d'instances fédérées"

View File

@ -235,6 +235,8 @@ resetAreYouSure: "リセットしますか?"
saved: "保存しました" saved: "保存しました"
messaging: "チャット" messaging: "チャット"
upload: "アップロード" upload: "アップロード"
keepOriginalUploading: "オリジナル画像を保持"
keepOriginalUploadingDescription: "画像をアップロードする時にオリジナル版を保持します。オフにするとアップロード時にブラウザでWeb公開用画像を生成します。"
fromDrive: "ドライブから" fromDrive: "ドライブから"
fromUrl: "URLから" fromUrl: "URLから"
uploadFromUrl: "URLアップロード" uploadFromUrl: "URLアップロード"

View File

@ -106,6 +106,7 @@ clickToShow: "클릭하여 보기"
sensitive: "열람주의" sensitive: "열람주의"
add: "추가" add: "추가"
reaction: "리액션" reaction: "리액션"
reactionSetting: "선택기에 표시할 리액션"
reactionSettingDescription2: "끌어서 순서 변경, 클릭해서 삭제, +를 눌러서 추가할 수 있습니다." reactionSettingDescription2: "끌어서 순서 변경, 클릭해서 삭제, +를 눌러서 추가할 수 있습니다."
rememberNoteVisibility: "공개 범위를 기억하기" rememberNoteVisibility: "공개 범위를 기억하기"
attachCancel: "첨부 취소" attachCancel: "첨부 취소"
@ -234,6 +235,8 @@ resetAreYouSure: "초기화 하시겠습니까?"
saved: "저장하였습니다" saved: "저장하였습니다"
messaging: "대화" messaging: "대화"
upload: "업로드" upload: "업로드"
keepOriginalUploading: "원본 이미지를 유지"
keepOriginalUploadingDescription: "이미지를 업로드할 때에 원본을 그대로 유지합니다. 비활성화하면 업로드할 때 브라우저에서 웹 공개용 이미지를 생성합니다."
fromDrive: "드라이브에서" fromDrive: "드라이브에서"
fromUrl: "URL로부터" fromUrl: "URL로부터"
uploadFromUrl: "URL 업로드" uploadFromUrl: "URL 업로드"
@ -446,6 +449,7 @@ uiLanguage: "UI 표시 언어"
groupInvited: "그룹에 초대되었습니다" groupInvited: "그룹에 초대되었습니다"
aboutX: "{x}에 대하여" aboutX: "{x}에 대하여"
useOsNativeEmojis: "OS 기본 이모지를 사용" useOsNativeEmojis: "OS 기본 이모지를 사용"
disableDrawer: "드로어 메뉴를 사용하지 않기"
youHaveNoGroups: "그룹이 없습니다" youHaveNoGroups: "그룹이 없습니다"
joinOrCreateGroup: "다른 그룹의 초대를 받거나, 직접 새 그룹을 만들어 보세요." joinOrCreateGroup: "다른 그룹의 초대를 받거나, 직접 새 그룹을 만들어 보세요."
noHistory: "기록이 없습니다" noHistory: "기록이 없습니다"
@ -617,8 +621,11 @@ reportAbuse: "신고"
reportAbuseOf: "{name}을 신고하기" reportAbuseOf: "{name}을 신고하기"
fillAbuseReportDescription: "신고하려는 이유를 자세히 알려주세요. 특정 게시물을 신고할 때에는 게시물의 URL도 포함해 주세요." fillAbuseReportDescription: "신고하려는 이유를 자세히 알려주세요. 특정 게시물을 신고할 때에는 게시물의 URL도 포함해 주세요."
abuseReported: "신고를 보냈습니다. 신고해 주셔서 감사합니다." abuseReported: "신고를 보냈습니다. 신고해 주셔서 감사합니다."
reporter: "신고자"
reporteeOrigin: "피신고자" reporteeOrigin: "피신고자"
reporterOrigin: "신고자" reporterOrigin: "신고자"
forwardReport: "리모트 인스턴스에도 신고 내용 보내기"
forwardReportIsAnonymous: "리모트 인스턴스에서는 나의 정보를 볼 수 없으며, 익명의 시스템 계정으로 표시됩니다."
send: "전송" send: "전송"
abuseMarkAsResolved: "해결됨으로 표시" abuseMarkAsResolved: "해결됨으로 표시"
openInNewTab: "새 탭에서 열기" openInNewTab: "새 탭에서 열기"
@ -680,6 +687,7 @@ center: "가운데"
wide: "넓게" wide: "넓게"
narrow: "좁게" narrow: "좁게"
reloadToApplySetting: "이 설정을 적용하려면 페이지를 새로고침해야 합니다. 바로 새로고침하시겠습니까?" reloadToApplySetting: "이 설정을 적용하려면 페이지를 새로고침해야 합니다. 바로 새로고침하시겠습니까?"
needReloadToApply: "변경 사항은 새로고침하면 적용됩니다."
showTitlebar: "타이틀 바를 표시하기" showTitlebar: "타이틀 바를 표시하기"
clearCache: "캐시 비우기" clearCache: "캐시 비우기"
onlineUsersCount: "{n}명이 접속 중" onlineUsersCount: "{n}명이 접속 중"
@ -740,6 +748,7 @@ notRecommended: "추천하지 않음"
botProtection: "Bot 방어" botProtection: "Bot 방어"
instanceBlocking: "인스턴스 차단" instanceBlocking: "인스턴스 차단"
selectAccount: "계정 선택" selectAccount: "계정 선택"
switchAccount: "계정 바꾸기"
enabled: "활성화" enabled: "활성화"
disabled: "비활성화" disabled: "비활성화"
quickAction: "빠른 동작" quickAction: "빠른 동작"
@ -808,6 +817,11 @@ deleteAccountConfirm: "계정이 삭제되고 되돌릴 수 없게 됩니다.
incorrectPassword: "비밀번호가 올바르지 않습니다." incorrectPassword: "비밀번호가 올바르지 않습니다."
voteConfirm: "\"{choice}\"에 투표하시겠습니까?" voteConfirm: "\"{choice}\"에 투표하시겠습니까?"
hide: "숨기기" hide: "숨기기"
leaveGroup: "그룹 나가기"
leaveGroupConfirm: "\"{name}\"에서 나갈까요?"
useDrawerReactionPickerForMobile: "모바일에서 드로어 메뉴로 표시"
welcomeBackWithName: "환영합니다, {name}님"
clickToFinishEmailVerification: "[{ok}]를 눌러 이메일 인증을 완료하세요."
_emailUnavailable: _emailUnavailable:
used: "이 메일 주소는 사용중입니다" used: "이 메일 주소는 사용중입니다"
format: "형식이 올바르지 않습니다" format: "형식이 올바르지 않습니다"

View File

@ -235,6 +235,8 @@ resetAreYouSure: "恢复默认设置?"
saved: "已保存" saved: "已保存"
messaging: "聊天" messaging: "聊天"
upload: "本地上传" upload: "本地上传"
keepOriginalUploading: "保留原图"
keepOriginalUploadingDescription: "上传图片时保留原始图片。关闭时浏览器会在上传时生成一张用于web发布的图片。"
fromDrive: "从网盘中" fromDrive: "从网盘中"
fromUrl: "从 URL" fromUrl: "从 URL"
uploadFromUrl: "从网址上传" uploadFromUrl: "从网址上传"
@ -619,8 +621,11 @@ reportAbuse: "举报"
reportAbuseOf: "举报{name}" reportAbuseOf: "举报{name}"
fillAbuseReportDescription: "请填写举报的详细原因。如果有对方发的帖子请同时填写URL地址。" fillAbuseReportDescription: "请填写举报的详细原因。如果有对方发的帖子请同时填写URL地址。"
abuseReported: "内容已发送。感谢您的报告。" abuseReported: "内容已发送。感谢您的报告。"
reporter: "报告者"
reporteeOrigin: "举报来源" reporteeOrigin: "举报来源"
reporterOrigin: "举报者来源" reporterOrigin: "举报者来源"
forwardReport: "将报告转发给远程实例"
forwardReportIsAnonymous: "在远程实例上显示的报告者是匿名的系统账号,而不是您的账号。"
send: "发送" send: "发送"
abuseMarkAsResolved: "处理完毕" abuseMarkAsResolved: "处理完毕"
openInNewTab: "在新标签页中打开" openInNewTab: "在新标签页中打开"

View File

@ -1,6 +1,6 @@
{ {
"name": "misskey", "name": "misskey",
"version": "12.102.1", "version": "12.103.0",
"codename": "indigo", "codename": "indigo",
"repository": { "repository": {
"type": "git", "type": "git",

View File

@ -122,7 +122,7 @@
"langmap": "0.0.16", "langmap": "0.0.16",
"mfm-js": "0.21.0", "mfm-js": "0.21.0",
"mime-types": "2.1.34", "mime-types": "2.1.34",
"misskey-js": "0.0.13", "misskey-js": "0.0.14",
"mocha": "8.4.0", "mocha": "8.4.0",
"ms": "3.0.0-canary.1", "ms": "3.0.0-canary.1",
"multer": "1.4.4", "multer": "1.4.4",

View File

@ -32,7 +32,7 @@ export default (endpoint: IEndpoint, ctx: Koa.Context) => new Promise((res) => {
// Authentication // Authentication
authenticate(body['i']).then(([user, app]) => { authenticate(body['i']).then(([user, app]) => {
// API invoking // API invoking
call(endpoint.name, user, app, body, (ctx as any).file).then((res: any) => { call(endpoint.name, user, app, body, ctx).then((res: any) => {
reply(res); reply(res);
}).catch((e: ApiError) => { }).catch((e: ApiError) => {
reply(e.httpStatusCode ? e.httpStatusCode : e.kind === 'client' ? 400 : 500, e); reply(e.httpStatusCode ? e.httpStatusCode : e.kind === 'client' ? 400 : 500, e);

View File

@ -1,3 +1,4 @@
import * as Koa from 'koa';
import { performance } from 'perf_hooks'; import { performance } from 'perf_hooks';
import { limiter } from './limiter'; import { limiter } from './limiter';
import { User } from '@/models/entities/user'; import { User } from '@/models/entities/user';
@ -12,7 +13,7 @@ const accessDenied = {
id: '56f35758-7dd5-468b-8439-5d6fb8ec9b8e', id: '56f35758-7dd5-468b-8439-5d6fb8ec9b8e',
}; };
export default async (endpoint: string, user: User | null | undefined, token: AccessToken | null | undefined, data: any, file?: any) => { export default async (endpoint: string, user: User | null | undefined, token: AccessToken | null | undefined, data: any, ctx?: Koa.Context) => {
const isSecure = user != null && token == null; const isSecure = user != null && token == null;
const ep = endpoints.find(e => e.name === endpoint); const ep = endpoints.find(e => e.name === endpoint);
@ -76,9 +77,20 @@ export default async (endpoint: string, user: User | null | undefined, token: Ac
}); });
} }
// Cast non JSON input
if (ep.meta.requireFile && ep.meta.params) {
const body = (ctx!.request as any).body;
for (const k of Object.keys(ep.meta.params)) {
const param = ep.meta.params[k];
if (['Boolean', 'Number'].includes(param.validator.name) && typeof body[k] === 'string') {
body[k] = JSON.parse(body[k]);
}
}
}
// API invoking // API invoking
const before = performance.now(); const before = performance.now();
return await ep.exec(data, user, token, file).catch((e: Error) => { return await ep.exec(data, user, token, ctx!.file).catch((e: Error) => {
if (e instanceof ApiError) { if (e instanceof ApiError) {
throw e; throw e;
} else { } else {

View File

@ -39,15 +39,13 @@ export const meta = {
}, },
isSensitive: { isSensitive: {
validator: $.optional.either($.bool, $.str), validator: $.optional.bool,
default: false, default: false,
transform: (v: any): boolean => v === true || v === 'true',
}, },
force: { force: {
validator: $.optional.either($.bool, $.str), validator: $.optional.bool,
default: false, default: false,
transform: (v: any): boolean => v === true || v === 'true',
}, },
}, },

View File

@ -18,7 +18,7 @@ const _dirname = dirname(_filename);
const app = new Koa(); const app = new Koa();
app.use(cors()); app.use(cors());
app.use(async (ctx, next) => { app.use(async (ctx, next) => {
ctx.set('Content-Security-Policy', `default-src 'none'; style-src 'unsafe-inline'`); ctx.set('Content-Security-Policy', `default-src 'none'; img-src 'self'; media-src 'self'; style-src 'unsafe-inline'`);
await next(); await next();
}); });

View File

@ -11,7 +11,7 @@ import { proxyMedia } from './proxy-media';
const app = new Koa(); const app = new Koa();
app.use(cors()); app.use(cors());
app.use(async (ctx, next) => { app.use(async (ctx, next) => {
ctx.set('Content-Security-Policy', `default-src 'none'; style-src 'unsafe-inline'`); ctx.set('Content-Security-Policy', `default-src 'none'; img-src 'self'; media-src 'self'; style-src 'unsafe-inline'`);
await next(); await next();
}); });

View File

@ -4967,10 +4967,10 @@ minizlib@^2.0.0, minizlib@^2.1.1:
minipass "^3.0.0" minipass "^3.0.0"
yallist "^4.0.0" yallist "^4.0.0"
misskey-js@0.0.13: misskey-js@0.0.14:
version "0.0.13" version "0.0.14"
resolved "https://registry.yarnpkg.com/misskey-js/-/misskey-js-0.0.13.tgz#03a4e469186e28752d599dc4093519eb64647970" resolved "https://registry.yarnpkg.com/misskey-js/-/misskey-js-0.0.14.tgz#1a616bdfbe81c6ee6900219eaf425bb5c714dd4d"
integrity sha512-kBdJdfe281gtykzzsrN3IAxWUQIimzPiJGyKWf863ggWJlWYVPmP9hTFlX2z8oPOaypgVBPEPHyw/jNUdc2DbQ== integrity sha512-bvLx6U3OwQwqHfp/WKwIVwdvNYAAPk0+YblXyxmSG3dwlzCgBRRLcB8o6bNruUDyJgh3t73pLDcOz3myxcUmww==
dependencies: dependencies:
autobind-decorator "^2.4.0" autobind-decorator "^2.4.0"
eventemitter3 "^4.0.7" eventemitter3 "^4.0.7"

View File

@ -18,6 +18,7 @@ module.exports = {
// data の禁止理由: 抽象的すぎるため // data の禁止理由: 抽象的すぎるため
// e の禁止理由: error や event など、複数のキーワードの頭文字であり分かりにくいため // e の禁止理由: error や event など、複数のキーワードの頭文字であり分かりにくいため
"id-denylist": ["error", "window", "data", "e"], "id-denylist": ["error", "window", "data", "e"],
'eqeqeq': ['error', 'always', { 'null': 'ignore' }],
"vue/attributes-order": ["error", { "vue/attributes-order": ["error", {
"alphabetical": false "alphabetical": false
}], }],

View File

@ -69,7 +69,7 @@
"langmap": "0.0.16", "langmap": "0.0.16",
"matter-js": "0.18.0", "matter-js": "0.18.0",
"mfm-js": "0.21.0", "mfm-js": "0.21.0",
"misskey-js": "0.0.13", "misskey-js": "0.0.14",
"mocha": "8.4.0", "mocha": "8.4.0",
"ms": "2.1.3", "ms": "2.1.3",
"nested-property": "4.0.0", "nested-property": "4.0.0",

View File

@ -192,31 +192,31 @@ export async function openAccountMenu(opts: {
if (opts.withExtraOperation) { if (opts.withExtraOperation) {
popupMenu([...[{ popupMenu([...[{
type: 'link', type: 'link',
text: i18n.locale.profile, text: i18n.ts.profile,
to: `/@${ $i.username }`, to: `/@${ $i.username }`,
avatar: $i, avatar: $i,
}, null, ...(opts.includeCurrentAccount ? [createItem($i)] : []), ...accountItemPromises, { }, null, ...(opts.includeCurrentAccount ? [createItem($i)] : []), ...accountItemPromises, {
icon: 'fas fa-plus', icon: 'fas fa-plus',
text: i18n.locale.addAccount, text: i18n.ts.addAccount,
action: () => { action: () => {
popupMenu([{ popupMenu([{
text: i18n.locale.existingAccount, text: i18n.ts.existingAccount,
action: () => { showSigninDialog(); }, action: () => { showSigninDialog(); },
}, { }, {
text: i18n.locale.createAccount, text: i18n.ts.createAccount,
action: () => { createAccount(); }, action: () => { createAccount(); },
}], ev.currentTarget || ev.target); }], ev.currentTarget ?? ev.target);
}, },
}, { }, {
type: 'link', type: 'link',
icon: 'fas fa-users', icon: 'fas fa-users',
text: i18n.locale.manageAccounts, text: i18n.ts.manageAccounts,
to: `/settings/accounts`, to: `/settings/accounts`,
}]], ev.currentTarget || ev.target, { }]], ev.currentTarget ?? ev.target, {
align: 'left' align: 'left'
}); });
} else { } else {
popupMenu([...(opts.includeCurrentAccount ? [createItem($i)] : []), ...accountItemPromises], ev.currentTarget || ev.target, { popupMenu([...(opts.includeCurrentAccount ? [createItem($i)] : []), ...accountItemPromises], ev.currentTarget ?? ev.target, {
align: 'left' align: 'left'
}); });
} }

View File

@ -2,7 +2,7 @@
<XWindow ref="window" :initial-width="400" :initial-height="500" :can-resize="true" @closed="emit('closed')"> <XWindow ref="window" :initial-width="400" :initial-height="500" :can-resize="true" @closed="emit('closed')">
<template #header> <template #header>
<i class="fas fa-exclamation-circle" style="margin-right: 0.5em;"></i> <i class="fas fa-exclamation-circle" style="margin-right: 0.5em;"></i>
<I18n :src="i18n.locale.reportAbuseOf" tag="span"> <I18n :src="i18n.ts.reportAbuseOf" tag="span">
<template #name> <template #name>
<b><MkAcct :user="user"/></b> <b><MkAcct :user="user"/></b>
</template> </template>
@ -11,12 +11,12 @@
<div class="dpvffvvy _monolithic_"> <div class="dpvffvvy _monolithic_">
<div class="_section"> <div class="_section">
<MkTextarea v-model="comment"> <MkTextarea v-model="comment">
<template #label>{{ i18n.locale.details }}</template> <template #label>{{ i18n.ts.details }}</template>
<template #caption>{{ i18n.locale.fillAbuseReportDescription }}</template> <template #caption>{{ i18n.ts.fillAbuseReportDescription }}</template>
</MkTextarea> </MkTextarea>
</div> </div>
<div class="_section"> <div class="_section">
<MkButton primary full :disabled="comment.length === 0" @click="send">{{ i18n.locale.send }}</MkButton> <MkButton primary full :disabled="comment.length === 0" @click="send">{{ i18n.ts.send }}</MkButton>
</div> </div>
</div> </div>
</XWindow> </XWindow>
@ -50,7 +50,7 @@ function send() {
}, undefined).then(res => { }, undefined).then(res => {
os.alert({ os.alert({
type: 'success', type: 'success',
text: i18n.locale.abuseReported text: i18n.ts.abuseReported
}); });
window.value?.close(); window.value?.close();
emit('closed'); emit('closed');

View File

@ -8,7 +8,7 @@
</span> </span>
<span class="username">@{{ acct(user) }}</span> <span class="username">@{{ acct(user) }}</span>
</li> </li>
<li tabindex="-1" class="choose" @click="chooseUser()" @keydown="onKeydown">{{ i18n.locale.selectUser }}</li> <li tabindex="-1" class="choose" @click="chooseUser()" @keydown="onKeydown">{{ i18n.ts.selectUser }}</li>
</ol> </ol>
<ol v-else-if="hashtags.length > 0" ref="suggests" class="hashtags"> <ol v-else-if="hashtags.length > 0" ref="suggests" class="hashtags">
<li v-for="hashtag in hashtags" tabindex="-1" @click="complete(type, hashtag)" @keydown="onKeydown"> <li v-for="hashtag in hashtags" tabindex="-1" @click="complete(type, hashtag)" @keydown="onKeydown">

View File

@ -1,6 +1,6 @@
<template> <template>
<div> <div>
<span v-if="!available">{{ i18n.locale.waiting }}<MkEllipsis/></span> <span v-if="!available">{{ i18n.ts.waiting }}<MkEllipsis/></span>
<div ref="captchaEl"></div> <div ref="captchaEl"></div>
</div> </div>
</template> </template>

View File

@ -6,14 +6,14 @@
> >
<template v-if="!wait"> <template v-if="!wait">
<template v-if="isFollowing"> <template v-if="isFollowing">
<span v-if="full">{{ i18n.locale.unfollow }}</span><i class="fas fa-minus"></i> <span v-if="full">{{ i18n.ts.unfollow }}</span><i class="fas fa-minus"></i>
</template> </template>
<template v-else> <template v-else>
<span v-if="full">{{ i18n.locale.follow }}</span><i class="fas fa-plus"></i> <span v-if="full">{{ i18n.ts.follow }}</span><i class="fas fa-plus"></i>
</template> </template>
</template> </template>
<template v-else> <template v-else>
<span v-if="full">{{ i18n.locale.processing }}</span><i class="fas fa-spinner fa-pulse fa-fw"></i> <span v-if="full">{{ i18n.ts.processing }}</span><i class="fas fa-spinner fa-pulse fa-fw"></i>
</template> </template>
</button> </button>
</template> </template>

View File

@ -6,7 +6,7 @@
<div class="status"> <div class="status">
<div> <div>
<i class="fas fa-users fa-fw"></i> <i class="fas fa-users fa-fw"></i>
<I18n :src="i18n.locale._channel.usersCount" tag="span" style="margin-left: 4px;"> <I18n :src="i18n.ts._channel.usersCount" tag="span" style="margin-left: 4px;">
<template #n> <template #n>
<b>{{ channel.usersCount }}</b> <b>{{ channel.usersCount }}</b>
</template> </template>
@ -14,7 +14,7 @@
</div> </div>
<div> <div>
<i class="fas fa-pencil-alt fa-fw"></i> <i class="fas fa-pencil-alt fa-fw"></i>
<I18n :src="i18n.locale._channel.notesCount" tag="span" style="margin-left: 4px;"> <I18n :src="i18n.ts._channel.notesCount" tag="span" style="margin-left: 4px;">
<template #n> <template #n>
<b>{{ channel.notesCount }}</b> <b>{{ channel.notesCount }}</b>
</template> </template>
@ -27,7 +27,7 @@
</article> </article>
<footer> <footer>
<span v-if="channel.lastNotedAt"> <span v-if="channel.lastNotedAt">
{{ i18n.locale.updatedAt }}: <MkTime :time="channel.lastNotedAt"/> {{ i18n.ts.updatedAt }}: <MkTime :time="channel.lastNotedAt"/>
</span> </span>
</footer> </footer>
</MkA> </MkA>

View File

@ -0,0 +1,51 @@
<template>
<MkTooltip ref="tooltip" :showing="showing" :x="x" :y="y" :max-width="340" @closed="emit('closed')">
<div v-if="title" class="qpcyisrl">
<div class="title">{{ title }}</div>
<div v-for="x in series" class="series">
<span class="color" :style="{ background: x.backgroundColor, borderColor: x.borderColor }"></span>
<span>{{ x.text }}</span>
</div>
</div>
</MkTooltip>
</template>
<script lang="ts" setup>
import { } from 'vue';
import MkTooltip from './ui/tooltip.vue';
const props = defineProps<{
showing: boolean;
x: number;
y: number;
title: string;
series: {
backgroundColor: string;
borderColor: string;
text: string;
}[];
}>();
const emit = defineEmits<{
(ev: 'closed'): void;
}>();
</script>
<style lang="scss" scoped>
.qpcyisrl {
> .title {
margin-bottom: 4px;
}
> .series {
> .color {
display: inline-block;
width: 8px;
height: 8px;
border-width: 1px;
border-style: solid;
margin-right: 8px;
}
}
}
</style>

View File

@ -8,7 +8,7 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent, onMounted, ref, watch, PropType } from 'vue'; import { defineComponent, onMounted, ref, watch, PropType, onUnmounted, shallowRef } from 'vue';
import { import {
Chart, Chart,
ArcElement, ArcElement,
@ -31,6 +31,7 @@ import { enUS } from 'date-fns/locale';
import zoomPlugin from 'chartjs-plugin-zoom'; import zoomPlugin from 'chartjs-plugin-zoom';
import * as os from '@/os'; import * as os from '@/os';
import { defaultStore } from '@/store'; import { defaultStore } from '@/store';
import MkChartTooltip from '@/components/chart-tooltip.vue';
Chart.register( Chart.register(
ArcElement, ArcElement,
@ -137,12 +138,50 @@ export default defineComponent({
})); }));
}; };
const tooltipShowing = ref(false);
const tooltipX = ref(0);
const tooltipY = ref(0);
const tooltipTitle = ref(null);
const tooltipSeries = ref(null);
let disposeTooltipComponent;
os.popup(MkChartTooltip, {
showing: tooltipShowing,
x: tooltipX,
y: tooltipY,
title: tooltipTitle,
series: tooltipSeries,
}, {}).then(({ dispose }) => {
disposeTooltipComponent = dispose;
});
function externalTooltipHandler(context) {
if (context.tooltip.opacity === 0) {
tooltipShowing.value = false;
return;
}
tooltipTitle.value = context.tooltip.title[0];
tooltipSeries.value = context.tooltip.body.map((b, i) => ({
backgroundColor: context.tooltip.labelColors[i].backgroundColor,
borderColor: context.tooltip.labelColors[i].borderColor,
text: b.lines[0],
}));
const rect = context.chart.canvas.getBoundingClientRect();
tooltipShowing.value = true;
tooltipX.value = rect.left + window.pageXOffset + context.tooltip.caretX;
tooltipY.value = rect.top + window.pageYOffset + context.tooltip.caretY;
}
const render = () => { const render = () => {
if (chartInstance) { if (chartInstance) {
chartInstance.destroy(); chartInstance.destroy();
} }
const gridColor = defaultStore.state.darkMode ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)'; const gridColor = defaultStore.state.darkMode ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)';
const vLineColor = defaultStore.state.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)';
// //
Chart.defaults.color = getComputedStyle(document.documentElement).getPropertyValue('--fg'); Chart.defaults.color = getComputedStyle(document.documentElement).getPropertyValue('--fg');
@ -221,10 +260,12 @@ export default defineComponent({
}, },
}, },
tooltip: { tooltip: {
enabled: false,
mode: 'index', mode: 'index',
animation: { animation: {
duration: 0, duration: 0,
}, },
external: externalTooltipHandler,
}, },
zoom: { zoom: {
pan: { pan: {
@ -255,6 +296,27 @@ export default defineComponent({
}, },
}, },
}, },
plugins: [{
id: 'vLine',
beforeDraw(chart, args, options) {
if (chart.tooltip._active && chart.tooltip._active.length) {
const activePoint = chart.tooltip._active[0];
const ctx = chart.ctx;
const x = activePoint.element.x;
const topY = chart.scales.y.top;
const bottomY = chart.scales.y.bottom;
ctx.save();
ctx.beginPath();
ctx.moveTo(x, bottomY);
ctx.lineTo(x, topY);
ctx.lineWidth = 1;
ctx.strokeStyle = vLineColor;
ctx.stroke();
ctx.restore();
}
}
}]
}); });
}; };
@ -662,6 +724,10 @@ export default defineComponent({
fetchAndRender(); fetchAndRender();
}); });
onUnmounted(() => {
if (disposeTooltipComponent) disposeTooltipComponent();
});
return { return {
chartEl, chartEl,
fetching, fetching,

View File

@ -1,6 +1,6 @@
<template> <template>
<button class="nrvgflfu _button" @click="toggle"> <button class="nrvgflfu _button" @click="toggle">
<b>{{ modelValue ? i18n.locale._cw.hide : i18n.locale._cw.show }}</b> <b>{{ modelValue ? i18n.ts._cw.hide : i18n.ts._cw.show }}</b>
<span v-if="!modelValue">{{ label }}</span> <span v-if="!modelValue">{{ label }}</span>
</button> </button>
</template> </template>
@ -25,7 +25,7 @@ const label = computed(() => {
return concat([ return concat([
props.note.text ? [i18n.t('_cw.chars', { count: length(props.note.text) })] : [], props.note.text ? [i18n.t('_cw.chars', { count: length(props.note.text) })] : [],
props.note.files && props.note.files.length !== 0 ? [i18n.t('_cw.files', { count: props.note.files.length }) ] : [], props.note.files && props.note.files.length !== 0 ? [i18n.t('_cw.files', { count: props.note.files.length }) ] : [],
props.note.poll != null ? [i18n.locale.poll] : [] props.note.poll != null ? [i18n.ts.poll] : []
] as string[][]).join(' / '); ] as string[][]).join(' / ');
}); });

View File

@ -28,8 +28,8 @@
</template> </template>
</MkSelect> </MkSelect>
<div v-if="(showOkButton || showCancelButton) && !actions" class="buttons"> <div v-if="(showOkButton || showCancelButton) && !actions" class="buttons">
<MkButton v-if="showOkButton" inline primary :autofocus="!input && !select" @click="ok">{{ (showCancelButton || input || select) ? i18n.locale.ok : i18n.locale.gotIt }}</MkButton> <MkButton v-if="showOkButton" inline primary :autofocus="!input && !select" @click="ok">{{ (showCancelButton || input || select) ? i18n.ts.ok : i18n.ts.gotIt }}</MkButton>
<MkButton v-if="showCancelButton || input || select" inline @click="cancel">{{ i18n.locale.cancel }}</MkButton> <MkButton v-if="showCancelButton || input || select" inline @click="cancel">{{ i18n.ts.cancel }}</MkButton>
</div> </div>
<div v-if="actions" class="buttons"> <div v-if="actions" class="buttons">
<MkButton v-for="action in actions" :key="action.text" inline :primary="action.primary" @click="() => { action.callback(); close(); }">{{ action.text }}</MkButton> <MkButton v-for="action in actions" :key="action.text" inline :primary="action.primary" @click="() => { action.callback(); close(); }">{{ action.text }}</MkButton>

View File

@ -10,7 +10,7 @@
@closed="emit('closed')" @closed="emit('closed')"
> >
<template #header> <template #header>
{{ multiple ? ((type === 'file') ? i18n.locale.selectFiles : i18n.locale.selectFolders) : ((type === 'file') ? i18n.locale.selectFile : i18n.locale.selectFolder) }} {{ multiple ? ((type === 'file') ? i18n.ts.selectFiles : i18n.ts.selectFolders) : ((type === 'file') ? i18n.ts.selectFile : i18n.ts.selectFolder) }}
<span v-if="selected.length > 0" style="margin-left: 8px; opacity: 0.5;">({{ number(selected.length) }})</span> <span v-if="selected.length > 0" style="margin-left: 8px; opacity: 0.5;">({{ number(selected.length) }})</span>
</template> </template>
<XDrive :multiple="multiple" :select="type" @changeSelection="onChangeSelection" @selected="ok()"/> <XDrive :multiple="multiple" :select="type" @changeSelection="onChangeSelection" @selected="ok()"/>

View File

@ -6,7 +6,7 @@
@closed="emit('closed')" @closed="emit('closed')"
> >
<template #header> <template #header>
{{ i18n.locale.drive }} {{ i18n.ts.drive }}
</template> </template>
<XDrive :initial-folder="initialFolder"/> <XDrive :initial-folder="initialFolder"/>
</XWindow> </XWindow>

View File

@ -10,15 +10,15 @@
> >
<div v-if="$i?.avatarId == file.id" class="label"> <div v-if="$i?.avatarId == file.id" class="label">
<img src="/client-assets/label.svg"/> <img src="/client-assets/label.svg"/>
<p>{{ i18n.locale.avatar }}</p> <p>{{ i18n.ts.avatar }}</p>
</div> </div>
<div v-if="$i?.bannerId == file.id" class="label"> <div v-if="$i?.bannerId == file.id" class="label">
<img src="/client-assets/label.svg"/> <img src="/client-assets/label.svg"/>
<p>{{ i18n.locale.banner }}</p> <p>{{ i18n.ts.banner }}</p>
</div> </div>
<div v-if="file.isSensitive" class="label red"> <div v-if="file.isSensitive" class="label red">
<img src="/client-assets/label-red.svg"/> <img src="/client-assets/label-red.svg"/>
<p>{{ i18n.locale.nsfw }}</p> <p>{{ i18n.ts.nsfw }}</p>
</div> </div>
<MkDriveFileThumbnail class="thumbnail" :file="file" fit="contain"/> <MkDriveFileThumbnail class="thumbnail" :file="file" fit="contain"/>
@ -61,30 +61,30 @@ const title = computed(() => `${props.file.name}\n${props.file.type} ${bytes(pro
function getMenu() { function getMenu() {
return [{ return [{
text: i18n.locale.rename, text: i18n.ts.rename,
icon: 'fas fa-i-cursor', icon: 'fas fa-i-cursor',
action: rename action: rename
}, { }, {
text: props.file.isSensitive ? i18n.locale.unmarkAsSensitive : i18n.locale.markAsSensitive, text: props.file.isSensitive ? i18n.ts.unmarkAsSensitive : i18n.ts.markAsSensitive,
icon: props.file.isSensitive ? 'fas fa-eye' : 'fas fa-eye-slash', icon: props.file.isSensitive ? 'fas fa-eye' : 'fas fa-eye-slash',
action: toggleSensitive action: toggleSensitive
}, { }, {
text: i18n.locale.describeFile, text: i18n.ts.describeFile,
icon: 'fas fa-i-cursor', icon: 'fas fa-i-cursor',
action: describe action: describe
}, null, { }, null, {
text: i18n.locale.copyUrl, text: i18n.ts.copyUrl,
icon: 'fas fa-link', icon: 'fas fa-link',
action: copyUrl action: copyUrl
}, { }, {
type: 'a', type: 'a',
href: props.file.url, href: props.file.url,
target: '_blank', target: '_blank',
text: i18n.locale.download, text: i18n.ts.download,
icon: 'fas fa-download', icon: 'fas fa-download',
download: props.file.name download: props.file.name
}, null, { }, null, {
text: i18n.locale.delete, text: i18n.ts.delete,
icon: 'fas fa-trash-alt', icon: 'fas fa-trash-alt',
danger: true, danger: true,
action: deleteFile action: deleteFile
@ -95,7 +95,7 @@ function onClick(ev: MouseEvent) {
if (props.selectMode) { if (props.selectMode) {
emit('chosen', props.file); emit('chosen', props.file);
} else { } else {
os.popupMenu(getMenu(), (ev.currentTarget || ev.target || undefined) as HTMLElement | undefined); os.popupMenu(getMenu(), (ev.currentTarget ?? ev.target ?? undefined) as HTMLElement | undefined);
} }
} }
@ -120,8 +120,8 @@ function onDragend() {
function rename() { function rename() {
os.inputText({ os.inputText({
title: i18n.locale.renameFile, title: i18n.ts.renameFile,
placeholder: i18n.locale.inputNewFileName, placeholder: i18n.ts.inputNewFileName,
default: props.file.name, default: props.file.name,
}).then(({ canceled, result: name }) => { }).then(({ canceled, result: name }) => {
if (canceled) return; if (canceled) return;
@ -134,9 +134,9 @@ function rename() {
function describe() { function describe() {
os.popup(import('@/components/media-caption.vue'), { os.popup(import('@/components/media-caption.vue'), {
title: i18n.locale.describeFile, title: i18n.ts.describeFile,
input: { input: {
placeholder: i18n.locale.inputNewDescription, placeholder: i18n.ts.inputNewDescription,
default: props.file.comment !== null ? props.file.comment : '', default: props.file.comment !== null ? props.file.comment : '',
}, },
image: props.file image: props.file

View File

@ -20,7 +20,7 @@
{{ folder.name }} {{ folder.name }}
</p> </p>
<p v-if="defaultStore.state.uploadFolder == folder.id" class="upload"> <p v-if="defaultStore.state.uploadFolder == folder.id" class="upload">
{{ i18n.locale.uploadFolder }} {{ i18n.ts.uploadFolder }}
</p> </p>
<button v-if="selectMode" class="checkbox _button" :class="{ checked: isSelected }" @click.prevent.stop="checkboxClicked"></button> <button v-if="selectMode" class="checkbox _button" :class="{ checked: isSelected }" @click.prevent.stop="checkboxClicked"></button>
</div> </div>
@ -146,14 +146,14 @@ function onDrop(ev: DragEvent) {
switch (err) { switch (err) {
case 'detected-circular-definition': case 'detected-circular-definition':
os.alert({ os.alert({
title: i18n.locale.unableToProcess, title: i18n.ts.unableToProcess,
text: i18n.locale.circularReferenceFolder text: i18n.ts.circularReferenceFolder
}); });
break; break;
default: default:
os.alert({ os.alert({
type: 'error', type: 'error',
text: i18n.locale.somethingHappened text: i18n.ts.somethingHappened
}); });
} }
}); });
@ -184,8 +184,8 @@ function go() {
function rename() { function rename() {
os.inputText({ os.inputText({
title: i18n.locale.renameFolder, title: i18n.ts.renameFolder,
placeholder: i18n.locale.inputNewFolderName, placeholder: i18n.ts.inputNewFolderName,
default: props.folder.name default: props.folder.name
}).then(({ canceled, result: name }) => { }).then(({ canceled, result: name }) => {
if (canceled) return; if (canceled) return;
@ -208,14 +208,14 @@ function deleteFolder() {
case 'b0fc8a17-963c-405d-bfbc-859a487295e1': case 'b0fc8a17-963c-405d-bfbc-859a487295e1':
os.alert({ os.alert({
type: 'error', type: 'error',
title: i18n.locale.unableToDelete, title: i18n.ts.unableToDelete,
text: i18n.locale.hasChildFilesOrFolders text: i18n.ts.hasChildFilesOrFolders
}); });
break; break;
default: default:
os.alert({ os.alert({
type: 'error', type: 'error',
text: i18n.locale.unableToDelete text: i18n.ts.unableToDelete
}); });
} }
}); });
@ -227,7 +227,7 @@ function setAsUploadFolder() {
function onContextmenu(ev: MouseEvent) { function onContextmenu(ev: MouseEvent) {
os.contextMenu([{ os.contextMenu([{
text: i18n.locale.openInWindow, text: i18n.ts.openInWindow,
icon: 'fas fa-window-restore', icon: 'fas fa-window-restore',
action: () => { action: () => {
os.popup(import('./drive-window.vue'), { os.popup(import('./drive-window.vue'), {
@ -236,11 +236,11 @@ function onContextmenu(ev: MouseEvent) {
}, 'closed'); }, 'closed');
} }
}, null, { }, null, {
text: i18n.locale.rename, text: i18n.ts.rename,
icon: 'fas fa-i-cursor', icon: 'fas fa-i-cursor',
action: rename, action: rename,
}, null, { }, null, {
text: i18n.locale.delete, text: i18n.ts.delete,
icon: 'fas fa-trash-alt', icon: 'fas fa-trash-alt',
danger: true, danger: true,
action: deleteFolder, action: deleteFolder,

View File

@ -8,7 +8,7 @@
@drop.stop="onDrop" @drop.stop="onDrop"
> >
<i v-if="folder == null" class="fas fa-cloud"></i> <i v-if="folder == null" class="fas fa-cloud"></i>
<span>{{ folder == null ? i18n.locale.drive : folder.name }}</span> <span>{{ folder == null ? i18n.ts.drive : folder.name }}</span>
</div> </div>
</template> </template>

View File

@ -54,7 +54,7 @@
/> />
<!-- SEE: https://stackoverflow.com/questions/18744164/flex-box-align-last-row-to-grid --> <!-- SEE: https://stackoverflow.com/questions/18744164/flex-box-align-last-row-to-grid -->
<div v-for="(n, i) in 16" :key="i" class="padding"></div> <div v-for="(n, i) in 16" :key="i" class="padding"></div>
<MkButton v-if="moreFolders" ref="moreFolders">{{ i18n.locale.loadMore }}</MkButton> <MkButton v-if="moreFolders" ref="moreFolders">{{ i18n.ts.loadMore }}</MkButton>
</div> </div>
<div v-show="files.length > 0" ref="filesContainer" class="files"> <div v-show="files.length > 0" ref="filesContainer" class="files">
<XFile <XFile
@ -71,12 +71,12 @@
/> />
<!-- SEE: https://stackoverflow.com/questions/18744164/flex-box-align-last-row-to-grid --> <!-- SEE: https://stackoverflow.com/questions/18744164/flex-box-align-last-row-to-grid -->
<div v-for="(n, i) in 16" :key="i" class="padding"></div> <div v-for="(n, i) in 16" :key="i" class="padding"></div>
<MkButton v-show="moreFiles" ref="loadMoreFiles" @click="fetchMoreFiles">{{ i18n.locale.loadMore }}</MkButton> <MkButton v-show="moreFiles" ref="loadMoreFiles" @click="fetchMoreFiles">{{ i18n.ts.loadMore }}</MkButton>
</div> </div>
<div v-if="files.length == 0 && folders.length == 0 && !fetching" class="empty"> <div v-if="files.length == 0 && folders.length == 0 && !fetching" class="empty">
<p v-if="draghover">{{ i18n.t('empty-draghover') }}</p> <p v-if="draghover">{{ i18n.t('empty-draghover') }}</p>
<p v-if="!draghover && folder == null"><strong>{{ i18n.locale.emptyDrive }}</strong><br/>{{ i18n.t('empty-drive-description') }}</p> <p v-if="!draghover && folder == null"><strong>{{ i18n.ts.emptyDrive }}</strong><br/>{{ i18n.t('empty-drive-description') }}</p>
<p v-if="!draghover && folder != null">{{ i18n.locale.emptyFolder }}</p> <p v-if="!draghover && folder != null">{{ i18n.ts.emptyFolder }}</p>
</div> </div>
</div> </div>
<MkLoading v-if="fetching"/> <MkLoading v-if="fetching"/>
@ -253,14 +253,14 @@ function onDrop(e: DragEvent): any {
switch (err) { switch (err) {
case 'detected-circular-definition': case 'detected-circular-definition':
os.alert({ os.alert({
title: i18n.locale.unableToProcess, title: i18n.ts.unableToProcess,
text: i18n.locale.circularReferenceFolder text: i18n.ts.circularReferenceFolder
}); });
break; break;
default: default:
os.alert({ os.alert({
type: 'error', type: 'error',
text: i18n.locale.somethingHappened text: i18n.ts.somethingHappened
}); });
} }
}); });
@ -274,9 +274,9 @@ function selectLocalFile() {
function urlUpload() { function urlUpload() {
os.inputText({ os.inputText({
title: i18n.locale.uploadFromUrl, title: i18n.ts.uploadFromUrl,
type: 'url', type: 'url',
placeholder: i18n.locale.uploadFromUrlDescription placeholder: i18n.ts.uploadFromUrlDescription
}).then(({ canceled, result: url }) => { }).then(({ canceled, result: url }) => {
if (canceled || !url) return; if (canceled || !url) return;
os.api('drive/files/upload-from-url', { os.api('drive/files/upload-from-url', {
@ -285,16 +285,16 @@ function urlUpload() {
}); });
os.alert({ os.alert({
title: i18n.locale.uploadFromUrlRequested, title: i18n.ts.uploadFromUrlRequested,
text: i18n.locale.uploadFromUrlMayTakeTime text: i18n.ts.uploadFromUrlMayTakeTime
}); });
}); });
} }
function createFolder() { function createFolder() {
os.inputText({ os.inputText({
title: i18n.locale.createFolder, title: i18n.ts.createFolder,
placeholder: i18n.locale.folderName placeholder: i18n.ts.folderName
}).then(({ canceled, result: name }) => { }).then(({ canceled, result: name }) => {
if (canceled) return; if (canceled) return;
os.api('drive/folders/create', { os.api('drive/folders/create', {
@ -308,8 +308,8 @@ function createFolder() {
function renameFolder(folderToRename: Misskey.entities.DriveFolder) { function renameFolder(folderToRename: Misskey.entities.DriveFolder) {
os.inputText({ os.inputText({
title: i18n.locale.renameFolder, title: i18n.ts.renameFolder,
placeholder: i18n.locale.inputNewFolderName, placeholder: i18n.ts.inputNewFolderName,
default: folderToRename.name default: folderToRename.name
}).then(({ canceled, result: name }) => { }).then(({ canceled, result: name }) => {
if (canceled) return; if (canceled) return;
@ -334,14 +334,14 @@ function deleteFolder(folderToDelete: Misskey.entities.DriveFolder) {
case 'b0fc8a17-963c-405d-bfbc-859a487295e1': case 'b0fc8a17-963c-405d-bfbc-859a487295e1':
os.alert({ os.alert({
type: 'error', type: 'error',
title: i18n.locale.unableToDelete, title: i18n.ts.unableToDelete,
text: i18n.locale.hasChildFilesOrFolders text: i18n.ts.hasChildFilesOrFolders
}); });
break; break;
default: default:
os.alert({ os.alert({
type: 'error', type: 'error',
text: i18n.locale.unableToDelete text: i18n.ts.unableToDelete
}); });
} }
}); });
@ -562,36 +562,36 @@ function fetchMoreFiles() {
function getMenu() { function getMenu() {
return [{ return [{
text: i18n.locale.addFile, text: i18n.ts.addFile,
type: 'label' type: 'label'
}, { }, {
text: i18n.locale.upload, text: i18n.ts.upload,
icon: 'fas fa-upload', icon: 'fas fa-upload',
action: () => { selectLocalFile(); } action: () => { selectLocalFile(); }
}, { }, {
text: i18n.locale.fromUrl, text: i18n.ts.fromUrl,
icon: 'fas fa-link', icon: 'fas fa-link',
action: () => { urlUpload(); } action: () => { urlUpload(); }
}, null, { }, null, {
text: folder.value ? folder.value.name : i18n.locale.drive, text: folder.value ? folder.value.name : i18n.ts.drive,
type: 'label' type: 'label'
}, folder.value ? { }, folder.value ? {
text: i18n.locale.renameFolder, text: i18n.ts.renameFolder,
icon: 'fas fa-i-cursor', icon: 'fas fa-i-cursor',
action: () => { renameFolder(folder.value); } action: () => { renameFolder(folder.value); }
} : undefined, folder.value ? { } : undefined, folder.value ? {
text: i18n.locale.deleteFolder, text: i18n.ts.deleteFolder,
icon: 'fas fa-trash-alt', icon: 'fas fa-trash-alt',
action: () => { deleteFolder(folder.value as Misskey.entities.DriveFolder); } action: () => { deleteFolder(folder.value as Misskey.entities.DriveFolder); }
} : undefined, { } : undefined, {
text: i18n.locale.createFolder, text: i18n.ts.createFolder,
icon: 'fas fa-folder-plus', icon: 'fas fa-folder-plus',
action: () => { createFolder(); } action: () => { createFolder(); }
}]; }];
} }
function showMenu(ev: MouseEvent) { function showMenu(ev: MouseEvent) {
os.popupMenu(getMenu(), (ev.currentTarget || ev.target || undefined) as HTMLElement | undefined); os.popupMenu(getMenu(), (ev.currentTarget ?? ev.target ?? undefined) as HTMLElement | undefined);
} }
function onContextmenu(ev: MouseEvent) { function onContextmenu(ev: MouseEvent) {

View File

@ -32,20 +32,20 @@ import MkEmojiPicker from '@/components/emoji-picker.vue';
import { defaultStore } from '@/store'; import { defaultStore } from '@/store';
withDefaults(defineProps<{ withDefaults(defineProps<{
manualShowing?: boolean; manualShowing?: boolean | null;
src?: HTMLElement; src?: HTMLElement;
showPinned?: boolean; showPinned?: boolean;
asReactionPicker?: boolean; asReactionPicker?: boolean;
}>(), { }>(), {
manualShowing: false, manualShowing: null,
showPinned: true, showPinned: true,
asReactionPicker: false, asReactionPicker: false,
}); });
const emit = defineEmits<{ const emit = defineEmits<{
(e: 'done', v: any): void; (ev: 'done', v: any): void;
(e: 'close'): void; (ev: 'close'): void;
(e: 'closed'): void; (ev: 'closed'): void;
}>(); }>();
const modal = ref<InstanceType<typeof MkModal>>(); const modal = ref<InstanceType<typeof MkModal>>();

View File

@ -1,6 +1,6 @@
<template> <template>
<div class="omfetrab" :class="['w' + width, 'h' + height, { big, asDrawer }]" :style="{ maxHeight: maxHeight ? maxHeight + 'px' : undefined }"> <div class="omfetrab" :class="['w' + width, 'h' + height, { big, asDrawer }]" :style="{ maxHeight: maxHeight ? maxHeight + 'px' : undefined }">
<input ref="search" v-model.trim="q" class="search" data-prevent-emoji-insert :class="{ filled: q != null && q != '' }" :placeholder="i18n.locale.search" @paste.stop="paste" @keyup.enter="done()"> <input ref="search" v-model.trim="q" class="search" data-prevent-emoji-insert :class="{ filled: q != null && q != '' }" :placeholder="i18n.ts.search" @paste.stop="paste" @keyup.enter="done()">
<div ref="emojis" class="emojis"> <div ref="emojis" class="emojis">
<section class="result"> <section class="result">
<div v-if="searchResultCustom.length > 0"> <div v-if="searchResultCustom.length > 0">
@ -43,7 +43,7 @@
</section> </section>
<section> <section>
<header class="_acrylic"><i class="far fa-clock fa-fw"></i> {{ i18n.locale.recentUsed }}</header> <header class="_acrylic"><i class="far fa-clock fa-fw"></i> {{ i18n.ts.recentUsed }}</header>
<div> <div>
<button v-for="emoji in recentlyUsedEmojis" <button v-for="emoji in recentlyUsedEmojis"
:key="emoji" :key="emoji"
@ -56,11 +56,11 @@
</section> </section>
</div> </div>
<div> <div>
<header class="_acrylic">{{ i18n.locale.customEmojis }}</header> <header class="_acrylic">{{ i18n.ts.customEmojis }}</header>
<XSection v-for="category in customEmojiCategories" :key="'custom:' + category" :initial-shown="false" :emojis="customEmojis.filter(e => e.category === category).map(e => ':' + e.name + ':')" @chosen="chosen">{{ category || i18n.locale.other }}</XSection> <XSection v-for="category in customEmojiCategories" :key="'custom:' + category" :initial-shown="false" :emojis="customEmojis.filter(e => e.category === category).map(e => ':' + e.name + ':')" @chosen="chosen">{{ category || i18n.ts.other }}</XSection>
</div> </div>
<div> <div>
<header class="_acrylic">{{ i18n.locale.emoji }}</header> <header class="_acrylic">{{ i18n.ts.emoji }}</header>
<XSection v-for="category in categories" :emojis="emojilist.filter(e => e.category === category).map(e => e.char)" @chosen="chosen">{{ category }}</XSection> <XSection v-for="category in categories" :emojis="emojilist.filter(e => e.category === category).map(e => e.char)" @chosen="chosen">{{ category }}</XSection>
</div> </div>
</div> </div>
@ -280,7 +280,7 @@ function getKey(emoji: string | Misskey.entities.CustomEmoji | UnicodeEmojiDef):
} }
function chosen(emoji: any, ev?: MouseEvent) { function chosen(emoji: any, ev?: MouseEvent) {
const el = ev && (ev.currentTarget || ev.target) as HTMLElement | null | undefined; const el = ev && (ev.currentTarget ?? ev.target) as HTMLElement | null | undefined;
if (el) { if (el) {
const rect = el.getBoundingClientRect(); const rect = el.getBoundingClientRect();
const x = rect.left + (el.offsetWidth / 2); const x = rect.left + (el.offsetWidth / 2);

View File

@ -6,23 +6,23 @@
> >
<template v-if="!wait"> <template v-if="!wait">
<template v-if="hasPendingFollowRequestFromYou && user.isLocked"> <template v-if="hasPendingFollowRequestFromYou && user.isLocked">
<span v-if="full">{{ i18n.locale.followRequestPending }}</span><i class="fas fa-hourglass-half"></i> <span v-if="full">{{ i18n.ts.followRequestPending }}</span><i class="fas fa-hourglass-half"></i>
</template> </template>
<template v-else-if="hasPendingFollowRequestFromYou && !user.isLocked"> <!-- つまりリモートフォローの場合 --> <template v-else-if="hasPendingFollowRequestFromYou && !user.isLocked"> <!-- つまりリモートフォローの場合 -->
<span v-if="full">{{ i18n.locale.processing }}</span><i class="fas fa-spinner fa-pulse"></i> <span v-if="full">{{ i18n.ts.processing }}</span><i class="fas fa-spinner fa-pulse"></i>
</template> </template>
<template v-else-if="isFollowing"> <template v-else-if="isFollowing">
<span v-if="full">{{ i18n.locale.unfollow }}</span><i class="fas fa-minus"></i> <span v-if="full">{{ i18n.ts.unfollow }}</span><i class="fas fa-minus"></i>
</template> </template>
<template v-else-if="!isFollowing && user.isLocked"> <template v-else-if="!isFollowing && user.isLocked">
<span v-if="full">{{ i18n.locale.followRequest }}</span><i class="fas fa-plus"></i> <span v-if="full">{{ i18n.ts.followRequest }}</span><i class="fas fa-plus"></i>
</template> </template>
<template v-else-if="!isFollowing && !user.isLocked"> <template v-else-if="!isFollowing && !user.isLocked">
<span v-if="full">{{ i18n.locale.follow }}</span><i class="fas fa-plus"></i> <span v-if="full">{{ i18n.ts.follow }}</span><i class="fas fa-plus"></i>
</template> </template>
</template> </template>
<template v-else> <template v-else>
<span v-if="full">{{ i18n.locale.processing }}</span><i class="fas fa-spinner fa-pulse fa-fw"></i> <span v-if="full">{{ i18n.ts.processing }}</span><i class="fas fa-spinner fa-pulse fa-fw"></i>
</template> </template>
</button> </button>
</template> </template>

View File

@ -5,28 +5,28 @@
@close="dialog.close()" @close="dialog.close()"
@closed="emit('closed')" @closed="emit('closed')"
> >
<template #header>{{ i18n.locale.forgotPassword }}</template> <template #header>{{ i18n.ts.forgotPassword }}</template>
<form v-if="instance.enableEmail" class="bafeceda" @submit.prevent="onSubmit"> <form v-if="instance.enableEmail" class="bafeceda" @submit.prevent="onSubmit">
<div class="main _formRoot"> <div class="main _formRoot">
<MkInput v-model="username" class="_formBlock" type="text" pattern="^[a-zA-Z0-9_]+$" spellcheck="false" autofocus required> <MkInput v-model="username" class="_formBlock" type="text" pattern="^[a-zA-Z0-9_]+$" spellcheck="false" autofocus required>
<template #label>{{ i18n.locale.username }}</template> <template #label>{{ i18n.ts.username }}</template>
<template #prefix>@</template> <template #prefix>@</template>
</MkInput> </MkInput>
<MkInput v-model="email" class="_formBlock" type="email" spellcheck="false" required> <MkInput v-model="email" class="_formBlock" type="email" spellcheck="false" required>
<template #label>{{ i18n.locale.emailAddress }}</template> <template #label>{{ i18n.ts.emailAddress }}</template>
<template #caption>{{ i18n.locale._forgotPassword.enterEmail }}</template> <template #caption>{{ i18n.ts._forgotPassword.enterEmail }}</template>
</MkInput> </MkInput>
<MkButton class="_formBlock" type="submit" :disabled="processing" primary style="margin: 0 auto;">{{ i18n.locale.send }}</MkButton> <MkButton class="_formBlock" type="submit" :disabled="processing" primary style="margin: 0 auto;">{{ i18n.ts.send }}</MkButton>
</div> </div>
<div class="sub"> <div class="sub">
<MkA to="/about" class="_link">{{ i18n.locale._forgotPassword.ifNoEmail }}</MkA> <MkA to="/about" class="_link">{{ i18n.ts._forgotPassword.ifNoEmail }}</MkA>
</div> </div>
</form> </form>
<div v-else class="bafecedb"> <div v-else class="bafecedb">
{{ i18n.locale._forgotPassword.contactAdmin }} {{ i18n.ts._forgotPassword.contactAdmin }}
</div> </div>
</XModalWindow> </XModalWindow>
</template> </template>

View File

@ -117,7 +117,7 @@ export default defineComponent({
text: computed(() => { text: computed(() => {
return props.textConverter(finalValue.value); return props.textConverter(finalValue.value);
}), }),
source: thumbEl, targetElement: thumbEl,
}, {}, 'closed'); }, {}, 'closed');
const style = document.createElement('style'); const style = document.createElement('style');

View File

@ -20,45 +20,33 @@
</div> </div>
</template> </template>
<script lang="ts"> <script lang="ts" setup>
import { defineComponent, ref, toRefs } from 'vue'; import { toRefs, Ref } from 'vue';
import * as os from '@/os'; import * as os from '@/os';
import Ripple from '@/components/ripple.vue'; import Ripple from '@/components/ripple.vue';
export default defineComponent({ const props = defineProps<{
props: { modelValue: boolean | Ref<boolean>;
modelValue: { disabled?: boolean;
type: Boolean, }>();
default: false
},
disabled: {
type: Boolean,
default: false
}
},
setup(props, context) { const emit = defineEmits<{
const button = ref<HTMLElement>(); (e: 'update:modelValue', v: boolean): void;
const checked = toRefs(props).modelValue; }>();
const toggle = () => {
if (props.disabled) return;
context.emit('update:modelValue', !checked.value);
if (!checked.value) { let button = $ref<HTMLElement>();
const rect = button.value.getBoundingClientRect(); const checked = toRefs(props).modelValue;
const x = rect.left + (button.value.offsetWidth / 2); const toggle = () => {
const y = rect.top + (button.value.offsetHeight / 2); if (props.disabled) return;
os.popup(Ripple, { x, y, particle: false }, {}, 'end'); emit('update:modelValue', !checked.value);
}
};
return { if (!checked.value) {
button, const rect = button.getBoundingClientRect();
checked, const x = rect.left + (button.offsetWidth / 2);
toggle, const y = rect.top + (button.offsetHeight / 2);
}; os.popup(Ripple, { x, y, particle: false }, {}, 'end');
}, }
}); };
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@ -23,8 +23,9 @@ const props = withDefaults(defineProps<{
behavior: null, behavior: null,
}); });
const navHook = inject('navHook', null); type Navigate = (path: string, record?: boolean) => void;
const sideViewHook = inject('sideViewHook', null); const navHook = inject<null | Navigate>('navHook', null);
const sideViewHook = inject<null | Navigate>('sideViewHook', null);
const active = $computed(() => { const active = $computed(() => {
if (props.activeClass == null) return false; if (props.activeClass == null) return false;
@ -43,31 +44,31 @@ function onContextmenu(ev) {
text: props.to, text: props.to,
}, { }, {
icon: 'fas fa-window-maximize', icon: 'fas fa-window-maximize',
text: i18n.locale.openInWindow, text: i18n.ts.openInWindow,
action: () => { action: () => {
os.pageWindow(props.to); os.pageWindow(props.to);
} }
}, sideViewHook ? { }, sideViewHook ? {
icon: 'fas fa-columns', icon: 'fas fa-columns',
text: i18n.locale.openInSideView, text: i18n.ts.openInSideView,
action: () => { action: () => {
sideViewHook(props.to); sideViewHook(props.to);
} }
} : undefined, { } : undefined, {
icon: 'fas fa-expand-alt', icon: 'fas fa-expand-alt',
text: i18n.locale.showInPage, text: i18n.ts.showInPage,
action: () => { action: () => {
router.push(props.to); router.push(props.to);
} }
}, null, { }, null, {
icon: 'fas fa-external-link-alt', icon: 'fas fa-external-link-alt',
text: i18n.locale.openInNewTab, text: i18n.ts.openInNewTab,
action: () => { action: () => {
window.open(props.to, '_blank'); window.open(props.to, '_blank');
} }
}, { }, {
icon: 'fas fa-link', icon: 'fas fa-link',
text: i18n.locale.copyLink, text: i18n.ts.copyLink,
action: () => { action: () => {
copyToClipboard(`${url}${props.to}`); copyToClipboard(`${url}${props.to}`);
} }

View File

@ -104,7 +104,7 @@ export default defineComponent({
if (props.info.share) { if (props.info.share) {
if (menu.length > 0) menu.push(null); if (menu.length > 0) menu.push(null);
menu.push({ menu.push({
text: i18n.locale.share, text: i18n.ts.share,
icon: 'fas fa-share-alt', icon: 'fas fa-share-alt',
action: share action: share
}); });
@ -113,7 +113,7 @@ export default defineComponent({
if (menu.length > 0) menu.push(null); if (menu.length > 0) menu.push(null);
menu = menu.concat(props.menu); menu = menu.concat(props.menu);
} }
popupMenu(menu, ev.currentTarget || ev.target); popupMenu(menu, ev.currentTarget ?? ev.target);
}; };
const showTabsPopup = (ev: MouseEvent) => { const showTabsPopup = (ev: MouseEvent) => {
@ -126,7 +126,7 @@ export default defineComponent({
icon: tab.icon, icon: tab.icon,
action: tab.onClick, action: tab.onClick,
})); }));
popupMenu(menu, ev.currentTarget || ev.target); popupMenu(menu, ev.currentTarget ?? ev.target);
}; };
const preventDrag = (ev: TouchEvent) => { const preventDrag = (ev: TouchEvent) => {

View File

@ -24,16 +24,16 @@ let now = $ref(new Date());
const relative = $computed(() => { const relative = $computed(() => {
const ago = (now.getTime() - _time.getTime()) / 1000/*ms*/; const ago = (now.getTime() - _time.getTime()) / 1000/*ms*/;
return ( return (
ago >= 31536000 ? i18n.t('_ago.yearsAgo', { n: (~~(ago / 31536000)).toString() }) : ago >= 31536000 ? i18n.t('_ago.yearsAgo', { n: Math.round(ago / 31536000).toString() }) :
ago >= 2592000 ? i18n.t('_ago.monthsAgo', { n: (~~(ago / 2592000)).toString() }) : ago >= 2592000 ? i18n.t('_ago.monthsAgo', { n: Math.round(ago / 2592000).toString() }) :
ago >= 604800 ? i18n.t('_ago.weeksAgo', { n: (~~(ago / 604800)).toString() }) : ago >= 604800 ? i18n.t('_ago.weeksAgo', { n: Math.round(ago / 604800).toString() }) :
ago >= 86400 ? i18n.t('_ago.daysAgo', { n: (~~(ago / 86400)).toString() }) : ago >= 86400 ? i18n.t('_ago.daysAgo', { n: Math.round(ago / 86400).toString() }) :
ago >= 3600 ? i18n.t('_ago.hoursAgo', { n: (~~(ago / 3600)).toString() }) : ago >= 3600 ? i18n.t('_ago.hoursAgo', { n: Math.round(ago / 3600).toString() }) :
ago >= 60 ? i18n.t('_ago.minutesAgo', { n: (~~(ago / 60)).toString() }) : ago >= 60 ? i18n.t('_ago.minutesAgo', { n: (~~(ago / 60)).toString() }) :
ago >= 10 ? i18n.t('_ago.secondsAgo', { n: (~~(ago % 60)).toString() }) : ago >= 10 ? i18n.t('_ago.secondsAgo', { n: (~~(ago % 60)).toString() }) :
ago >= -1 ? i18n.locale._ago.justNow : ago >= -1 ? i18n.ts._ago.justNow :
ago < -1 ? i18n.locale._ago.future : ago < -1 ? i18n.ts._ago.future :
i18n.locale._ago.unknown); i18n.ts._ago.unknown);
}); });
function tick() { function tick() {

View File

@ -20,52 +20,32 @@
</div> </div>
</template> </template>
<script lang="ts"> <script lang="ts" setup>
import { defineComponent } from 'vue'; import { watch } from 'vue';
import * as misskey from 'misskey-js';
import { getStaticImageUrl } from '@/scripts/get-static-image-url'; import { getStaticImageUrl } from '@/scripts/get-static-image-url';
import ImgWithBlurhash from '@/components/img-with-blurhash.vue'; import ImgWithBlurhash from '@/components/img-with-blurhash.vue';
import * as os from '@/os'; import { defaultStore } from '@/store';
export default defineComponent({ const props = defineProps<{
components: { image: misskey.entities.DriveFile;
ImgWithBlurhash raw?: boolean;
}, }>();
props: {
image: {
type: Object,
required: true
},
raw: {
default: false
}
},
data() {
return {
hide: true,
};
},
computed: {
url(): any {
let url = this.$store.state.disableShowingAnimatedImages
? getStaticImageUrl(this.image.thumbnailUrl)
: this.image.thumbnailUrl;
if (this.raw || this.$store.state.loadRawImages) { let hide = $ref(true);
url = this.image.url;
}
return url; const url = (props.raw || defaultStore.state.loadRawImages)
} ? props.image.url
}, : defaultStore.state.disableShowingAnimatedImages
created() { ? getStaticImageUrl(props.image.thumbnailUrl)
// Plugin:register_note_view_interruptor 使watch : props.image.thumbnailUrl;
this.$watch('image', () => {
this.hide = (this.$store.state.nsfw === 'force') ? true : this.image.isSensitive && (this.$store.state.nsfw !== 'ignore'); // Plugin:register_note_view_interruptor 使watch
}, { watch(() => props.image, () => {
deep: true, hide = (defaultStore.state.nsfw === 'force') ? true : props.image.isSensitive && (defaultStore.state.nsfw !== 'ignore');
immediate: true, }, {
}); deep: true,
}, immediate: true,
}); });
</script> </script>

View File

@ -12,8 +12,8 @@
</div> </div>
</template> </template>
<script lang="ts"> <script lang="ts" setup>
import { defineComponent, onMounted, PropType, ref } from 'vue'; import { onMounted, ref } from 'vue';
import * as misskey from 'misskey-js'; import * as misskey from 'misskey-js';
import PhotoSwipeLightbox from 'photoswipe/dist/photoswipe-lightbox.esm.js'; import PhotoSwipeLightbox from 'photoswipe/dist/photoswipe-lightbox.esm.js';
import PhotoSwipe from 'photoswipe/dist/photoswipe.esm.js'; import PhotoSwipe from 'photoswipe/dist/photoswipe.esm.js';
@ -25,98 +25,80 @@ import * as os from '@/os';
import { FILE_TYPE_BROWSERSAFE } from '@/const'; import { FILE_TYPE_BROWSERSAFE } from '@/const';
import { defaultStore } from '@/store'; import { defaultStore } from '@/store';
export default defineComponent({ const props = defineProps<{
components: { mediaList: misskey.entities.DriveFile[];
XBanner, raw?: boolean;
XImage, }>();
XVideo,
},
props: {
mediaList: {
type: Array as PropType<misskey.entities.DriveFile[]>,
required: true,
},
raw: {
default: false
},
},
setup(props) {
const gallery = ref(null);
onMounted(() => { const gallery = ref(null);
const lightbox = new PhotoSwipeLightbox({ const pswpZIndex = os.claimZIndex('middle');
dataSource: props.mediaList
.filter(media => {
if (media.type === 'image/svg+xml') return true; // svgwebpublicpngtrue
return media.type.startsWith('image') && FILE_TYPE_BROWSERSAFE.includes(media.type);
})
.map(media => {
const item = {
src: media.url,
w: media.properties.width,
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,
children: '.image',
thumbSelector: '.image',
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) => { onMounted(() => {
const { itemData } = e; const lightbox = new PhotoSwipeLightbox({
dataSource: props.mediaList
// element is children .filter(media => {
const { element } = itemData; if (media.type === 'image/svg+xml') return true; // svgwebpublicpngtrue
return media.type.startsWith('image') && FILE_TYPE_BROWSERSAFE.includes(media.type);
const id = element.dataset.id; })
const file = props.mediaList.find(media => media.id === id); .map(media => {
const item = {
itemData.src = file.url; src: media.url,
itemData.w = Number(file.properties.width); w: media.properties.width,
itemData.h = Number(file.properties.height); h: media.properties.height,
if (file.properties.orientation != null && file.properties.orientation >= 5) { alt: media.name,
[itemData.w, itemData.h] = [itemData.h, itemData.w]; };
if (media.properties.orientation != null && media.properties.orientation >= 5) {
[item.w, item.h] = [item.h, item.w];
} }
itemData.msrc = file.thumbnailUrl; return item;
itemData.thumbCropped = true; }),
}); gallery: gallery.value,
children: '.image',
thumbSelector: '.image',
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.init(); lightbox.on('itemData', (ev) => {
}); const { itemData } = ev;
const previewable = (file: misskey.entities.DriveFile): boolean => { // element is children
if (file.type === 'image/svg+xml') return true; // svgwebpublic/thumbnailpngtrue const { element } = itemData;
// FILE_TYPE_BROWSERSAFE
return (file.type.startsWith('video') || file.type.startsWith('image')) && FILE_TYPE_BROWSERSAFE.includes(file.type);
};
return { const id = element.dataset.id;
previewable, const file = props.mediaList.find(media => media.id === id);
gallery,
pswpZIndex: os.claimZIndex('middle'), itemData.src = file.url;
}; itemData.w = Number(file.properties.width);
}, 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.thumbCropped = true;
});
lightbox.init();
}); });
const previewable = (file: misskey.entities.DriveFile): boolean => {
if (file.type === 'image/svg+xml') return true; // svgwebpublic/thumbnailpngtrue
// FILE_TYPE_BROWSERSAFE
return (file.type.startsWith('video') || file.type.startsWith('image')) && FILE_TYPE_BROWSERSAFE.includes(file.type);
};
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@ -250,7 +250,7 @@ function menu(viaKeyboard = false): void {
function showRenoteMenu(viaKeyboard = false): void { function showRenoteMenu(viaKeyboard = false): void {
if (!isMyRenote) return; if (!isMyRenote) return;
os.popupMenu([{ os.popupMenu([{
text: i18n.locale.unrenote, text: i18n.ts.unrenote,
icon: 'fas fa-trash-alt', icon: 'fas fa-trash-alt',
danger: true, danger: true,
action: () => { action: () => {

View File

@ -10,13 +10,13 @@
:class="{ renote: isRenote }" :class="{ renote: isRenote }"
> >
<MkNoteSub v-if="appearNote.reply" :note="appearNote.reply" class="reply-to"/> <MkNoteSub v-if="appearNote.reply" :note="appearNote.reply" class="reply-to"/>
<div v-if="pinned" class="info"><i class="fas fa-thumbtack"></i> {{ i18n.locale.pinnedNote }}</div> <div v-if="pinned" class="info"><i class="fas fa-thumbtack"></i> {{ i18n.ts.pinnedNote }}</div>
<div v-if="appearNote._prId_" class="info"><i class="fas fa-bullhorn"></i> {{ i18n.locale.promotion }}<button class="_textButton hide" @click="readPromo()">{{ i18n.locale.hideThisNote }} <i class="fas fa-times"></i></button></div> <div v-if="appearNote._prId_" class="info"><i class="fas fa-bullhorn"></i> {{ i18n.ts.promotion }}<button class="_textButton hide" @click="readPromo()">{{ i18n.ts.hideThisNote }} <i class="fas fa-times"></i></button></div>
<div v-if="appearNote._featuredId_" class="info"><i class="fas fa-bolt"></i> {{ i18n.locale.featured }}</div> <div v-if="appearNote._featuredId_" class="info"><i class="fas fa-bolt"></i> {{ i18n.ts.featured }}</div>
<div v-if="isRenote" class="renote"> <div v-if="isRenote" class="renote">
<MkAvatar class="avatar" :user="note.user"/> <MkAvatar class="avatar" :user="note.user"/>
<i class="fas fa-retweet"></i> <i class="fas fa-retweet"></i>
<I18n :src="i18n.locale.renotedBy" tag="span"> <I18n :src="i18n.ts.renotedBy" tag="span">
<template #user> <template #user>
<MkA v-user-preview="note.userId" class="name" :to="userPage(note.user)"> <MkA v-user-preview="note.userId" class="name" :to="userPage(note.user)">
<MkUserName :user="note.user"/> <MkUserName :user="note.user"/>
@ -48,7 +48,7 @@
</p> </p>
<div v-show="appearNote.cw == null || showContent" class="content" :class="{ collapsed }"> <div v-show="appearNote.cw == null || showContent" class="content" :class="{ collapsed }">
<div class="text"> <div class="text">
<span v-if="appearNote.isHidden" style="opacity: 0.5">({{ i18n.locale.private }})</span> <span v-if="appearNote.isHidden" style="opacity: 0.5">({{ i18n.ts.private }})</span>
<MkA v-if="appearNote.replyId" class="reply" :to="`/notes/${appearNote.replyId}`"><i class="fas fa-reply"></i></MkA> <MkA v-if="appearNote.replyId" class="reply" :to="`/notes/${appearNote.replyId}`"><i class="fas fa-reply"></i></MkA>
<Mfm v-if="appearNote.text" :text="appearNote.text" :author="appearNote.user" :i="$i" :custom-emojis="appearNote.emojis"/> <Mfm v-if="appearNote.text" :text="appearNote.text" :author="appearNote.user" :i="$i" :custom-emojis="appearNote.emojis"/>
<a v-if="appearNote.renote != null" class="rp">RN:</a> <a v-if="appearNote.renote != null" class="rp">RN:</a>
@ -67,7 +67,7 @@
<MkUrlPreview v-for="url in urls" :key="url" :url="url" :compact="true" :detail="false" class="url-preview"/> <MkUrlPreview v-for="url in urls" :key="url" :url="url" :compact="true" :detail="false" class="url-preview"/>
<div v-if="appearNote.renote" class="renote"><XNoteSimple :note="appearNote.renote"/></div> <div v-if="appearNote.renote" class="renote"><XNoteSimple :note="appearNote.renote"/></div>
<button v-if="collapsed" class="fade _button" @click="collapsed = false"> <button v-if="collapsed" class="fade _button" @click="collapsed = false">
<span>{{ i18n.locale.showMore }}</span> <span>{{ i18n.ts.showMore }}</span>
</button> </button>
</div> </div>
<MkA v-if="appearNote.channel && !inChannel" class="channel" :to="`/channels/${appearNote.channel.id}`"><i class="fas fa-satellite-dish"></i> {{ appearNote.channel.name }}</MkA> <MkA v-if="appearNote.channel && !inChannel" class="channel" :to="`/channels/${appearNote.channel.id}`"><i class="fas fa-satellite-dish"></i> {{ appearNote.channel.name }}</MkA>
@ -94,7 +94,7 @@
</article> </article>
</div> </div>
<div v-else class="muted" @click="muted = false"> <div v-else class="muted" @click="muted = false">
<I18n :src="i18n.locale.userSaysSomething" tag="small"> <I18n :src="i18n.ts.userSaysSomething" tag="small">
<template #name> <template #name>
<MkA v-user-preview="appearNote.userId" class="name" :to="userPage(appearNote.user)"> <MkA v-user-preview="appearNote.userId" class="name" :to="userPage(appearNote.user)">
<MkUserName :user="appearNote.user"/> <MkUserName :user="appearNote.user"/>
@ -238,7 +238,7 @@ function menu(viaKeyboard = false): void {
function showRenoteMenu(viaKeyboard = false): void { function showRenoteMenu(viaKeyboard = false): void {
if (!isMyRenote) return; if (!isMyRenote) return;
os.popupMenu([{ os.popupMenu([{
text: i18n.locale.unrenote, text: i18n.ts.unrenote,
icon: 'fas fa-trash-alt', icon: 'fas fa-trash-alt',
danger: true, danger: true,
action: () => { action: () => {

View File

@ -153,7 +153,7 @@ export default defineComponent({
showing, showing,
reaction: props.notification.reaction ? props.notification.reaction.replace(/^:(\w+):$/, ':$1@.:') : props.notification.reaction, reaction: props.notification.reaction ? props.notification.reaction.replace(/^:(\w+):$/, ':$1@.:') : props.notification.reaction,
emojis: props.notification.note.emojis, emojis: props.notification.note.emojis,
source: reactionRef.value.$el, targetElement: reactionRef.value.$el,
}, {}, 'closed'); }, {}, 'closed');
}); });

View File

@ -160,7 +160,7 @@ export default defineComponent({
action: () => { action: () => {
copyToClipboard(this.url); copyToClipboard(this.url);
} }
}], ev.currentTarget || ev.target); }], ev.currentTarget ?? ev.target);
}, },
back() { back() {

View File

@ -127,7 +127,7 @@ export default defineComponent({
text: this.$ts.attachCancel, text: this.$ts.attachCancel,
icon: 'fas fa-times-circle', icon: 'fas fa-times-circle',
action: () => { this.detachMedia(file.id) } action: () => { this.detachMedia(file.id) }
}], ev.currentTarget || ev.target).then(() => this.menu = null); }], ev.currentTarget ?? ev.target).then(() => this.menu = null);
} }
} }
}); });

View File

@ -8,28 +8,28 @@
> >
<header> <header>
<button v-if="!fixed" class="cancel _button" @click="cancel"><i class="fas fa-times"></i></button> <button v-if="!fixed" class="cancel _button" @click="cancel"><i class="fas fa-times"></i></button>
<button v-click-anime v-tooltip="i18n.locale.switchAccount" class="account _button" @click="openAccountMenu"> <button v-click-anime v-tooltip="i18n.ts.switchAccount" class="account _button" @click="openAccountMenu">
<MkAvatar :user="postAccount ?? $i" class="avatar"/> <MkAvatar :user="postAccount ?? $i" class="avatar"/>
</button> </button>
<div> <div>
<span class="text-count" :class="{ over: textLength > maxTextLength }">{{ maxTextLength - textLength }}</span> <span class="text-count" :class="{ over: textLength > maxTextLength }">{{ maxTextLength - textLength }}</span>
<span v-if="localOnly" class="local-only"><i class="fas fa-biohazard"></i></span> <span v-if="localOnly" class="local-only"><i class="fas fa-biohazard"></i></span>
<button ref="visibilityButton" v-tooltip="i18n.locale.visibility" class="_button visibility" :disabled="channel != null" @click="setVisibility"> <button ref="visibilityButton" v-tooltip="i18n.ts.visibility" class="_button visibility" :disabled="channel != null" @click="setVisibility">
<span v-if="visibility === 'public'"><i class="fas fa-globe"></i></span> <span v-if="visibility === 'public'"><i class="fas fa-globe"></i></span>
<span v-if="visibility === 'home'"><i class="fas fa-home"></i></span> <span v-if="visibility === 'home'"><i class="fas fa-home"></i></span>
<span v-if="visibility === 'followers'"><i class="fas fa-unlock"></i></span> <span v-if="visibility === 'followers'"><i class="fas fa-unlock"></i></span>
<span v-if="visibility === 'specified'"><i class="fas fa-envelope"></i></span> <span v-if="visibility === 'specified'"><i class="fas fa-envelope"></i></span>
</button> </button>
<button v-tooltip="i18n.locale.previewNoteText" class="_button preview" :class="{ active: showPreview }" @click="showPreview = !showPreview"><i class="fas fa-file-code"></i></button> <button v-tooltip="i18n.ts.previewNoteText" class="_button preview" :class="{ active: showPreview }" @click="showPreview = !showPreview"><i class="fas fa-file-code"></i></button>
<button class="submit _buttonGradate" :disabled="!canPost" data-cy-open-post-form-submit @click="post">{{ submitText }}<i :class="reply ? 'fas fa-reply' : renote ? 'fas fa-quote-right' : 'fas fa-paper-plane'"></i></button> <button class="submit _buttonGradate" :disabled="!canPost" data-cy-open-post-form-submit @click="post">{{ submitText }}<i :class="reply ? 'fas fa-reply' : renote ? 'fas fa-quote-right' : 'fas fa-paper-plane'"></i></button>
</div> </div>
</header> </header>
<div class="form" :class="{ fixed }"> <div class="form" :class="{ fixed }">
<XNoteSimple v-if="reply" class="preview" :note="reply"/> <XNoteSimple v-if="reply" class="preview" :note="reply"/>
<XNoteSimple v-if="renote" class="preview" :note="renote"/> <XNoteSimple v-if="renote" class="preview" :note="renote"/>
<div v-if="quoteId" class="with-quote"><i class="fas fa-quote-left"></i> {{ i18n.locale.quoteAttached }}<button @click="quoteId = null"><i class="fas fa-times"></i></button></div> <div v-if="quoteId" class="with-quote"><i class="fas fa-quote-left"></i> {{ i18n.ts.quoteAttached }}<button @click="quoteId = null"><i class="fas fa-times"></i></button></div>
<div v-if="visibility === 'specified'" class="to-specified"> <div v-if="visibility === 'specified'" class="to-specified">
<span style="margin-right: 8px;">{{ i18n.locale.recipient }}</span> <span style="margin-right: 8px;">{{ i18n.ts.recipient }}</span>
<div class="visibleUsers"> <div class="visibleUsers">
<span v-for="u in visibleUsers" :key="u.id"> <span v-for="u in visibleUsers" :key="u.id">
<MkAcct :user="u"/> <MkAcct :user="u"/>
@ -38,21 +38,21 @@
<button class="_buttonPrimary" @click="addVisibleUser"><i class="fas fa-plus fa-fw"></i></button> <button class="_buttonPrimary" @click="addVisibleUser"><i class="fas fa-plus fa-fw"></i></button>
</div> </div>
</div> </div>
<MkInfo v-if="hasNotSpecifiedMentions" warn class="hasNotSpecifiedMentions">{{ i18n.locale.notSpecifiedMentionWarning }} - <button class="_textButton" @click="addMissingMention()">{{ i18n.locale.add }}</button></MkInfo> <MkInfo v-if="hasNotSpecifiedMentions" warn class="hasNotSpecifiedMentions">{{ i18n.ts.notSpecifiedMentionWarning }} - <button class="_textButton" @click="addMissingMention()">{{ i18n.ts.add }}</button></MkInfo>
<input v-show="useCw" ref="cwInputEl" v-model="cw" class="cw" :placeholder="i18n.locale.annotation" @keydown="onKeydown"> <input v-show="useCw" ref="cwInputEl" v-model="cw" class="cw" :placeholder="i18n.ts.annotation" @keydown="onKeydown">
<textarea ref="textareaEl" v-model="text" class="text" :class="{ withCw: useCw }" :disabled="posting" :placeholder="placeholder" data-cy-post-form-text @keydown="onKeydown" @paste="onPaste" @compositionupdate="onCompositionUpdate" @compositionend="onCompositionEnd"/> <textarea ref="textareaEl" v-model="text" class="text" :class="{ withCw: useCw }" :disabled="posting" :placeholder="placeholder" data-cy-post-form-text @keydown="onKeydown" @paste="onPaste" @compositionupdate="onCompositionUpdate" @compositionend="onCompositionEnd"/>
<input v-show="withHashtags" ref="hashtagsInputEl" v-model="hashtags" class="hashtags" :placeholder="i18n.locale.hashtags" list="hashtags"> <input v-show="withHashtags" ref="hashtagsInputEl" v-model="hashtags" class="hashtags" :placeholder="i18n.ts.hashtags" list="hashtags">
<XPostFormAttaches class="attaches" :files="files" @updated="updateFiles" @detach="detachFile" @changeSensitive="updateFileSensitive" @changeName="updateFileName"/> <XPostFormAttaches class="attaches" :files="files" @updated="updateFiles" @detach="detachFile" @changeSensitive="updateFileSensitive" @changeName="updateFileName"/>
<XPollEditor v-if="poll" v-model="poll" @destroyed="poll = null"/> <XPollEditor v-if="poll" v-model="poll" @destroyed="poll = null"/>
<XNotePreview v-if="showPreview" class="preview" :text="text"/> <XNotePreview v-if="showPreview" class="preview" :text="text"/>
<footer> <footer>
<button v-tooltip="i18n.locale.attachFile" class="_button" @click="chooseFileFrom"><i class="fas fa-photo-video"></i></button> <button v-tooltip="i18n.ts.attachFile" class="_button" @click="chooseFileFrom"><i class="fas fa-photo-video"></i></button>
<button v-tooltip="i18n.locale.poll" class="_button" :class="{ active: poll }" @click="togglePoll"><i class="fas fa-poll-h"></i></button> <button v-tooltip="i18n.ts.poll" class="_button" :class="{ active: poll }" @click="togglePoll"><i class="fas fa-poll-h"></i></button>
<button v-tooltip="i18n.locale.useCw" class="_button" :class="{ active: useCw }" @click="useCw = !useCw"><i class="fas fa-eye-slash"></i></button> <button v-tooltip="i18n.ts.useCw" class="_button" :class="{ active: useCw }" @click="useCw = !useCw"><i class="fas fa-eye-slash"></i></button>
<button v-tooltip="i18n.locale.mention" class="_button" @click="insertMention"><i class="fas fa-at"></i></button> <button v-tooltip="i18n.ts.mention" class="_button" @click="insertMention"><i class="fas fa-at"></i></button>
<button v-tooltip="i18n.locale.hashtags" class="_button" :class="{ active: withHashtags }" @click="withHashtags = !withHashtags"><i class="fas fa-hashtag"></i></button> <button v-tooltip="i18n.ts.hashtags" class="_button" :class="{ active: withHashtags }" @click="withHashtags = !withHashtags"><i class="fas fa-hashtag"></i></button>
<button v-tooltip="i18n.locale.emoji" class="_button" @click="insertEmoji"><i class="fas fa-laugh-squint"></i></button> <button v-tooltip="i18n.ts.emoji" class="_button" @click="insertEmoji"><i class="fas fa-laugh-squint"></i></button>
<button v-if="postFormActions.length > 0" v-tooltip="i18n.locale.plugin" class="_button" @click="showActions"><i class="fas fa-plug"></i></button> <button v-if="postFormActions.length > 0" v-tooltip="i18n.ts.plugin" class="_button" @click="showActions"><i class="fas fa-plug"></i></button>
</footer> </footer>
<datalist id="hashtags"> <datalist id="hashtags">
<option v-for="hashtag in recentHashtags" :key="hashtag" :value="hashtag"/> <option v-for="hashtag in recentHashtags" :key="hashtag" :value="hashtag"/>
@ -135,7 +135,10 @@ let showPreview = $ref(false);
let cw = $ref<string | null>(null); let cw = $ref<string | null>(null);
let localOnly = $ref<boolean>(props.initialLocalOnly ?? defaultStore.state.rememberNoteVisibility ? defaultStore.state.localOnly : defaultStore.state.defaultNoteLocalOnly); let localOnly = $ref<boolean>(props.initialLocalOnly ?? defaultStore.state.rememberNoteVisibility ? defaultStore.state.localOnly : defaultStore.state.defaultNoteLocalOnly);
let visibility = $ref(props.initialVisibility ?? (defaultStore.state.rememberNoteVisibility ? defaultStore.state.visibility : defaultStore.state.defaultNoteVisibility) as typeof misskey.noteVisibilities[number]); let visibility = $ref(props.initialVisibility ?? (defaultStore.state.rememberNoteVisibility ? defaultStore.state.visibility : defaultStore.state.defaultNoteVisibility) as typeof misskey.noteVisibilities[number]);
let visibleUsers = $ref(props.initialVisibleUsers ?? []); let visibleUsers = $ref([]);
if (props.initialVisibleUsers) {
props.initialVisibleUsers.forEach(pushVisibleUser);
}
let autocomplete = $ref(null); let autocomplete = $ref(null);
let draghover = $ref(false); let draghover = $ref(false);
let quoteId = $ref(null); let quoteId = $ref(null);
@ -165,19 +168,19 @@ const draftKey = $computed((): string => {
const placeholder = $computed((): string => { const placeholder = $computed((): string => {
if (props.renote) { if (props.renote) {
return i18n.locale._postForm.quotePlaceholder; return i18n.ts._postForm.quotePlaceholder;
} else if (props.reply) { } else if (props.reply) {
return i18n.locale._postForm.replyPlaceholder; return i18n.ts._postForm.replyPlaceholder;
} else if (props.channel) { } else if (props.channel) {
return i18n.locale._postForm.channelPlaceholder; return i18n.ts._postForm.channelPlaceholder;
} else { } else {
const xs = [ const xs = [
i18n.locale._postForm._placeholders.a, i18n.ts._postForm._placeholders.a,
i18n.locale._postForm._placeholders.b, i18n.ts._postForm._placeholders.b,
i18n.locale._postForm._placeholders.c, i18n.ts._postForm._placeholders.c,
i18n.locale._postForm._placeholders.d, i18n.ts._postForm._placeholders.d,
i18n.locale._postForm._placeholders.e, i18n.ts._postForm._placeholders.e,
i18n.locale._postForm._placeholders.f i18n.ts._postForm._placeholders.f
]; ];
return xs[Math.floor(Math.random() * xs.length)]; return xs[Math.floor(Math.random() * xs.length)];
} }
@ -185,10 +188,10 @@ const placeholder = $computed((): string => {
const submitText = $computed((): string => { const submitText = $computed((): string => {
return props.renote return props.renote
? i18n.locale.quote ? i18n.ts.quote
: props.reply : props.reply
? i18n.locale.reply ? i18n.ts.reply
: i18n.locale.note; : i18n.ts.note;
}); });
const textLength = $computed((): number => { const textLength = $computed((): number => {
@ -262,12 +265,12 @@ if (props.reply && ['home', 'followers', 'specified'].includes(props.reply.visib
os.api('users/show', { os.api('users/show', {
userIds: props.reply.visibleUserIds.filter(uid => uid !== $i.id && uid !== props.reply.userId) userIds: props.reply.visibleUserIds.filter(uid => uid !== $i.id && uid !== props.reply.userId)
}).then(users => { }).then(users => {
visibleUsers.push(...users); users.forEach(pushVisibleUser);
}); });
if (props.reply.userId !== $i.id) { if (props.reply.userId !== $i.id) {
os.api('users/show', { userId: props.reply.userId }).then(user => { os.api('users/show', { userId: props.reply.userId }).then(user => {
visibleUsers.push(user); pushVisibleUser(user);
}); });
} }
} }
@ -275,7 +278,7 @@ if (props.reply && ['home', 'followers', 'specified'].includes(props.reply.visib
if (props.specified) { if (props.specified) {
visibility = 'specified'; visibility = 'specified';
visibleUsers.push(props.specified); pushVisibleUser(props.specified);
} }
// keep cw when reply // keep cw when reply
@ -342,7 +345,7 @@ function focus() {
} }
function chooseFileFrom(ev) { function chooseFileFrom(ev) {
selectFiles(ev.currentTarget || ev.target, i18n.locale.attachFile).then(files_ => { selectFiles(ev.currentTarget ?? ev.target, i18n.ts.attachFile).then(files_ => {
for (const file of files_) { for (const file of files_) {
files.push(file); files.push(file);
} }
@ -397,9 +400,15 @@ function setVisibility() {
}, 'closed'); }, 'closed');
} }
function pushVisibleUser(user) {
if (!visibleUsers.some(u => u.username === user.username && u.host === user.host)) {
visibleUsers.push(user);
}
}
function addVisibleUser() { function addVisibleUser() {
os.selectUser().then(user => { os.selectUser().then(user => {
visibleUsers.push(user); pushVisibleUser(user);
}); });
} }
@ -447,7 +456,7 @@ async function onPaste(e: ClipboardEvent) {
os.confirm({ os.confirm({
type: 'info', type: 'info',
text: i18n.locale.quoteQuestion, text: i18n.ts.quoteQuestion,
}).then(({ canceled }) => { }).then(({ canceled }) => {
if (canceled) { if (canceled) {
insertTextAtCursor(textareaEl, paste); insertTextAtCursor(textareaEl, paste);
@ -540,8 +549,8 @@ async function post() {
}; };
if (withHashtags && hashtags && hashtags.trim() !== '') { if (withHashtags && hashtags && hashtags.trim() !== '') {
const hashtags = hashtags.trim().split(' ').map(x => x.startsWith('#') ? x : '#' + x).join(' '); const hashtags_ = hashtags.trim().split(' ').map(x => x.startsWith('#') ? x : '#' + x).join(' ');
data.text = data.text ? `${data.text} ${hashtags}` : hashtags; data.text = data.text ? `${data.text} ${hashtags_}` : hashtags_;
} }
// plugin // plugin
@ -565,9 +574,9 @@ async function post() {
deleteDraft(); deleteDraft();
emit('posted'); emit('posted');
if (data.text && data.text != '') { if (data.text && data.text != '') {
const hashtags = mfm.parse(data.text).filter(x => x.type === 'hashtag').map(x => x.props.hashtag); const hashtags_ = mfm.parse(data.text).filter(x => x.type === 'hashtag').map(x => x.props.hashtag);
const history = JSON.parse(localStorage.getItem('hashtags') || '[]') as string[]; const history = JSON.parse(localStorage.getItem('hashtags') || '[]') as string[];
localStorage.setItem('hashtags', JSON.stringify(unique(hashtags.concat(history)))); localStorage.setItem('hashtags', JSON.stringify(unique(hashtags_.concat(history))));
} }
posting = false; posting = false;
postAccount = null; postAccount = null;
@ -592,7 +601,7 @@ function insertMention() {
} }
async function insertEmoji(ev: MouseEvent) { async function insertEmoji(ev: MouseEvent) {
os.openEmojiPicker(ev.currentTarget || ev.target, {}, textareaEl); os.openEmojiPicker(ev.currentTarget ?? ev.target, {}, textareaEl);
} }
function showActions(ev) { function showActions(ev) {
@ -605,7 +614,7 @@ function showActions(ev) {
if (key === 'text') { text = value; } if (key === 'text') { text = value; }
}); });
} }
})), ev.currentTarget || ev.target); })), ev.currentTarget ?? ev.target);
} }
let postAccount = $ref<misskey.entities.UserDetailed | null>(null); let postAccount = $ref<misskey.entities.UserDetailed | null>(null);

View File

@ -1,5 +1,5 @@
<template> <template>
<MkTooltip ref="tooltip" :source="source" :max-width="340" @closed="emit('closed')"> <MkTooltip ref="tooltip" :target-element="targetElement" :max-width="340" @closed="emit('closed')">
<div class="beeadbfb"> <div class="beeadbfb">
<XReactionIcon :reaction="reaction" :custom-emojis="emojis" class="icon" :no-style="true"/> <XReactionIcon :reaction="reaction" :custom-emojis="emojis" class="icon" :no-style="true"/>
<div class="name">{{ reaction.replace('@.', '') }}</div> <div class="name">{{ reaction.replace('@.', '') }}</div>
@ -15,11 +15,11 @@ import XReactionIcon from './reaction-icon.vue';
const props = defineProps<{ const props = defineProps<{
reaction: string; reaction: string;
emojis: any[]; // TODO emojis: any[]; // TODO
source: any; // TODO targetElement: HTMLElement;
}>(); }>();
const emit = defineEmits<{ const emit = defineEmits<{
(e: 'closed'): void; (ev: 'closed'): void;
}>(); }>();
</script> </script>

View File

@ -1,5 +1,5 @@
<template> <template>
<MkTooltip ref="tooltip" :source="source" :max-width="340" @closed="emit('closed')"> <MkTooltip ref="tooltip" :target-element="targetElement" :max-width="340" @closed="emit('closed')">
<div class="bqxuuuey"> <div class="bqxuuuey">
<div class="reaction"> <div class="reaction">
<XReactionIcon :reaction="reaction" :custom-emojis="emojis" class="icon" :no-style="true"/> <XReactionIcon :reaction="reaction" :custom-emojis="emojis" class="icon" :no-style="true"/>
@ -26,11 +26,11 @@ const props = defineProps<{
users: any[]; // TODO users: any[]; // TODO
count: number; count: number;
emojis: any[]; // TODO emojis: any[]; // TODO
source: any; // TODO targetElement: HTMLElement;
}>(); }>();
const emit = defineEmits<{ const emit = defineEmits<{
(e: 'closed'): void; (ev: 'closed'): void;
}>(); }>();
</script> </script>

View File

@ -59,7 +59,7 @@ export default defineComponent({
const renote = (viaKeyboard = false) => { const renote = (viaKeyboard = false) => {
pleaseLogin(); pleaseLogin();
os.popupMenu([{ os.popupMenu([{
text: i18n.locale.renote, text: i18n.ts.renote,
icon: 'fas fa-retweet', icon: 'fas fa-retweet',
action: () => { action: () => {
os.api('notes/create', { os.api('notes/create', {
@ -67,7 +67,7 @@ export default defineComponent({
}); });
} }
}, { }, {
text: i18n.locale.quote, text: i18n.ts.quote,
icon: 'fas fa-quote-right', icon: 'fas fa-quote-right',
action: () => { action: () => {
os.post({ os.post({

View File

@ -1,5 +1,5 @@
<template> <template>
<MkTooltip ref="tooltip" :source="source" :max-width="250" @closed="emit('closed')"> <MkTooltip ref="tooltip" :target-element="targetElement" :max-width="250" @closed="emit('closed')">
<div class="beaffaef"> <div class="beaffaef">
<div v-for="u in users" :key="u.id" class="user"> <div v-for="u in users" :key="u.id" class="user">
<MkAvatar class="avatar" :user="u"/> <MkAvatar class="avatar" :user="u"/>
@ -17,11 +17,11 @@ import MkTooltip from './ui/tooltip.vue';
const props = defineProps<{ const props = defineProps<{
users: any[]; // TODO users: any[]; // TODO
count: number; count: number;
source: any; // TODO targetElement: HTMLElement;
}>(); }>();
const emit = defineEmits<{ const emit = defineEmits<{
(e: 'closed'): void; (ev: 'closed'): void;
}>(); }>();
</script> </script>

View File

@ -109,7 +109,7 @@ export default defineComponent({
text: 'Delete some bananas', text: 'Delete some bananas',
danger: true, danger: true,
action: () => {}, action: () => {},
}], ev.currentTarget || ev.target); }], ev.currentTarget ?? ev.target);
}, },
} }
}); });

View File

@ -1,88 +1,71 @@
<template> <template>
<transition :name="$store.state.animation ? 'fade' : ''" appear> <transition :name="$store.state.animation ? 'fade' : ''" appear>
<div class="nvlagfpb" :style="{ zIndex }" @contextmenu.prevent.stop="() => {}"> <div ref="rootEl" class="nvlagfpb" :style="{ zIndex }" @contextmenu.prevent.stop="() => {}">
<MkMenu :items="items" class="_popup _shadow" :align="'left'" @close="$emit('closed')"/> <MkMenu :items="items" class="_popup _shadow" :align="'left'" @close="$emit('closed')"/>
</div> </div>
</transition> </transition>
</template> </template>
<script lang="ts"> <script lang="ts" setup>
import { defineComponent } from 'vue'; import { onMounted, onBeforeUnmount } from 'vue';
import contains from '@/scripts/contains'; import contains from '@/scripts/contains';
import MkMenu from './menu.vue'; import MkMenu from './menu.vue';
import { MenuItem } from './types/menu.vue';
import * as os from '@/os'; import * as os from '@/os';
export default defineComponent({ const props = defineProps<{
components: { items: MenuItem[];
MkMenu, ev: MouseEvent;
}, }>();
props: {
items: {
type: Array,
required: true
},
ev: {
required: true
},
viaKeyboard: {
type: Boolean,
required: false
},
},
emits: ['closed'],
data() {
return {
zIndex: os.claimZIndex('high'),
};
},
computed: {
keymap(): any {
return {
'esc': () => this.$emit('closed'),
};
},
},
mounted() {
let left = this.ev.pageX + 1; // + 1
let top = this.ev.pageY + 1; // + 1
const width = this.$el.offsetWidth; const emit = defineEmits<{
const height = this.$el.offsetHeight; (e: 'closed'): void;
}>();
if (left + width - window.pageXOffset > window.innerWidth) { let rootEl = $ref<HTMLDivElement>();
left = window.innerWidth - width + window.pageXOffset;
}
if (top + height - window.pageYOffset > window.innerHeight) { let zIndex = $ref<number>(os.claimZIndex('high'));
top = window.innerHeight - height + window.pageYOffset;
}
if (top < 0) { onMounted(() => {
top = 0; let left = props.ev.pageX + 1; // + 1
} let top = props.ev.pageY + 1; // + 1
if (left < 0) { const width = rootEl.offsetWidth;
left = 0; const height = rootEl.offsetHeight;
}
this.$el.style.top = top + 'px'; if (left + width - window.pageXOffset > window.innerWidth) {
this.$el.style.left = left + 'px'; left = window.innerWidth - width + window.pageXOffset;
}
for (const el of Array.from(document.querySelectorAll('body *'))) { if (top + height - window.pageYOffset > window.innerHeight) {
el.addEventListener('mousedown', this.onMousedown); top = window.innerHeight - height + window.pageYOffset;
} }
},
beforeUnmount() { if (top < 0) {
for (const el of Array.from(document.querySelectorAll('body *'))) { top = 0;
el.removeEventListener('mousedown', this.onMousedown); }
}
}, if (left < 0) {
methods: { left = 0;
onMousedown(e) { }
if (!contains(this.$el, e.target) && (this.$el != e.target)) this.$emit('closed');
}, rootEl.style.top = `${top}px`;
rootEl.style.left = `${left}px`;
for (const el of Array.from(document.querySelectorAll('body *'))) {
el.addEventListener('mousedown', onMousedown);
} }
}); });
onBeforeUnmount(() => {
for (const el of Array.from(document.querySelectorAll('body *'))) {
el.removeEventListener('mousedown', onMousedown);
}
});
function onMousedown(e: Event) {
if (!contains(rootEl, e.target) && (rootEl != e.target)) emit('closed');
}
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@ -1,8 +1,8 @@
<template> <template>
<div ref="items" v-hotkey="keymap" <div ref="itemsEl" v-hotkey="keymap"
class="rrevdjwt" class="rrevdjwt"
:class="{ center: align === 'center', asDrawer }" :class="{ center: align === 'center', asDrawer }"
:style="{ width: (width && !asDrawer) ? width + 'px' : null, maxHeight: maxHeight ? maxHeight + 'px' : null }" :style="{ width: (width && !asDrawer) ? width + 'px' : '', maxHeight: maxHeight ? maxHeight + 'px' : '' }"
@contextmenu.self="e => e.preventDefault()" @contextmenu.self="e => e.preventDefault()"
> >
<template v-for="(item, i) in items2"> <template v-for="(item, i) in items2">
@ -28,6 +28,9 @@
<MkAvatar :user="item.user" class="avatar"/><MkUserName :user="item.user"/> <MkAvatar :user="item.user" class="avatar"/><MkUserName :user="item.user"/>
<span v-if="item.indicate" class="indicator"><i class="fas fa-circle"></i></span> <span v-if="item.indicate" class="indicator"><i class="fas fa-circle"></i></span>
</button> </button>
<span v-else-if="item.type === 'switch'" :tabindex="i" class="item">
<FormSwitch v-model="item.ref" :disabled="item.disabled" class="form-switch">{{ item.text }}</FormSwitch>
</span>
<button v-else :tabindex="i" class="_button item" :class="{ danger: item.danger, active: item.active }" :disabled="item.active" @click="clicked(item.action, $event)"> <button v-else :tabindex="i" class="_button item" :class="{ danger: item.danger, active: item.active }" :disabled="item.active" @click="clicked(item.action, $event)">
<i v-if="item.icon" class="fa-fw" :class="item.icon"></i> <i v-if="item.icon" class="fa-fw" :class="item.icon"></i>
<MkAvatar v-if="item.avatar" :user="item.avatar" class="avatar"/> <MkAvatar v-if="item.avatar" :user="item.avatar" class="avatar"/>
@ -41,114 +44,78 @@
</div> </div>
</template> </template>
<script lang="ts"> <script lang="ts" setup>
import { defineComponent, ref, unref } from 'vue'; import { nextTick, onMounted, watch } from 'vue';
import { focusPrev, focusNext } from '@/scripts/focus'; import { focusPrev, focusNext } from '@/scripts/focus';
import contains from '@/scripts/contains'; import FormSwitch from '@/components/form/switch.vue';
import { MenuItem, InnerMenuItem, MenuPending, MenuAction } from '@/types/menu';
export default defineComponent({ const props = defineProps<{
props: { items: MenuItem[];
items: { viaKeyboard?: boolean;
type: Array, asDrawer?: boolean;
required: true align?: 'center' | string;
}, width?: number;
viaKeyboard: { maxHeight?: number;
type: Boolean, }>();
required: false
},
asDrawer: {
type: Boolean,
required: false
},
align: {
type: String,
requried: false
},
width: {
type: Number,
required: false
},
maxHeight: {
type: Number,
required: false
},
},
emits: ['close'],
data() {
return {
items2: [],
};
},
computed: {
keymap(): any {
return {
'up|k|shift+tab': this.focusUp,
'down|j|tab': this.focusDown,
'esc': this.close,
};
},
},
watch: {
items: {
handler() {
const items = ref(unref(this.items).filter(item => item !== undefined));
for (let i = 0; i < items.value.length; i++) { const emit = defineEmits<{
const item = items.value[i]; (e: 'close'): void;
}>();
if (item && item.then) { // if item is Promise
items.value[i] = { type: 'pending' };
item.then(actualItem => {
items.value[i] = actualItem;
});
}
}
this.items2 = items; let itemsEl = $ref<HTMLDivElement>();
},
immediate: true let items2: InnerMenuItem[] = $ref([]);
}
}, let keymap = $computed(() => ({
mounted() { 'up|k|shift+tab': focusUp,
if (this.viaKeyboard) { 'down|j|tab': focusDown,
this.$nextTick(() => { 'esc': close,
focusNext(this.$refs.items.children[0], true, false); }));
watch(() => props.items, () => {
const items: (MenuItem | MenuPending)[] = [...props.items].filter(item => item !== undefined);
for (let i = 0; i < items.length; i++) {
const item = items[i];
if (item && 'then' in item) { // if item is Promise
items[i] = { type: 'pending' };
item.then(actualItem => {
items2[i] = actualItem;
}); });
} }
}
if (this.contextmenuEvent) { items2 = items as InnerMenuItem[];
this.$el.style.top = this.contextmenuEvent.pageY + 'px'; }, {
this.$el.style.left = this.contextmenuEvent.pageX + 'px'; immediate: true,
});
for (const el of Array.from(document.querySelectorAll('body *'))) { onMounted(() => {
el.addEventListener('mousedown', this.onMousedown); if (props.viaKeyboard) {
} nextTick(() => {
} focusNext(itemsEl.children[0], true, false);
}, });
beforeUnmount() {
for (const el of Array.from(document.querySelectorAll('body *'))) {
el.removeEventListener('mousedown', this.onMousedown);
}
},
methods: {
clicked(fn, ev) {
fn(ev);
this.close();
},
close() {
this.$emit('close');
},
focusUp() {
focusPrev(document.activeElement);
},
focusDown() {
focusNext(document.activeElement);
},
onMousedown(e) {
if (!contains(this.$el, e.target) && (this.$el != e.target)) this.close();
},
} }
}); });
function clicked(fn: MenuAction, ev: MouseEvent) {
fn(ev);
close();
}
function close() {
emit('close');
}
function focusUp() {
focusPrev(document.activeElement);
}
function focusDown() {
focusNext(document.activeElement);
}
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@ -1,5 +1,5 @@
<template> <template>
<transition :name="$store.state.animation ? (type === 'drawer') ? 'modal-drawer' : (type === 'popup') ? 'modal-popup' : 'modal' : ''" :duration="$store.state.animation ? 200 : 0" appear @after-leave="$emit('closed')" @enter="$emit('opening')" @after-enter="childRendered"> <transition :name="$store.state.animation ? (type === 'drawer') ? 'modal-drawer' : (type === 'popup') ? 'modal-popup' : 'modal' : ''" :duration="$store.state.animation ? 200 : 0" appear @after-leave="emit('closed')" @enter="emit('opening')" @after-enter="childRendered">
<div v-show="manualShowing != null ? manualShowing : showing" v-hotkey.global="keymap" class="qzhlnise" :class="{ drawer: type === 'drawer', dialog: type === 'dialog' || type === 'dialog:top', popup: type === 'popup' }" :style="{ zIndex, pointerEvents: (manualShowing != null ? manualShowing : showing) ? 'auto' : 'none', '--transformOrigin': transformOrigin }"> <div v-show="manualShowing != null ? manualShowing : showing" v-hotkey.global="keymap" class="qzhlnise" :class="{ drawer: type === 'drawer', dialog: type === 'dialog' || type === 'dialog:top', popup: type === 'popup' }" :style="{ zIndex, pointerEvents: (manualShowing != null ? manualShowing : showing) ? 'auto' : 'none', '--transformOrigin': transformOrigin }">
<div class="bg _modalBg" :class="{ transparent: transparentBg && (type === 'popup') }" :style="{ zIndex }" @click="onBgClick" @contextmenu.prevent.stop="() => {}"></div> <div class="bg _modalBg" :class="{ transparent: transparentBg && (type === 'popup') }" :style="{ zIndex }" @click="onBgClick" @contextmenu.prevent.stop="() => {}"></div>
<div ref="content" class="content" :class="{ fixed, top: type === 'dialog:top' }" :style="{ zIndex }" @click.self="onBgClick"> <div ref="content" class="content" :class="{ fixed, top: type === 'dialog:top' }" :style="{ zIndex }" @click.self="onBgClick">
@ -9,8 +9,8 @@
</transition> </transition>
</template> </template>
<script lang="ts"> <script lang="ts" setup>
import { defineComponent, nextTick, onMounted, computed, PropType, ref, watch } from 'vue'; import { nextTick, onMounted, computed, ref, watch, provide } from 'vue';
import * as os from '@/os'; import * as os from '@/os';
import { isTouchUsing } from '@/scripts/touch'; import { isTouchUsing } from '@/scripts/touch';
import { defaultStore } from '@/store'; import { defaultStore } from '@/store';
@ -25,234 +25,206 @@ function getFixedContainer(el: Element | null): Element | null {
} }
} }
export default defineComponent({ type ModalTypes = 'popup' | 'dialog' | 'dialog:top' | 'drawer';
provide: {
modal: true
},
props: { const props = withDefaults(defineProps<{
manualShowing: { manualShowing?: boolean | null;
type: Boolean, srcCenter?: boolean;
required: false, src?: HTMLElement;
default: null, preferType?: ModalTypes | 'auto';
}, zPriority?: 'low' | 'middle' | 'high';
srcCenter: { noOverlap?: boolean;
type: Boolean, transparentBg?: boolean;
required: false }>(), {
}, manualShowing: null,
src: { src: null,
type: Object as PropType<HTMLElement>, preferType: 'auto',
required: false, zPriority: 'low',
default: null, noOverlap: true,
}, transparentBg: false,
preferType: { });
required: false,
type: String,
default: 'auto',
},
zPriority: {
type: String as PropType<'low' | 'middle' | 'high'>,
required: false,
default: 'low',
},
noOverlap: {
type: Boolean,
required: false,
default: true,
},
transparentBg: {
type: Boolean,
required: false,
default: false,
},
},
emits: ['opening', 'click', 'esc', 'close', 'closed'], const emit = defineEmits<{
(ev: 'opening'): void;
(ev: 'click'): void;
(ev: 'esc'): void;
(ev: 'close'): void;
(ev: 'closed'): void;
}>();
setup(props, context) { provide('modal', true);
const maxHeight = ref<number>();
const fixed = ref(false);
const transformOrigin = ref('center');
const showing = ref(true);
const content = ref<HTMLElement>();
const zIndex = os.claimZIndex(props.zPriority);
const type = computed(() => {
if (props.preferType === 'auto') {
if (!defaultStore.state.disableDrawer && isTouchUsing && window.innerWidth < 500 && window.innerHeight < 1000) {
return 'drawer';
} else {
return props.src != null ? 'popup' : 'dialog';
}
} else {
return props.preferType;
}
});
let contentClicking = false;
const close = () => { const maxHeight = ref<number>();
// eslint-disable-next-line vue/no-mutating-props const fixed = ref(false);
if (props.src) props.src.style.pointerEvents = 'auto'; const transformOrigin = ref('center');
showing.value = false; const showing = ref(true);
context.emit('close'); const content = ref<HTMLElement>();
}; const zIndex = os.claimZIndex(props.zPriority);
const type = computed(() => {
if (props.preferType === 'auto') {
if (!defaultStore.state.disableDrawer && isTouchUsing && window.innerWidth < 500 && window.innerHeight < 1000) {
return 'drawer';
} else {
return props.src != null ? 'popup' : 'dialog';
}
} else {
return props.preferType!;
}
});
const onBgClick = () => { let contentClicking = false;
if (contentClicking) return;
context.emit('click');
};
if (type.value === 'drawer') { const close = () => {
maxHeight.value = window.innerHeight / 2; // eslint-disable-next-line vue/no-mutating-props
if (props.src) props.src.style.pointerEvents = 'auto';
showing.value = false;
emit('close');
};
const onBgClick = () => {
if (contentClicking) return;
emit('click');
};
if (type.value === 'drawer') {
maxHeight.value = window.innerHeight / 2;
}
const keymap = {
'esc': () => emit('esc'),
};
const MARGIN = 16;
const align = () => {
if (props.src == null) return;
if (type.value === 'drawer') return;
const popover = content.value!;
if (popover == null) return;
const rect = props.src.getBoundingClientRect();
const width = popover.offsetWidth;
const height = popover.offsetHeight;
let left;
let top;
if (props.srcCenter) {
const x = rect.left + (fixed.value ? 0 : window.pageXOffset) + (props.src.offsetWidth / 2);
const y = rect.top + (fixed.value ? 0 : window.pageYOffset) + (props.src.offsetHeight / 2);
left = (x - (width / 2));
top = (y - (height / 2));
} else {
const x = rect.left + (fixed.value ? 0 : window.pageXOffset) + (props.src.offsetWidth / 2);
const y = rect.top + (fixed.value ? 0 : window.pageYOffset) + props.src.offsetHeight;
left = (x - (width / 2));
top = y;
}
if (fixed.value) {
//
if (left + width > window.innerWidth) {
left = window.innerWidth - width;
} }
const keymap = { //
'esc': () => context.emit('esc'), if (top + height > (window.innerHeight - MARGIN)) {
}; if (props.noOverlap) {
const underSpace = (window.innerHeight - MARGIN) - top;
const MARGIN = 16; const upperSpace = (rect.top - MARGIN);
if (underSpace >= (upperSpace / 3)) {
const align = () => { maxHeight.value = underSpace;
if (props.src == null) return; } else {
if (type.value === 'drawer') return; maxHeight.value = upperSpace;
top = (upperSpace + MARGIN) - height;
const popover = content.value!;
if (popover == null) return;
const rect = props.src.getBoundingClientRect();
const width = popover.offsetWidth;
const height = popover.offsetHeight;
let left;
let top;
if (props.srcCenter) {
const x = rect.left + (fixed.value ? 0 : window.pageXOffset) + (props.src.offsetWidth / 2);
const y = rect.top + (fixed.value ? 0 : window.pageYOffset) + (props.src.offsetHeight / 2);
left = (x - (width / 2));
top = (y - (height / 2));
} else {
const x = rect.left + (fixed.value ? 0 : window.pageXOffset) + (props.src.offsetWidth / 2);
const y = rect.top + (fixed.value ? 0 : window.pageYOffset) + props.src.offsetHeight;
left = (x - (width / 2));
top = y;
}
if (fixed.value) {
//
if (left + width > window.innerWidth) {
left = window.innerWidth - width;
}
//
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 {
// top = (window.innerHeight - MARGIN) - height;
if (left + width - window.pageXOffset > window.innerWidth) { }
left = window.innerWidth - width + window.pageXOffset - 1; }
} else {
//
if (left + width - window.pageXOffset > window.innerWidth) {
left = window.innerWidth - width + window.pageXOffset - 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);
} }
//
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) {
top = MARGIN;
}
if (left < 0) {
left = 0;
}
if (top > rect.top + (fixed.value ? 0 : window.pageYOffset)) {
transformOrigin.value = 'center top';
} else if ((top + height) <= rect.top + (fixed.value ? 0 : window.pageYOffset)) {
transformOrigin.value = 'center bottom';
} else { } else {
transformOrigin.value = 'center'; top = (window.innerHeight - MARGIN) - height + window.pageYOffset - 1;
} }
}
}
popover.style.left = left + 'px'; if (top < 0) {
popover.style.top = top + 'px'; top = MARGIN;
}; }
const childRendered = () => { if (left < 0) {
// left = 0;
const el = content.value!.children[0]; }
el.addEventListener('mousedown', e => {
contentClicking = true;
window.addEventListener('mouseup', e => {
// click mouseup
window.setTimeout(() => {
contentClicking = false;
}, 100);
}, { passive: true, once: true });
}, { passive: true });
};
onMounted(() => { if (top > rect.top + (fixed.value ? 0 : window.pageYOffset)) {
watch(() => props.src, async () => { transformOrigin.value = 'center top';
if (props.src) { } else if ((top + height) <= rect.top + (fixed.value ? 0 : window.pageYOffset)) {
// eslint-disable-next-line vue/no-mutating-props transformOrigin.value = 'center bottom';
props.src.style.pointerEvents = 'none'; } else {
} transformOrigin.value = 'center';
fixed.value = (type.value === 'drawer') || (getFixedContainer(props.src) != null); }
await nextTick() popover.style.left = left + 'px';
popover.style.top = top + 'px';
align(); };
}, { immediate: true, });
nextTick(() => { const childRendered = () => {
const popover = content.value; //
new ResizeObserver((entries, observer) => { const el = content.value!.children[0];
align(); el.addEventListener('mousedown', ev => {
}).observe(popover!); contentClicking = true;
}); window.addEventListener('mouseup', ev => {
}); // click mouseup
window.setTimeout(() => {
contentClicking = false;
}, 100);
}, { passive: true, once: true });
}, { passive: true });
};
return { onMounted(() => {
showing, watch(() => props.src, async () => {
type, if (props.src) {
fixed, // eslint-disable-next-line vue/no-mutating-props
content, props.src.style.pointerEvents = 'none';
transformOrigin, }
maxHeight, fixed.value = (type.value === 'drawer') || (getFixedContainer(props.src) != null);
close,
zIndex, await nextTick()
keymap,
onBgClick, align();
childRendered, }, { immediate: true, });
};
}, nextTick(() => {
const popover = content.value;
new ResizeObserver((entries, observer) => {
align();
}).observe(popover!);
});
});
defineExpose({
close,
}); });
</script> </script>

View File

@ -1,44 +1,28 @@
<template> <template>
<MkModal ref="modal" v-slot="{ type, maxHeight }" :z-priority="'high'" :src="src" :transparent-bg="true" @click="$refs.modal.close()" @closed="$emit('closed')"> <MkModal ref="modal" v-slot="{ type, maxHeight }" :z-priority="'high'" :src="src" :transparent-bg="true" @click="modal.close()" @closed="emit('closed')">
<MkMenu :items="items" :align="align" :width="width" :max-height="maxHeight" :as-drawer="type === 'drawer'" class="sfhdhdhq _popup _shadow" :class="{ drawer: type === 'drawer' }" @close="$refs.modal.close()"/> <MkMenu :items="items" :align="align" :width="width" :max-height="maxHeight" :as-drawer="type === 'drawer'" class="sfhdhdhq _popup _shadow" :class="{ drawer: type === 'drawer' }" @close="modal.close()"/>
</MkModal> </MkModal>
</template> </template>
<script lang="ts"> <script lang="ts" setup>
import { defineComponent } from 'vue'; import { } from 'vue';
import MkModal from './modal.vue'; import MkModal from './modal.vue';
import MkMenu from './menu.vue'; import MkMenu from './menu.vue';
import { MenuItem } from '@/types/menu';
export default defineComponent({ defineProps<{
components: { items: MenuItem[];
MkModal, align?: 'center' | string;
MkMenu, width?: number;
}, viaKeyboard?: boolean;
src?: any;
}>();
props: { const emit = defineEmits<{
items: { (e: 'closed'): void;
type: Array, }>();
required: true
},
align: {
type: String,
required: false
},
width: {
type: Number,
required: false
},
viaKeyboard: {
type: Boolean,
required: false
},
src: {
required: false
},
},
emits: ['close', 'closed'], let modal = $ref<InstanceType<typeof MkModal>>();
});
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@ -1,99 +1,96 @@
<template> <template>
<transition :name="$store.state.animation ? 'tooltip' : ''" appear @after-leave="$emit('closed')"> <transition :name="$store.state.animation ? 'tooltip' : ''" appear @after-leave="emit('closed')">
<div v-show="showing" ref="el" class="buebdbiu _acrylic _shadow" :style="{ zIndex, maxWidth: maxWidth + 'px' }"> <div v-show="showing" ref="el" class="buebdbiu _acrylic _shadow" :style="{ zIndex, maxWidth: maxWidth + 'px' }">
<slot>{{ text }}</slot> <slot>{{ text }}</slot>
</div> </div>
</transition> </transition>
</template> </template>
<script lang="ts"> <script lang="ts" setup>
import { defineComponent, nextTick, onMounted, onUnmounted, ref } from 'vue'; import { nextTick, onMounted, onUnmounted, ref } from 'vue';
import * as os from '@/os'; import * as os from '@/os';
export default defineComponent({ const props = withDefaults(defineProps<{
props: { showing: boolean;
showing: { targetElement?: HTMLElement;
type: Boolean, x?: number;
required: true, y?: number;
}, text?: string;
source: { maxWidth?: number;
required: true, }>(), {
}, maxWidth: 250,
text: { });
type: String,
required: false
},
maxWidth: {
type: Number,
required: false,
default: 250,
},
},
emits: ['closed'], const emit = defineEmits<{
(ev: 'closed'): void;
}>();
setup(props, context) { const el = ref<HTMLElement>();
const el = ref<HTMLElement>(); const zIndex = os.claimZIndex('high');
const zIndex = os.claimZIndex('high');
const setPosition = () => { const setPosition = () => {
if (el.value == null) return; if (el.value == null) return;
const rect = props.source.getBoundingClientRect(); const contentWidth = el.value.offsetWidth;
const contentHeight = el.value.offsetHeight;
const contentWidth = el.value.offsetWidth; let left: number;
const contentHeight = el.value.offsetHeight; let top: number;
let left = rect.left + window.pageXOffset + (props.source.offsetWidth / 2); let rect: DOMRect;
let top = rect.top + window.pageYOffset - contentHeight;
left -= (el.value.offsetWidth / 2); if (props.targetElement) {
rect = props.targetElement.getBoundingClientRect();
if (left + contentWidth - window.pageXOffset > window.innerWidth) { left = rect.left + window.pageXOffset + (props.targetElement.offsetWidth / 2);
left = window.innerWidth - contentWidth + window.pageXOffset - 1; top = rect.top + window.pageYOffset - contentHeight;
}
if (top - window.pageYOffset < 0) { el.value.style.transformOrigin = 'center bottom';
top = rect.top + window.pageYOffset + props.source.offsetHeight; } else {
el.value.style.transformOrigin = 'center top'; left = props.x;
} top = props.y - contentHeight;
}
el.value.style.left = left + 'px'; left -= (el.value.offsetWidth / 2);
el.value.style.top = top + 'px';
};
onMounted(() => { if (left + contentWidth - window.pageXOffset > window.innerWidth) {
nextTick(() => { left = window.innerWidth - contentWidth + window.pageXOffset - 1;
if (props.source == null) { }
context.emit('closed');
return;
}
//
if (top - window.pageYOffset < 0) {
if (props.targetElement) {
top = rect.top + window.pageYOffset + props.targetElement.offsetHeight;
el.value.style.transformOrigin = 'center top';
} else {
top = props.y;
}
}
el.value.style.left = left + 'px';
el.value.style.top = top + 'px';
};
onMounted(() => {
nextTick(() => {
setPosition();
let loopHandler;
const loop = () => {
loopHandler = window.requestAnimationFrame(() => {
setPosition(); setPosition();
let loopHandler;
const loop = () => {
loopHandler = window.requestAnimationFrame(() => {
setPosition();
loop();
});
};
loop(); loop();
onUnmounted(() => {
window.cancelAnimationFrame(loopHandler);
});
}); });
});
return {
el,
zIndex,
}; };
},
}) loop();
onUnmounted(() => {
window.cancelAnimationFrame(loopHandler);
});
});
});
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@ -118,6 +115,6 @@ export default defineComponent({
border-radius: 4px; border-radius: 4px;
border: solid 0.5px var(--divider); border: solid 0.5px var(--divider);
pointer-events: none; pointer-events: none;
transform-origin: center bottom; transform-origin: center center;
} }
</style> </style>

View File

@ -13,10 +13,10 @@ const props = defineProps<{
const text = $computed(() => { const text = $computed(() => {
switch (props.user.onlineStatus) { switch (props.user.onlineStatus) {
case 'online': return i18n.locale.online; case 'online': return i18n.ts.online;
case 'active': return i18n.locale.active; case 'active': return i18n.ts.active;
case 'offline': return i18n.locale.offline; case 'offline': return i18n.ts.offline;
case 'unknown': return i18n.locale.unknown; case 'unknown': return i18n.ts.unknown;
} }
}); });
</script> </script>

View File

@ -48,7 +48,7 @@ export default {
popup(import('@/components/ui/tooltip.vue'), { popup(import('@/components/ui/tooltip.vue'), {
showing, showing,
text: self.text, text: self.text,
source: el targetElement: el,
}, {}, 'closed'); }, {}, 'closed');
self._close = () => { self._close = () => {
@ -56,8 +56,8 @@ export default {
}; };
}; };
el.addEventListener('selectstart', e => { el.addEventListener('selectstart', ev => {
e.preventDefault(); ev.preventDefault();
}); });
el.addEventListener(start, () => { el.addEventListener(start, () => {

View File

@ -185,7 +185,7 @@ app.config.globalProperties = {
$store: defaultStore, $store: defaultStore,
$instance: instance, $instance: instance,
$t: i18n.t, $t: i18n.t,
$ts: i18n.locale, $ts: i18n.ts,
}; };
app.use(router); app.use(router);
@ -299,8 +299,8 @@ stream.on('_disconnected_', async () => {
reloadDialogShowing = true; reloadDialogShowing = true;
const { canceled } = await confirm({ const { canceled } = await confirm({
type: 'warning', type: 'warning',
title: i18n.locale.disconnectedFromServer, title: i18n.ts.disconnectedFromServer,
text: i18n.locale.reloadConfirm, text: i18n.ts.reloadConfirm,
}); });
reloadDialogShowing = false; reloadDialogShowing = false;
if (!canceled) { if (!canceled) {
@ -324,7 +324,7 @@ if ($i) {
if ($i.isDeleted) { if ($i.isDeleted) {
alert({ alert({
type: 'warning', type: 'warning',
text: i18n.locale.accountDeletionInProgress, text: i18n.ts.accountDeletionInProgress,
}); });
} }

View File

@ -73,12 +73,12 @@ export const menuDef = reactive({
})), null, { })), null, {
type: 'link', type: 'link',
to: '/my/lists', to: '/my/lists',
text: i18n.locale.manageLists, text: i18n.ts.manageLists,
icon: 'fas fa-cog', icon: 'fas fa-cog',
}]; }];
items.value = _items; items.value = _items;
}); });
os.popupMenu(items, ev.currentTarget || ev.target); os.popupMenu(items, ev.currentTarget ?? ev.target);
}, },
}, },
groups: { groups: {
@ -104,12 +104,12 @@ export const menuDef = reactive({
})), null, { })), null, {
type: 'link', type: 'link',
to: '/my/antennas', to: '/my/antennas',
text: i18n.locale.manageAntennas, text: i18n.ts.manageAntennas,
icon: 'fas fa-cog', icon: 'fas fa-cog',
}]; }];
items.value = _items; items.value = _items;
}); });
os.popupMenu(items, ev.currentTarget || ev.target); os.popupMenu(items, ev.currentTarget ?? ev.target);
}, },
}, },
mentions: { mentions: {
@ -173,34 +173,34 @@ export const menuDef = reactive({
icon: 'fas fa-columns', icon: 'fas fa-columns',
action: (ev) => { action: (ev) => {
os.popupMenu([{ os.popupMenu([{
text: i18n.locale.default, text: i18n.ts.default,
active: ui === 'default' || ui === null, active: ui === 'default' || ui === null,
action: () => { action: () => {
localStorage.setItem('ui', 'default'); localStorage.setItem('ui', 'default');
unisonReload(); unisonReload();
} }
}, { }, {
text: i18n.locale.deck, text: i18n.ts.deck,
active: ui === 'deck', active: ui === 'deck',
action: () => { action: () => {
localStorage.setItem('ui', 'deck'); localStorage.setItem('ui', 'deck');
unisonReload(); unisonReload();
} }
}, { }, {
text: i18n.locale.classic, text: i18n.ts.classic,
active: ui === 'classic', active: ui === 'classic',
action: () => { action: () => {
localStorage.setItem('ui', 'classic'); localStorage.setItem('ui', 'classic');
unisonReload(); unisonReload();
} }
}, /*{ }, /*{
text: i18n.locale.desktop + ' (β)', text: i18n.ts.desktop + ' (β)',
active: ui === 'desktop', active: ui === 'desktop',
action: () => { action: () => {
localStorage.setItem('ui', 'desktop'); localStorage.setItem('ui', 'desktop');
unisonReload(); unisonReload();
} }
}*/], ev.currentTarget || ev.target); }*/], ev.currentTarget ?? ev.target);
}, },
}, },
}); });

View File

@ -7,8 +7,10 @@ import * as Misskey from 'misskey-js';
import { apiUrl, url } from '@/config'; import { apiUrl, url } from '@/config';
import MkPostFormDialog from '@/components/post-form-dialog.vue'; import MkPostFormDialog from '@/components/post-form-dialog.vue';
import MkWaitingDialog from '@/components/waiting-dialog.vue'; import MkWaitingDialog from '@/components/waiting-dialog.vue';
import { MenuItem } from '@/types/menu';
import { resolve } from '@/router'; import { resolve } from '@/router';
import { $i } from '@/account'; import { $i } from '@/account';
import { defaultStore } from '@/store';
export const pendingApiRequestsCount = ref(0); export const pendingApiRequestsCount = ref(0);
@ -403,7 +405,7 @@ export async function selectDriveFolder(multiple: boolean) {
}); });
} }
export async function pickEmoji(src?: HTMLElement, opts) { export async function pickEmoji(src: HTMLElement | null, opts) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
popup(import('@/components/emoji-picker-dialog.vue'), { popup(import('@/components/emoji-picker-dialog.vue'), {
src, src,
@ -470,7 +472,7 @@ export async function openEmojiPicker(src?: HTMLElement, opts, initialTextarea:
}); });
} }
export function popupMenu(items: any[] | Ref<any[]>, src?: HTMLElement, options?: { export function popupMenu(items: MenuItem[] | Ref<MenuItem[]>, src?: HTMLElement, options?: {
align?: string; align?: string;
width?: number; width?: number;
viaKeyboard?: boolean; viaKeyboard?: boolean;
@ -494,7 +496,7 @@ export function popupMenu(items: any[] | Ref<any[]>, src?: HTMLElement, options?
}); });
} }
export function contextMenu(items: any[], ev: MouseEvent) { export function contextMenu(items: MenuItem[] | Ref<MenuItem[]>, ev: MouseEvent) {
ev.preventDefault(); ev.preventDefault();
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
let dispose; let dispose;
@ -541,7 +543,7 @@ export const uploads = ref<{
img: string; img: string;
}[]>([]); }[]>([]);
export function upload(file: File, folder?: any, name?: string): Promise<Misskey.entities.DriveFile> { export function upload(file: File, folder?: any, name?: string, keepOriginal: boolean = defaultStore.state.keepOriginalUploading): Promise<Misskey.entities.DriveFile> {
if (folder && typeof folder == 'object') folder = folder.id; if (folder && typeof folder == 'object') folder = folder.id;
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
@ -559,6 +561,8 @@ export function upload(file: File, folder?: any, name?: string): Promise<Misskey
uploads.value.push(ctx); uploads.value.push(ctx);
console.log(keepOriginal);
const data = new FormData(); const data = new FormData();
data.append('i', $i.token); data.append('i', $i.token);
data.append('force', 'true'); data.append('force', 'true');

View File

@ -3,15 +3,15 @@
<transition :name="$store.state.animation ? 'zoom' : ''" appear> <transition :name="$store.state.animation ? 'zoom' : ''" appear>
<div v-show="loaded" class="mjndxjch"> <div v-show="loaded" class="mjndxjch">
<img src="https://xn--931a.moe/assets/error.jpg" class="_ghost"/> <img src="https://xn--931a.moe/assets/error.jpg" class="_ghost"/>
<p><b><i class="fas fa-exclamation-triangle"></i> {{ i18n.locale.pageLoadError }}</b></p> <p><b><i class="fas fa-exclamation-triangle"></i> {{ i18n.ts.pageLoadError }}</b></p>
<p v-if="meta && (version === meta.version)">{{ i18n.locale.pageLoadErrorDescription }}</p> <p v-if="meta && (version === meta.version)">{{ i18n.ts.pageLoadErrorDescription }}</p>
<p v-else-if="serverIsDead">{{ i18n.locale.serverIsDead }}</p> <p v-else-if="serverIsDead">{{ i18n.ts.serverIsDead }}</p>
<template v-else> <template v-else>
<p>{{ i18n.locale.newVersionOfClientAvailable }}</p> <p>{{ i18n.ts.newVersionOfClientAvailable }}</p>
<p>{{ i18n.locale.youShouldUpgradeClient }}</p> <p>{{ i18n.ts.youShouldUpgradeClient }}</p>
<MkButton class="button primary" @click="reload">{{ i18n.locale.reload }}</MkButton> <MkButton class="button primary" @click="reload">{{ i18n.ts.reload }}</MkButton>
</template> </template>
<p><MkA to="/docs/general/troubleshooting" class="_link">{{ i18n.locale.troubleshooting }}</MkA></p> <p><MkA to="/docs/general/troubleshooting" class="_link">{{ i18n.ts.troubleshooting }}</MkA></p>
<p v-if="error" class="error">ERROR: {{ error }}</p> <p v-if="error" class="error">ERROR: {{ error }}</p>
</div> </div>
</transition> </transition>
@ -54,7 +54,7 @@ function reload() {
defineExpose({ defineExpose({
[symbols.PAGE_INFO]: { [symbols.PAGE_INFO]: {
title: i18n.locale.error, title: i18n.ts.error,
icon: 'fas fa-exclamation-triangle', icon: 'fas fa-exclamation-triangle',
}, },
}); });

View File

@ -10,7 +10,7 @@
<span v-for="emoji in easterEggEmojis" :key="emoji.id" class="emoji" :data-physics-x="emoji.left" :data-physics-y="emoji.top" :class="{ _physics_circle_: !emoji.emoji.startsWith(':') }"><MkEmoji class="emoji" :emoji="emoji.emoji" :custom-emojis="$instance.emojis" :is-reaction="false" :normal="true" :no-style="true"/></span> <span v-for="emoji in easterEggEmojis" :key="emoji.id" class="emoji" :data-physics-x="emoji.left" :data-physics-y="emoji.top" :class="{ _physics_circle_: !emoji.emoji.startsWith(':') }"><MkEmoji class="emoji" :emoji="emoji.emoji" :custom-emojis="$instance.emojis" :is-reaction="false" :normal="true" :no-style="true"/></span>
</div> </div>
<div class="_formBlock" style="text-align: center;"> <div class="_formBlock" style="text-align: center;">
{{ i18n.locale._aboutMisskey.about }}<br><a href="https://misskey-hub.net/docs/misskey.html" target="_blank" class="_link">{{ i18n.locale.learnMore }}</a> {{ i18n.ts._aboutMisskey.about }}<br><a href="https://misskey-hub.net/docs/misskey.html" target="_blank" class="_link">{{ i18n.ts.learnMore }}</a>
</div> </div>
<div class="_formBlock" style="text-align: center;"> <div class="_formBlock" style="text-align: center;">
<MkButton primary rounded inline @click="iLoveMisskey">I <Mfm text="$[jelly ❤]"/> #Misskey</MkButton> <MkButton primary rounded inline @click="iLoveMisskey">I <Mfm text="$[jelly ❤]"/> #Misskey</MkButton>
@ -19,23 +19,23 @@
<div class="_formLinks"> <div class="_formLinks">
<FormLink to="https://github.com/misskey-dev/misskey" external> <FormLink to="https://github.com/misskey-dev/misskey" external>
<template #icon><i class="fas fa-code"></i></template> <template #icon><i class="fas fa-code"></i></template>
{{ i18n.locale._aboutMisskey.source }} {{ i18n.ts._aboutMisskey.source }}
<template #suffix>GitHub</template> <template #suffix>GitHub</template>
</FormLink> </FormLink>
<FormLink to="https://crowdin.com/project/misskey" external> <FormLink to="https://crowdin.com/project/misskey" external>
<template #icon><i class="fas fa-language"></i></template> <template #icon><i class="fas fa-language"></i></template>
{{ i18n.locale._aboutMisskey.translation }} {{ i18n.ts._aboutMisskey.translation }}
<template #suffix>Crowdin</template> <template #suffix>Crowdin</template>
</FormLink> </FormLink>
<FormLink to="https://www.patreon.com/syuilo" external> <FormLink to="https://www.patreon.com/syuilo" external>
<template #icon><i class="fas fa-hand-holding-medical"></i></template> <template #icon><i class="fas fa-hand-holding-medical"></i></template>
{{ i18n.locale._aboutMisskey.donate }} {{ i18n.ts._aboutMisskey.donate }}
<template #suffix>Patreon</template> <template #suffix>Patreon</template>
</FormLink> </FormLink>
</div> </div>
</FormSection> </FormSection>
<FormSection> <FormSection>
<template #label>{{ i18n.locale._aboutMisskey.contributors }}</template> <template #label>{{ i18n.ts._aboutMisskey.contributors }}</template>
<div class="_formLinks"> <div class="_formLinks">
<FormLink to="https://github.com/syuilo" external>@syuilo</FormLink> <FormLink to="https://github.com/syuilo" external>@syuilo</FormLink>
<FormLink to="https://github.com/AyaMorisawa" external>@AyaMorisawa</FormLink> <FormLink to="https://github.com/AyaMorisawa" external>@AyaMorisawa</FormLink>
@ -47,12 +47,12 @@
<FormLink to="https://github.com/u1-liquid" external>@u1-liquid</FormLink> <FormLink to="https://github.com/u1-liquid" external>@u1-liquid</FormLink>
<FormLink to="https://github.com/marihachi" external>@marihachi</FormLink> <FormLink to="https://github.com/marihachi" external>@marihachi</FormLink>
</div> </div>
<template #caption><MkLink url="https://github.com/misskey-dev/misskey/graphs/contributors">{{ i18n.locale._aboutMisskey.allContributors }}</MkLink></template> <template #caption><MkLink url="https://github.com/misskey-dev/misskey/graphs/contributors">{{ i18n.ts._aboutMisskey.allContributors }}</MkLink></template>
</FormSection> </FormSection>
<FormSection> <FormSection>
<template #label><Mfm text="$[jelly ❤]"/> {{ i18n.locale._aboutMisskey.patrons }}</template> <template #label><Mfm text="$[jelly ❤]"/> {{ i18n.ts._aboutMisskey.patrons }}</template>
<div v-for="patron in patrons" :key="patron">{{ patron }}</div> <div v-for="patron in patrons" :key="patron">{{ patron }}</div>
<template #caption>{{ i18n.locale._aboutMisskey.morePatrons }}</template> <template #caption>{{ i18n.ts._aboutMisskey.morePatrons }}</template>
</FormSection> </FormSection>
</div> </div>
</MkSpacer> </MkSpacer>
@ -194,7 +194,7 @@ onBeforeUnmount(() => {
defineExpose({ defineExpose({
[symbols.PAGE_INFO]: { [symbols.PAGE_INFO]: {
title: i18n.locale.aboutMisskey, title: i18n.ts.aboutMisskey,
icon: null, icon: null,
bg: 'var(--bg)', bg: 'var(--bg)',
}, },

View File

@ -90,7 +90,7 @@ const initStats = () => os.api('stats', {
defineExpose({ defineExpose({
[symbols.PAGE_INFO]: { [symbols.PAGE_INFO]: {
title: i18n.locale.instanceInfo, title: i18n.ts.instanceInfo,
icon: 'fas fa-info-circle', icon: 'fas fa-info-circle',
bg: 'var(--bg)', bg: 'var(--bg)',
}, },

View File

@ -118,7 +118,7 @@ const toggleSelect = (emoji) => {
}; };
const add = async (ev: MouseEvent) => { const add = async (ev: MouseEvent) => {
const files = await selectFiles(ev.currentTarget || ev.target, null); const files = await selectFiles(ev.currentTarget ?? ev.target, null);
const promise = Promise.all(files.map(file => os.api('admin/emoji/add', { const promise = Promise.all(files.map(file => os.api('admin/emoji/add', {
fileId: file.id, fileId: file.id,
@ -157,23 +157,23 @@ const remoteMenu = (emoji, ev: MouseEvent) => {
type: 'label', type: 'label',
text: ':' + emoji.name + ':', text: ':' + emoji.name + ':',
}, { }, {
text: i18n.locale.import, text: i18n.ts.import,
icon: 'fas fa-plus', icon: 'fas fa-plus',
action: () => { im(emoji) } action: () => { im(emoji) }
}], ev.currentTarget || ev.target); }], ev.currentTarget ?? ev.target);
}; };
const menu = (ev: MouseEvent) => { const menu = (ev: MouseEvent) => {
os.popupMenu([{ os.popupMenu([{
icon: 'fas fa-download', icon: 'fas fa-download',
text: i18n.locale.export, text: i18n.ts.export,
action: async () => { action: async () => {
os.api('export-custom-emojis', { os.api('export-custom-emojis', {
}) })
.then(() => { .then(() => {
os.alert({ os.alert({
type: 'info', type: 'info',
text: i18n.locale.exportRequested, text: i18n.ts.exportRequested,
}); });
}).catch((e) => { }).catch((e) => {
os.alert({ os.alert({
@ -184,16 +184,16 @@ const menu = (ev: MouseEvent) => {
} }
}, { }, {
icon: 'fas fa-upload', icon: 'fas fa-upload',
text: i18n.locale.import, text: i18n.ts.import,
action: async () => { action: async () => {
const file = await selectFile(ev.currentTarget || ev.target); const file = await selectFile(ev.currentTarget ?? ev.target);
os.api('admin/emoji/import-zip', { os.api('admin/emoji/import-zip', {
fileId: file.id, fileId: file.id,
}) })
.then(() => { .then(() => {
os.alert({ os.alert({
type: 'info', type: 'info',
text: i18n.locale.importRequested, text: i18n.ts.importRequested,
}); });
}).catch((e) => { }).catch((e) => {
os.alert({ os.alert({
@ -202,7 +202,7 @@ const menu = (ev: MouseEvent) => {
}); });
}); });
} }
}], ev.currentTarget || ev.target); }], ev.currentTarget ?? ev.target);
}; };
const setCategoryBulk = async () => { const setCategoryBulk = async () => {
@ -256,7 +256,7 @@ const setTagBulk = async () => {
const delBulk = async () => { const delBulk = async () => {
const { canceled } = await os.confirm({ const { canceled } = await os.confirm({
type: 'warning', type: 'warning',
text: i18n.locale.deleteConfirm, text: i18n.ts.deleteConfirm,
}); });
if (canceled) return; if (canceled) return;
await os.apiWithDialog('admin/emoji/delete-bulk', { await os.apiWithDialog('admin/emoji/delete-bulk', {
@ -267,13 +267,13 @@ const delBulk = async () => {
defineExpose({ defineExpose({
[symbols.PAGE_INFO]: computed(() => ({ [symbols.PAGE_INFO]: computed(() => ({
title: i18n.locale.customEmojis, title: i18n.ts.customEmojis,
icon: 'fas fa-laugh', icon: 'fas fa-laugh',
bg: 'var(--bg)', bg: 'var(--bg)',
actions: [{ actions: [{
asFullButton: true, asFullButton: true,
icon: 'fas fa-plus', icon: 'fas fa-plus',
text: i18n.locale.addEmoji, text: i18n.ts.addEmoji,
handler: add, handler: add,
}, { }, {
icon: 'fas fa-ellipsis-h', icon: 'fas fa-ellipsis-h',
@ -281,11 +281,11 @@ defineExpose({
}], }],
tabs: [{ tabs: [{
active: tab.value === 'local', active: tab.value === 'local',
title: i18n.locale.local, title: i18n.ts.local,
onClick: () => { tab.value = 'local'; }, onClick: () => { tab.value = 'local'; },
}, { }, {
active: tab.value === 'remote', active: tab.value === 'remote',
title: i18n.locale.remote, title: i18n.ts.remote,
onClick: () => { tab.value = 'remote'; }, onClick: () => { tab.value = 'remote'; },
},] },]
})), })),

View File

@ -55,7 +55,7 @@ export default defineComponent({
setup(props, context) { setup(props, context) {
const indexInfo = { const indexInfo = {
title: i18n.locale.controlPanel, title: i18n.ts.controlPanel,
icon: 'fas fa-cog', icon: 'fas fa-cog',
bg: 'var(--bg)', bg: 'var(--bg)',
hideHeader: true, hideHeader: true,
@ -91,119 +91,119 @@ export default defineComponent({
}); });
const menuDef = computed(() => [{ const menuDef = computed(() => [{
title: i18n.locale.quickAction, title: i18n.ts.quickAction,
items: [{ items: [{
type: 'button', type: 'button',
icon: 'fas fa-search', icon: 'fas fa-search',
text: i18n.locale.lookup, text: i18n.ts.lookup,
action: lookup, action: lookup,
}, ...(instance.disableRegistration ? [{ }, ...(instance.disableRegistration ? [{
type: 'button', type: 'button',
icon: 'fas fa-user', icon: 'fas fa-user',
text: i18n.locale.invite, text: i18n.ts.invite,
action: invite, action: invite,
}] : [])], }] : [])],
}, { }, {
title: i18n.locale.administration, title: i18n.ts.administration,
items: [{ items: [{
icon: 'fas fa-tachometer-alt', icon: 'fas fa-tachometer-alt',
text: i18n.locale.dashboard, text: i18n.ts.dashboard,
to: '/admin/overview', to: '/admin/overview',
active: page.value === 'overview', active: page.value === 'overview',
}, { }, {
icon: 'fas fa-users', icon: 'fas fa-users',
text: i18n.locale.users, text: i18n.ts.users,
to: '/admin/users', to: '/admin/users',
active: page.value === 'users', active: page.value === 'users',
}, { }, {
icon: 'fas fa-laugh', icon: 'fas fa-laugh',
text: i18n.locale.customEmojis, text: i18n.ts.customEmojis,
to: '/admin/emojis', to: '/admin/emojis',
active: page.value === 'emojis', active: page.value === 'emojis',
}, { }, {
icon: 'fas fa-globe', icon: 'fas fa-globe',
text: i18n.locale.federation, text: i18n.ts.federation,
to: '/admin/federation', to: '/admin/federation',
active: page.value === 'federation', active: page.value === 'federation',
}, { }, {
icon: 'fas fa-clipboard-list', icon: 'fas fa-clipboard-list',
text: i18n.locale.jobQueue, text: i18n.ts.jobQueue,
to: '/admin/queue', to: '/admin/queue',
active: page.value === 'queue', active: page.value === 'queue',
}, { }, {
icon: 'fas fa-cloud', icon: 'fas fa-cloud',
text: i18n.locale.files, text: i18n.ts.files,
to: '/admin/files', to: '/admin/files',
active: page.value === 'files', active: page.value === 'files',
}, { }, {
icon: 'fas fa-broadcast-tower', icon: 'fas fa-broadcast-tower',
text: i18n.locale.announcements, text: i18n.ts.announcements,
to: '/admin/announcements', to: '/admin/announcements',
active: page.value === 'announcements', active: page.value === 'announcements',
}, { }, {
icon: 'fas fa-audio-description', icon: 'fas fa-audio-description',
text: i18n.locale.ads, text: i18n.ts.ads,
to: '/admin/ads', to: '/admin/ads',
active: page.value === 'ads', active: page.value === 'ads',
}, { }, {
icon: 'fas fa-exclamation-circle', icon: 'fas fa-exclamation-circle',
text: i18n.locale.abuseReports, text: i18n.ts.abuseReports,
to: '/admin/abuses', to: '/admin/abuses',
active: page.value === 'abuses', active: page.value === 'abuses',
}], }],
}, { }, {
title: i18n.locale.settings, title: i18n.ts.settings,
items: [{ items: [{
icon: 'fas fa-cog', icon: 'fas fa-cog',
text: i18n.locale.general, text: i18n.ts.general,
to: '/admin/settings', to: '/admin/settings',
active: page.value === 'settings', active: page.value === 'settings',
}, { }, {
icon: 'fas fa-envelope', icon: 'fas fa-envelope',
text: i18n.locale.emailServer, text: i18n.ts.emailServer,
to: '/admin/email-settings', to: '/admin/email-settings',
active: page.value === 'email-settings', active: page.value === 'email-settings',
}, { }, {
icon: 'fas fa-cloud', icon: 'fas fa-cloud',
text: i18n.locale.objectStorage, text: i18n.ts.objectStorage,
to: '/admin/object-storage', to: '/admin/object-storage',
active: page.value === 'object-storage', active: page.value === 'object-storage',
}, { }, {
icon: 'fas fa-lock', icon: 'fas fa-lock',
text: i18n.locale.security, text: i18n.ts.security,
to: '/admin/security', to: '/admin/security',
active: page.value === 'security', active: page.value === 'security',
}, { }, {
icon: 'fas fa-globe', icon: 'fas fa-globe',
text: i18n.locale.relays, text: i18n.ts.relays,
to: '/admin/relays', to: '/admin/relays',
active: page.value === 'relays', active: page.value === 'relays',
}, { }, {
icon: 'fas fa-share-alt', icon: 'fas fa-share-alt',
text: i18n.locale.integration, text: i18n.ts.integration,
to: '/admin/integrations', to: '/admin/integrations',
active: page.value === 'integrations', active: page.value === 'integrations',
}, { }, {
icon: 'fas fa-ban', icon: 'fas fa-ban',
text: i18n.locale.instanceBlocking, text: i18n.ts.instanceBlocking,
to: '/admin/instance-block', to: '/admin/instance-block',
active: page.value === 'instance-block', active: page.value === 'instance-block',
}, { }, {
icon: 'fas fa-ghost', icon: 'fas fa-ghost',
text: i18n.locale.proxyAccount, text: i18n.ts.proxyAccount,
to: '/admin/proxy-account', to: '/admin/proxy-account',
active: page.value === 'proxy-account', active: page.value === 'proxy-account',
}, { }, {
icon: 'fas fa-cogs', icon: 'fas fa-cogs',
text: i18n.locale.other, text: i18n.ts.other,
to: '/admin/other-settings', to: '/admin/other-settings',
active: page.value === 'other-settings', active: page.value === 'other-settings',
}], }],
}, { }, {
title: i18n.locale.info, title: i18n.ts.info,
items: [{ items: [{
icon: 'fas fa-database', icon: 'fas fa-database',
text: i18n.locale.database, text: i18n.ts.database,
to: '/admin/database', to: '/admin/database',
active: page.value === 'database', active: page.value === 'database',
}], }],
@ -275,37 +275,37 @@ export default defineComponent({
const lookup = (ev) => { const lookup = (ev) => {
os.popupMenu([{ os.popupMenu([{
text: i18n.locale.user, text: i18n.ts.user,
icon: 'fas fa-user', icon: 'fas fa-user',
action: () => { action: () => {
lookupUser(); lookupUser();
} }
}, { }, {
text: i18n.locale.note, text: i18n.ts.note,
icon: 'fas fa-pencil-alt', icon: 'fas fa-pencil-alt',
action: () => { action: () => {
alert('TODO'); alert('TODO');
} }
}, { }, {
text: i18n.locale.file, text: i18n.ts.file,
icon: 'fas fa-cloud', icon: 'fas fa-cloud',
action: () => { action: () => {
alert('TODO'); alert('TODO');
} }
}, { }, {
text: i18n.locale.instance, text: i18n.ts.instance,
icon: 'fas fa-globe', icon: 'fas fa-globe',
action: () => { action: () => {
alert('TODO'); alert('TODO');
} }
}], ev.currentTarget || ev.target); }], ev.currentTarget ?? ev.target);
}; };
return { return {
[symbols.PAGE_INFO]: INFO, [symbols.PAGE_INFO]: INFO,
menuDef, menuDef,
header: { header: {
title: i18n.locale.controlPanel, title: i18n.ts.controlPanel,
}, },
noMaintainerInformation, noMaintainerInformation,
noBotProtection, noBotProtection,

View File

@ -112,7 +112,7 @@ export default defineComponent({
}, },
setBannerImage(e) { setBannerImage(e) {
selectFile(e.currentTarget || e.target, null).then(file => { selectFile(e.currentTarget ?? e.target, null).then(file => {
this.bannerId = file.id; this.bannerId = file.id;
}); });
}, },

View File

@ -127,7 +127,7 @@ export default defineComponent({
clipId: this.clip.id, clipId: this.clip.id,
}); });
} }
} : undefined], ev.currentTarget || ev.target); } : undefined], ev.currentTarget ?? ev.target);
} }
} }
}); });

View File

@ -15,7 +15,7 @@ let folder = $ref(null);
defineExpose({ defineExpose({
[symbols.PAGE_INFO]: computed(() => ({ [symbols.PAGE_INFO]: computed(() => ({
title: folder ? folder.name : i18n.locale.drive, title: folder ? folder.name : i18n.ts.drive,
icon: 'fas fa-cloud', icon: 'fas fa-cloud',
bg: 'var(--bg)', bg: 'var(--bg)',
hideHeader: true, hideHeader: true,

View File

@ -23,13 +23,13 @@ function menu(ev) {
type: 'label', type: 'label',
text: ':' + props.emoji.name + ':', text: ':' + props.emoji.name + ':',
}, { }, {
text: i18n.locale.copy, text: i18n.ts.copy,
icon: 'fas fa-copy', icon: 'fas fa-copy',
action: () => { action: () => {
copyToClipboard(`:${props.emoji.name}:`); copyToClipboard(`:${props.emoji.name}:`);
os.success(); os.success();
} }
}], ev.currentTarget || ev.target); }], ev.currentTarget ?? ev.target);
} }
</script> </script>

View File

@ -16,14 +16,14 @@ const tab = ref('category');
function menu(ev) { function menu(ev) {
os.popupMenu([{ os.popupMenu([{
icon: 'fas fa-download', icon: 'fas fa-download',
text: i18n.locale.export, text: i18n.ts.export,
action: async () => { action: async () => {
os.api('export-custom-emojis', { os.api('export-custom-emojis', {
}) })
.then(() => { .then(() => {
os.alert({ os.alert({
type: 'info', type: 'info',
text: i18n.locale.exportRequested, text: i18n.ts.exportRequested,
}); });
}).catch((e) => { }).catch((e) => {
os.alert({ os.alert({
@ -32,12 +32,12 @@ function menu(ev) {
}); });
}); });
} }
}], ev.currentTarget || ev.target); }], ev.currentTarget ?? ev.target);
} }
defineExpose({ defineExpose({
[symbols.PAGE_INFO]: { [symbols.PAGE_INFO]: {
title: i18n.locale.customEmojis, title: i18n.ts.customEmojis,
icon: 'fas fa-laugh', icon: 'fas fa-laugh',
bg: 'var(--bg)', bg: 'var(--bg)',
actions: [{ actions: [{

View File

@ -34,7 +34,7 @@ const pagingComponent = ref<InstanceType<typeof MkPagination>>();
defineExpose({ defineExpose({
[symbols.PAGE_INFO]: { [symbols.PAGE_INFO]: {
title: i18n.locale.favorites, title: i18n.ts.favorites,
icon: 'fas fa-star', icon: 'fas fa-star',
bg: 'var(--bg)', bg: 'var(--bg)',
}, },

View File

@ -17,7 +17,7 @@ const pagination = {
defineExpose({ defineExpose({
[symbols.PAGE_INFO]: { [symbols.PAGE_INFO]: {
title: i18n.locale.featured, title: i18n.ts.featured,
icon: 'fas fa-fire-alt', icon: 'fas fa-fire-alt',
bg: 'var(--bg)', bg: 'var(--bg)',
}, },

View File

@ -115,7 +115,7 @@ const pagination = {
offsetMode: true, offsetMode: true,
params: computed(() => ({ params: computed(() => ({
sort: sort, sort: sort,
host: host != '' ? host : null, host: host !== '' ? host : null,
...( ...(
state === 'federating' ? { federating: true } : state === 'federating' ? { federating: true } :
state === 'subscribing' ? { subscribing: true } : state === 'subscribing' ? { subscribing: true } :
@ -135,7 +135,7 @@ function getStatus(instance) {
defineExpose({ defineExpose({
[symbols.PAGE_INFO]: { [symbols.PAGE_INFO]: {
title: i18n.locale.federation, title: i18n.ts.federation,
icon: 'fas fa-globe', icon: 'fas fa-globe',
bg: 'var(--bg)', bg: 'var(--bg)',
}, },
@ -157,11 +157,10 @@ defineExpose({
> .instance { > .instance {
padding: 16px; padding: 16px;
border: solid 1px var(--divider); background: var(--panel);
border-radius: 6px; border-radius: 8px;
&:hover { &:hover {
border: solid 1px var(--accent);
text-decoration: none; text-decoration: none;
} }

View File

@ -60,7 +60,7 @@ function reject(user) {
defineExpose({ defineExpose({
[symbols.PAGE_INFO]: computed(() => ({ [symbols.PAGE_INFO]: computed(() => ({
title: i18n.locale.followRequests, title: i18n.ts.followRequests,
icon: 'fas fa-user-clock', icon: 'fas fa-user-clock',
bg: 'var(--bg)', bg: 'var(--bg)',
})), })),

View File

@ -92,7 +92,7 @@ export default defineComponent({
methods: { methods: {
selectFile(e) { selectFile(e) {
selectFiles(e.currentTarget || e.target, null).then(files => { selectFiles(e.currentTarget ?? e.target, null).then(files => {
this.files = this.files.concat(files); this.files = this.files.concat(files);
}); });
}, },

View File

@ -29,6 +29,7 @@
<template #label>Moderation</template> <template #label>Moderation</template>
<FormSwitch v-model="suspended" class="_formBlock" @update:modelValue="toggleSuspend">{{ $ts.stopActivityDelivery }}</FormSwitch> <FormSwitch v-model="suspended" class="_formBlock" @update:modelValue="toggleSuspend">{{ $ts.stopActivityDelivery }}</FormSwitch>
<FormSwitch v-model="isBlocked" class="_formBlock" @update:modelValue="toggleBlock">{{ $ts.blockThisInstance }}</FormSwitch> <FormSwitch v-model="isBlocked" class="_formBlock" @update:modelValue="toggleBlock">{{ $ts.blockThisInstance }}</FormSwitch>
<MkButton @click="refreshMetadata">Refresh metadata</MkButton>
</FormSection> </FormSection>
<FormSection> <FormSection>
@ -111,6 +112,7 @@ import MkChart from '@/components/chart.vue';
import MkObjectView from '@/components/object-view.vue'; import MkObjectView from '@/components/object-view.vue';
import FormLink from '@/components/form/link.vue'; import FormLink from '@/components/form/link.vue';
import MkLink from '@/components/link.vue'; import MkLink from '@/components/link.vue';
import MkButton from '@/components/ui/button.vue';
import FormSection from '@/components/form/section.vue'; import FormSection from '@/components/form/section.vue';
import MkKeyValue from '@/components/key-value.vue'; import MkKeyValue from '@/components/key-value.vue';
import MkSelect from '@/components/form/select.vue'; import MkSelect from '@/components/form/select.vue';
@ -155,6 +157,15 @@ async function toggleSuspend(v) {
}); });
} }
function refreshMetadata() {
os.api('admin/federation/refresh-remote-instance-metadata', {
host: instance.host,
});
os.alert({
text: 'Refresh requested',
});
}
fetch(); fetch();
defineExpose({ defineExpose({

View File

@ -16,7 +16,7 @@ const pagination = {
defineExpose({ defineExpose({
[symbols.PAGE_INFO]: { [symbols.PAGE_INFO]: {
title: i18n.locale.mentions, title: i18n.ts.mentions,
icon: 'fas fa-at', icon: 'fas fa-at',
bg: 'var(--bg)', bg: 'var(--bg)',
}, },

View File

@ -12,14 +12,14 @@ import { i18n } from '@/i18n';
const pagination = { const pagination = {
endpoint: 'notes/mentions' as const, endpoint: 'notes/mentions' as const,
limit: 10, limit: 10,
params: () => ({ params: {
visibility: 'specified' visibility: 'specified'
}), },
}; };
defineExpose({ defineExpose({
[symbols.PAGE_INFO]: { [symbols.PAGE_INFO]: {
title: i18n.locale.directNotes, title: i18n.ts.directNotes,
icon: 'fas fa-envelope', icon: 'fas fa-envelope',
bg: 'var(--bg)', bg: 'var(--bg)',
}, },

View File

@ -128,7 +128,7 @@ export default defineComponent({
text: this.$ts.messagingWithGroup, text: this.$ts.messagingWithGroup,
icon: 'fas fa-users', icon: 'fas fa-users',
action: () => { this.startGroup() } action: () => { this.startGroup() }
}], ev.currentTarget || ev.target); }], ev.currentTarget ?? ev.target);
}, },
async startUser() { async startUser() {

View File

@ -154,7 +154,7 @@ export default defineComponent({
}, },
chooseFile(e) { chooseFile(e) {
selectFile(e.currentTarget || e.target, this.$ts.selectFile).then(file => { selectFile(e.currentTarget ?? e.target, this.$ts.selectFile).then(file => {
this.file = file; this.file = file;
}); });
}, },
@ -214,7 +214,7 @@ export default defineComponent({
}, },
async insertEmoji(ev) { async insertEmoji(ev) {
os.openEmojiPicker(ev.currentTarget || ev.target, {}, this.$refs.text); os.openEmojiPicker(ev.currentTarget ?? ev.target, {}, this.$refs.text);
} }
} }
}); });

View File

@ -335,7 +335,7 @@ const Component = defineComponent({
popout(path); popout(path);
this.$router.back(); this.$router.back();
}, },
}], ev.currentTarget || ev.target); }], ev.currentTarget ?? ev.target);
} }
} }
}); });

View File

@ -31,7 +31,7 @@ function onAntennaCreated() {
defineExpose({ defineExpose({
[symbols.PAGE_INFO]: { [symbols.PAGE_INFO]: {
title: i18n.locale.manageAntennas, title: i18n.ts.manageAntennas,
icon: 'fas fa-satellite', icon: 'fas fa-satellite',
bg: 'var(--bg)', bg: 'var(--bg)',
}, },

View File

@ -19,7 +19,7 @@ import MkPagination from '@/components/ui/pagination.vue';
import MkButton from '@/components/ui/button.vue'; import MkButton from '@/components/ui/button.vue';
import * as os from '@/os'; import * as os from '@/os';
import * as symbols from '@/symbols'; import * as symbols from '@/symbols';
import i18n from '@/components/global/i18n'; import { i18n } from '@/i18n';
const pagination = { const pagination = {
endpoint: 'clips/list' as const, endpoint: 'clips/list' as const,
@ -29,20 +29,20 @@ const pagination = {
const pagingComponent = $ref<InstanceType<typeof MkPagination>>(); const pagingComponent = $ref<InstanceType<typeof MkPagination>>();
async function create() { async function create() {
const { canceled, result } = await os.form(i18n.locale.createNewClip, { const { canceled, result } = await os.form(i18n.ts.createNewClip, {
name: { name: {
type: 'string', type: 'string',
label: i18n.locale.name, label: i18n.ts.name,
}, },
description: { description: {
type: 'string', type: 'string',
required: false, required: false,
multiline: true, multiline: true,
label: i18n.locale.description, label: i18n.ts.description,
}, },
isPublic: { isPublic: {
type: 'boolean', type: 'boolean',
label: i18n.locale.public, label: i18n.ts.public,
default: false, default: false,
}, },
}); });
@ -63,7 +63,7 @@ function onClipDeleted() {
defineExpose({ defineExpose({
[symbols.PAGE_INFO]: { [symbols.PAGE_INFO]: {
title: i18n.locale.clip, title: i18n.ts.clip,
icon: 'fas fa-paperclip', icon: 'fas fa-paperclip',
bg: 'var(--bg)', bg: 'var(--bg)',
action: { action: {

View File

@ -31,7 +31,7 @@ const pagination = {
async function create() { async function create() {
const { canceled, result: name } = await os.inputText({ const { canceled, result: name } = await os.inputText({
title: i18n.locale.enterListName, title: i18n.ts.enterListName,
}); });
if (canceled) return; if (canceled) return;
await os.apiWithDialog('users/lists/create', { name: name }); await os.apiWithDialog('users/lists/create', { name: name });
@ -40,7 +40,7 @@ async function create() {
defineExpose({ defineExpose({
[symbols.PAGE_INFO]: { [symbols.PAGE_INFO]: {
title: i18n.locale.manageLists, title: i18n.ts.manageLists,
icon: 'fas fa-list-ul', icon: 'fas fa-list-ul',
bg: 'var(--bg)', bg: 'var(--bg)',
action: { action: {

View File

@ -13,7 +13,7 @@ import { i18n } from '@/i18n';
defineExpose({ defineExpose({
[symbols.PAGE_INFO]: { [symbols.PAGE_INFO]: {
title: i18n.locale.notFound, title: i18n.ts.notFound,
icon: 'fas fa-exclamation-triangle', icon: 'fas fa-exclamation-triangle',
bg: 'var(--bg)', bg: 'var(--bg)',
}, },

View File

@ -27,26 +27,26 @@ function setFilter(ev) {
})); }));
const items = includeTypes != null ? [{ const items = includeTypes != null ? [{
icon: 'fas fa-times', icon: 'fas fa-times',
text: i18n.locale.clear, text: i18n.ts.clear,
action: () => { action: () => {
includeTypes = null; includeTypes = null;
} }
}, null, ...typeItems] : typeItems; }, null, ...typeItems] : typeItems;
os.popupMenu(items, ev.currentTarget || ev.target); os.popupMenu(items, ev.currentTarget ?? ev.target);
} }
defineExpose({ defineExpose({
[symbols.PAGE_INFO]: computed(() => ({ [symbols.PAGE_INFO]: computed(() => ({
title: i18n.locale.notifications, title: i18n.ts.notifications,
icon: 'fas fa-bell', icon: 'fas fa-bell',
bg: 'var(--bg)', bg: 'var(--bg)',
actions: [{ actions: [{
text: i18n.locale.filter, text: i18n.ts.filter,
icon: 'fas fa-filter', icon: 'fas fa-filter',
highlighted: includeTypes != null, highlighted: includeTypes != null,
handler: setFilter, handler: setFilter,
}, { }, {
text: i18n.locale.markAllAsRead, text: i18n.ts.markAllAsRead,
icon: 'fas fa-check', icon: 'fas fa-check',
handler: () => { handler: () => {
os.apiWithDialog('notifications/mark-all-as-read'); os.apiWithDialog('notifications/mark-all-as-read');
@ -54,11 +54,11 @@ defineExpose({
}], }],
tabs: [{ tabs: [{
active: tab === 'all', active: tab === 'all',
title: i18n.locale.all, title: i18n.ts.all,
onClick: () => { tab = 'all'; }, onClick: () => { tab = 'all'; },
}, { }, {
active: tab === 'unread', active: tab === 'unread',
title: i18n.locale.unread, title: i18n.ts.unread,
onClick: () => { tab = 'unread'; }, onClick: () => { tab = 'unread'; },
},] },]
})), })),

View File

@ -448,7 +448,7 @@ export default defineComponent({
}, },
setEyeCatchingImage(e) { setEyeCatchingImage(e) {
selectFile(e.currentTarget || e.target, null).then(file => { selectFile(e.currentTarget ?? e.target, null).then(file => {
this.eyeCatchingImageId = file.id; this.eyeCatchingImageId = file.id;
}); });
}, },

View File

@ -12,7 +12,7 @@ import { i18n } from '@/i18n';
defineExpose({ defineExpose({
[symbols.PAGE_INFO]: computed(() => ({ [symbols.PAGE_INFO]: computed(() => ({
title: i18n.locale.preview, title: i18n.ts.preview,
icon: 'fas fa-eye', icon: 'fas fa-eye',
bg: 'var(--bg)', bg: 'var(--bg)',
})), })),

View File

@ -3,10 +3,10 @@
<div class="_formRoot"> <div class="_formRoot">
<FormInput v-model="password" type="password" class="_formBlock"> <FormInput v-model="password" type="password" class="_formBlock">
<template #prefix><i class="fas fa-lock"></i></template> <template #prefix><i class="fas fa-lock"></i></template>
<template #label>{{ i18n.locale.newPassword }}</template> <template #label>{{ i18n.ts.newPassword }}</template>
</FormInput> </FormInput>
<FormButton primary class="_formBlock" @click="save">{{ i18n.locale.save }}</FormButton> <FormButton primary class="_formBlock" @click="save">{{ i18n.ts.save }}</FormButton>
</div> </div>
</MkSpacer> </MkSpacer>
</template> </template>
@ -43,7 +43,7 @@ onMounted(() => {
defineExpose({ defineExpose({
[symbols.PAGE_INFO]: { [symbols.PAGE_INFO]: {
title: i18n.locale.resetPassword, title: i18n.ts.resetPassword,
icon: 'fas fa-lock', icon: 'fas fa-lock',
bg: 'var(--bg)', bg: 'var(--bg)',
}, },

View File

@ -64,7 +64,7 @@ export default defineComponent({
icon: 'fas fa-trash-alt', icon: 'fas fa-trash-alt',
danger: true, danger: true,
action: () => this.removeAccount(account), action: () => this.removeAccount(account),
}], ev.currentTarget || ev.target); }], ev.currentTarget ?? ev.target);
}, },
addAccount(ev) { addAccount(ev) {
@ -74,7 +74,7 @@ export default defineComponent({
}, { }, {
text: this.$ts.createAccount, text: this.$ts.createAccount,
action: () => { this.createAccount(); }, action: () => { this.createAccount(); },
}], ev.currentTarget || ev.target); }], ev.currentTarget ?? ev.target);
}, },
addExistingAccount() { addExistingAccount() {

View File

@ -28,6 +28,7 @@
<template #suffix>{{ uploadFolder ? uploadFolder.name : '-' }}</template> <template #suffix>{{ uploadFolder ? uploadFolder.name : '-' }}</template>
<template #suffixIcon><i class="fas fa-folder-open"></i></template> <template #suffixIcon><i class="fas fa-folder-open"></i></template>
</FormLink> </FormLink>
<FormSwitch v-model="keepOriginalUploading" class="_formBlock">{{ $ts.keepOriginalUploading }}<template #caption>{{ $ts.keepOriginalUploadingDescription }}</template></FormSwitch>
</FormSection> </FormSection>
</div> </div>
</template> </template>
@ -36,18 +37,21 @@
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
import * as tinycolor from 'tinycolor2'; import * as tinycolor from 'tinycolor2';
import FormLink from '@/components/form/link.vue'; import FormLink from '@/components/form/link.vue';
import FormSwitch from '@/components/form/switch.vue';
import FormSection from '@/components/form/section.vue'; import FormSection from '@/components/form/section.vue';
import MkKeyValue from '@/components/key-value.vue'; import MkKeyValue from '@/components/key-value.vue';
import FormSplit from '@/components/form/split.vue'; import FormSplit from '@/components/form/split.vue';
import * as os from '@/os'; import * as os from '@/os';
import bytes from '@/filters/bytes'; import bytes from '@/filters/bytes';
import * as symbols from '@/symbols'; import * as symbols from '@/symbols';
import { defaultStore } from '@/store';
// TODO: render chart // TODO: render chart
export default defineComponent({ export default defineComponent({
components: { components: {
FormLink, FormLink,
FormSwitch,
FormSection, FormSection,
MkKeyValue, MkKeyValue,
FormSplit, FormSplit,
@ -79,7 +83,8 @@ export default defineComponent({
l: 0.5 l: 0.5
}) })
}; };
} },
keepOriginalUploading: defaultStore.makeGetterSetter('keepOriginalUploading'),
}, },
async created() { async created() {

View File

@ -62,7 +62,7 @@ export default defineComponent({
const emailAddress = ref($i.email); const emailAddress = ref($i.email);
const INFO = { const INFO = {
title: i18n.locale.email, title: i18n.ts.email,
icon: 'fas fa-envelope', icon: 'fas fa-envelope',
bg: 'var(--bg)', bg: 'var(--bg)',
}; };
@ -75,7 +75,7 @@ export default defineComponent({
const saveEmailAddress = () => { const saveEmailAddress = () => {
os.inputText({ os.inputText({
title: i18n.locale.password, title: i18n.ts.password,
type: 'password' type: 'password'
}).then(({ canceled, result: password }) => { }).then(({ canceled, result: password }) => {
if (canceled) return; if (canceled) return;

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