diff --git a/CHANGELOG.md b/CHANGELOG.md index cabb4ad46e..b7ef640fee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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) ### Bugfixes - チャットが表示できない問題を修正 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 27f5598a66..662fa709b5 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -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.** 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. ## Issues diff --git a/cypress/integration/basic.js b/cypress/integration/basic.js index aca44ef15d..7d27b649f4 100644 --- a/cypress/integration/basic.js +++ b/cypress/integration/basic.js @@ -176,3 +176,7 @@ describe('After user singed in', () => { cy.contains('Hello, Misskey!'); }); }); + +// TODO: 投稿フォームの公開範囲指定のテスト +// TODO: 投稿フォームのファイル添付のテスト +// TODO: 投稿フォームのハッシュタグ保持フィールドのテスト diff --git a/locales/bn-BD.yml b/locales/bn-BD.yml index ed97d539c0..02f18cd1e9 100644 --- a/locales/bn-BD.yml +++ b/locales/bn-BD.yml @@ -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: "উল্লেখসমূহ" diff --git a/locales/de-DE.yml b/locales/de-DE.yml index 2f327a905c..05360e1703 100644 --- a/locales/de-DE.yml +++ b/locales/de-DE.yml @@ -235,6 +235,8 @@ resetAreYouSure: "Wirklich zurücksetzen?" saved: "Gespeichert" messaging: "Chat" 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" fromUrl: "Von einer URL" uploadFromUrl: "Von einer URL hochladen" diff --git a/locales/en-US.yml b/locales/en-US.yml index 6bbe848210..9a2b0bf5c5 100644 --- a/locales/en-US.yml +++ b/locales/en-US.yml @@ -235,6 +235,8 @@ resetAreYouSure: "Really reset?" saved: "Saved" messaging: "Chat" 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" fromUrl: "From URL" uploadFromUrl: "Upload from a URL" diff --git a/locales/fr-FR.yml b/locales/fr-FR.yml index 58dd000ccc..62f85bef8d 100644 --- a/locales/fr-FR.yml +++ b/locales/fr-FR.yml @@ -235,6 +235,7 @@ resetAreYouSure: "Voulez-vous réinitialiser ?" saved: "Enregistré" messaging: "Discuter" upload: "Téléverser" +keepOriginalUploading: "Garder l’image d’origine" fromDrive: "Depuis le Drive" fromUrl: "Depuis une URL" uploadFromUrl: "Téléverser via une URL" @@ -743,6 +744,7 @@ notRecommended: "Déconseillé" botProtection: "Protection contre les bots" instanceBlocking: "Instances bloquées" selectAccount: "Sélectionner un compte" +switchAccount: "Changer de compte" enabled: "Activé" disabled: "Désactivé" 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." classic: "Classique" muteThread: "Mettre ce thread en sourdine" +unmuteThread: "Ne plus masquer le fil" ffVisibility: "Visibilité des abonnés/abonnements" ffVisibilityDescription: "Permet de configurer qui peut voir les personnes que tu suis et les personnes qui te suivent." continueThread: "Afficher la suite du fil" @@ -1241,6 +1244,7 @@ _exportOrImport: muteList: "Comptes masqués" blockingList: "Comptes bloqués" userLists: "Listes" + excludeMutingUsers: "Exclure les utilisateur·rice·s mis en sourdine" excludeInactiveUsers: "Exclure les utilisateur·rice·s inactifs" _charts: federationInstancesIncDec: "Variation du nombre d'instances fédérées" diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index b3279d78b8..8fd41e533b 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -235,6 +235,8 @@ resetAreYouSure: "リセットしますか?" saved: "保存しました" messaging: "チャット" upload: "アップロード" +keepOriginalUploading: "オリジナル画像を保持" +keepOriginalUploadingDescription: "画像をアップロードする時にオリジナル版を保持します。オフにするとアップロード時にブラウザでWeb公開用画像を生成します。" fromDrive: "ドライブから" fromUrl: "URLから" uploadFromUrl: "URLアップロード" diff --git a/locales/ko-KR.yml b/locales/ko-KR.yml index 7451603a66..38a328862f 100644 --- a/locales/ko-KR.yml +++ b/locales/ko-KR.yml @@ -106,6 +106,7 @@ clickToShow: "클릭하여 보기" sensitive: "열람주의" add: "추가" reaction: "리액션" +reactionSetting: "선택기에 표시할 리액션" reactionSettingDescription2: "끌어서 순서 변경, 클릭해서 삭제, +를 눌러서 추가할 수 있습니다." rememberNoteVisibility: "공개 범위를 기억하기" attachCancel: "첨부 취소" @@ -234,6 +235,8 @@ resetAreYouSure: "초기화 하시겠습니까?" saved: "저장하였습니다" messaging: "대화" upload: "업로드" +keepOriginalUploading: "원본 이미지를 유지" +keepOriginalUploadingDescription: "이미지를 업로드할 때에 원본을 그대로 유지합니다. 비활성화하면 업로드할 때 브라우저에서 웹 공개용 이미지를 생성합니다." fromDrive: "드라이브에서" fromUrl: "URL로부터" uploadFromUrl: "URL 업로드" @@ -446,6 +449,7 @@ uiLanguage: "UI 표시 언어" groupInvited: "그룹에 초대되었습니다" aboutX: "{x}에 대하여" useOsNativeEmojis: "OS 기본 이모지를 사용" +disableDrawer: "드로어 메뉴를 사용하지 않기" youHaveNoGroups: "그룹이 없습니다" joinOrCreateGroup: "다른 그룹의 초대를 받거나, 직접 새 그룹을 만들어 보세요." noHistory: "기록이 없습니다" @@ -617,8 +621,11 @@ reportAbuse: "신고" reportAbuseOf: "{name}을 신고하기" fillAbuseReportDescription: "신고하려는 이유를 자세히 알려주세요. 특정 게시물을 신고할 때에는 게시물의 URL도 포함해 주세요." abuseReported: "신고를 보냈습니다. 신고해 주셔서 감사합니다." +reporter: "신고자" reporteeOrigin: "피신고자" reporterOrigin: "신고자" +forwardReport: "리모트 인스턴스에도 신고 내용 보내기" +forwardReportIsAnonymous: "리모트 인스턴스에서는 나의 정보를 볼 수 없으며, 익명의 시스템 계정으로 표시됩니다." send: "전송" abuseMarkAsResolved: "해결됨으로 표시" openInNewTab: "새 탭에서 열기" @@ -680,6 +687,7 @@ center: "가운데" wide: "넓게" narrow: "좁게" reloadToApplySetting: "이 설정을 적용하려면 페이지를 새로고침해야 합니다. 바로 새로고침하시겠습니까?" +needReloadToApply: "변경 사항은 새로고침하면 적용됩니다." showTitlebar: "타이틀 바를 표시하기" clearCache: "캐시 비우기" onlineUsersCount: "{n}명이 접속 중" @@ -740,6 +748,7 @@ notRecommended: "추천하지 않음" botProtection: "Bot 방어" instanceBlocking: "인스턴스 차단" selectAccount: "계정 선택" +switchAccount: "계정 바꾸기" enabled: "활성화" disabled: "비활성화" quickAction: "빠른 동작" @@ -808,6 +817,11 @@ deleteAccountConfirm: "계정이 삭제되고 되돌릴 수 없게 됩니다. incorrectPassword: "비밀번호가 올바르지 않습니다." voteConfirm: "\"{choice}\"에 투표하시겠습니까?" hide: "숨기기" +leaveGroup: "그룹 나가기" +leaveGroupConfirm: "\"{name}\"에서 나갈까요?" +useDrawerReactionPickerForMobile: "모바일에서 드로어 메뉴로 표시" +welcomeBackWithName: "환영합니다, {name}님" +clickToFinishEmailVerification: "[{ok}]를 눌러 이메일 인증을 완료하세요." _emailUnavailable: used: "이 메일 주소는 사용중입니다" format: "형식이 올바르지 않습니다" diff --git a/locales/zh-CN.yml b/locales/zh-CN.yml index 5815c92f43..c54e64214a 100644 --- a/locales/zh-CN.yml +++ b/locales/zh-CN.yml @@ -235,6 +235,8 @@ resetAreYouSure: "恢复默认设置?" saved: "已保存" messaging: "聊天" upload: "本地上传" +keepOriginalUploading: "保留原图" +keepOriginalUploadingDescription: "上传图片时保留原始图片。关闭时,浏览器会在上传时生成一张用于web发布的图片。" fromDrive: "从网盘中" fromUrl: "从 URL" uploadFromUrl: "从网址上传" @@ -619,8 +621,11 @@ reportAbuse: "举报" reportAbuseOf: "举报{name}" fillAbuseReportDescription: "请填写举报的详细原因。如果有对方发的帖子,请同时填写URL地址。" abuseReported: "内容已发送。感谢您的报告。" +reporter: "报告者" reporteeOrigin: "举报来源" reporterOrigin: "举报者来源" +forwardReport: "将报告转发给远程实例" +forwardReportIsAnonymous: "在远程实例上显示的报告者是匿名的系统账号,而不是您的账号。" send: "发送" abuseMarkAsResolved: "处理完毕" openInNewTab: "在新标签页中打开" diff --git a/package.json b/package.json index d945672987..8243935bfd 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "misskey", - "version": "12.102.1", + "version": "12.103.0", "codename": "indigo", "repository": { "type": "git", diff --git a/packages/backend/package.json b/packages/backend/package.json index 3d3a901f34..3541e803f3 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -122,7 +122,7 @@ "langmap": "0.0.16", "mfm-js": "0.21.0", "mime-types": "2.1.34", - "misskey-js": "0.0.13", + "misskey-js": "0.0.14", "mocha": "8.4.0", "ms": "3.0.0-canary.1", "multer": "1.4.4", diff --git a/packages/backend/src/server/api/api-handler.ts b/packages/backend/src/server/api/api-handler.ts index faa35d12d4..362bbb0f57 100644 --- a/packages/backend/src/server/api/api-handler.ts +++ b/packages/backend/src/server/api/api-handler.ts @@ -32,7 +32,7 @@ export default (endpoint: IEndpoint, ctx: Koa.Context) => new Promise((res) => { // Authentication authenticate(body['i']).then(([user, app]) => { // 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); }).catch((e: ApiError) => { reply(e.httpStatusCode ? e.httpStatusCode : e.kind === 'client' ? 400 : 500, e); diff --git a/packages/backend/src/server/api/call.ts b/packages/backend/src/server/api/call.ts index 399ee65bde..5bc7d2f25e 100644 --- a/packages/backend/src/server/api/call.ts +++ b/packages/backend/src/server/api/call.ts @@ -1,3 +1,4 @@ +import * as Koa from 'koa'; import { performance } from 'perf_hooks'; import { limiter } from './limiter'; import { User } from '@/models/entities/user'; @@ -12,7 +13,7 @@ const accessDenied = { 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 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 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) { throw e; } else { diff --git a/packages/backend/src/server/api/endpoints/drive/files/create.ts b/packages/backend/src/server/api/endpoints/drive/files/create.ts index dd65ab0611..877e76677e 100644 --- a/packages/backend/src/server/api/endpoints/drive/files/create.ts +++ b/packages/backend/src/server/api/endpoints/drive/files/create.ts @@ -39,15 +39,13 @@ export const meta = { }, isSensitive: { - validator: $.optional.either($.bool, $.str), + validator: $.optional.bool, default: false, - transform: (v: any): boolean => v === true || v === 'true', }, force: { - validator: $.optional.either($.bool, $.str), + validator: $.optional.bool, default: false, - transform: (v: any): boolean => v === true || v === 'true', }, }, diff --git a/packages/backend/src/server/file/index.ts b/packages/backend/src/server/file/index.ts index a455acd1cf..6fe6110dc9 100644 --- a/packages/backend/src/server/file/index.ts +++ b/packages/backend/src/server/file/index.ts @@ -18,7 +18,7 @@ const _dirname = dirname(_filename); const app = new Koa(); app.use(cors()); 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(); }); diff --git a/packages/backend/src/server/proxy/index.ts b/packages/backend/src/server/proxy/index.ts index b8993f19f8..7a3094311c 100644 --- a/packages/backend/src/server/proxy/index.ts +++ b/packages/backend/src/server/proxy/index.ts @@ -11,7 +11,7 @@ import { proxyMedia } from './proxy-media'; const app = new Koa(); app.use(cors()); 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(); }); diff --git a/packages/backend/yarn.lock b/packages/backend/yarn.lock index 99e2e2306e..5bf6e05a71 100644 --- a/packages/backend/yarn.lock +++ b/packages/backend/yarn.lock @@ -4967,10 +4967,10 @@ minizlib@^2.0.0, minizlib@^2.1.1: minipass "^3.0.0" yallist "^4.0.0" -misskey-js@0.0.13: - version "0.0.13" - resolved "https://registry.yarnpkg.com/misskey-js/-/misskey-js-0.0.13.tgz#03a4e469186e28752d599dc4093519eb64647970" - integrity sha512-kBdJdfe281gtykzzsrN3IAxWUQIimzPiJGyKWf863ggWJlWYVPmP9hTFlX2z8oPOaypgVBPEPHyw/jNUdc2DbQ== +misskey-js@0.0.14: + version "0.0.14" + resolved "https://registry.yarnpkg.com/misskey-js/-/misskey-js-0.0.14.tgz#1a616bdfbe81c6ee6900219eaf425bb5c714dd4d" + integrity sha512-bvLx6U3OwQwqHfp/WKwIVwdvNYAAPk0+YblXyxmSG3dwlzCgBRRLcB8o6bNruUDyJgh3t73pLDcOz3myxcUmww== dependencies: autobind-decorator "^2.4.0" eventemitter3 "^4.0.7" diff --git a/packages/client/.eslintrc.js b/packages/client/.eslintrc.js index d414f86ed3..acbb7c0c6b 100644 --- a/packages/client/.eslintrc.js +++ b/packages/client/.eslintrc.js @@ -18,6 +18,7 @@ module.exports = { // data の禁止理由: 抽象的すぎるため // e の禁止理由: error や event など、複数のキーワードの頭文字であり分かりにくいため "id-denylist": ["error", "window", "data", "e"], + 'eqeqeq': ['error', 'always', { 'null': 'ignore' }], "vue/attributes-order": ["error", { "alphabetical": false }], diff --git a/packages/client/package.json b/packages/client/package.json index 71dd89bea4..b840bafe8c 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -69,7 +69,7 @@ "langmap": "0.0.16", "matter-js": "0.18.0", "mfm-js": "0.21.0", - "misskey-js": "0.0.13", + "misskey-js": "0.0.14", "mocha": "8.4.0", "ms": "2.1.3", "nested-property": "4.0.0", diff --git a/packages/client/src/account.ts b/packages/client/src/account.ts index 5a935e1dc7..4aeceeccab 100644 --- a/packages/client/src/account.ts +++ b/packages/client/src/account.ts @@ -192,31 +192,31 @@ export async function openAccountMenu(opts: { if (opts.withExtraOperation) { popupMenu([...[{ type: 'link', - text: i18n.locale.profile, + text: i18n.ts.profile, to: `/@${ $i.username }`, avatar: $i, }, null, ...(opts.includeCurrentAccount ? [createItem($i)] : []), ...accountItemPromises, { icon: 'fas fa-plus', - text: i18n.locale.addAccount, + text: i18n.ts.addAccount, action: () => { popupMenu([{ - text: i18n.locale.existingAccount, + text: i18n.ts.existingAccount, action: () => { showSigninDialog(); }, }, { - text: i18n.locale.createAccount, + text: i18n.ts.createAccount, action: () => { createAccount(); }, - }], ev.currentTarget || ev.target); + }], ev.currentTarget ?? ev.target); }, }, { type: 'link', icon: 'fas fa-users', - text: i18n.locale.manageAccounts, + text: i18n.ts.manageAccounts, to: `/settings/accounts`, - }]], ev.currentTarget || ev.target, { + }]], ev.currentTarget ?? ev.target, { align: 'left' }); } else { - popupMenu([...(opts.includeCurrentAccount ? [createItem($i)] : []), ...accountItemPromises], ev.currentTarget || ev.target, { + popupMenu([...(opts.includeCurrentAccount ? [createItem($i)] : []), ...accountItemPromises], ev.currentTarget ?? ev.target, { align: 'left' }); } diff --git a/packages/client/src/components/abuse-report-window.vue b/packages/client/src/components/abuse-report-window.vue index cd04f62bca..f2cb369802 100644 --- a/packages/client/src/components/abuse-report-window.vue +++ b/packages/client/src/components/abuse-report-window.vue @@ -2,7 +2,7 @@ <XWindow ref="window" :initial-width="400" :initial-height="500" :can-resize="true" @closed="emit('closed')"> <template #header> <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> <b><MkAcct :user="user"/></b> </template> @@ -11,12 +11,12 @@ <div class="dpvffvvy _monolithic_"> <div class="_section"> <MkTextarea v-model="comment"> - <template #label>{{ i18n.locale.details }}</template> - <template #caption>{{ i18n.locale.fillAbuseReportDescription }}</template> + <template #label>{{ i18n.ts.details }}</template> + <template #caption>{{ i18n.ts.fillAbuseReportDescription }}</template> </MkTextarea> </div> <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> </XWindow> @@ -50,7 +50,7 @@ function send() { }, undefined).then(res => { os.alert({ type: 'success', - text: i18n.locale.abuseReported + text: i18n.ts.abuseReported }); window.value?.close(); emit('closed'); diff --git a/packages/client/src/components/autocomplete.vue b/packages/client/src/components/autocomplete.vue index 7ba83b7cb1..91a50ffa59 100644 --- a/packages/client/src/components/autocomplete.vue +++ b/packages/client/src/components/autocomplete.vue @@ -8,7 +8,7 @@ </span> <span class="username">@{{ acct(user) }}</span> </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 v-else-if="hashtags.length > 0" ref="suggests" class="hashtags"> <li v-for="hashtag in hashtags" tabindex="-1" @click="complete(type, hashtag)" @keydown="onKeydown"> diff --git a/packages/client/src/components/captcha.vue b/packages/client/src/components/captcha.vue index 307fc312bc..963ae25f8e 100644 --- a/packages/client/src/components/captcha.vue +++ b/packages/client/src/components/captcha.vue @@ -1,6 +1,6 @@ <template> <div> - <span v-if="!available">{{ i18n.locale.waiting }}<MkEllipsis/></span> + <span v-if="!available">{{ i18n.ts.waiting }}<MkEllipsis/></span> <div ref="captchaEl"></div> </div> </template> diff --git a/packages/client/src/components/channel-follow-button.vue b/packages/client/src/components/channel-follow-button.vue index 0ad5384cd5..7bbf5ae663 100644 --- a/packages/client/src/components/channel-follow-button.vue +++ b/packages/client/src/components/channel-follow-button.vue @@ -6,14 +6,14 @@ > <template v-if="!wait"> <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 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 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> </button> </template> diff --git a/packages/client/src/components/channel-preview.vue b/packages/client/src/components/channel-preview.vue index 8d135a192f..dd3794a657 100644 --- a/packages/client/src/components/channel-preview.vue +++ b/packages/client/src/components/channel-preview.vue @@ -6,7 +6,7 @@ <div class="status"> <div> <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> <b>{{ channel.usersCount }}</b> </template> @@ -14,7 +14,7 @@ </div> <div> <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> <b>{{ channel.notesCount }}</b> </template> @@ -27,7 +27,7 @@ </article> <footer> <span v-if="channel.lastNotedAt"> - {{ i18n.locale.updatedAt }}: <MkTime :time="channel.lastNotedAt"/> + {{ i18n.ts.updatedAt }}: <MkTime :time="channel.lastNotedAt"/> </span> </footer> </MkA> diff --git a/packages/client/src/components/chart-tooltip.vue b/packages/client/src/components/chart-tooltip.vue new file mode 100644 index 0000000000..b080eaf2b4 --- /dev/null +++ b/packages/client/src/components/chart-tooltip.vue @@ -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> diff --git a/packages/client/src/components/chart.vue b/packages/client/src/components/chart.vue index 1959271f5d..3e46c51b47 100644 --- a/packages/client/src/components/chart.vue +++ b/packages/client/src/components/chart.vue @@ -8,7 +8,7 @@ </template> <script lang="ts"> -import { defineComponent, onMounted, ref, watch, PropType } from 'vue'; +import { defineComponent, onMounted, ref, watch, PropType, onUnmounted, shallowRef } from 'vue'; import { Chart, ArcElement, @@ -31,6 +31,7 @@ import { enUS } from 'date-fns/locale'; import zoomPlugin from 'chartjs-plugin-zoom'; import * as os from '@/os'; import { defaultStore } from '@/store'; +import MkChartTooltip from '@/components/chart-tooltip.vue'; Chart.register( 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 = () => { if (chartInstance) { chartInstance.destroy(); } 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'); @@ -221,10 +260,12 @@ export default defineComponent({ }, }, tooltip: { + enabled: false, mode: 'index', animation: { duration: 0, }, + external: externalTooltipHandler, }, zoom: { 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(); }); + onUnmounted(() => { + if (disposeTooltipComponent) disposeTooltipComponent(); + }); + return { chartEl, fetching, diff --git a/packages/client/src/components/cw-button.vue b/packages/client/src/components/cw-button.vue index ccfd11462a..e7c9aabe4e 100644 --- a/packages/client/src/components/cw-button.vue +++ b/packages/client/src/components/cw-button.vue @@ -1,6 +1,6 @@ <template> <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> </button> </template> @@ -25,7 +25,7 @@ const label = computed(() => { return concat([ 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.poll != null ? [i18n.locale.poll] : [] + props.note.poll != null ? [i18n.ts.poll] : [] ] as string[][]).join(' / '); }); diff --git a/packages/client/src/components/dialog.vue b/packages/client/src/components/dialog.vue index b6b649cde9..3e106a4f0c 100644 --- a/packages/client/src/components/dialog.vue +++ b/packages/client/src/components/dialog.vue @@ -28,8 +28,8 @@ </template> </MkSelect> <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="showCancelButton || input || select" inline @click="cancel">{{ i18n.locale.cancel }}</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.ts.cancel }}</MkButton> </div> <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> diff --git a/packages/client/src/components/drive-select-dialog.vue b/packages/client/src/components/drive-select-dialog.vue index 6d84511277..f6c59457d1 100644 --- a/packages/client/src/components/drive-select-dialog.vue +++ b/packages/client/src/components/drive-select-dialog.vue @@ -10,7 +10,7 @@ @closed="emit('closed')" > <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> </template> <XDrive :multiple="multiple" :select="type" @changeSelection="onChangeSelection" @selected="ok()"/> diff --git a/packages/client/src/components/drive-window.vue b/packages/client/src/components/drive-window.vue index 8b60bf7794..d08c5fb674 100644 --- a/packages/client/src/components/drive-window.vue +++ b/packages/client/src/components/drive-window.vue @@ -6,7 +6,7 @@ @closed="emit('closed')" > <template #header> - {{ i18n.locale.drive }} + {{ i18n.ts.drive }} </template> <XDrive :initial-folder="initialFolder"/> </XWindow> diff --git a/packages/client/src/components/drive.file.vue b/packages/client/src/components/drive.file.vue index fd6a813838..262eae0de1 100644 --- a/packages/client/src/components/drive.file.vue +++ b/packages/client/src/components/drive.file.vue @@ -10,15 +10,15 @@ > <div v-if="$i?.avatarId == file.id" class="label"> <img src="/client-assets/label.svg"/> - <p>{{ i18n.locale.avatar }}</p> + <p>{{ i18n.ts.avatar }}</p> </div> <div v-if="$i?.bannerId == file.id" class="label"> <img src="/client-assets/label.svg"/> - <p>{{ i18n.locale.banner }}</p> + <p>{{ i18n.ts.banner }}</p> </div> <div v-if="file.isSensitive" class="label red"> <img src="/client-assets/label-red.svg"/> - <p>{{ i18n.locale.nsfw }}</p> + <p>{{ i18n.ts.nsfw }}</p> </div> <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() { return [{ - text: i18n.locale.rename, + text: i18n.ts.rename, icon: 'fas fa-i-cursor', 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', action: toggleSensitive }, { - text: i18n.locale.describeFile, + text: i18n.ts.describeFile, icon: 'fas fa-i-cursor', action: describe }, null, { - text: i18n.locale.copyUrl, + text: i18n.ts.copyUrl, icon: 'fas fa-link', action: copyUrl }, { type: 'a', href: props.file.url, target: '_blank', - text: i18n.locale.download, + text: i18n.ts.download, icon: 'fas fa-download', download: props.file.name }, null, { - text: i18n.locale.delete, + text: i18n.ts.delete, icon: 'fas fa-trash-alt', danger: true, action: deleteFile @@ -95,7 +95,7 @@ function onClick(ev: MouseEvent) { if (props.selectMode) { emit('chosen', props.file); } 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() { os.inputText({ - title: i18n.locale.renameFile, - placeholder: i18n.locale.inputNewFileName, + title: i18n.ts.renameFile, + placeholder: i18n.ts.inputNewFileName, default: props.file.name, }).then(({ canceled, result: name }) => { if (canceled) return; @@ -134,9 +134,9 @@ function rename() { function describe() { os.popup(import('@/components/media-caption.vue'), { - title: i18n.locale.describeFile, + title: i18n.ts.describeFile, input: { - placeholder: i18n.locale.inputNewDescription, + placeholder: i18n.ts.inputNewDescription, default: props.file.comment !== null ? props.file.comment : '', }, image: props.file diff --git a/packages/client/src/components/drive.folder.vue b/packages/client/src/components/drive.folder.vue index 20a6343cfe..57621bf097 100644 --- a/packages/client/src/components/drive.folder.vue +++ b/packages/client/src/components/drive.folder.vue @@ -20,7 +20,7 @@ {{ folder.name }} </p> <p v-if="defaultStore.state.uploadFolder == folder.id" class="upload"> - {{ i18n.locale.uploadFolder }} + {{ i18n.ts.uploadFolder }} </p> <button v-if="selectMode" class="checkbox _button" :class="{ checked: isSelected }" @click.prevent.stop="checkboxClicked"></button> </div> @@ -146,14 +146,14 @@ function onDrop(ev: DragEvent) { switch (err) { case 'detected-circular-definition': os.alert({ - title: i18n.locale.unableToProcess, - text: i18n.locale.circularReferenceFolder + title: i18n.ts.unableToProcess, + text: i18n.ts.circularReferenceFolder }); break; default: os.alert({ type: 'error', - text: i18n.locale.somethingHappened + text: i18n.ts.somethingHappened }); } }); @@ -184,8 +184,8 @@ function go() { function rename() { os.inputText({ - title: i18n.locale.renameFolder, - placeholder: i18n.locale.inputNewFolderName, + title: i18n.ts.renameFolder, + placeholder: i18n.ts.inputNewFolderName, default: props.folder.name }).then(({ canceled, result: name }) => { if (canceled) return; @@ -208,14 +208,14 @@ function deleteFolder() { case 'b0fc8a17-963c-405d-bfbc-859a487295e1': os.alert({ type: 'error', - title: i18n.locale.unableToDelete, - text: i18n.locale.hasChildFilesOrFolders + title: i18n.ts.unableToDelete, + text: i18n.ts.hasChildFilesOrFolders }); break; default: os.alert({ type: 'error', - text: i18n.locale.unableToDelete + text: i18n.ts.unableToDelete }); } }); @@ -227,7 +227,7 @@ function setAsUploadFolder() { function onContextmenu(ev: MouseEvent) { os.contextMenu([{ - text: i18n.locale.openInWindow, + text: i18n.ts.openInWindow, icon: 'fas fa-window-restore', action: () => { os.popup(import('./drive-window.vue'), { @@ -236,11 +236,11 @@ function onContextmenu(ev: MouseEvent) { }, 'closed'); } }, null, { - text: i18n.locale.rename, + text: i18n.ts.rename, icon: 'fas fa-i-cursor', action: rename, }, null, { - text: i18n.locale.delete, + text: i18n.ts.delete, icon: 'fas fa-trash-alt', danger: true, action: deleteFolder, diff --git a/packages/client/src/components/drive.nav-folder.vue b/packages/client/src/components/drive.nav-folder.vue index 7c35c5d3da..67223267c1 100644 --- a/packages/client/src/components/drive.nav-folder.vue +++ b/packages/client/src/components/drive.nav-folder.vue @@ -8,7 +8,7 @@ @drop.stop="onDrop" > <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> </template> diff --git a/packages/client/src/components/drive.vue b/packages/client/src/components/drive.vue index e27b0a5fbb..e044c67523 100644 --- a/packages/client/src/components/drive.vue +++ b/packages/client/src/components/drive.vue @@ -54,7 +54,7 @@ /> <!-- 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> - <MkButton v-if="moreFolders" ref="moreFolders">{{ i18n.locale.loadMore }}</MkButton> + <MkButton v-if="moreFolders" ref="moreFolders">{{ i18n.ts.loadMore }}</MkButton> </div> <div v-show="files.length > 0" ref="filesContainer" class="files"> <XFile @@ -71,12 +71,12 @@ /> <!-- 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> - <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 v-if="files.length == 0 && folders.length == 0 && !fetching" class="empty"> <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">{{ i18n.locale.emptyFolder }}</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.ts.emptyFolder }}</p> </div> </div> <MkLoading v-if="fetching"/> @@ -253,14 +253,14 @@ function onDrop(e: DragEvent): any { switch (err) { case 'detected-circular-definition': os.alert({ - title: i18n.locale.unableToProcess, - text: i18n.locale.circularReferenceFolder + title: i18n.ts.unableToProcess, + text: i18n.ts.circularReferenceFolder }); break; default: os.alert({ type: 'error', - text: i18n.locale.somethingHappened + text: i18n.ts.somethingHappened }); } }); @@ -274,9 +274,9 @@ function selectLocalFile() { function urlUpload() { os.inputText({ - title: i18n.locale.uploadFromUrl, + title: i18n.ts.uploadFromUrl, type: 'url', - placeholder: i18n.locale.uploadFromUrlDescription + placeholder: i18n.ts.uploadFromUrlDescription }).then(({ canceled, result: url }) => { if (canceled || !url) return; os.api('drive/files/upload-from-url', { @@ -285,16 +285,16 @@ function urlUpload() { }); os.alert({ - title: i18n.locale.uploadFromUrlRequested, - text: i18n.locale.uploadFromUrlMayTakeTime + title: i18n.ts.uploadFromUrlRequested, + text: i18n.ts.uploadFromUrlMayTakeTime }); }); } function createFolder() { os.inputText({ - title: i18n.locale.createFolder, - placeholder: i18n.locale.folderName + title: i18n.ts.createFolder, + placeholder: i18n.ts.folderName }).then(({ canceled, result: name }) => { if (canceled) return; os.api('drive/folders/create', { @@ -308,8 +308,8 @@ function createFolder() { function renameFolder(folderToRename: Misskey.entities.DriveFolder) { os.inputText({ - title: i18n.locale.renameFolder, - placeholder: i18n.locale.inputNewFolderName, + title: i18n.ts.renameFolder, + placeholder: i18n.ts.inputNewFolderName, default: folderToRename.name }).then(({ canceled, result: name }) => { if (canceled) return; @@ -334,14 +334,14 @@ function deleteFolder(folderToDelete: Misskey.entities.DriveFolder) { case 'b0fc8a17-963c-405d-bfbc-859a487295e1': os.alert({ type: 'error', - title: i18n.locale.unableToDelete, - text: i18n.locale.hasChildFilesOrFolders + title: i18n.ts.unableToDelete, + text: i18n.ts.hasChildFilesOrFolders }); break; default: os.alert({ type: 'error', - text: i18n.locale.unableToDelete + text: i18n.ts.unableToDelete }); } }); @@ -562,36 +562,36 @@ function fetchMoreFiles() { function getMenu() { return [{ - text: i18n.locale.addFile, + text: i18n.ts.addFile, type: 'label' }, { - text: i18n.locale.upload, + text: i18n.ts.upload, icon: 'fas fa-upload', action: () => { selectLocalFile(); } }, { - text: i18n.locale.fromUrl, + text: i18n.ts.fromUrl, icon: 'fas fa-link', action: () => { urlUpload(); } }, null, { - text: folder.value ? folder.value.name : i18n.locale.drive, + text: folder.value ? folder.value.name : i18n.ts.drive, type: 'label' }, folder.value ? { - text: i18n.locale.renameFolder, + text: i18n.ts.renameFolder, icon: 'fas fa-i-cursor', action: () => { renameFolder(folder.value); } } : undefined, folder.value ? { - text: i18n.locale.deleteFolder, + text: i18n.ts.deleteFolder, icon: 'fas fa-trash-alt', action: () => { deleteFolder(folder.value as Misskey.entities.DriveFolder); } } : undefined, { - text: i18n.locale.createFolder, + text: i18n.ts.createFolder, icon: 'fas fa-folder-plus', action: () => { createFolder(); } }]; } 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) { diff --git a/packages/client/src/components/emoji-picker-dialog.vue b/packages/client/src/components/emoji-picker-dialog.vue index f06a24636c..2c0b2e9a8b 100644 --- a/packages/client/src/components/emoji-picker-dialog.vue +++ b/packages/client/src/components/emoji-picker-dialog.vue @@ -32,20 +32,20 @@ import MkEmojiPicker from '@/components/emoji-picker.vue'; import { defaultStore } from '@/store'; withDefaults(defineProps<{ - manualShowing?: boolean; + manualShowing?: boolean | null; src?: HTMLElement; showPinned?: boolean; asReactionPicker?: boolean; }>(), { - manualShowing: false, + manualShowing: null, showPinned: true, asReactionPicker: false, }); const emit = defineEmits<{ - (e: 'done', v: any): void; - (e: 'close'): void; - (e: 'closed'): void; + (ev: 'done', v: any): void; + (ev: 'close'): void; + (ev: 'closed'): void; }>(); const modal = ref<InstanceType<typeof MkModal>>(); diff --git a/packages/client/src/components/emoji-picker.vue b/packages/client/src/components/emoji-picker.vue index 96670fa58c..6999ad6517 100644 --- a/packages/client/src/components/emoji-picker.vue +++ b/packages/client/src/components/emoji-picker.vue @@ -1,6 +1,6 @@ <template> <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"> <section class="result"> <div v-if="searchResultCustom.length > 0"> @@ -43,7 +43,7 @@ </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> <button v-for="emoji in recentlyUsedEmojis" :key="emoji" @@ -56,11 +56,11 @@ </section> </div> <div> - <header class="_acrylic">{{ i18n.locale.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> + <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.ts.other }}</XSection> </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> </div> </div> @@ -280,7 +280,7 @@ function getKey(emoji: string | Misskey.entities.CustomEmoji | UnicodeEmojiDef): } 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) { const rect = el.getBoundingClientRect(); const x = rect.left + (el.offsetWidth / 2); diff --git a/packages/client/src/components/follow-button.vue b/packages/client/src/components/follow-button.vue index 345edb6441..93c9e891c1 100644 --- a/packages/client/src/components/follow-button.vue +++ b/packages/client/src/components/follow-button.vue @@ -6,23 +6,23 @@ > <template v-if="!wait"> <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 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 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 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 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 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> </button> </template> diff --git a/packages/client/src/components/forgot-password.vue b/packages/client/src/components/forgot-password.vue index c74e1ac75e..46cbf6bd70 100644 --- a/packages/client/src/components/forgot-password.vue +++ b/packages/client/src/components/forgot-password.vue @@ -5,28 +5,28 @@ @close="dialog.close()" @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"> <div class="main _formRoot"> <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> </MkInput> <MkInput v-model="email" class="_formBlock" type="email" spellcheck="false" required> - <template #label>{{ i18n.locale.emailAddress }}</template> - <template #caption>{{ i18n.locale._forgotPassword.enterEmail }}</template> + <template #label>{{ i18n.ts.emailAddress }}</template> + <template #caption>{{ i18n.ts._forgotPassword.enterEmail }}</template> </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 class="sub"> - <MkA to="/about" class="_link">{{ i18n.locale._forgotPassword.ifNoEmail }}</MkA> + <MkA to="/about" class="_link">{{ i18n.ts._forgotPassword.ifNoEmail }}</MkA> </div> </form> <div v-else class="bafecedb"> - {{ i18n.locale._forgotPassword.contactAdmin }} + {{ i18n.ts._forgotPassword.contactAdmin }} </div> </XModalWindow> </template> diff --git a/packages/client/src/components/form/range.vue b/packages/client/src/components/form/range.vue index 3e02cacb9b..a82348d317 100644 --- a/packages/client/src/components/form/range.vue +++ b/packages/client/src/components/form/range.vue @@ -117,7 +117,7 @@ export default defineComponent({ text: computed(() => { return props.textConverter(finalValue.value); }), - source: thumbEl, + targetElement: thumbEl, }, {}, 'closed'); const style = document.createElement('style'); diff --git a/packages/client/src/components/form/switch.vue b/packages/client/src/components/form/switch.vue index f8a07b4caa..b5a30d635c 100644 --- a/packages/client/src/components/form/switch.vue +++ b/packages/client/src/components/form/switch.vue @@ -20,45 +20,33 @@ </div> </template> -<script lang="ts"> -import { defineComponent, ref, toRefs } from 'vue'; +<script lang="ts" setup> +import { toRefs, Ref } from 'vue'; import * as os from '@/os'; import Ripple from '@/components/ripple.vue'; -export default defineComponent({ - props: { - modelValue: { - type: Boolean, - default: false - }, - disabled: { - type: Boolean, - default: false - } - }, +const props = defineProps<{ + modelValue: boolean | Ref<boolean>; + disabled?: boolean; +}>(); - setup(props, context) { - const button = ref<HTMLElement>(); - const checked = toRefs(props).modelValue; - const toggle = () => { - if (props.disabled) return; - context.emit('update:modelValue', !checked.value); +const emit = defineEmits<{ + (e: 'update:modelValue', v: boolean): void; +}>(); - if (!checked.value) { - const rect = button.value.getBoundingClientRect(); - const x = rect.left + (button.value.offsetWidth / 2); - const y = rect.top + (button.value.offsetHeight / 2); - os.popup(Ripple, { x, y, particle: false }, {}, 'end'); - } - }; +let button = $ref<HTMLElement>(); +const checked = toRefs(props).modelValue; +const toggle = () => { + if (props.disabled) return; + emit('update:modelValue', !checked.value); - return { - button, - checked, - toggle, - }; - }, -}); + if (!checked.value) { + const rect = button.getBoundingClientRect(); + const x = rect.left + (button.offsetWidth / 2); + const y = rect.top + (button.offsetHeight / 2); + os.popup(Ripple, { x, y, particle: false }, {}, 'end'); + } +}; </script> <style lang="scss" scoped> diff --git a/packages/client/src/components/global/a.vue b/packages/client/src/components/global/a.vue index cf7385ca22..52fef50f9b 100644 --- a/packages/client/src/components/global/a.vue +++ b/packages/client/src/components/global/a.vue @@ -23,8 +23,9 @@ const props = withDefaults(defineProps<{ behavior: null, }); -const navHook = inject('navHook', null); -const sideViewHook = inject('sideViewHook', null); +type Navigate = (path: string, record?: boolean) => void; +const navHook = inject<null | Navigate>('navHook', null); +const sideViewHook = inject<null | Navigate>('sideViewHook', null); const active = $computed(() => { if (props.activeClass == null) return false; @@ -43,31 +44,31 @@ function onContextmenu(ev) { text: props.to, }, { icon: 'fas fa-window-maximize', - text: i18n.locale.openInWindow, + text: i18n.ts.openInWindow, action: () => { os.pageWindow(props.to); } }, sideViewHook ? { icon: 'fas fa-columns', - text: i18n.locale.openInSideView, + text: i18n.ts.openInSideView, action: () => { sideViewHook(props.to); } } : undefined, { icon: 'fas fa-expand-alt', - text: i18n.locale.showInPage, + text: i18n.ts.showInPage, action: () => { router.push(props.to); } }, null, { icon: 'fas fa-external-link-alt', - text: i18n.locale.openInNewTab, + text: i18n.ts.openInNewTab, action: () => { window.open(props.to, '_blank'); } }, { icon: 'fas fa-link', - text: i18n.locale.copyLink, + text: i18n.ts.copyLink, action: () => { copyToClipboard(`${url}${props.to}`); } diff --git a/packages/client/src/components/global/header.vue b/packages/client/src/components/global/header.vue index a241ece407..e558614c12 100644 --- a/packages/client/src/components/global/header.vue +++ b/packages/client/src/components/global/header.vue @@ -104,7 +104,7 @@ export default defineComponent({ if (props.info.share) { if (menu.length > 0) menu.push(null); menu.push({ - text: i18n.locale.share, + text: i18n.ts.share, icon: 'fas fa-share-alt', action: share }); @@ -113,7 +113,7 @@ export default defineComponent({ if (menu.length > 0) menu.push(null); menu = menu.concat(props.menu); } - popupMenu(menu, ev.currentTarget || ev.target); + popupMenu(menu, ev.currentTarget ?? ev.target); }; const showTabsPopup = (ev: MouseEvent) => { @@ -126,7 +126,7 @@ export default defineComponent({ icon: tab.icon, action: tab.onClick, })); - popupMenu(menu, ev.currentTarget || ev.target); + popupMenu(menu, ev.currentTarget ?? ev.target); }; const preventDrag = (ev: TouchEvent) => { diff --git a/packages/client/src/components/global/time.vue b/packages/client/src/components/global/time.vue index d2788264c5..5748d9de61 100644 --- a/packages/client/src/components/global/time.vue +++ b/packages/client/src/components/global/time.vue @@ -24,16 +24,16 @@ let now = $ref(new Date()); const relative = $computed(() => { const ago = (now.getTime() - _time.getTime()) / 1000/*ms*/; return ( - ago >= 31536000 ? i18n.t('_ago.yearsAgo', { n: (~~(ago / 31536000)).toString() }) : - ago >= 2592000 ? i18n.t('_ago.monthsAgo', { n: (~~(ago / 2592000)).toString() }) : - ago >= 604800 ? i18n.t('_ago.weeksAgo', { n: (~~(ago / 604800)).toString() }) : - ago >= 86400 ? i18n.t('_ago.daysAgo', { n: (~~(ago / 86400)).toString() }) : - ago >= 3600 ? i18n.t('_ago.hoursAgo', { n: (~~(ago / 3600)).toString() }) : + ago >= 31536000 ? i18n.t('_ago.yearsAgo', { n: Math.round(ago / 31536000).toString() }) : + ago >= 2592000 ? i18n.t('_ago.monthsAgo', { n: Math.round(ago / 2592000).toString() }) : + ago >= 604800 ? i18n.t('_ago.weeksAgo', { n: Math.round(ago / 604800).toString() }) : + ago >= 86400 ? i18n.t('_ago.daysAgo', { n: Math.round(ago / 86400).toString() }) : + ago >= 3600 ? i18n.t('_ago.hoursAgo', { n: Math.round(ago / 3600).toString() }) : ago >= 60 ? i18n.t('_ago.minutesAgo', { n: (~~(ago / 60)).toString() }) : ago >= 10 ? i18n.t('_ago.secondsAgo', { n: (~~(ago % 60)).toString() }) : - ago >= -1 ? i18n.locale._ago.justNow : - ago < -1 ? i18n.locale._ago.future : - i18n.locale._ago.unknown); + ago >= -1 ? i18n.ts._ago.justNow : + ago < -1 ? i18n.ts._ago.future : + i18n.ts._ago.unknown); }); function tick() { diff --git a/packages/client/src/components/media-image.vue b/packages/client/src/components/media-image.vue index 3e2cabae0a..43639f6771 100644 --- a/packages/client/src/components/media-image.vue +++ b/packages/client/src/components/media-image.vue @@ -20,52 +20,32 @@ </div> </template> -<script lang="ts"> -import { defineComponent } from 'vue'; +<script lang="ts" setup> +import { watch } from 'vue'; +import * as misskey from 'misskey-js'; import { getStaticImageUrl } from '@/scripts/get-static-image-url'; import ImgWithBlurhash from '@/components/img-with-blurhash.vue'; -import * as os from '@/os'; +import { defaultStore } from '@/store'; -export default defineComponent({ - components: { - ImgWithBlurhash - }, - 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; +const props = defineProps<{ + image: misskey.entities.DriveFile; + raw?: boolean; +}>(); - if (this.raw || this.$store.state.loadRawImages) { - url = this.image.url; - } +let hide = $ref(true); - return url; - } - }, - created() { - // Plugin:register_note_view_interruptor を使って書き換えられる可能性があるためwatchする - this.$watch('image', () => { - this.hide = (this.$store.state.nsfw === 'force') ? true : this.image.isSensitive && (this.$store.state.nsfw !== 'ignore'); - }, { - deep: true, - immediate: true, - }); - }, +const url = (props.raw || defaultStore.state.loadRawImages) + ? props.image.url + : defaultStore.state.disableShowingAnimatedImages + ? getStaticImageUrl(props.image.thumbnailUrl) + : props.image.thumbnailUrl; + +// Plugin:register_note_view_interruptor を使って書き換えられる可能性があるためwatchする +watch(() => props.image, () => { + hide = (defaultStore.state.nsfw === 'force') ? true : props.image.isSensitive && (defaultStore.state.nsfw !== 'ignore'); +}, { + deep: true, + immediate: true, }); </script> diff --git a/packages/client/src/components/media-list.vue b/packages/client/src/components/media-list.vue index efcbb12922..532627edbd 100644 --- a/packages/client/src/components/media-list.vue +++ b/packages/client/src/components/media-list.vue @@ -12,8 +12,8 @@ </div> </template> -<script lang="ts"> -import { defineComponent, onMounted, PropType, ref } from 'vue'; +<script lang="ts" setup> +import { onMounted, ref } from 'vue'; import * as misskey from 'misskey-js'; import PhotoSwipeLightbox from 'photoswipe/dist/photoswipe-lightbox.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 { defaultStore } from '@/store'; -export default defineComponent({ - components: { - XBanner, - XImage, - XVideo, - }, - props: { - mediaList: { - type: Array as PropType<misskey.entities.DriveFile[]>, - required: true, - }, - raw: { - default: false - }, - }, - setup(props) { - const gallery = ref(null); +const props = defineProps<{ + mediaList: misskey.entities.DriveFile[]; + raw?: boolean; +}>(); - onMounted(() => { - const lightbox = new PhotoSwipeLightbox({ - dataSource: props.mediaList - .filter(media => { - if (media.type === 'image/svg+xml') return true; // svgのwebpublicはpngなのでtrue - 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, - }); +const gallery = ref(null); +const pswpZIndex = os.claimZIndex('middle'); - lightbox.on('itemData', (e) => { - const { itemData } = e; - - // element is children - const { element } = itemData; - - const id = element.dataset.id; - const file = props.mediaList.find(media => media.id === id); - - 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]; +onMounted(() => { + const lightbox = new PhotoSwipeLightbox({ + dataSource: props.mediaList + .filter(media => { + if (media.type === 'image/svg+xml') return true; // svgのwebpublicはpngなのでtrue + 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]; } - itemData.msrc = file.thumbnailUrl; - itemData.thumbCropped = true; - }); + 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.init(); - }); + lightbox.on('itemData', (ev) => { + const { itemData } = ev; - const previewable = (file: misskey.entities.DriveFile): boolean => { - if (file.type === 'image/svg+xml') return true; // svgのwebpublic/thumbnailはpngなのでtrue - // FILE_TYPE_BROWSERSAFEに適合しないものはブラウザで表示するのに不適切 - return (file.type.startsWith('video') || file.type.startsWith('image')) && FILE_TYPE_BROWSERSAFE.includes(file.type); - }; + // element is children + const { element } = itemData; - return { - previewable, - gallery, - pswpZIndex: os.claimZIndex('middle'), - }; - }, + const id = element.dataset.id; + const file = props.mediaList.find(media => media.id === id); + + 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; // svgのwebpublic/thumbnailはpngなのでtrue + // FILE_TYPE_BROWSERSAFEに適合しないものはブラウザで表示するのに不適切 + return (file.type.startsWith('video') || file.type.startsWith('image')) && FILE_TYPE_BROWSERSAFE.includes(file.type); +}; </script> <style lang="scss" scoped> diff --git a/packages/client/src/components/note-detailed.vue b/packages/client/src/components/note-detailed.vue index a3b30f726e..5fc3a0f334 100644 --- a/packages/client/src/components/note-detailed.vue +++ b/packages/client/src/components/note-detailed.vue @@ -250,7 +250,7 @@ function menu(viaKeyboard = false): void { function showRenoteMenu(viaKeyboard = false): void { if (!isMyRenote) return; os.popupMenu([{ - text: i18n.locale.unrenote, + text: i18n.ts.unrenote, icon: 'fas fa-trash-alt', danger: true, action: () => { diff --git a/packages/client/src/components/note.vue b/packages/client/src/components/note.vue index fc89c2777b..6c596fb60d 100644 --- a/packages/client/src/components/note.vue +++ b/packages/client/src/components/note.vue @@ -10,13 +10,13 @@ :class="{ renote: isRenote }" > <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="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._featuredId_" class="info"><i class="fas fa-bolt"></i> {{ i18n.locale.featured }}</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.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.ts.featured }}</div> <div v-if="isRenote" class="renote"> <MkAvatar class="avatar" :user="note.user"/> <i class="fas fa-retweet"></i> - <I18n :src="i18n.locale.renotedBy" tag="span"> + <I18n :src="i18n.ts.renotedBy" tag="span"> <template #user> <MkA v-user-preview="note.userId" class="name" :to="userPage(note.user)"> <MkUserName :user="note.user"/> @@ -48,7 +48,7 @@ </p> <div v-show="appearNote.cw == null || showContent" class="content" :class="{ collapsed }"> <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> <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> @@ -67,7 +67,7 @@ <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> <button v-if="collapsed" class="fade _button" @click="collapsed = false"> - <span>{{ i18n.locale.showMore }}</span> + <span>{{ i18n.ts.showMore }}</span> </button> </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> @@ -94,7 +94,7 @@ </article> </div> <div v-else class="muted" @click="muted = false"> - <I18n :src="i18n.locale.userSaysSomething" tag="small"> + <I18n :src="i18n.ts.userSaysSomething" tag="small"> <template #name> <MkA v-user-preview="appearNote.userId" class="name" :to="userPage(appearNote.user)"> <MkUserName :user="appearNote.user"/> @@ -238,7 +238,7 @@ function menu(viaKeyboard = false): void { function showRenoteMenu(viaKeyboard = false): void { if (!isMyRenote) return; os.popupMenu([{ - text: i18n.locale.unrenote, + text: i18n.ts.unrenote, icon: 'fas fa-trash-alt', danger: true, action: () => { diff --git a/packages/client/src/components/notification.vue b/packages/client/src/components/notification.vue index 5659c899be..d855f81f8a 100644 --- a/packages/client/src/components/notification.vue +++ b/packages/client/src/components/notification.vue @@ -153,7 +153,7 @@ export default defineComponent({ showing, reaction: props.notification.reaction ? props.notification.reaction.replace(/^:(\w+):$/, ':$1@.:') : props.notification.reaction, emojis: props.notification.note.emojis, - source: reactionRef.value.$el, + targetElement: reactionRef.value.$el, }, {}, 'closed'); }); diff --git a/packages/client/src/components/page-window.vue b/packages/client/src/components/page-window.vue index ec7451d5aa..7455236bad 100644 --- a/packages/client/src/components/page-window.vue +++ b/packages/client/src/components/page-window.vue @@ -160,7 +160,7 @@ export default defineComponent({ action: () => { copyToClipboard(this.url); } - }], ev.currentTarget || ev.target); + }], ev.currentTarget ?? ev.target); }, back() { diff --git a/packages/client/src/components/post-form-attaches.vue b/packages/client/src/components/post-form-attaches.vue index 0c8181b481..9dd69a0ee5 100644 --- a/packages/client/src/components/post-form-attaches.vue +++ b/packages/client/src/components/post-form-attaches.vue @@ -127,7 +127,7 @@ export default defineComponent({ text: this.$ts.attachCancel, icon: 'fas fa-times-circle', action: () => { this.detachMedia(file.id) } - }], ev.currentTarget || ev.target).then(() => this.menu = null); + }], ev.currentTarget ?? ev.target).then(() => this.menu = null); } } }); diff --git a/packages/client/src/components/post-form.vue b/packages/client/src/components/post-form.vue index 2eda97e14d..535218ecf9 100644 --- a/packages/client/src/components/post-form.vue +++ b/packages/client/src/components/post-form.vue @@ -8,28 +8,28 @@ > <header> <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"/> </button> <div> <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> - <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 === 'home'"><i class="fas fa-home"></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> </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> </div> </header> <div class="form" :class="{ fixed }"> <XNoteSimple v-if="reply" class="preview" :note="reply"/> <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"> - <span style="margin-right: 8px;">{{ i18n.locale.recipient }}</span> + <span style="margin-right: 8px;">{{ i18n.ts.recipient }}</span> <div class="visibleUsers"> <span v-for="u in visibleUsers" :key="u.id"> <MkAcct :user="u"/> @@ -38,21 +38,21 @@ <button class="_buttonPrimary" @click="addVisibleUser"><i class="fas fa-plus fa-fw"></i></button> </div> </div> - <MkInfo v-if="hasNotSpecifiedMentions" warn class="hasNotSpecifiedMentions">{{ i18n.locale.notSpecifiedMentionWarning }} - <button class="_textButton" @click="addMissingMention()">{{ i18n.locale.add }}</button></MkInfo> - <input v-show="useCw" ref="cwInputEl" v-model="cw" class="cw" :placeholder="i18n.locale.annotation" @keydown="onKeydown"> + <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.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"/> - <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"/> <XPollEditor v-if="poll" v-model="poll" @destroyed="poll = null"/> <XNotePreview v-if="showPreview" class="preview" :text="text"/> <footer> - <button v-tooltip="i18n.locale.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.locale.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.locale.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-if="postFormActions.length > 0" v-tooltip="i18n.locale.plugin" class="_button" @click="showActions"><i class="fas fa-plug"></i></button> + <button v-tooltip="i18n.ts.attachFile" class="_button" @click="chooseFileFrom"><i class="fas fa-photo-video"></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.ts.useCw" class="_button" :class="{ active: useCw }" @click="useCw = !useCw"><i class="fas fa-eye-slash"></i></button> + <button v-tooltip="i18n.ts.mention" class="_button" @click="insertMention"><i class="fas fa-at"></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.ts.emoji" class="_button" @click="insertEmoji"><i class="fas fa-laugh-squint"></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> <datalist id="hashtags"> <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 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 visibleUsers = $ref(props.initialVisibleUsers ?? []); +let visibleUsers = $ref([]); +if (props.initialVisibleUsers) { + props.initialVisibleUsers.forEach(pushVisibleUser); +} let autocomplete = $ref(null); let draghover = $ref(false); let quoteId = $ref(null); @@ -165,19 +168,19 @@ const draftKey = $computed((): string => { const placeholder = $computed((): string => { if (props.renote) { - return i18n.locale._postForm.quotePlaceholder; + return i18n.ts._postForm.quotePlaceholder; } else if (props.reply) { - return i18n.locale._postForm.replyPlaceholder; + return i18n.ts._postForm.replyPlaceholder; } else if (props.channel) { - return i18n.locale._postForm.channelPlaceholder; + return i18n.ts._postForm.channelPlaceholder; } else { const xs = [ - i18n.locale._postForm._placeholders.a, - i18n.locale._postForm._placeholders.b, - i18n.locale._postForm._placeholders.c, - i18n.locale._postForm._placeholders.d, - i18n.locale._postForm._placeholders.e, - i18n.locale._postForm._placeholders.f + i18n.ts._postForm._placeholders.a, + i18n.ts._postForm._placeholders.b, + i18n.ts._postForm._placeholders.c, + i18n.ts._postForm._placeholders.d, + i18n.ts._postForm._placeholders.e, + i18n.ts._postForm._placeholders.f ]; return xs[Math.floor(Math.random() * xs.length)]; } @@ -185,10 +188,10 @@ const placeholder = $computed((): string => { const submitText = $computed((): string => { return props.renote - ? i18n.locale.quote + ? i18n.ts.quote : props.reply - ? i18n.locale.reply - : i18n.locale.note; + ? i18n.ts.reply + : i18n.ts.note; }); const textLength = $computed((): number => { @@ -262,12 +265,12 @@ if (props.reply && ['home', 'followers', 'specified'].includes(props.reply.visib os.api('users/show', { userIds: props.reply.visibleUserIds.filter(uid => uid !== $i.id && uid !== props.reply.userId) }).then(users => { - visibleUsers.push(...users); + users.forEach(pushVisibleUser); }); if (props.reply.userId !== $i.id) { 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) { visibility = 'specified'; - visibleUsers.push(props.specified); + pushVisibleUser(props.specified); } // keep cw when reply @@ -342,7 +345,7 @@ function focus() { } 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_) { files.push(file); } @@ -397,9 +400,15 @@ function setVisibility() { }, 'closed'); } +function pushVisibleUser(user) { + if (!visibleUsers.some(u => u.username === user.username && u.host === user.host)) { + visibleUsers.push(user); + } +} + function addVisibleUser() { os.selectUser().then(user => { - visibleUsers.push(user); + pushVisibleUser(user); }); } @@ -447,7 +456,7 @@ async function onPaste(e: ClipboardEvent) { os.confirm({ type: 'info', - text: i18n.locale.quoteQuestion, + text: i18n.ts.quoteQuestion, }).then(({ canceled }) => { if (canceled) { insertTextAtCursor(textareaEl, paste); @@ -540,8 +549,8 @@ async function post() { }; if (withHashtags && hashtags && hashtags.trim() !== '') { - const hashtags = hashtags.trim().split(' ').map(x => x.startsWith('#') ? x : '#' + x).join(' '); - data.text = data.text ? `${data.text} ${hashtags}` : hashtags; + const hashtags_ = hashtags.trim().split(' ').map(x => x.startsWith('#') ? x : '#' + x).join(' '); + data.text = data.text ? `${data.text} ${hashtags_}` : hashtags_; } // plugin @@ -565,9 +574,9 @@ async function post() { deleteDraft(); emit('posted'); 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[]; - localStorage.setItem('hashtags', JSON.stringify(unique(hashtags.concat(history)))); + localStorage.setItem('hashtags', JSON.stringify(unique(hashtags_.concat(history)))); } posting = false; postAccount = null; @@ -592,7 +601,7 @@ function insertMention() { } async function insertEmoji(ev: MouseEvent) { - os.openEmojiPicker(ev.currentTarget || ev.target, {}, textareaEl); + os.openEmojiPicker(ev.currentTarget ?? ev.target, {}, textareaEl); } function showActions(ev) { @@ -605,7 +614,7 @@ function showActions(ev) { if (key === 'text') { text = value; } }); } - })), ev.currentTarget || ev.target); + })), ev.currentTarget ?? ev.target); } let postAccount = $ref<misskey.entities.UserDetailed | null>(null); diff --git a/packages/client/src/components/reaction-tooltip.vue b/packages/client/src/components/reaction-tooltip.vue index 1b2a024e21..b53061df48 100644 --- a/packages/client/src/components/reaction-tooltip.vue +++ b/packages/client/src/components/reaction-tooltip.vue @@ -1,5 +1,5 @@ <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"> <XReactionIcon :reaction="reaction" :custom-emojis="emojis" class="icon" :no-style="true"/> <div class="name">{{ reaction.replace('@.', '') }}</div> @@ -15,11 +15,11 @@ import XReactionIcon from './reaction-icon.vue'; const props = defineProps<{ reaction: string; emojis: any[]; // TODO - source: any; // TODO + targetElement: HTMLElement; }>(); const emit = defineEmits<{ - (e: 'closed'): void; + (ev: 'closed'): void; }>(); </script> diff --git a/packages/client/src/components/reactions-viewer.details.vue b/packages/client/src/components/reactions-viewer.details.vue index 8cec8dfa2f..eb889c4888 100644 --- a/packages/client/src/components/reactions-viewer.details.vue +++ b/packages/client/src/components/reactions-viewer.details.vue @@ -1,5 +1,5 @@ <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="reaction"> <XReactionIcon :reaction="reaction" :custom-emojis="emojis" class="icon" :no-style="true"/> @@ -26,11 +26,11 @@ const props = defineProps<{ users: any[]; // TODO count: number; emojis: any[]; // TODO - source: any; // TODO + targetElement: HTMLElement; }>(); const emit = defineEmits<{ - (e: 'closed'): void; + (ev: 'closed'): void; }>(); </script> diff --git a/packages/client/src/components/renote-button.vue b/packages/client/src/components/renote-button.vue index 446686de10..c1c0d285e1 100644 --- a/packages/client/src/components/renote-button.vue +++ b/packages/client/src/components/renote-button.vue @@ -59,7 +59,7 @@ export default defineComponent({ const renote = (viaKeyboard = false) => { pleaseLogin(); os.popupMenu([{ - text: i18n.locale.renote, + text: i18n.ts.renote, icon: 'fas fa-retweet', action: () => { os.api('notes/create', { @@ -67,7 +67,7 @@ export default defineComponent({ }); } }, { - text: i18n.locale.quote, + text: i18n.ts.quote, icon: 'fas fa-quote-right', action: () => { os.post({ diff --git a/packages/client/src/components/renote.details.vue b/packages/client/src/components/renote.details.vue index cdbc71bdce..2df19bcd3f 100644 --- a/packages/client/src/components/renote.details.vue +++ b/packages/client/src/components/renote.details.vue @@ -1,5 +1,5 @@ <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 v-for="u in users" :key="u.id" class="user"> <MkAvatar class="avatar" :user="u"/> @@ -17,11 +17,11 @@ import MkTooltip from './ui/tooltip.vue'; const props = defineProps<{ users: any[]; // TODO count: number; - source: any; // TODO + targetElement: HTMLElement; }>(); const emit = defineEmits<{ - (e: 'closed'): void; + (ev: 'closed'): void; }>(); </script> diff --git a/packages/client/src/components/sample.vue b/packages/client/src/components/sample.vue index 03ad6a9838..65249ff7e9 100644 --- a/packages/client/src/components/sample.vue +++ b/packages/client/src/components/sample.vue @@ -109,7 +109,7 @@ export default defineComponent({ text: 'Delete some bananas', danger: true, action: () => {}, - }], ev.currentTarget || ev.target); + }], ev.currentTarget ?? ev.target); }, } }); diff --git a/packages/client/src/components/ui/context-menu.vue b/packages/client/src/components/ui/context-menu.vue index 85606bf6d5..f491b43b46 100644 --- a/packages/client/src/components/ui/context-menu.vue +++ b/packages/client/src/components/ui/context-menu.vue @@ -1,88 +1,71 @@ <template> <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')"/> </div> </transition> </template> -<script lang="ts"> -import { defineComponent } from 'vue'; +<script lang="ts" setup> +import { onMounted, onBeforeUnmount } from 'vue'; import contains from '@/scripts/contains'; import MkMenu from './menu.vue'; +import { MenuItem } from './types/menu.vue'; import * as os from '@/os'; -export default defineComponent({ - components: { - MkMenu, - }, - 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 props = defineProps<{ + items: MenuItem[]; + ev: MouseEvent; +}>(); - const width = this.$el.offsetWidth; - const height = this.$el.offsetHeight; +const emit = defineEmits<{ + (e: 'closed'): void; +}>(); - if (left + width - window.pageXOffset > window.innerWidth) { - left = window.innerWidth - width + window.pageXOffset; - } +let rootEl = $ref<HTMLDivElement>(); - if (top + height - window.pageYOffset > window.innerHeight) { - top = window.innerHeight - height + window.pageYOffset; - } +let zIndex = $ref<number>(os.claimZIndex('high')); - if (top < 0) { - top = 0; - } +onMounted(() => { + let left = props.ev.pageX + 1; // 間違って右ダブルクリックした場合に意図せずアイテムがクリックされるのを防ぐため + 1 + let top = props.ev.pageY + 1; // 間違って右ダブルクリックした場合に意図せずアイテムがクリックされるのを防ぐため + 1 - if (left < 0) { - left = 0; - } + const width = rootEl.offsetWidth; + const height = rootEl.offsetHeight; - this.$el.style.top = top + 'px'; - this.$el.style.left = left + 'px'; + if (left + width - window.pageXOffset > window.innerWidth) { + left = window.innerWidth - width + window.pageXOffset; + } - for (const el of Array.from(document.querySelectorAll('body *'))) { - el.addEventListener('mousedown', this.onMousedown); - } - }, - beforeUnmount() { - for (const el of Array.from(document.querySelectorAll('body *'))) { - el.removeEventListener('mousedown', this.onMousedown); - } - }, - methods: { - onMousedown(e) { - if (!contains(this.$el, e.target) && (this.$el != e.target)) this.$emit('closed'); - }, + if (top + height - window.pageYOffset > window.innerHeight) { + top = window.innerHeight - height + window.pageYOffset; + } + + if (top < 0) { + top = 0; + } + + if (left < 0) { + left = 0; + } + + 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> <style lang="scss" scoped> diff --git a/packages/client/src/components/ui/menu.vue b/packages/client/src/components/ui/menu.vue index 41165c8d33..a93cc8cda8 100644 --- a/packages/client/src/components/ui/menu.vue +++ b/packages/client/src/components/ui/menu.vue @@ -1,8 +1,8 @@ <template> -<div ref="items" v-hotkey="keymap" +<div ref="itemsEl" v-hotkey="keymap" class="rrevdjwt" :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()" > <template v-for="(item, i) in items2"> @@ -28,6 +28,9 @@ <MkAvatar :user="item.user" class="avatar"/><MkUserName :user="item.user"/> <span v-if="item.indicate" class="indicator"><i class="fas fa-circle"></i></span> </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)"> <i v-if="item.icon" class="fa-fw" :class="item.icon"></i> <MkAvatar v-if="item.avatar" :user="item.avatar" class="avatar"/> @@ -41,114 +44,78 @@ </div> </template> -<script lang="ts"> -import { defineComponent, ref, unref } from 'vue'; +<script lang="ts" setup> +import { nextTick, onMounted, watch } from 'vue'; 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({ - props: { - items: { - type: Array, - required: true - }, - viaKeyboard: { - 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)); +const props = defineProps<{ + items: MenuItem[]; + viaKeyboard?: boolean; + asDrawer?: boolean; + align?: 'center' | string; + width?: number; + maxHeight?: number; +}>(); - for (let i = 0; i < items.value.length; i++) { - const item = items.value[i]; - - if (item && item.then) { // if item is Promise - items.value[i] = { type: 'pending' }; - item.then(actualItem => { - items.value[i] = actualItem; - }); - } - } +const emit = defineEmits<{ + (e: 'close'): void; +}>(); - this.items2 = items; - }, - immediate: true - } - }, - mounted() { - if (this.viaKeyboard) { - this.$nextTick(() => { - focusNext(this.$refs.items.children[0], true, false); +let itemsEl = $ref<HTMLDivElement>(); + +let items2: InnerMenuItem[] = $ref([]); + +let keymap = $computed(() => ({ + 'up|k|shift+tab': focusUp, + 'down|j|tab': focusDown, + 'esc': close, +})); + +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) { - this.$el.style.top = this.contextmenuEvent.pageY + 'px'; - this.$el.style.left = this.contextmenuEvent.pageX + 'px'; + items2 = items as InnerMenuItem[]; +}, { + immediate: true, +}); - for (const el of Array.from(document.querySelectorAll('body *'))) { - el.addEventListener('mousedown', this.onMousedown); - } - } - }, - 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(); - }, +onMounted(() => { + if (props.viaKeyboard) { + nextTick(() => { + focusNext(itemsEl.children[0], true, false); + }); } }); + +function clicked(fn: MenuAction, ev: MouseEvent) { + fn(ev); + close(); +} + +function close() { + emit('close'); +} + +function focusUp() { + focusPrev(document.activeElement); +} + +function focusDown() { + focusNext(document.activeElement); +} </script> <style lang="scss" scoped> diff --git a/packages/client/src/components/ui/modal.vue b/packages/client/src/components/ui/modal.vue index c691c8c6d0..3c3bb5c226 100644 --- a/packages/client/src/components/ui/modal.vue +++ b/packages/client/src/components/ui/modal.vue @@ -1,5 +1,5 @@ <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 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"> @@ -9,8 +9,8 @@ </transition> </template> -<script lang="ts"> -import { defineComponent, nextTick, onMounted, computed, PropType, ref, watch } from 'vue'; +<script lang="ts" setup> +import { nextTick, onMounted, computed, ref, watch, provide } from 'vue'; import * as os from '@/os'; import { isTouchUsing } from '@/scripts/touch'; import { defaultStore } from '@/store'; @@ -25,234 +25,206 @@ function getFixedContainer(el: Element | null): Element | null { } } -export default defineComponent({ - provide: { - modal: true - }, +type ModalTypes = 'popup' | 'dialog' | 'dialog:top' | 'drawer'; - props: { - manualShowing: { - type: Boolean, - required: false, - default: null, - }, - srcCenter: { - type: Boolean, - required: false - }, - src: { - type: Object as PropType<HTMLElement>, - required: false, - default: null, - }, - 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, - }, - }, +const props = withDefaults(defineProps<{ + manualShowing?: boolean | null; + srcCenter?: boolean; + src?: HTMLElement; + preferType?: ModalTypes | 'auto'; + zPriority?: 'low' | 'middle' | 'high'; + noOverlap?: boolean; + transparentBg?: boolean; +}>(), { + manualShowing: null, + src: null, + preferType: 'auto', + zPriority: 'low', + noOverlap: true, + transparentBg: 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) { - 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; +provide('modal', true); - const close = () => { - // eslint-disable-next-line vue/no-mutating-props - if (props.src) props.src.style.pointerEvents = 'auto'; - showing.value = false; - context.emit('close'); - }; +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!; + } +}); - const onBgClick = () => { - if (contentClicking) return; - context.emit('click'); - }; +let contentClicking = false; - if (type.value === 'drawer') { - maxHeight.value = window.innerHeight / 2; +const close = () => { + // 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'), - }; - - 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; - } - - // 画面から縦にはみ出る場合 - 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; - } + // 画面から縦にはみ出る場合 + 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 { - // 画面から横にはみ出る場合 - if (left + width - window.pageXOffset > window.innerWidth) { - left = window.innerWidth - width + window.pageXOffset - 1; + top = (window.innerHeight - MARGIN) - height; + } + } + } 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 { - transformOrigin.value = 'center'; + top = (window.innerHeight - MARGIN) - height + window.pageYOffset - 1; } + } + } - popover.style.left = left + 'px'; - popover.style.top = top + 'px'; - }; + if (top < 0) { + top = MARGIN; + } - const childRendered = () => { - // モーダルコンテンツにマウスボタンが押され、コンテンツ外でマウスボタンが離されたときにモーダルバックグラウンドクリックと判定させないためにマウスイベントを監視しフラグ管理する - 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 }); - }; + if (left < 0) { + left = 0; + } - onMounted(() => { - watch(() => props.src, async () => { - if (props.src) { - // eslint-disable-next-line vue/no-mutating-props - props.src.style.pointerEvents = 'none'; - } - fixed.value = (type.value === 'drawer') || (getFixedContainer(props.src) != null); + 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 { + transformOrigin.value = 'center'; + } - await nextTick() - - align(); - }, { immediate: true, }); + popover.style.left = left + 'px'; + popover.style.top = top + 'px'; +}; - nextTick(() => { - const popover = content.value; - new ResizeObserver((entries, observer) => { - align(); - }).observe(popover!); - }); - }); +const childRendered = () => { + // モーダルコンテンツにマウスボタンが押され、コンテンツ外でマウスボタンが離されたときにモーダルバックグラウンドクリックと判定させないためにマウスイベントを監視しフラグ管理する + const el = content.value!.children[0]; + el.addEventListener('mousedown', ev => { + contentClicking = true; + window.addEventListener('mouseup', ev => { + // click イベントより先に mouseup イベントが発生するかもしれないのでちょっと待つ + window.setTimeout(() => { + contentClicking = false; + }, 100); + }, { passive: true, once: true }); + }, { passive: true }); +}; - return { - showing, - type, - fixed, - content, - transformOrigin, - maxHeight, - close, - zIndex, - keymap, - onBgClick, - childRendered, - }; - }, +onMounted(() => { + watch(() => props.src, async () => { + if (props.src) { + // eslint-disable-next-line vue/no-mutating-props + props.src.style.pointerEvents = 'none'; + } + fixed.value = (type.value === 'drawer') || (getFixedContainer(props.src) != null); + + await nextTick() + + align(); + }, { immediate: true, }); + + nextTick(() => { + const popover = content.value; + new ResizeObserver((entries, observer) => { + align(); + }).observe(popover!); + }); +}); + +defineExpose({ + close, }); </script> diff --git a/packages/client/src/components/ui/popup-menu.vue b/packages/client/src/components/ui/popup-menu.vue index 8ffc4ad195..8d6c1b5695 100644 --- a/packages/client/src/components/ui/popup-menu.vue +++ b/packages/client/src/components/ui/popup-menu.vue @@ -1,44 +1,28 @@ <template> -<MkModal ref="modal" v-slot="{ type, maxHeight }" :z-priority="'high'" :src="src" :transparent-bg="true" @click="$refs.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()"/> +<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="modal.close()"/> </MkModal> </template> -<script lang="ts"> -import { defineComponent } from 'vue'; +<script lang="ts" setup> +import { } from 'vue'; import MkModal from './modal.vue'; import MkMenu from './menu.vue'; +import { MenuItem } from '@/types/menu'; -export default defineComponent({ - components: { - MkModal, - MkMenu, - }, +defineProps<{ + items: MenuItem[]; + align?: 'center' | string; + width?: number; + viaKeyboard?: boolean; + src?: any; +}>(); - props: { - items: { - type: Array, - required: true - }, - align: { - type: String, - required: false - }, - width: { - type: Number, - required: false - }, - viaKeyboard: { - type: Boolean, - required: false - }, - src: { - required: false - }, - }, +const emit = defineEmits<{ + (e: 'closed'): void; +}>(); - emits: ['close', 'closed'], -}); +let modal = $ref<InstanceType<typeof MkModal>>(); </script> <style lang="scss" scoped> diff --git a/packages/client/src/components/ui/tooltip.vue b/packages/client/src/components/ui/tooltip.vue index 394b068352..1892877cc1 100644 --- a/packages/client/src/components/ui/tooltip.vue +++ b/packages/client/src/components/ui/tooltip.vue @@ -1,99 +1,96 @@ <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' }"> <slot>{{ text }}</slot> </div> </transition> </template> -<script lang="ts"> -import { defineComponent, nextTick, onMounted, onUnmounted, ref } from 'vue'; +<script lang="ts" setup> +import { nextTick, onMounted, onUnmounted, ref } from 'vue'; import * as os from '@/os'; -export default defineComponent({ - props: { - showing: { - type: Boolean, - required: true, - }, - source: { - required: true, - }, - text: { - type: String, - required: false - }, - maxWidth: { - type: Number, - required: false, - default: 250, - }, - }, +const props = withDefaults(defineProps<{ + showing: boolean; + targetElement?: HTMLElement; + x?: number; + y?: number; + text?: string; + maxWidth?: number; +}>(), { + maxWidth: 250, +}); - emits: ['closed'], +const emit = defineEmits<{ + (ev: 'closed'): void; +}>(); - setup(props, context) { - const el = ref<HTMLElement>(); - const zIndex = os.claimZIndex('high'); +const el = ref<HTMLElement>(); +const zIndex = os.claimZIndex('high'); - const setPosition = () => { - if (el.value == null) return; +const setPosition = () => { + 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; - const contentHeight = el.value.offsetHeight; + let left: number; + let top: number; - let left = rect.left + window.pageXOffset + (props.source.offsetWidth / 2); - let top = rect.top + window.pageYOffset - contentHeight; + let rect: DOMRect; - left -= (el.value.offsetWidth / 2); + if (props.targetElement) { + rect = props.targetElement.getBoundingClientRect(); - if (left + contentWidth - window.pageXOffset > window.innerWidth) { - left = window.innerWidth - contentWidth + window.pageXOffset - 1; - } + left = rect.left + window.pageXOffset + (props.targetElement.offsetWidth / 2); + top = rect.top + window.pageYOffset - contentHeight; - if (top - window.pageYOffset < 0) { - top = rect.top + window.pageYOffset + props.source.offsetHeight; - el.value.style.transformOrigin = 'center top'; - } + el.value.style.transformOrigin = 'center bottom'; + } else { + left = props.x; + top = props.y - contentHeight; + } - el.value.style.left = left + 'px'; - el.value.style.top = top + 'px'; - }; + left -= (el.value.offsetWidth / 2); - onMounted(() => { - nextTick(() => { - if (props.source == null) { - context.emit('closed'); - return; - } + if (left + contentWidth - window.pageXOffset > window.innerWidth) { + left = window.innerWidth - contentWidth + window.pageXOffset - 1; + } + // ツールチップを上に向かって表示するスペースがなければ下に向かって出す + 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(); - - let loopHandler; - - const loop = () => { - loopHandler = window.requestAnimationFrame(() => { - setPosition(); - loop(); - }); - }; - loop(); - - onUnmounted(() => { - window.cancelAnimationFrame(loopHandler); - }); }); - }); - - return { - el, - zIndex, }; - }, -}) + + loop(); + + onUnmounted(() => { + window.cancelAnimationFrame(loopHandler); + }); + }); +}); </script> <style lang="scss" scoped> @@ -118,6 +115,6 @@ export default defineComponent({ border-radius: 4px; border: solid 0.5px var(--divider); pointer-events: none; - transform-origin: center bottom; + transform-origin: center center; } </style> diff --git a/packages/client/src/components/user-online-indicator.vue b/packages/client/src/components/user-online-indicator.vue index a87b0aeff5..a4f6f80383 100644 --- a/packages/client/src/components/user-online-indicator.vue +++ b/packages/client/src/components/user-online-indicator.vue @@ -13,10 +13,10 @@ const props = defineProps<{ const text = $computed(() => { switch (props.user.onlineStatus) { - case 'online': return i18n.locale.online; - case 'active': return i18n.locale.active; - case 'offline': return i18n.locale.offline; - case 'unknown': return i18n.locale.unknown; + case 'online': return i18n.ts.online; + case 'active': return i18n.ts.active; + case 'offline': return i18n.ts.offline; + case 'unknown': return i18n.ts.unknown; } }); </script> diff --git a/packages/client/src/directives/tooltip.ts b/packages/client/src/directives/tooltip.ts index fffde14874..dd715227a4 100644 --- a/packages/client/src/directives/tooltip.ts +++ b/packages/client/src/directives/tooltip.ts @@ -48,7 +48,7 @@ export default { popup(import('@/components/ui/tooltip.vue'), { showing, text: self.text, - source: el + targetElement: el, }, {}, 'closed'); self._close = () => { @@ -56,8 +56,8 @@ export default { }; }; - el.addEventListener('selectstart', e => { - e.preventDefault(); + el.addEventListener('selectstart', ev => { + ev.preventDefault(); }); el.addEventListener(start, () => { diff --git a/packages/client/src/init.ts b/packages/client/src/init.ts index af70aec70a..81e41febd1 100644 --- a/packages/client/src/init.ts +++ b/packages/client/src/init.ts @@ -185,7 +185,7 @@ app.config.globalProperties = { $store: defaultStore, $instance: instance, $t: i18n.t, - $ts: i18n.locale, + $ts: i18n.ts, }; app.use(router); @@ -299,8 +299,8 @@ stream.on('_disconnected_', async () => { reloadDialogShowing = true; const { canceled } = await confirm({ type: 'warning', - title: i18n.locale.disconnectedFromServer, - text: i18n.locale.reloadConfirm, + title: i18n.ts.disconnectedFromServer, + text: i18n.ts.reloadConfirm, }); reloadDialogShowing = false; if (!canceled) { @@ -324,7 +324,7 @@ if ($i) { if ($i.isDeleted) { alert({ type: 'warning', - text: i18n.locale.accountDeletionInProgress, + text: i18n.ts.accountDeletionInProgress, }); } diff --git a/packages/client/src/menu.ts b/packages/client/src/menu.ts index 184779f21f..ebc7898101 100644 --- a/packages/client/src/menu.ts +++ b/packages/client/src/menu.ts @@ -73,12 +73,12 @@ export const menuDef = reactive({ })), null, { type: 'link', to: '/my/lists', - text: i18n.locale.manageLists, + text: i18n.ts.manageLists, icon: 'fas fa-cog', }]; items.value = _items; }); - os.popupMenu(items, ev.currentTarget || ev.target); + os.popupMenu(items, ev.currentTarget ?? ev.target); }, }, groups: { @@ -104,12 +104,12 @@ export const menuDef = reactive({ })), null, { type: 'link', to: '/my/antennas', - text: i18n.locale.manageAntennas, + text: i18n.ts.manageAntennas, icon: 'fas fa-cog', }]; items.value = _items; }); - os.popupMenu(items, ev.currentTarget || ev.target); + os.popupMenu(items, ev.currentTarget ?? ev.target); }, }, mentions: { @@ -173,34 +173,34 @@ export const menuDef = reactive({ icon: 'fas fa-columns', action: (ev) => { os.popupMenu([{ - text: i18n.locale.default, + text: i18n.ts.default, active: ui === 'default' || ui === null, action: () => { localStorage.setItem('ui', 'default'); unisonReload(); } }, { - text: i18n.locale.deck, + text: i18n.ts.deck, active: ui === 'deck', action: () => { localStorage.setItem('ui', 'deck'); unisonReload(); } }, { - text: i18n.locale.classic, + text: i18n.ts.classic, active: ui === 'classic', action: () => { localStorage.setItem('ui', 'classic'); unisonReload(); } }, /*{ - text: i18n.locale.desktop + ' (β)', + text: i18n.ts.desktop + ' (β)', active: ui === 'desktop', action: () => { localStorage.setItem('ui', 'desktop'); unisonReload(); } - }*/], ev.currentTarget || ev.target); + }*/], ev.currentTarget ?? ev.target); }, }, }); diff --git a/packages/client/src/os.ts b/packages/client/src/os.ts index c16ea717ad..95b4e87a1f 100644 --- a/packages/client/src/os.ts +++ b/packages/client/src/os.ts @@ -7,8 +7,10 @@ import * as Misskey from 'misskey-js'; import { apiUrl, url } from '@/config'; import MkPostFormDialog from '@/components/post-form-dialog.vue'; import MkWaitingDialog from '@/components/waiting-dialog.vue'; +import { MenuItem } from '@/types/menu'; import { resolve } from '@/router'; import { $i } from '@/account'; +import { defaultStore } from '@/store'; 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) => { popup(import('@/components/emoji-picker-dialog.vue'), { 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; width?: number; 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(); return new Promise((resolve, reject) => { let dispose; @@ -541,7 +543,7 @@ export const uploads = ref<{ 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; return new Promise((resolve, reject) => { @@ -559,6 +561,8 @@ export function upload(file: File, folder?: any, name?: string): Promise<Misskey uploads.value.push(ctx); + console.log(keepOriginal); + const data = new FormData(); data.append('i', $i.token); data.append('force', 'true'); diff --git a/packages/client/src/pages/_error_.vue b/packages/client/src/pages/_error_.vue index 7540995707..4cfe2e255c 100644 --- a/packages/client/src/pages/_error_.vue +++ b/packages/client/src/pages/_error_.vue @@ -3,15 +3,15 @@ <transition :name="$store.state.animation ? 'zoom' : ''" appear> <div v-show="loaded" class="mjndxjch"> <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 v-if="meta && (version === meta.version)">{{ i18n.locale.pageLoadErrorDescription }}</p> - <p v-else-if="serverIsDead">{{ i18n.locale.serverIsDead }}</p> + <p><b><i class="fas fa-exclamation-triangle"></i> {{ i18n.ts.pageLoadError }}</b></p> + <p v-if="meta && (version === meta.version)">{{ i18n.ts.pageLoadErrorDescription }}</p> + <p v-else-if="serverIsDead">{{ i18n.ts.serverIsDead }}</p> <template v-else> - <p>{{ i18n.locale.newVersionOfClientAvailable }}</p> - <p>{{ i18n.locale.youShouldUpgradeClient }}</p> - <MkButton class="button primary" @click="reload">{{ i18n.locale.reload }}</MkButton> + <p>{{ i18n.ts.newVersionOfClientAvailable }}</p> + <p>{{ i18n.ts.youShouldUpgradeClient }}</p> + <MkButton class="button primary" @click="reload">{{ i18n.ts.reload }}</MkButton> </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> </div> </transition> @@ -54,7 +54,7 @@ function reload() { defineExpose({ [symbols.PAGE_INFO]: { - title: i18n.locale.error, + title: i18n.ts.error, icon: 'fas fa-exclamation-triangle', }, }); diff --git a/packages/client/src/pages/about-misskey.vue b/packages/client/src/pages/about-misskey.vue index f887e29cc0..0ffb6b9e1d 100644 --- a/packages/client/src/pages/about-misskey.vue +++ b/packages/client/src/pages/about-misskey.vue @@ -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> </div> <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 class="_formBlock" style="text-align: center;"> <MkButton primary rounded inline @click="iLoveMisskey">I <Mfm text="$[jelly ❤]"/> #Misskey</MkButton> @@ -19,23 +19,23 @@ <div class="_formLinks"> <FormLink to="https://github.com/misskey-dev/misskey" external> <template #icon><i class="fas fa-code"></i></template> - {{ i18n.locale._aboutMisskey.source }} + {{ i18n.ts._aboutMisskey.source }} <template #suffix>GitHub</template> </FormLink> <FormLink to="https://crowdin.com/project/misskey" external> <template #icon><i class="fas fa-language"></i></template> - {{ i18n.locale._aboutMisskey.translation }} + {{ i18n.ts._aboutMisskey.translation }} <template #suffix>Crowdin</template> </FormLink> <FormLink to="https://www.patreon.com/syuilo" external> <template #icon><i class="fas fa-hand-holding-medical"></i></template> - {{ i18n.locale._aboutMisskey.donate }} + {{ i18n.ts._aboutMisskey.donate }} <template #suffix>Patreon</template> </FormLink> </div> </FormSection> <FormSection> - <template #label>{{ i18n.locale._aboutMisskey.contributors }}</template> + <template #label>{{ i18n.ts._aboutMisskey.contributors }}</template> <div class="_formLinks"> <FormLink to="https://github.com/syuilo" external>@syuilo</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/marihachi" external>@marihachi</FormLink> </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> - <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> - <template #caption>{{ i18n.locale._aboutMisskey.morePatrons }}</template> + <template #caption>{{ i18n.ts._aboutMisskey.morePatrons }}</template> </FormSection> </div> </MkSpacer> @@ -194,7 +194,7 @@ onBeforeUnmount(() => { defineExpose({ [symbols.PAGE_INFO]: { - title: i18n.locale.aboutMisskey, + title: i18n.ts.aboutMisskey, icon: null, bg: 'var(--bg)', }, diff --git a/packages/client/src/pages/about.vue b/packages/client/src/pages/about.vue index a5984c548d..d5bab4baf8 100644 --- a/packages/client/src/pages/about.vue +++ b/packages/client/src/pages/about.vue @@ -90,7 +90,7 @@ const initStats = () => os.api('stats', { defineExpose({ [symbols.PAGE_INFO]: { - title: i18n.locale.instanceInfo, + title: i18n.ts.instanceInfo, icon: 'fas fa-info-circle', bg: 'var(--bg)', }, diff --git a/packages/client/src/pages/admin/emojis.vue b/packages/client/src/pages/admin/emojis.vue index 5b1dfe565a..a080ee9c23 100644 --- a/packages/client/src/pages/admin/emojis.vue +++ b/packages/client/src/pages/admin/emojis.vue @@ -118,7 +118,7 @@ const toggleSelect = (emoji) => { }; 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', { fileId: file.id, @@ -157,23 +157,23 @@ const remoteMenu = (emoji, ev: MouseEvent) => { type: 'label', text: ':' + emoji.name + ':', }, { - text: i18n.locale.import, + text: i18n.ts.import, icon: 'fas fa-plus', action: () => { im(emoji) } - }], ev.currentTarget || ev.target); + }], ev.currentTarget ?? ev.target); }; const menu = (ev: MouseEvent) => { os.popupMenu([{ icon: 'fas fa-download', - text: i18n.locale.export, + text: i18n.ts.export, action: async () => { os.api('export-custom-emojis', { }) .then(() => { os.alert({ type: 'info', - text: i18n.locale.exportRequested, + text: i18n.ts.exportRequested, }); }).catch((e) => { os.alert({ @@ -184,16 +184,16 @@ const menu = (ev: MouseEvent) => { } }, { icon: 'fas fa-upload', - text: i18n.locale.import, + text: i18n.ts.import, action: async () => { - const file = await selectFile(ev.currentTarget || ev.target); + const file = await selectFile(ev.currentTarget ?? ev.target); os.api('admin/emoji/import-zip', { fileId: file.id, }) .then(() => { os.alert({ type: 'info', - text: i18n.locale.importRequested, + text: i18n.ts.importRequested, }); }).catch((e) => { os.alert({ @@ -202,7 +202,7 @@ const menu = (ev: MouseEvent) => { }); }); } - }], ev.currentTarget || ev.target); + }], ev.currentTarget ?? ev.target); }; const setCategoryBulk = async () => { @@ -256,7 +256,7 @@ const setTagBulk = async () => { const delBulk = async () => { const { canceled } = await os.confirm({ type: 'warning', - text: i18n.locale.deleteConfirm, + text: i18n.ts.deleteConfirm, }); if (canceled) return; await os.apiWithDialog('admin/emoji/delete-bulk', { @@ -267,13 +267,13 @@ const delBulk = async () => { defineExpose({ [symbols.PAGE_INFO]: computed(() => ({ - title: i18n.locale.customEmojis, + title: i18n.ts.customEmojis, icon: 'fas fa-laugh', bg: 'var(--bg)', actions: [{ asFullButton: true, icon: 'fas fa-plus', - text: i18n.locale.addEmoji, + text: i18n.ts.addEmoji, handler: add, }, { icon: 'fas fa-ellipsis-h', @@ -281,11 +281,11 @@ defineExpose({ }], tabs: [{ active: tab.value === 'local', - title: i18n.locale.local, + title: i18n.ts.local, onClick: () => { tab.value = 'local'; }, }, { active: tab.value === 'remote', - title: i18n.locale.remote, + title: i18n.ts.remote, onClick: () => { tab.value = 'remote'; }, },] })), diff --git a/packages/client/src/pages/admin/index.vue b/packages/client/src/pages/admin/index.vue index 350e7defc6..6b11650f48 100644 --- a/packages/client/src/pages/admin/index.vue +++ b/packages/client/src/pages/admin/index.vue @@ -55,7 +55,7 @@ export default defineComponent({ setup(props, context) { const indexInfo = { - title: i18n.locale.controlPanel, + title: i18n.ts.controlPanel, icon: 'fas fa-cog', bg: 'var(--bg)', hideHeader: true, @@ -91,119 +91,119 @@ export default defineComponent({ }); const menuDef = computed(() => [{ - title: i18n.locale.quickAction, + title: i18n.ts.quickAction, items: [{ type: 'button', icon: 'fas fa-search', - text: i18n.locale.lookup, + text: i18n.ts.lookup, action: lookup, }, ...(instance.disableRegistration ? [{ type: 'button', icon: 'fas fa-user', - text: i18n.locale.invite, + text: i18n.ts.invite, action: invite, }] : [])], }, { - title: i18n.locale.administration, + title: i18n.ts.administration, items: [{ icon: 'fas fa-tachometer-alt', - text: i18n.locale.dashboard, + text: i18n.ts.dashboard, to: '/admin/overview', active: page.value === 'overview', }, { icon: 'fas fa-users', - text: i18n.locale.users, + text: i18n.ts.users, to: '/admin/users', active: page.value === 'users', }, { icon: 'fas fa-laugh', - text: i18n.locale.customEmojis, + text: i18n.ts.customEmojis, to: '/admin/emojis', active: page.value === 'emojis', }, { icon: 'fas fa-globe', - text: i18n.locale.federation, + text: i18n.ts.federation, to: '/admin/federation', active: page.value === 'federation', }, { icon: 'fas fa-clipboard-list', - text: i18n.locale.jobQueue, + text: i18n.ts.jobQueue, to: '/admin/queue', active: page.value === 'queue', }, { icon: 'fas fa-cloud', - text: i18n.locale.files, + text: i18n.ts.files, to: '/admin/files', active: page.value === 'files', }, { icon: 'fas fa-broadcast-tower', - text: i18n.locale.announcements, + text: i18n.ts.announcements, to: '/admin/announcements', active: page.value === 'announcements', }, { icon: 'fas fa-audio-description', - text: i18n.locale.ads, + text: i18n.ts.ads, to: '/admin/ads', active: page.value === 'ads', }, { icon: 'fas fa-exclamation-circle', - text: i18n.locale.abuseReports, + text: i18n.ts.abuseReports, to: '/admin/abuses', active: page.value === 'abuses', }], }, { - title: i18n.locale.settings, + title: i18n.ts.settings, items: [{ icon: 'fas fa-cog', - text: i18n.locale.general, + text: i18n.ts.general, to: '/admin/settings', active: page.value === 'settings', }, { icon: 'fas fa-envelope', - text: i18n.locale.emailServer, + text: i18n.ts.emailServer, to: '/admin/email-settings', active: page.value === 'email-settings', }, { icon: 'fas fa-cloud', - text: i18n.locale.objectStorage, + text: i18n.ts.objectStorage, to: '/admin/object-storage', active: page.value === 'object-storage', }, { icon: 'fas fa-lock', - text: i18n.locale.security, + text: i18n.ts.security, to: '/admin/security', active: page.value === 'security', }, { icon: 'fas fa-globe', - text: i18n.locale.relays, + text: i18n.ts.relays, to: '/admin/relays', active: page.value === 'relays', }, { icon: 'fas fa-share-alt', - text: i18n.locale.integration, + text: i18n.ts.integration, to: '/admin/integrations', active: page.value === 'integrations', }, { icon: 'fas fa-ban', - text: i18n.locale.instanceBlocking, + text: i18n.ts.instanceBlocking, to: '/admin/instance-block', active: page.value === 'instance-block', }, { icon: 'fas fa-ghost', - text: i18n.locale.proxyAccount, + text: i18n.ts.proxyAccount, to: '/admin/proxy-account', active: page.value === 'proxy-account', }, { icon: 'fas fa-cogs', - text: i18n.locale.other, + text: i18n.ts.other, to: '/admin/other-settings', active: page.value === 'other-settings', }], }, { - title: i18n.locale.info, + title: i18n.ts.info, items: [{ icon: 'fas fa-database', - text: i18n.locale.database, + text: i18n.ts.database, to: '/admin/database', active: page.value === 'database', }], @@ -275,37 +275,37 @@ export default defineComponent({ const lookup = (ev) => { os.popupMenu([{ - text: i18n.locale.user, + text: i18n.ts.user, icon: 'fas fa-user', action: () => { lookupUser(); } }, { - text: i18n.locale.note, + text: i18n.ts.note, icon: 'fas fa-pencil-alt', action: () => { alert('TODO'); } }, { - text: i18n.locale.file, + text: i18n.ts.file, icon: 'fas fa-cloud', action: () => { alert('TODO'); } }, { - text: i18n.locale.instance, + text: i18n.ts.instance, icon: 'fas fa-globe', action: () => { alert('TODO'); } - }], ev.currentTarget || ev.target); + }], ev.currentTarget ?? ev.target); }; return { [symbols.PAGE_INFO]: INFO, menuDef, header: { - title: i18n.locale.controlPanel, + title: i18n.ts.controlPanel, }, noMaintainerInformation, noBotProtection, diff --git a/packages/client/src/pages/channel-editor.vue b/packages/client/src/pages/channel-editor.vue index 58c644be62..3818c7481a 100644 --- a/packages/client/src/pages/channel-editor.vue +++ b/packages/client/src/pages/channel-editor.vue @@ -112,7 +112,7 @@ export default defineComponent({ }, setBannerImage(e) { - selectFile(e.currentTarget || e.target, null).then(file => { + selectFile(e.currentTarget ?? e.target, null).then(file => { this.bannerId = file.id; }); }, diff --git a/packages/client/src/pages/clip.vue b/packages/client/src/pages/clip.vue index 6b49221d32..c999f1bfc9 100644 --- a/packages/client/src/pages/clip.vue +++ b/packages/client/src/pages/clip.vue @@ -127,7 +127,7 @@ export default defineComponent({ clipId: this.clip.id, }); } - } : undefined], ev.currentTarget || ev.target); + } : undefined], ev.currentTarget ?? ev.target); } } }); diff --git a/packages/client/src/pages/drive.vue b/packages/client/src/pages/drive.vue index 1e17bea0cc..68777bb083 100644 --- a/packages/client/src/pages/drive.vue +++ b/packages/client/src/pages/drive.vue @@ -15,7 +15,7 @@ let folder = $ref(null); defineExpose({ [symbols.PAGE_INFO]: computed(() => ({ - title: folder ? folder.name : i18n.locale.drive, + title: folder ? folder.name : i18n.ts.drive, icon: 'fas fa-cloud', bg: 'var(--bg)', hideHeader: true, diff --git a/packages/client/src/pages/emojis.emoji.vue b/packages/client/src/pages/emojis.emoji.vue index 83539ce7a3..b2801694db 100644 --- a/packages/client/src/pages/emojis.emoji.vue +++ b/packages/client/src/pages/emojis.emoji.vue @@ -23,13 +23,13 @@ function menu(ev) { type: 'label', text: ':' + props.emoji.name + ':', }, { - text: i18n.locale.copy, + text: i18n.ts.copy, icon: 'fas fa-copy', action: () => { copyToClipboard(`:${props.emoji.name}:`); os.success(); } - }], ev.currentTarget || ev.target); + }], ev.currentTarget ?? ev.target); } </script> diff --git a/packages/client/src/pages/emojis.vue b/packages/client/src/pages/emojis.vue index 6577f5abd9..886b5f7119 100644 --- a/packages/client/src/pages/emojis.vue +++ b/packages/client/src/pages/emojis.vue @@ -16,14 +16,14 @@ const tab = ref('category'); function menu(ev) { os.popupMenu([{ icon: 'fas fa-download', - text: i18n.locale.export, + text: i18n.ts.export, action: async () => { os.api('export-custom-emojis', { }) .then(() => { os.alert({ type: 'info', - text: i18n.locale.exportRequested, + text: i18n.ts.exportRequested, }); }).catch((e) => { os.alert({ @@ -32,12 +32,12 @@ function menu(ev) { }); }); } - }], ev.currentTarget || ev.target); + }], ev.currentTarget ?? ev.target); } defineExpose({ [symbols.PAGE_INFO]: { - title: i18n.locale.customEmojis, + title: i18n.ts.customEmojis, icon: 'fas fa-laugh', bg: 'var(--bg)', actions: [{ diff --git a/packages/client/src/pages/favorites.vue b/packages/client/src/pages/favorites.vue index 8965b30d60..b4f6ff35bc 100644 --- a/packages/client/src/pages/favorites.vue +++ b/packages/client/src/pages/favorites.vue @@ -34,7 +34,7 @@ const pagingComponent = ref<InstanceType<typeof MkPagination>>(); defineExpose({ [symbols.PAGE_INFO]: { - title: i18n.locale.favorites, + title: i18n.ts.favorites, icon: 'fas fa-star', bg: 'var(--bg)', }, diff --git a/packages/client/src/pages/featured.vue b/packages/client/src/pages/featured.vue index 725c70f0f7..14fe0cb740 100644 --- a/packages/client/src/pages/featured.vue +++ b/packages/client/src/pages/featured.vue @@ -17,7 +17,7 @@ const pagination = { defineExpose({ [symbols.PAGE_INFO]: { - title: i18n.locale.featured, + title: i18n.ts.featured, icon: 'fas fa-fire-alt', bg: 'var(--bg)', }, diff --git a/packages/client/src/pages/federation.vue b/packages/client/src/pages/federation.vue index 6a4a28b6b4..3c5050cdb8 100644 --- a/packages/client/src/pages/federation.vue +++ b/packages/client/src/pages/federation.vue @@ -115,7 +115,7 @@ const pagination = { offsetMode: true, params: computed(() => ({ sort: sort, - host: host != '' ? host : null, + host: host !== '' ? host : null, ...( state === 'federating' ? { federating: true } : state === 'subscribing' ? { subscribing: true } : @@ -135,7 +135,7 @@ function getStatus(instance) { defineExpose({ [symbols.PAGE_INFO]: { - title: i18n.locale.federation, + title: i18n.ts.federation, icon: 'fas fa-globe', bg: 'var(--bg)', }, @@ -157,11 +157,10 @@ defineExpose({ > .instance { padding: 16px; - border: solid 1px var(--divider); - border-radius: 6px; + background: var(--panel); + border-radius: 8px; &:hover { - border: solid 1px var(--accent); text-decoration: none; } diff --git a/packages/client/src/pages/follow-requests.vue b/packages/client/src/pages/follow-requests.vue index 764daa0d3e..6adc1a404b 100644 --- a/packages/client/src/pages/follow-requests.vue +++ b/packages/client/src/pages/follow-requests.vue @@ -60,7 +60,7 @@ function reject(user) { defineExpose({ [symbols.PAGE_INFO]: computed(() => ({ - title: i18n.locale.followRequests, + title: i18n.ts.followRequests, icon: 'fas fa-user-clock', bg: 'var(--bg)', })), diff --git a/packages/client/src/pages/gallery/edit.vue b/packages/client/src/pages/gallery/edit.vue index e3fa1a0fcd..25ee513186 100644 --- a/packages/client/src/pages/gallery/edit.vue +++ b/packages/client/src/pages/gallery/edit.vue @@ -92,7 +92,7 @@ export default defineComponent({ methods: { selectFile(e) { - selectFiles(e.currentTarget || e.target, null).then(files => { + selectFiles(e.currentTarget ?? e.target, null).then(files => { this.files = this.files.concat(files); }); }, diff --git a/packages/client/src/pages/instance-info.vue b/packages/client/src/pages/instance-info.vue index fa36db0659..f19cb9d1a2 100644 --- a/packages/client/src/pages/instance-info.vue +++ b/packages/client/src/pages/instance-info.vue @@ -29,6 +29,7 @@ <template #label>Moderation</template> <FormSwitch v-model="suspended" class="_formBlock" @update:modelValue="toggleSuspend">{{ $ts.stopActivityDelivery }}</FormSwitch> <FormSwitch v-model="isBlocked" class="_formBlock" @update:modelValue="toggleBlock">{{ $ts.blockThisInstance }}</FormSwitch> + <MkButton @click="refreshMetadata">Refresh metadata</MkButton> </FormSection> <FormSection> @@ -111,6 +112,7 @@ import MkChart from '@/components/chart.vue'; import MkObjectView from '@/components/object-view.vue'; import FormLink from '@/components/form/link.vue'; import MkLink from '@/components/link.vue'; +import MkButton from '@/components/ui/button.vue'; import FormSection from '@/components/form/section.vue'; import MkKeyValue from '@/components/key-value.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(); defineExpose({ diff --git a/packages/client/src/pages/mentions.vue b/packages/client/src/pages/mentions.vue index bda56fc729..9b57c956bf 100644 --- a/packages/client/src/pages/mentions.vue +++ b/packages/client/src/pages/mentions.vue @@ -16,7 +16,7 @@ const pagination = { defineExpose({ [symbols.PAGE_INFO]: { - title: i18n.locale.mentions, + title: i18n.ts.mentions, icon: 'fas fa-at', bg: 'var(--bg)', }, diff --git a/packages/client/src/pages/messages.vue b/packages/client/src/pages/messages.vue index 8efdc55586..9c5fb9b341 100644 --- a/packages/client/src/pages/messages.vue +++ b/packages/client/src/pages/messages.vue @@ -12,14 +12,14 @@ import { i18n } from '@/i18n'; const pagination = { endpoint: 'notes/mentions' as const, limit: 10, - params: () => ({ + params: { visibility: 'specified' - }), + }, }; defineExpose({ [symbols.PAGE_INFO]: { - title: i18n.locale.directNotes, + title: i18n.ts.directNotes, icon: 'fas fa-envelope', bg: 'var(--bg)', }, diff --git a/packages/client/src/pages/messaging/index.vue b/packages/client/src/pages/messaging/index.vue index 554ebc4b6b..88a1e07afc 100644 --- a/packages/client/src/pages/messaging/index.vue +++ b/packages/client/src/pages/messaging/index.vue @@ -128,7 +128,7 @@ export default defineComponent({ text: this.$ts.messagingWithGroup, icon: 'fas fa-users', action: () => { this.startGroup() } - }], ev.currentTarget || ev.target); + }], ev.currentTarget ?? ev.target); }, async startUser() { diff --git a/packages/client/src/pages/messaging/messaging-room.form.vue b/packages/client/src/pages/messaging/messaging-room.form.vue index 1b9421ca9a..3863c8f82b 100644 --- a/packages/client/src/pages/messaging/messaging-room.form.vue +++ b/packages/client/src/pages/messaging/messaging-room.form.vue @@ -154,7 +154,7 @@ export default defineComponent({ }, 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; }); }, @@ -214,7 +214,7 @@ export default defineComponent({ }, async insertEmoji(ev) { - os.openEmojiPicker(ev.currentTarget || ev.target, {}, this.$refs.text); + os.openEmojiPicker(ev.currentTarget ?? ev.target, {}, this.$refs.text); } } }); diff --git a/packages/client/src/pages/messaging/messaging-room.vue b/packages/client/src/pages/messaging/messaging-room.vue index 65c44ce113..2ecc68eb54 100644 --- a/packages/client/src/pages/messaging/messaging-room.vue +++ b/packages/client/src/pages/messaging/messaging-room.vue @@ -335,7 +335,7 @@ const Component = defineComponent({ popout(path); this.$router.back(); }, - }], ev.currentTarget || ev.target); + }], ev.currentTarget ?? ev.target); } } }); diff --git a/packages/client/src/pages/my-antennas/create.vue b/packages/client/src/pages/my-antennas/create.vue index 427c9935c3..a08bece731 100644 --- a/packages/client/src/pages/my-antennas/create.vue +++ b/packages/client/src/pages/my-antennas/create.vue @@ -31,7 +31,7 @@ function onAntennaCreated() { defineExpose({ [symbols.PAGE_INFO]: { - title: i18n.locale.manageAntennas, + title: i18n.ts.manageAntennas, icon: 'fas fa-satellite', bg: 'var(--bg)', }, diff --git a/packages/client/src/pages/my-clips/index.vue b/packages/client/src/pages/my-clips/index.vue index 97b563f6f8..e287357a42 100644 --- a/packages/client/src/pages/my-clips/index.vue +++ b/packages/client/src/pages/my-clips/index.vue @@ -19,7 +19,7 @@ import MkPagination from '@/components/ui/pagination.vue'; import MkButton from '@/components/ui/button.vue'; import * as os from '@/os'; import * as symbols from '@/symbols'; -import i18n from '@/components/global/i18n'; +import { i18n } from '@/i18n'; const pagination = { endpoint: 'clips/list' as const, @@ -29,20 +29,20 @@ const pagination = { const pagingComponent = $ref<InstanceType<typeof MkPagination>>(); async function create() { - const { canceled, result } = await os.form(i18n.locale.createNewClip, { + const { canceled, result } = await os.form(i18n.ts.createNewClip, { name: { type: 'string', - label: i18n.locale.name, + label: i18n.ts.name, }, description: { type: 'string', required: false, multiline: true, - label: i18n.locale.description, + label: i18n.ts.description, }, isPublic: { type: 'boolean', - label: i18n.locale.public, + label: i18n.ts.public, default: false, }, }); @@ -63,7 +63,7 @@ function onClipDeleted() { defineExpose({ [symbols.PAGE_INFO]: { - title: i18n.locale.clip, + title: i18n.ts.clip, icon: 'fas fa-paperclip', bg: 'var(--bg)', action: { diff --git a/packages/client/src/pages/my-lists/index.vue b/packages/client/src/pages/my-lists/index.vue index e6fcba1b34..9ed9e2960e 100644 --- a/packages/client/src/pages/my-lists/index.vue +++ b/packages/client/src/pages/my-lists/index.vue @@ -31,7 +31,7 @@ const pagination = { async function create() { const { canceled, result: name } = await os.inputText({ - title: i18n.locale.enterListName, + title: i18n.ts.enterListName, }); if (canceled) return; await os.apiWithDialog('users/lists/create', { name: name }); @@ -40,7 +40,7 @@ async function create() { defineExpose({ [symbols.PAGE_INFO]: { - title: i18n.locale.manageLists, + title: i18n.ts.manageLists, icon: 'fas fa-list-ul', bg: 'var(--bg)', action: { diff --git a/packages/client/src/pages/not-found.vue b/packages/client/src/pages/not-found.vue index 914fdb9297..cdeb54b88b 100644 --- a/packages/client/src/pages/not-found.vue +++ b/packages/client/src/pages/not-found.vue @@ -13,7 +13,7 @@ import { i18n } from '@/i18n'; defineExpose({ [symbols.PAGE_INFO]: { - title: i18n.locale.notFound, + title: i18n.ts.notFound, icon: 'fas fa-exclamation-triangle', bg: 'var(--bg)', }, diff --git a/packages/client/src/pages/notifications.vue b/packages/client/src/pages/notifications.vue index 090e80f99a..36e423e534 100644 --- a/packages/client/src/pages/notifications.vue +++ b/packages/client/src/pages/notifications.vue @@ -27,26 +27,26 @@ function setFilter(ev) { })); const items = includeTypes != null ? [{ icon: 'fas fa-times', - text: i18n.locale.clear, + text: i18n.ts.clear, action: () => { includeTypes = null; } }, null, ...typeItems] : typeItems; - os.popupMenu(items, ev.currentTarget || ev.target); + os.popupMenu(items, ev.currentTarget ?? ev.target); } defineExpose({ [symbols.PAGE_INFO]: computed(() => ({ - title: i18n.locale.notifications, + title: i18n.ts.notifications, icon: 'fas fa-bell', bg: 'var(--bg)', actions: [{ - text: i18n.locale.filter, + text: i18n.ts.filter, icon: 'fas fa-filter', highlighted: includeTypes != null, handler: setFilter, }, { - text: i18n.locale.markAllAsRead, + text: i18n.ts.markAllAsRead, icon: 'fas fa-check', handler: () => { os.apiWithDialog('notifications/mark-all-as-read'); @@ -54,11 +54,11 @@ defineExpose({ }], tabs: [{ active: tab === 'all', - title: i18n.locale.all, + title: i18n.ts.all, onClick: () => { tab = 'all'; }, }, { active: tab === 'unread', - title: i18n.locale.unread, + title: i18n.ts.unread, onClick: () => { tab = 'unread'; }, },] })), diff --git a/packages/client/src/pages/page-editor/page-editor.vue b/packages/client/src/pages/page-editor/page-editor.vue index fe207555f8..f302ac4f90 100644 --- a/packages/client/src/pages/page-editor/page-editor.vue +++ b/packages/client/src/pages/page-editor/page-editor.vue @@ -448,7 +448,7 @@ export default defineComponent({ }, setEyeCatchingImage(e) { - selectFile(e.currentTarget || e.target, null).then(file => { + selectFile(e.currentTarget ?? e.target, null).then(file => { this.eyeCatchingImageId = file.id; }); }, diff --git a/packages/client/src/pages/preview.vue b/packages/client/src/pages/preview.vue index 8eb4549516..4accac4192 100644 --- a/packages/client/src/pages/preview.vue +++ b/packages/client/src/pages/preview.vue @@ -12,7 +12,7 @@ import { i18n } from '@/i18n'; defineExpose({ [symbols.PAGE_INFO]: computed(() => ({ - title: i18n.locale.preview, + title: i18n.ts.preview, icon: 'fas fa-eye', bg: 'var(--bg)', })), diff --git a/packages/client/src/pages/reset-password.vue b/packages/client/src/pages/reset-password.vue index 8ef73858f6..7d008ae75c 100644 --- a/packages/client/src/pages/reset-password.vue +++ b/packages/client/src/pages/reset-password.vue @@ -3,10 +3,10 @@ <div class="_formRoot"> <FormInput v-model="password" type="password" class="_formBlock"> <template #prefix><i class="fas fa-lock"></i></template> - <template #label>{{ i18n.locale.newPassword }}</template> + <template #label>{{ i18n.ts.newPassword }}</template> </FormInput> - <FormButton primary class="_formBlock" @click="save">{{ i18n.locale.save }}</FormButton> + <FormButton primary class="_formBlock" @click="save">{{ i18n.ts.save }}</FormButton> </div> </MkSpacer> </template> @@ -43,7 +43,7 @@ onMounted(() => { defineExpose({ [symbols.PAGE_INFO]: { - title: i18n.locale.resetPassword, + title: i18n.ts.resetPassword, icon: 'fas fa-lock', bg: 'var(--bg)', }, diff --git a/packages/client/src/pages/settings/accounts.vue b/packages/client/src/pages/settings/accounts.vue index c795ede8ac..a744a031d4 100644 --- a/packages/client/src/pages/settings/accounts.vue +++ b/packages/client/src/pages/settings/accounts.vue @@ -64,7 +64,7 @@ export default defineComponent({ icon: 'fas fa-trash-alt', danger: true, action: () => this.removeAccount(account), - }], ev.currentTarget || ev.target); + }], ev.currentTarget ?? ev.target); }, addAccount(ev) { @@ -74,7 +74,7 @@ export default defineComponent({ }, { text: this.$ts.createAccount, action: () => { this.createAccount(); }, - }], ev.currentTarget || ev.target); + }], ev.currentTarget ?? ev.target); }, addExistingAccount() { diff --git a/packages/client/src/pages/settings/drive.vue b/packages/client/src/pages/settings/drive.vue index f1016ebd84..134fa63308 100644 --- a/packages/client/src/pages/settings/drive.vue +++ b/packages/client/src/pages/settings/drive.vue @@ -28,6 +28,7 @@ <template #suffix>{{ uploadFolder ? uploadFolder.name : '-' }}</template> <template #suffixIcon><i class="fas fa-folder-open"></i></template> </FormLink> + <FormSwitch v-model="keepOriginalUploading" class="_formBlock">{{ $ts.keepOriginalUploading }}<template #caption>{{ $ts.keepOriginalUploadingDescription }}</template></FormSwitch> </FormSection> </div> </template> @@ -36,18 +37,21 @@ import { defineComponent } from 'vue'; import * as tinycolor from 'tinycolor2'; import FormLink from '@/components/form/link.vue'; +import FormSwitch from '@/components/form/switch.vue'; import FormSection from '@/components/form/section.vue'; import MkKeyValue from '@/components/key-value.vue'; import FormSplit from '@/components/form/split.vue'; import * as os from '@/os'; import bytes from '@/filters/bytes'; import * as symbols from '@/symbols'; +import { defaultStore } from '@/store'; // TODO: render chart export default defineComponent({ components: { FormLink, + FormSwitch, FormSection, MkKeyValue, FormSplit, @@ -79,7 +83,8 @@ export default defineComponent({ l: 0.5 }) }; - } + }, + keepOriginalUploading: defaultStore.makeGetterSetter('keepOriginalUploading'), }, async created() { diff --git a/packages/client/src/pages/settings/email.vue b/packages/client/src/pages/settings/email.vue index 54557f8773..4697fec9b7 100644 --- a/packages/client/src/pages/settings/email.vue +++ b/packages/client/src/pages/settings/email.vue @@ -62,7 +62,7 @@ export default defineComponent({ const emailAddress = ref($i.email); const INFO = { - title: i18n.locale.email, + title: i18n.ts.email, icon: 'fas fa-envelope', bg: 'var(--bg)', }; @@ -75,7 +75,7 @@ export default defineComponent({ const saveEmailAddress = () => { os.inputText({ - title: i18n.locale.password, + title: i18n.ts.password, type: 'password' }).then(({ canceled, result: password }) => { if (canceled) return; diff --git a/packages/client/src/pages/settings/import-export.vue b/packages/client/src/pages/settings/import-export.vue index 21031c559e..c153b4d28c 100644 --- a/packages/client/src/pages/settings/import-export.vue +++ b/packages/client/src/pages/settings/import-export.vue @@ -60,7 +60,7 @@ export default defineComponent({ setup(props, context) { const INFO = { - title: i18n.locale.importAndExport, + title: i18n.ts.importAndExport, icon: 'fas fa-boxes', bg: 'var(--bg)', }; @@ -71,14 +71,14 @@ export default defineComponent({ const onExportSuccess = () => { os.alert({ type: 'info', - text: i18n.locale.exportRequested, + text: i18n.ts.exportRequested, }); }; const onImportSuccess = () => { os.alert({ type: 'info', - text: i18n.locale.importRequested, + text: i18n.ts.importRequested, }); }; @@ -114,22 +114,22 @@ export default defineComponent({ }; const importFollowing = async (ev) => { - const file = await selectFile(ev.currentTarget || ev.target); + const file = await selectFile(ev.currentTarget ?? ev.target); os.api('i/import-following', { fileId: file.id }).then(onImportSuccess).catch(onError); }; const importUserLists = async (ev) => { - const file = await selectFile(ev.currentTarget || ev.target); + const file = await selectFile(ev.currentTarget ?? ev.target); os.api('i/import-user-lists', { fileId: file.id }).then(onImportSuccess).catch(onError); }; const importMuting = async (ev) => { - const file = await selectFile(ev.currentTarget || ev.target); + const file = await selectFile(ev.currentTarget ?? ev.target); os.api('i/import-muting', { fileId: file.id }).then(onImportSuccess).catch(onError); }; const importBlocking = async (ev) => { - const file = await selectFile(ev.currentTarget || ev.target); + const file = await selectFile(ev.currentTarget ?? ev.target); os.api('i/import-blocking', { fileId: file.id }).then(onImportSuccess).catch(onError); }; diff --git a/packages/client/src/pages/settings/index.vue b/packages/client/src/pages/settings/index.vue index 66c8b147bb..ac8414ddbc 100644 --- a/packages/client/src/pages/settings/index.vue +++ b/packages/client/src/pages/settings/index.vue @@ -49,7 +49,7 @@ export default defineComponent({ setup(props, context) { const indexInfo = { - title: i18n.locale.settings, + title: i18n.ts.settings, icon: 'fas fa-cog', bg: 'var(--bg)', hideHeader: true, @@ -61,96 +61,96 @@ export default defineComponent({ const el = ref(null); const childInfo = ref(null); const menuDef = computed(() => [{ - title: i18n.locale.basicSettings, + title: i18n.ts.basicSettings, items: [{ icon: 'fas fa-user', - text: i18n.locale.profile, + text: i18n.ts.profile, to: '/settings/profile', active: page.value === 'profile', }, { icon: 'fas fa-lock-open', - text: i18n.locale.privacy, + text: i18n.ts.privacy, to: '/settings/privacy', active: page.value === 'privacy', }, { icon: 'fas fa-laugh', - text: i18n.locale.reaction, + text: i18n.ts.reaction, to: '/settings/reaction', active: page.value === 'reaction', }, { icon: 'fas fa-cloud', - text: i18n.locale.drive, + text: i18n.ts.drive, to: '/settings/drive', active: page.value === 'drive', }, { icon: 'fas fa-bell', - text: i18n.locale.notifications, + text: i18n.ts.notifications, to: '/settings/notifications', active: page.value === 'notifications', }, { icon: 'fas fa-envelope', - text: i18n.locale.email, + text: i18n.ts.email, to: '/settings/email', active: page.value === 'email', }, { icon: 'fas fa-share-alt', - text: i18n.locale.integration, + text: i18n.ts.integration, to: '/settings/integration', active: page.value === 'integration', }, { icon: 'fas fa-lock', - text: i18n.locale.security, + text: i18n.ts.security, to: '/settings/security', active: page.value === 'security', }], }, { - title: i18n.locale.clientSettings, + title: i18n.ts.clientSettings, items: [{ icon: 'fas fa-cogs', - text: i18n.locale.general, + text: i18n.ts.general, to: '/settings/general', active: page.value === 'general', }, { icon: 'fas fa-palette', - text: i18n.locale.theme, + text: i18n.ts.theme, to: '/settings/theme', active: page.value === 'theme', }, { icon: 'fas fa-list-ul', - text: i18n.locale.menu, + text: i18n.ts.menu, to: '/settings/menu', active: page.value === 'menu', }, { icon: 'fas fa-music', - text: i18n.locale.sounds, + text: i18n.ts.sounds, to: '/settings/sounds', active: page.value === 'sounds', }, { icon: 'fas fa-plug', - text: i18n.locale.plugins, + text: i18n.ts.plugins, to: '/settings/plugin', active: page.value === 'plugin', }], }, { - title: i18n.locale.otherSettings, + title: i18n.ts.otherSettings, items: [{ icon: 'fas fa-boxes', - text: i18n.locale.importAndExport, + text: i18n.ts.importAndExport, to: '/settings/import-export', active: page.value === 'import-export', }, { icon: 'fas fa-volume-mute', - text: i18n.locale.instanceMute, + text: i18n.ts.instanceMute, to: '/settings/instance-mute', active: page.value === 'instance-mute', }, { icon: 'fas fa-ban', - text: i18n.locale.muteAndBlock, + text: i18n.ts.muteAndBlock, to: '/settings/mute-block', active: page.value === 'mute-block', }, { icon: 'fas fa-comment-slash', - text: i18n.locale.wordMute, + text: i18n.ts.wordMute, to: '/settings/word-mute', active: page.value === 'word-mute', }, { @@ -160,7 +160,7 @@ export default defineComponent({ active: page.value === 'api', }, { icon: 'fas fa-ellipsis-h', - text: i18n.locale.other, + text: i18n.ts.other, to: '/settings/other', active: page.value === 'other', }], @@ -168,7 +168,7 @@ export default defineComponent({ items: [{ type: 'button', icon: 'fas fa-trash', - text: i18n.locale.clearCache, + text: i18n.ts.clearCache, action: () => { localStorage.removeItem('locale'); localStorage.removeItem('theme'); @@ -177,7 +177,7 @@ export default defineComponent({ }, { type: 'button', icon: 'fas fa-sign-in-alt fa-flip-horizontal', - text: i18n.locale.logout, + text: i18n.ts.logout, action: () => { signout(); }, diff --git a/packages/client/src/pages/settings/mute-block.vue b/packages/client/src/pages/settings/mute-block.vue index f4f9ebf8dd..28d11809e3 100644 --- a/packages/client/src/pages/settings/mute-block.vue +++ b/packages/client/src/pages/settings/mute-block.vue @@ -52,7 +52,7 @@ const blockingPagination = { defineExpose({ [symbols.PAGE_INFO]: { - title: i18n.locale.muteAndBlock, + title: i18n.ts.muteAndBlock, icon: 'fas fa-ban', bg: 'var(--bg)', }, diff --git a/packages/client/src/pages/settings/privacy.vue b/packages/client/src/pages/settings/privacy.vue index dd13ba4bd0..cfae7e9ca8 100644 --- a/packages/client/src/pages/settings/privacy.vue +++ b/packages/client/src/pages/settings/privacy.vue @@ -86,7 +86,7 @@ function save() { defineExpose({ [symbols.PAGE_INFO]: { - title: i18n.locale.privacy, + title: i18n.ts.privacy, icon: 'fas fa-lock-open', bg: 'var(--bg)', }, diff --git a/packages/client/src/pages/settings/profile.vue b/packages/client/src/pages/settings/profile.vue index f875146a2c..66b654d87f 100644 --- a/packages/client/src/pages/settings/profile.vue +++ b/packages/client/src/pages/settings/profile.vue @@ -3,45 +3,45 @@ <div class="llvierxe" :style="{ backgroundImage: $i.bannerUrl ? `url(${ $i.bannerUrl })` : null }"> <div class="avatar _acrylic"> <MkAvatar class="avatar" :user="$i" :disable-link="true" @click="changeAvatar"/> - <MkButton primary class="avatarEdit" @click="changeAvatar">{{ i18n.locale._profile.changeAvatar }}</MkButton> + <MkButton primary class="avatarEdit" @click="changeAvatar">{{ i18n.ts._profile.changeAvatar }}</MkButton> </div> - <MkButton primary class="bannerEdit" @click="changeBanner">{{ i18n.locale._profile.changeBanner }}</MkButton> + <MkButton primary class="bannerEdit" @click="changeBanner">{{ i18n.ts._profile.changeBanner }}</MkButton> </div> <FormInput v-model="profile.name" :max="30" manual-save class="_formBlock"> - <template #label>{{ i18n.locale._profile.name }}</template> + <template #label>{{ i18n.ts._profile.name }}</template> </FormInput> <FormTextarea v-model="profile.description" :max="500" tall manual-save class="_formBlock"> - <template #label>{{ i18n.locale._profile.description }}</template> - <template #caption>{{ i18n.locale._profile.youCanIncludeHashtags }}</template> + <template #label>{{ i18n.ts._profile.description }}</template> + <template #caption>{{ i18n.ts._profile.youCanIncludeHashtags }}</template> </FormTextarea> <FormInput v-model="profile.location" manual-save class="_formBlock"> - <template #label>{{ i18n.locale.location }}</template> + <template #label>{{ i18n.ts.location }}</template> <template #prefix><i class="fas fa-map-marker-alt"></i></template> </FormInput> <FormInput v-model="profile.birthday" type="date" manual-save class="_formBlock"> - <template #label>{{ i18n.locale.birthday }}</template> + <template #label>{{ i18n.ts.birthday }}</template> <template #prefix><i class="fas fa-birthday-cake"></i></template> </FormInput> <FormSelect v-model="profile.lang" class="_formBlock"> - <template #label>{{ i18n.locale.language }}</template> + <template #label>{{ i18n.ts.language }}</template> <option v-for="x in langs" :key="x[0]" :value="x[0]">{{ x[1] }}</option> </FormSelect> <FormSlot> - <MkButton @click="editMetadata">{{ i18n.locale._profile.metadataEdit }}</MkButton> - <template #caption>{{ i18n.locale._profile.metadataDescription }}</template> + <MkButton @click="editMetadata">{{ i18n.ts._profile.metadataEdit }}</MkButton> + <template #caption>{{ i18n.ts._profile.metadataDescription }}</template> </FormSlot> - <FormSwitch v-model="profile.isCat" class="_formBlock">{{ i18n.locale.flagAsCat }}<template #caption>{{ i18n.locale.flagAsCatDescription }}</template></FormSwitch> + <FormSwitch v-model="profile.isCat" class="_formBlock">{{ i18n.ts.flagAsCat }}<template #caption>{{ i18n.ts.flagAsCatDescription }}</template></FormSwitch> - <FormSwitch v-model="profile.isBot" class="_formBlock">{{ i18n.locale.flagAsBot }}<template #caption>{{ i18n.locale.flagAsBotDescription }}</template></FormSwitch> + <FormSwitch v-model="profile.isBot" class="_formBlock">{{ i18n.ts.flagAsBot }}<template #caption>{{ i18n.ts.flagAsBotDescription }}</template></FormSwitch> - <FormSwitch v-model="profile.alwaysMarkNsfw" class="_formBlock">{{ i18n.locale.alwaysMarkSensitive }}</FormSwitch> + <FormSwitch v-model="profile.alwaysMarkNsfw" class="_formBlock">{{ i18n.ts.alwaysMarkSensitive }}</FormSwitch> </div> </template> @@ -102,7 +102,7 @@ function save() { } function changeAvatar(ev) { - selectFile(ev.currentTarget || ev.target, i18n.locale.avatar).then(async (file) => { + selectFile(ev.currentTarget ?? ev.target, i18n.ts.avatar).then(async (file) => { const i = await os.apiWithDialog('i/update', { avatarId: file.id, }); @@ -112,7 +112,7 @@ function changeAvatar(ev) { } function changeBanner(ev) { - selectFile(ev.currentTarget || ev.target, i18n.locale.banner).then(async (file) => { + selectFile(ev.currentTarget ?? ev.target, i18n.ts.banner).then(async (file) => { const i = await os.apiWithDialog('i/update', { bannerId: file.id, }); @@ -122,45 +122,45 @@ function changeBanner(ev) { } async function editMetadata() { - const { canceled, result } = await os.form(i18n.locale._profile.metadata, { + const { canceled, result } = await os.form(i18n.ts._profile.metadata, { fieldName0: { type: 'string', - label: i18n.locale._profile.metadataLabel + ' 1', + label: i18n.ts._profile.metadataLabel + ' 1', default: additionalFields.fieldName0, }, fieldValue0: { type: 'string', - label: i18n.locale._profile.metadataContent + ' 1', + label: i18n.ts._profile.metadataContent + ' 1', default: additionalFields.fieldValue0, }, fieldName1: { type: 'string', - label: i18n.locale._profile.metadataLabel + ' 2', + label: i18n.ts._profile.metadataLabel + ' 2', default: additionalFields.fieldName1, }, fieldValue1: { type: 'string', - label: i18n.locale._profile.metadataContent + ' 2', + label: i18n.ts._profile.metadataContent + ' 2', default: additionalFields.fieldValue1, }, fieldName2: { type: 'string', - label: i18n.locale._profile.metadataLabel + ' 3', + label: i18n.ts._profile.metadataLabel + ' 3', default: additionalFields.fieldName2, }, fieldValue2: { type: 'string', - label: i18n.locale._profile.metadataContent + ' 3', + label: i18n.ts._profile.metadataContent + ' 3', default: additionalFields.fieldValue2, }, fieldName3: { type: 'string', - label: i18n.locale._profile.metadataLabel + ' 4', + label: i18n.ts._profile.metadataLabel + ' 4', default: additionalFields.fieldName3, }, fieldValue3: { type: 'string', - label: i18n.locale._profile.metadataContent + ' 4', + label: i18n.ts._profile.metadataContent + ' 4', default: additionalFields.fieldValue3, }, }); @@ -196,7 +196,7 @@ async function editMetadata() { defineExpose({ [symbols.PAGE_INFO]: { - title: i18n.locale.profile, + title: i18n.ts.profile, icon: 'fas fa-user', bg: 'var(--bg)', }, diff --git a/packages/client/src/pages/settings/reaction.vue b/packages/client/src/pages/settings/reaction.vue index e5b1189947..ae3e1a1187 100644 --- a/packages/client/src/pages/settings/reaction.vue +++ b/packages/client/src/pages/settings/reaction.vue @@ -44,8 +44,8 @@ </div> </template> -<script lang="ts"> -import { defineComponent } from 'vue'; +<script lang="ts" setup> +import { watch } from 'vue'; import XDraggable from 'vuedraggable'; import FormInput from '@/components/form/input.vue'; import FormRadios from '@/components/form/radios.vue'; @@ -56,91 +56,70 @@ import FormSwitch from '@/components/form/switch.vue'; import * as os from '@/os'; import { defaultStore } from '@/store'; import * as symbols from '@/symbols'; +import { i18n } from '@/i18n'; -export default defineComponent({ - components: { - FormInput, - FormButton, - FromSlot, - FormRadios, - FormSection, - FormSwitch, - XDraggable, - }, +let reactions = $ref(JSON.parse(JSON.stringify(defaultStore.state.reactions))); - emits: ['info'], - - data() { - return { - [symbols.PAGE_INFO]: { - title: this.$ts.reaction, - icon: 'fas fa-laugh', - action: { - icon: 'fas fa-eye', - handler: this.preview - }, - bg: 'var(--bg)', - }, - reactions: JSON.parse(JSON.stringify(this.$store.state.reactions)), +const reactionPickerWidth = $computed(defaultStore.makeGetterSetter('reactionPickerWidth')); +const reactionPickerHeight = $computed(defaultStore.makeGetterSetter('reactionPickerHeight')); +const reactionPickerUseDrawerForMobile = $computed(defaultStore.makeGetterSetter('reactionPickerUseDrawerForMobile')); + +function save() { + defaultStore.set('reactions', reactions); +} + +function remove(reaction, ev: MouseEvent) { + os.popupMenu([{ + text: i18n.ts.remove, + action: () => { + reactions = reactions.filter(x => x !== reaction); } - }, + }], ev.currentTarget ?? ev.target); +} - computed: { - reactionPickerWidth: defaultStore.makeGetterSetter('reactionPickerWidth'), - reactionPickerHeight: defaultStore.makeGetterSetter('reactionPickerHeight'), - reactionPickerUseDrawerForMobile: defaultStore.makeGetterSetter('reactionPickerUseDrawerForMobile'), - }, +function preview(ev: MouseEvent) { + os.popup(import('@/components/emoji-picker-dialog.vue'), { + asReactionPicker: true, + src: ev.currentTarget ?? ev.target, + }, {}, 'closed'); +} - watch: { - reactions: { - handler() { - this.save(); - }, - deep: true +async function setDefault() { + const { canceled } = await os.confirm({ + type: 'warning', + text: i18n.ts.resetAreYouSure, + }); + if (canceled) return; + + reactions = JSON.parse(JSON.stringify(defaultStore.def.reactions.default)); +} + +function chooseEmoji(ev: MouseEvent) { + os.pickEmoji(ev.currentTarget ?? ev.target, { + showPinned: false + }).then(emoji => { + if (!reactions.includes(emoji)) { + reactions.push(emoji); } + }); +} + +watch($$(reactions), () => { + save(); +}, { + deep: true, +}); + +defineExpose({ + [symbols.PAGE_INFO]: { + title: i18n.ts.reaction, + icon: 'fas fa-laugh', + action: { + icon: 'fas fa-eye', + handler: preview, + }, + bg: 'var(--bg)', }, - - methods: { - save() { - this.$store.set('reactions', this.reactions); - }, - - remove(reaction, ev) { - os.popupMenu([{ - text: this.$ts.remove, - action: () => { - this.reactions = this.reactions.filter(x => x !== reaction) - } - }], ev.currentTarget || ev.target); - }, - - preview(ev) { - os.popup(import('@/components/emoji-picker-dialog.vue'), { - asReactionPicker: true, - src: ev.currentTarget || ev.target, - }, {}, 'closed'); - }, - - async setDefault() { - const { canceled } = await os.confirm({ - type: 'warning', - text: this.$ts.resetAreYouSure, - }); - if (canceled) return; - - this.reactions = JSON.parse(JSON.stringify(this.$store.def.reactions.default)); - }, - - chooseEmoji(ev) { - os.pickEmoji(ev.currentTarget || ev.target, { - showPinned: false - }).then(emoji => { - if (!this.reactions.includes(emoji)) { - this.reactions.push(emoji); - } - }); - } - } }); </script> diff --git a/packages/client/src/pages/settings/theme.install.vue b/packages/client/src/pages/settings/theme.install.vue index e2a3f042b9..2d3514342e 100644 --- a/packages/client/src/pages/settings/theme.install.vue +++ b/packages/client/src/pages/settings/theme.install.vue @@ -1,12 +1,12 @@ <template> <div class="_formRoot"> <FormTextarea v-model="installThemeCode" class="_formBlock"> - <template #label>{{ i18n.locale._theme.code }}</template> + <template #label>{{ i18n.ts._theme.code }}</template> </FormTextarea> <div class="_formBlock" style="display: flex; gap: var(--margin); flex-wrap: wrap;"> - <FormButton :disabled="installThemeCode == null" inline @click="() => preview(installThemeCode)"><i class="fas fa-eye"></i> {{ i18n.locale.preview }}</FormButton> - <FormButton :disabled="installThemeCode == null" primary inline @click="() => install(installThemeCode)"><i class="fas fa-check"></i> {{ i18n.locale.install }}</FormButton> + <FormButton :disabled="installThemeCode == null" inline @click="() => preview(installThemeCode)"><i class="fas fa-eye"></i> {{ i18n.ts.preview }}</FormButton> + <FormButton :disabled="installThemeCode == null" primary inline @click="() => install(installThemeCode)"><i class="fas fa-check"></i> {{ i18n.ts.install }}</FormButton> </div> </div> </template> @@ -32,21 +32,21 @@ function parseThemeCode(code: string) { } catch (e) { os.alert({ type: 'error', - text: i18n.locale._theme.invalid + text: i18n.ts._theme.invalid }); return false; } if (!validateTheme(theme)) { os.alert({ type: 'error', - text: i18n.locale._theme.invalid + text: i18n.ts._theme.invalid }); return false; } if (getThemes().some(t => t.id === theme.id)) { os.alert({ type: 'info', - text: i18n.locale._theme.alreadyInstalled + text: i18n.ts._theme.alreadyInstalled }); return false; } @@ -71,7 +71,7 @@ async function install(code: string): Promise<void> { defineExpose({ [symbols.PAGE_INFO]: { - title: i18n.locale._theme.install, + title: i18n.ts._theme.install, icon: 'fas fa-download', bg: 'var(--bg)', }, diff --git a/packages/client/src/pages/settings/theme.vue b/packages/client/src/pages/settings/theme.vue index 658e36ec05..3e4ec1b2af 100644 --- a/packages/client/src/pages/settings/theme.vue +++ b/packages/client/src/pages/settings/theme.vue @@ -116,7 +116,7 @@ export default defineComponent({ setup(props, { emit }) { const INFO = { - title: i18n.locale.theme, + title: i18n.ts.theme, icon: 'fas fa-palette', bg: 'var(--bg)', }; @@ -184,7 +184,7 @@ export default defineComponent({ themesCount, wallpaper, setWallpaper(e) { - selectFile(e.currentTarget || e.target, null).then(file => { + selectFile(e.currentTarget ?? e.target, null).then(file => { wallpaper.value = file.url; }); }, diff --git a/packages/client/src/pages/signup-complete.vue b/packages/client/src/pages/signup-complete.vue index a10af1a4cc..344c9195f7 100644 --- a/packages/client/src/pages/signup-complete.vue +++ b/packages/client/src/pages/signup-complete.vue @@ -1,6 +1,6 @@ <template> <div> - {{ i18n.locale.processing }} + {{ i18n.ts.processing }} </div> </template> @@ -18,7 +18,7 @@ const props = defineProps<{ onMounted(async () => { await os.alert({ type: 'info', - text: i18n.t('clickToFinishEmailVerification', { ok: i18n.locale.gotIt }), + text: i18n.t('clickToFinishEmailVerification', { ok: i18n.ts.gotIt }), }); const res = await os.apiWithDialog('signup-pending', { code: props.code, @@ -28,7 +28,7 @@ onMounted(async () => { defineExpose({ [symbols.PAGE_INFO]: { - title: i18n.locale.signup, + title: i18n.ts.signup, icon: 'fas fa-user', }, }); diff --git a/packages/client/src/pages/theme-editor.vue b/packages/client/src/pages/theme-editor.vue index 80b8c7806c..a53e23c1c5 100644 --- a/packages/client/src/pages/theme-editor.vue +++ b/packages/client/src/pages/theme-editor.vue @@ -2,7 +2,7 @@ <MkSpacer :content-max="800" :margin-min="16" :margin-max="32"> <div class="cwepdizn _formRoot"> <FormFolder :default-open="true" class="_formBlock"> - <template #label>{{ i18n.locale.backgroundColor }}</template> + <template #label>{{ i18n.ts.backgroundColor }}</template> <div class="cwepdizn-colors"> <div class="row"> <button v-for="color in bgColors.filter(x => x.kind === 'light')" :key="color.color" class="color _button" :class="{ active: theme.props.bg === color.color }" @click="setBgColor(color)"> @@ -18,7 +18,7 @@ </FormFolder> <FormFolder :default-open="true" class="_formBlock"> - <template #label>{{ i18n.locale.accentColor }}</template> + <template #label>{{ i18n.ts.accentColor }}</template> <div class="cwepdizn-colors"> <div class="row"> <button v-for="color in accentColors" :key="color" class="color rounded _button" :class="{ active: theme.props.accent === color }" @click="setAccentColor(color)"> @@ -29,7 +29,7 @@ </FormFolder> <FormFolder :default-open="true" class="_formBlock"> - <template #label>{{ i18n.locale.textColor }}</template> + <template #label>{{ i18n.ts.textColor }}</template> <div class="cwepdizn-colors"> <div class="row"> <button v-for="color in fgColors" :key="color" class="color char _button" :class="{ active: (theme.props.fg === color.forLight) || (theme.props.fg === color.forDark) }" @click="setFgColor(color)"> @@ -41,22 +41,22 @@ <FormFolder :default-open="false" class="_formBlock"> <template #icon><i class="fas fa-code"></i></template> - <template #label>{{ i18n.locale.editCode }}</template> + <template #label>{{ i18n.ts.editCode }}</template> <div class="_formRoot"> <FormTextarea v-model="themeCode" tall class="_formBlock"> - <template #label>{{ i18n.locale._theme.code }}</template> + <template #label>{{ i18n.ts._theme.code }}</template> </FormTextarea> - <FormButton primary class="_formBlock" @click="applyThemeCode">{{ i18n.locale.apply }}</FormButton> + <FormButton primary class="_formBlock" @click="applyThemeCode">{{ i18n.ts.apply }}</FormButton> </div> </FormFolder> <FormFolder :default-open="false" class="_formBlock"> - <template #label>{{ i18n.locale.addDescription }}</template> + <template #label>{{ i18n.ts.addDescription }}</template> <div class="_formRoot"> <FormTextarea v-model="description"> - <template #label>{{ i18n.locale._theme.description }}</template> + <template #label>{{ i18n.ts._theme.description }}</template> </FormTextarea> </div> </FormFolder> @@ -167,7 +167,7 @@ function applyThemeCode() { } catch (err) { os.alert({ type: 'error', - text: i18n.locale._theme.invalid, + text: i18n.ts._theme.invalid, }); return; } @@ -177,7 +177,7 @@ function applyThemeCode() { async function saveAs() { const { canceled, result: name } = await os.inputText({ - title: i18n.locale.name, + title: i18n.ts.name, allowEmpty: false, }); if (canceled) return; @@ -204,18 +204,18 @@ watch($$(theme), apply, { deep: true }); defineExpose({ [symbols.PAGE_INFO]: { - title: i18n.locale.themeEditor, + title: i18n.ts.themeEditor, icon: 'fas fa-palette', bg: 'var(--bg)', actions: [{ asFullButton: true, icon: 'fas fa-eye', - text: i18n.locale.preview, + text: i18n.ts.preview, handler: showPreview, }, { asFullButton: true, icon: 'fas fa-check', - text: i18n.locale.saveAs, + text: i18n.ts.saveAs, handler: saveAs, }], }, diff --git a/packages/client/src/pages/timeline.vue b/packages/client/src/pages/timeline.vue index aabb953aec..b2266d22c3 100644 --- a/packages/client/src/pages/timeline.vue +++ b/packages/client/src/pages/timeline.vue @@ -64,7 +64,7 @@ async function chooseList(ev: MouseEvent): Promise<void> { text: list.name, to: `/timeline/list/${list.id}`, })); - os.popupMenu(items, ev.currentTarget || ev.target); + os.popupMenu(items, ev.currentTarget ?? ev.target); } async function chooseAntenna(ev: MouseEvent): Promise<void> { @@ -75,7 +75,7 @@ async function chooseAntenna(ev: MouseEvent): Promise<void> { indicate: antenna.hasUnreadNote, to: `/timeline/antenna/${antenna.id}`, })); - os.popupMenu(items, ev.currentTarget || ev.target); + os.popupMenu(items, ev.currentTarget ?? ev.target); } async function chooseChannel(ev: MouseEvent): Promise<void> { @@ -86,7 +86,7 @@ async function chooseChannel(ev: MouseEvent): Promise<void> { indicate: channel.hasUnreadNote, to: `/channels/${channel.id}`, })); - os.popupMenu(items, ev.currentTarget || ev.target); + os.popupMenu(items, ev.currentTarget ?? ev.target); } function saveSrc(): void { @@ -97,7 +97,7 @@ function saveSrc(): void { async function timetravel(): Promise<void> { const { canceled, result: date } = await os.inputDate({ - title: i18n.locale.date, + title: i18n.ts.date, }); if (canceled) return; @@ -110,47 +110,47 @@ function focus(): void { defineExpose({ [symbols.PAGE_INFO]: computed(() => ({ - title: i18n.locale.timeline, + title: i18n.ts.timeline, icon: src === 'local' ? 'fas fa-comments' : src === 'social' ? 'fas fa-share-alt' : src === 'global' ? 'fas fa-globe' : 'fas fa-home', bg: 'var(--bg)', actions: [{ icon: 'fas fa-list-ul', - text: i18n.locale.lists, + text: i18n.ts.lists, handler: chooseList, }, { icon: 'fas fa-satellite', - text: i18n.locale.antennas, + text: i18n.ts.antennas, handler: chooseAntenna, }, { icon: 'fas fa-satellite-dish', - text: i18n.locale.channel, + text: i18n.ts.channel, handler: chooseChannel, }, { icon: 'fas fa-calendar-alt', - text: i18n.locale.jumpToSpecifiedDate, + text: i18n.ts.jumpToSpecifiedDate, handler: timetravel, }], tabs: [{ active: src === 'home', - title: i18n.locale._timelines.home, + title: i18n.ts._timelines.home, icon: 'fas fa-home', iconOnly: true, onClick: () => { src = 'home'; saveSrc(); }, }, ...(isLocalTimelineAvailable ? [{ active: src === 'local', - title: i18n.locale._timelines.local, + title: i18n.ts._timelines.local, icon: 'fas fa-comments', iconOnly: true, onClick: () => { src = 'local'; saveSrc(); }, }, { active: src === 'social', - title: i18n.locale._timelines.social, + title: i18n.ts._timelines.social, icon: 'fas fa-share-alt', iconOnly: true, onClick: () => { src = 'social'; saveSrc(); }, }] : []), ...(isGlobalTimelineAvailable ? [{ active: src === 'global', - title: i18n.locale._timelines.global, + title: i18n.ts._timelines.global, icon: 'fas fa-globe', iconOnly: true, onClick: () => { src = 'global'; saveSrc(); }, diff --git a/packages/client/src/pages/user/index.vue b/packages/client/src/pages/user/index.vue index 599e24d81c..10a86243f9 100644 --- a/packages/client/src/pages/user/index.vue +++ b/packages/client/src/pages/user/index.vue @@ -264,7 +264,7 @@ export default defineComponent({ }, menu(ev) { - os.popupMenu(getUserMenu(this.user), ev.currentTarget || ev.target); + os.popupMenu(getUserMenu(this.user), ev.currentTarget ?? ev.target); }, parallaxLoop() { diff --git a/packages/client/src/pages/welcome.entrance.a.vue b/packages/client/src/pages/welcome.entrance.a.vue index efdc038b7e..47e1f12342 100644 --- a/packages/client/src/pages/welcome.entrance.a.vue +++ b/packages/client/src/pages/welcome.entrance.a.vue @@ -135,7 +135,7 @@ export default defineComponent({ action: () => { window.open(`https://misskey-hub.net/help.md`, '_blank'); } - }], ev.currentTarget || ev.target); + }], ev.currentTarget ?? ev.target); }, number diff --git a/packages/client/src/pages/welcome.entrance.b.vue b/packages/client/src/pages/welcome.entrance.b.vue index 93344dc9a8..053087fda0 100644 --- a/packages/client/src/pages/welcome.entrance.b.vue +++ b/packages/client/src/pages/welcome.entrance.b.vue @@ -119,7 +119,7 @@ export default defineComponent({ action: () => { window.open(`https://misskey-hub.net/help.md`, '_blank'); } - }], ev.currentTarget || ev.target); + }], ev.currentTarget ?? ev.target); }, number diff --git a/packages/client/src/pages/welcome.entrance.c.vue b/packages/client/src/pages/welcome.entrance.c.vue index 36b61647a6..6bf487e16e 100644 --- a/packages/client/src/pages/welcome.entrance.c.vue +++ b/packages/client/src/pages/welcome.entrance.c.vue @@ -139,7 +139,7 @@ export default defineComponent({ action: () => { window.open(`https://misskey-hub.net/help.md`, '_blank'); } - }], ev.currentTarget || ev.target); + }], ev.currentTarget ?? ev.target); }, number diff --git a/packages/client/src/scripts/get-note-menu.ts b/packages/client/src/scripts/get-note-menu.ts index 3634f39632..b19656d3cc 100644 --- a/packages/client/src/scripts/get-note-menu.ts +++ b/packages/client/src/scripts/get-note-menu.ts @@ -27,7 +27,7 @@ export function getNoteMenu(props: { function del(): void { os.confirm({ type: 'warning', - text: i18n.locale.noteDeleteConfirm, + text: i18n.ts.noteDeleteConfirm, }).then(({ canceled }) => { if (canceled) return; @@ -40,7 +40,7 @@ export function getNoteMenu(props: { function delEdit(): void { os.confirm({ type: 'warning', - text: i18n.locale.deleteAndEditConfirm, + text: i18n.ts.deleteAndEditConfirm, }).then(({ canceled }) => { if (canceled) return; @@ -87,7 +87,7 @@ export function getNoteMenu(props: { if (e.id === '72dab508-c64d-498f-8740-a8eec1ba385a') { os.alert({ type: 'error', - text: i18n.locale.pinLimitExceeded + text: i18n.ts.pinLimitExceeded }); } }); @@ -97,22 +97,22 @@ export function getNoteMenu(props: { const clips = await os.api('clips/list'); os.popupMenu([{ icon: 'fas fa-plus', - text: i18n.locale.createNew, + text: i18n.ts.createNew, action: async () => { - const { canceled, result } = await os.form(i18n.locale.createNewClip, { + const { canceled, result } = await os.form(i18n.ts.createNewClip, { name: { type: 'string', - label: i18n.locale.name + label: i18n.ts.name }, description: { type: 'string', required: false, multiline: true, - label: i18n.locale.description + label: i18n.ts.description }, isPublic: { type: 'boolean', - label: i18n.locale.public, + label: i18n.ts.public, default: false } }); @@ -133,7 +133,7 @@ export function getNoteMenu(props: { async function promote(): Promise<void> { const { canceled, result: days } = await os.inputNumber({ - title: i18n.locale.numberOfDays, + title: i18n.ts.numberOfDays, }); if (canceled) return; @@ -171,69 +171,69 @@ export function getNoteMenu(props: { menu = [{ icon: 'fas fa-copy', - text: i18n.locale.copyContent, + text: i18n.ts.copyContent, action: copyContent }, { icon: 'fas fa-link', - text: i18n.locale.copyLink, + text: i18n.ts.copyLink, action: copyLink }, (appearNote.url || appearNote.uri) ? { icon: 'fas fa-external-link-square-alt', - text: i18n.locale.showOnRemote, + text: i18n.ts.showOnRemote, action: () => { window.open(appearNote.url || appearNote.uri, '_blank'); } } : undefined, { icon: 'fas fa-share-alt', - text: i18n.locale.share, + text: i18n.ts.share, action: share }, instance.translatorAvailable ? { icon: 'fas fa-language', - text: i18n.locale.translate, + text: i18n.ts.translate, action: translate } : undefined, null, statePromise.then(state => state.isFavorited ? { icon: 'fas fa-star', - text: i18n.locale.unfavorite, + text: i18n.ts.unfavorite, action: () => toggleFavorite(false) } : { icon: 'fas fa-star', - text: i18n.locale.favorite, + text: i18n.ts.favorite, action: () => toggleFavorite(true) }), { icon: 'fas fa-paperclip', - text: i18n.locale.clip, + text: i18n.ts.clip, action: () => clip() }, (appearNote.userId != $i.id) ? statePromise.then(state => state.isWatching ? { icon: 'fas fa-eye-slash', - text: i18n.locale.unwatch, + text: i18n.ts.unwatch, action: () => toggleWatch(false) } : { icon: 'fas fa-eye', - text: i18n.locale.watch, + text: i18n.ts.watch, action: () => toggleWatch(true) }) : undefined, statePromise.then(state => state.isMutedThread ? { icon: 'fas fa-comment-slash', - text: i18n.locale.unmuteThread, + text: i18n.ts.unmuteThread, action: () => toggleThreadMute(false) } : { icon: 'fas fa-comment-slash', - text: i18n.locale.muteThread, + text: i18n.ts.muteThread, action: () => toggleThreadMute(true) }), appearNote.userId == $i.id ? ($i.pinnedNoteIds || []).includes(appearNote.id) ? { icon: 'fas fa-thumbtack', - text: i18n.locale.unpin, + text: i18n.ts.unpin, action: () => togglePin(false) } : { icon: 'fas fa-thumbtack', - text: i18n.locale.pin, + text: i18n.ts.pin, action: () => togglePin(true) } : undefined, /* @@ -241,7 +241,7 @@ export function getNoteMenu(props: { null, { icon: 'fas fa-bullhorn', - text: i18n.locale.promote, + text: i18n.ts.promote, action: promote }] : [] @@ -250,7 +250,7 @@ export function getNoteMenu(props: { null, { icon: 'fas fa-exclamation-circle', - text: i18n.locale.reportAbuse, + text: i18n.ts.reportAbuse, action: () => { const u = appearNote.url || appearNote.uri || `${url}/notes/${appearNote.id}`; os.popup(import('@/components/abuse-report-window.vue'), { @@ -265,12 +265,12 @@ export function getNoteMenu(props: { null, appearNote.userId == $i.id ? { icon: 'fas fa-edit', - text: i18n.locale.deleteAndEdit, + text: i18n.ts.deleteAndEdit, action: delEdit } : undefined, { icon: 'fas fa-trash-alt', - text: i18n.locale.delete, + text: i18n.ts.delete, danger: true, action: del }] @@ -280,15 +280,15 @@ export function getNoteMenu(props: { } else { menu = [{ icon: 'fas fa-copy', - text: i18n.locale.copyContent, + text: i18n.ts.copyContent, action: copyContent }, { icon: 'fas fa-link', - text: i18n.locale.copyLink, + text: i18n.ts.copyLink, action: copyLink }, (appearNote.url || appearNote.uri) ? { icon: 'fas fa-external-link-square-alt', - text: i18n.locale.showOnRemote, + text: i18n.ts.showOnRemote, action: () => { window.open(appearNote.url || appearNote.uri, '_blank'); } diff --git a/packages/client/src/scripts/get-note-summary.ts b/packages/client/src/scripts/get-note-summary.ts index bd394279cb..54b8d109d6 100644 --- a/packages/client/src/scripts/get-note-summary.ts +++ b/packages/client/src/scripts/get-note-summary.ts @@ -7,11 +7,11 @@ import { i18n } from '@/i18n'; */ export const getNoteSummary = (note: misskey.entities.Note): string => { if (note.deletedAt) { - return `(${i18n.locale.deletedNote})`; + return `(${i18n.ts.deletedNote})`; } if (note.isHidden) { - return `(${i18n.locale.invisibleNote})`; + return `(${i18n.ts.invisibleNote})`; } let summary = ''; @@ -30,7 +30,7 @@ export const getNoteSummary = (note: misskey.entities.Note): string => { // 投票が添付されているとき if (note.poll) { - summary += ` (${i18n.locale.poll})`; + summary += ` (${i18n.ts.poll})`; } // 返信のとき diff --git a/packages/client/src/scripts/get-user-menu.ts b/packages/client/src/scripts/get-user-menu.ts index 7b910a0083..6d1f25a942 100644 --- a/packages/client/src/scripts/get-user-menu.ts +++ b/packages/client/src/scripts/get-user-menu.ts @@ -11,12 +11,12 @@ export function getUserMenu(user) { const meId = $i ? $i.id : null; async function pushList() { - const t = i18n.locale.selectList; // なぜか後で参照すると null になるので最初にメモリに確保しておく + const t = i18n.ts.selectList; // なぜか後で参照すると null になるので最初にメモリに確保しておく const lists = await os.api('users/lists/list'); if (lists.length === 0) { os.alert({ type: 'error', - text: i18n.locale.youHaveNoLists + text: i18n.ts.youHaveNoLists }); return; } @@ -38,12 +38,12 @@ export function getUserMenu(user) { if (groups.length === 0) { os.alert({ type: 'error', - text: i18n.locale.youHaveNoGroups + text: i18n.ts.youHaveNoGroups }); return; } const { canceled, result: groupId } = await os.select({ - title: i18n.locale.group, + title: i18n.ts.group, items: groups.map(group => ({ value: group.id, text: group.name })) @@ -64,7 +64,7 @@ export function getUserMenu(user) { } async function toggleBlock() { - if (!await getConfirmed(user.isBlocking ? i18n.locale.unblockConfirm : i18n.locale.blockConfirm)) return; + if (!await getConfirmed(user.isBlocking ? i18n.ts.unblockConfirm : i18n.ts.blockConfirm)) return; os.apiWithDialog(user.isBlocking ? 'blocking/delete' : 'blocking/create', { userId: user.id @@ -119,70 +119,70 @@ export function getUserMenu(user) { let menu = [{ icon: 'fas fa-at', - text: i18n.locale.copyUsername, + text: i18n.ts.copyUsername, action: () => { copyToClipboard(`@${user.username}@${user.host || host}`); } }, { icon: 'fas fa-info-circle', - text: i18n.locale.info, + text: i18n.ts.info, action: () => { os.pageWindow(`/user-info/${user.id}`); } }, { icon: 'fas fa-envelope', - text: i18n.locale.sendMessage, + text: i18n.ts.sendMessage, action: () => { os.post({ specified: user }); } }, meId != user.id ? { type: 'link', icon: 'fas fa-comments', - text: i18n.locale.startMessaging, + text: i18n.ts.startMessaging, to: '/my/messaging/' + Acct.toString(user), } : undefined, null, { icon: 'fas fa-list-ul', - text: i18n.locale.addToList, + text: i18n.ts.addToList, action: pushList }, meId != user.id ? { icon: 'fas fa-users', - text: i18n.locale.inviteToGroup, + text: i18n.ts.inviteToGroup, action: inviteGroup } : undefined] as any; if ($i && meId != user.id) { menu = menu.concat([null, { icon: user.isMuted ? 'fas fa-eye' : 'fas fa-eye-slash', - text: user.isMuted ? i18n.locale.unmute : i18n.locale.mute, + text: user.isMuted ? i18n.ts.unmute : i18n.ts.mute, action: toggleMute }, { icon: 'fas fa-ban', - text: user.isBlocking ? i18n.locale.unblock : i18n.locale.block, + text: user.isBlocking ? i18n.ts.unblock : i18n.ts.block, action: toggleBlock }]); if (user.isFollowed) { menu = menu.concat([{ icon: 'fas fa-unlink', - text: i18n.locale.breakFollow, + text: i18n.ts.breakFollow, action: invalidateFollow }]); } menu = menu.concat([null, { icon: 'fas fa-exclamation-circle', - text: i18n.locale.reportAbuse, + text: i18n.ts.reportAbuse, action: reportAbuse }]); if (iAmModerator) { menu = menu.concat([null, { icon: 'fas fa-microphone-slash', - text: user.isSilenced ? i18n.locale.unsilence : i18n.locale.silence, + text: user.isSilenced ? i18n.ts.unsilence : i18n.ts.silence, action: toggleSilence }, { icon: 'fas fa-snowflake', - text: user.isSuspended ? i18n.locale.unsuspend : i18n.locale.suspend, + text: user.isSuspended ? i18n.ts.unsuspend : i18n.ts.suspend, action: toggleSuspend }]); } @@ -191,7 +191,7 @@ export function getUserMenu(user) { if ($i && meId === user.id) { menu = menu.concat([null, { icon: 'fas fa-pencil-alt', - text: i18n.locale.editProfile, + text: i18n.ts.editProfile, action: () => { router.push('/settings/profile'); } diff --git a/packages/client/src/scripts/i18n.ts b/packages/client/src/scripts/i18n.ts index 4fa398763a..3fe88e5514 100644 --- a/packages/client/src/scripts/i18n.ts +++ b/packages/client/src/scripts/i18n.ts @@ -1,8 +1,8 @@ export class I18n<T extends Record<string, any>> { - public locale: T; + public ts: T; constructor(locale: T) { - this.locale = locale; + this.ts = locale; //#region BIND this.t = this.t.bind(this); @@ -11,9 +11,9 @@ export class I18n<T extends Record<string, any>> { // string にしているのは、ドット区切りでのパス指定を許可するため // なるべくこのメソッド使うよりもlocale直接参照の方がvueのキャッシュ効いてパフォーマンスが良いかも - public t(key: string, args?: Record<string, any>): string { + public t(key: string, args?: Record<string, string>): string { try { - let str = key.split('.').reduce((o, i) => o[i], this.locale) as string; + let str = key.split('.').reduce((o, i) => o[i], this.ts) as unknown as string; if (args) { for (const [k, v] of Object.entries(args)) { @@ -21,7 +21,7 @@ export class I18n<T extends Record<string, any>> { } } return str; - } catch (e) { + } catch (err) { console.warn(`missing localization '${key}'`); return key; } diff --git a/packages/client/src/scripts/lookup-user.ts b/packages/client/src/scripts/lookup-user.ts index 64874f86f6..8de5c84ce8 100644 --- a/packages/client/src/scripts/lookup-user.ts +++ b/packages/client/src/scripts/lookup-user.ts @@ -4,7 +4,7 @@ import * as os from '@/os'; export async function lookupUser() { const { canceled, result } = await os.inputText({ - title: i18n.locale.usernameOrUserId, + title: i18n.ts.usernameOrUserId, }); if (canceled) return; @@ -19,7 +19,7 @@ export async function lookupUser() { if (_notFound) { os.alert({ type: 'error', - text: i18n.locale.noSuchUser + text: i18n.ts.noSuchUser }); } else { _notFound = true; diff --git a/packages/client/src/scripts/please-login.ts b/packages/client/src/scripts/please-login.ts index fe3919e4c7..aeaafa124b 100644 --- a/packages/client/src/scripts/please-login.ts +++ b/packages/client/src/scripts/please-login.ts @@ -6,7 +6,7 @@ export function pleaseLogin() { if ($i) return; alert({ - title: i18n.locale.signinRequired, + title: i18n.ts.signinRequired, text: null }); diff --git a/packages/client/src/scripts/search.ts b/packages/client/src/scripts/search.ts index a070b1121c..0aedee9c98 100644 --- a/packages/client/src/scripts/search.ts +++ b/packages/client/src/scripts/search.ts @@ -4,7 +4,7 @@ import { router } from '@/router'; export async function search() { const { canceled, result: query } = await os.inputText({ - title: i18n.locale.search, + title: i18n.ts.search, }); if (canceled || query == null || query === '') return; @@ -46,7 +46,7 @@ export async function search() { uri: q }); - os.promiseDialog(promise, null, null, i18n.locale.fetchingAsApObject); + os.promiseDialog(promise, null, null, i18n.ts.fetchingAsApObject); const res = await promise; diff --git a/packages/client/src/scripts/select-file.ts b/packages/client/src/scripts/select-file.ts index 6bb3f8bf8a..23df4edf54 100644 --- a/packages/client/src/scripts/select-file.ts +++ b/packages/client/src/scripts/select-file.ts @@ -1,3 +1,4 @@ +import { ref } from 'vue'; import * as os from '@/os'; import { stream } from '@/stream'; import { i18n } from '@/i18n'; @@ -6,12 +7,14 @@ import { DriveFile } from 'misskey-js/built/entities'; function select(src: any, label: string | null, multiple: boolean): Promise<DriveFile | DriveFile[]> { return new Promise((res, rej) => { + const keepOriginal = ref(defaultStore.state.keepOriginalUploading); + const chooseFileFromPc = () => { const input = document.createElement('input'); input.type = 'file'; input.multiple = multiple; input.onchange = () => { - const promises = Array.from(input.files).map(file => os.upload(file, defaultStore.state.uploadFolder)); + const promises = Array.from(input.files).map(file => os.upload(file, defaultStore.state.uploadFolder, undefined, keepOriginal.value)); Promise.all(promises).then(driveFiles => { res(multiple ? driveFiles : driveFiles[0]); @@ -41,9 +44,9 @@ function select(src: any, label: string | null, multiple: boolean): Promise<Driv const chooseFileFromUrl = () => { os.inputText({ - title: i18n.locale.uploadFromUrl, + title: i18n.ts.uploadFromUrl, type: 'url', - placeholder: i18n.locale.uploadFromUrlDescription + placeholder: i18n.ts.uploadFromUrlDescription }).then(({ canceled, result: url }) => { if (canceled) return; @@ -64,8 +67,8 @@ function select(src: any, label: string | null, multiple: boolean): Promise<Driv }); os.alert({ - title: i18n.locale.uploadFromUrlRequested, - text: i18n.locale.uploadFromUrlMayTakeTime + title: i18n.ts.uploadFromUrlRequested, + text: i18n.ts.uploadFromUrlMayTakeTime }); }); }; @@ -74,15 +77,19 @@ function select(src: any, label: string | null, multiple: boolean): Promise<Driv text: label, type: 'label' } : undefined, { - text: i18n.locale.upload, + type: 'switch', + text: i18n.ts.keepOriginalUploading, + ref: keepOriginal + }, { + text: i18n.ts.upload, icon: 'fas fa-upload', action: chooseFileFromPc }, { - text: i18n.locale.fromDrive, + text: i18n.ts.fromDrive, icon: 'fas fa-cloud', action: chooseFileFromDrive }, { - text: i18n.locale.fromUrl, + text: i18n.ts.fromUrl, icon: 'fas fa-link', action: chooseFileFromUrl }], src); diff --git a/packages/client/src/scripts/show-suspended-dialog.ts b/packages/client/src/scripts/show-suspended-dialog.ts index dcbb66933c..acfbc60e92 100644 --- a/packages/client/src/scripts/show-suspended-dialog.ts +++ b/packages/client/src/scripts/show-suspended-dialog.ts @@ -4,7 +4,7 @@ import { i18n } from '@/i18n'; export function showSuspendedDialog() { return os.alert({ type: 'error', - title: i18n.locale.yourAccountSuspendedTitle, - text: i18n.locale.yourAccountSuspendedDescription + title: i18n.ts.yourAccountSuspendedTitle, + text: i18n.ts.yourAccountSuspendedDescription }); } diff --git a/packages/client/src/scripts/use-leave-guard.ts b/packages/client/src/scripts/use-leave-guard.ts index 3984256251..33eea6b522 100644 --- a/packages/client/src/scripts/use-leave-guard.ts +++ b/packages/client/src/scripts/use-leave-guard.ts @@ -12,7 +12,7 @@ export function useLeaveGuard(enabled: Ref<boolean>) { const { canceled } = await os.confirm({ type: 'warning', - text: i18n.locale.leaveConfirm, + text: i18n.ts.leaveConfirm, }); return canceled; @@ -23,7 +23,7 @@ export function useLeaveGuard(enabled: Ref<boolean>) { const { canceled } = await os.confirm({ type: 'warning', - text: i18n.locale.leaveConfirm, + text: i18n.ts.leaveConfirm, }); return !canceled; diff --git a/packages/client/src/scripts/use-note-capture.ts b/packages/client/src/scripts/use-note-capture.ts index bb00e464e3..b7cf99d5e1 100644 --- a/packages/client/src/scripts/use-note-capture.ts +++ b/packages/client/src/scripts/use-note-capture.ts @@ -19,51 +19,41 @@ export function useNoteCapture(props: { case 'reacted': { const reaction = body.reaction; - const updated = JSON.parse(JSON.stringify(appearNote.value)); - if (body.emoji) { const emojis = appearNote.value.emojis || []; if (!emojis.includes(body.emoji)) { - updated.emojis = [...emojis, body.emoji]; + appearNote.value.emojis = [...emojis, body.emoji]; } } // TODO: reactionsプロパティがない場合ってあったっけ? なければ || {} は消せる const currentCount = (appearNote.value.reactions || {})[reaction] || 0; - updated.reactions[reaction] = currentCount + 1; + appearNote.value.reactions[reaction] = currentCount + 1; if ($i && (body.userId === $i.id)) { - updated.myReaction = reaction; + appearNote.value.myReaction = reaction; } - - appearNote.value = updated; break; } case 'unreacted': { const reaction = body.reaction; - const updated = JSON.parse(JSON.stringify(appearNote.value)); - // TODO: reactionsプロパティがない場合ってあったっけ? なければ || {} は消せる const currentCount = (appearNote.value.reactions || {})[reaction] || 0; - updated.reactions[reaction] = Math.max(0, currentCount - 1); + appearNote.value.reactions[reaction] = Math.max(0, currentCount - 1); if ($i && (body.userId === $i.id)) { - updated.myReaction = null; + appearNote.value.myReaction = null; } - - appearNote.value = updated; break; } case 'pollVoted': { const choice = body.choice; - const updated = JSON.parse(JSON.stringify(appearNote.value)); - const choices = [...appearNote.value.poll.choices]; choices[choice] = { ...choices[choice], @@ -73,16 +63,12 @@ export function useNoteCapture(props: { } : {}) }; - updated.poll.choices = choices; - - appearNote.value = updated; + appearNote.value.poll.choices = choices; break; } case 'deleted': { - const updated = JSON.parse(JSON.stringify(appearNote.value)); - updated.value = true; - appearNote.value = updated; + appearNote.value.deletedAt = new Date(); break; } } diff --git a/packages/client/src/store.ts b/packages/client/src/store.ts index cd358d29d0..b80fc8bbe3 100644 --- a/packages/client/src/store.ts +++ b/packages/client/src/store.ts @@ -43,6 +43,10 @@ export const defaultStore = markRaw(new Storage('base', { where: 'account', default: 'yyyy-MM-dd HH-mm-ss [{{number}}]' }, + keepOriginalUploading: { + where: 'account', + default: false + }, memo: { where: 'account', default: null diff --git a/packages/client/src/types/menu.ts b/packages/client/src/types/menu.ts new file mode 100644 index 0000000000..ed67e6ab88 --- /dev/null +++ b/packages/client/src/types/menu.ts @@ -0,0 +1,20 @@ +import * as Misskey from 'misskey-js'; +import { Ref } from 'vue'; + +export type MenuAction = (ev: MouseEvent) => void; + +export type MenuDivider = null; +export type MenuNull = undefined; +export type MenuLabel = { type: 'label', text: string }; +export type MenuLink = { type: 'link', to: string, text: string, icon?: string, indicate?: boolean, avatar?: Misskey.entities.User }; +export type MenuA = { type: 'a', href: string, target?: string, download?: string, text: string, icon?: string, indicate?: boolean }; +export type MenuUser = { type: 'user', user: Misskey.entities.User, active?: boolean, indicate?: boolean, action: MenuAction }; +export type MenuSwitch = { type: 'switch', ref: Ref<boolean>, text: string, disabled?: boolean }; +export type MenuButton = { type?: 'button', text: string, icon?: string, indicate?: boolean, danger?: boolean, active?: boolean, avatar?: Misskey.entities.User; action: MenuAction }; + +export type MenuPending = { type: 'pending' }; + +type OuterMenuItem = MenuDivider | MenuNull | MenuLabel | MenuLink | MenuA | MenuUser | MenuSwitch | MenuButton; +type OuterPromiseMenuItem = Promise<MenuLabel | MenuLink | MenuA | MenuUser | MenuSwitch | MenuButton>; +export type MenuItem = OuterMenuItem | OuterPromiseMenuItem; +export type InnerMenuItem = MenuDivider | MenuPending | MenuLabel | MenuLink | MenuA | MenuUser | MenuSwitch | MenuButton; diff --git a/packages/client/src/ui/classic.side.vue b/packages/client/src/ui/classic.side.vue index f816834141..6c2329194e 100644 --- a/packages/client/src/ui/classic.side.vue +++ b/packages/client/src/ui/classic.side.vue @@ -4,7 +4,7 @@ <header class="header" @contextmenu.prevent.stop="onContextmenu"> <button v-if="history.length > 0" class="_button" @click="back()"><i class="fas fa-chevron-left"></i></button> <button v-else class="_button" style="pointer-events: none;"><!-- マージンのバランスを取るためのダミー --></button> - <span class="title">{{ pageInfo.title }}</span> + <span class="title" v-text="pageInfo?.title" /> <button class="_button" @click="close()"><i class="fas fa-times"></i></button> </header> <MkHeader class="pageHeader" :info="pageInfo"/> @@ -13,99 +13,89 @@ </div> </template> -<script lang="ts"> -import { defineComponent } from 'vue'; +<script lang="ts" setup> +import { provide } from 'vue'; import * as os from '@/os'; import copyToClipboard from '@/scripts/copy-to-clipboard'; -import { resolve } from '@/router'; -import { url } from '@/config'; +import { resolve, router } from '@/router'; +import { url as root } from '@/config'; import * as symbols from '@/symbols'; +import { i18n } from '@/i18n'; -export default defineComponent({ - provide() { - return { - navHook: (path) => { - this.navigate(path); - } - }; - }, +provide('navHook', navigate); - data() { - return { - path: null, - component: null, - props: {}, - pageInfo: null, - history: [], - }; - }, +let path: string | null = $ref(null); +let component: ReturnType<typeof resolve>['component'] | null = $ref(null); +let props: any | null = $ref(null); +let pageInfo: any | null = $ref(null); +let history: string[] = $ref([]); - computed: { - url(): string { - return url + this.path; - } - }, +let url = $computed(() => `${root}${path}`); - methods: { - changePage(page) { - if (page == null) return; - if (page[symbols.PAGE_INFO]) { - this.pageInfo = page[symbols.PAGE_INFO]; - } - }, - - navigate(path, record = true) { - if (record && this.path) this.history.push(this.path); - this.path = path; - const { component, props } = resolve(path); - this.component = component; - this.props = props; - }, - - back() { - this.navigate(this.history.pop(), false); - }, - - close() { - this.path = null; - this.component = null; - this.props = {}; - }, - - onContextmenu(ev: MouseEvent) { - os.contextMenu([{ - type: 'label', - text: this.path, - }, { - icon: 'fas fa-expand-alt', - text: this.$ts.showInPage, - action: () => { - this.$router.push(this.path); - this.close(); - } - }, { - icon: 'fas fa-window-maximize', - text: this.$ts.openInWindow, - action: () => { - os.pageWindow(this.path); - this.close(); - } - }, null, { - icon: 'fas fa-external-link-alt', - text: this.$ts.openInNewTab, - action: () => { - window.open(this.url, '_blank'); - this.close(); - } - }, { - icon: 'fas fa-link', - text: this.$ts.copyLink, - action: () => { - copyToClipboard(this.url); - } - }], ev); - } +function changePage(page) { + if (page == null) return; + if (page[symbols.PAGE_INFO]) { + pageInfo = page[symbols.PAGE_INFO]; } +} + +function navigate(_path: string, record = true) { + if (record && path) history.push($$(path).value); + path = _path; + const resolved = resolve(path); + component = resolved.component; + props = resolved.props; +} + +function back() { + const prev = history.pop(); + if (prev) navigate(prev, false); +} + +function close() { + path = null; + component = null; + props = {}; +} + +function onContextmenu(ev: MouseEvent) { + os.contextMenu([{ + type: 'label', + text: path || '', + }, { + icon: 'fas fa-expand-alt', + text: i18n.ts.showInPage, + action: () => { + if (path) router.push(path); + close(); + } + }, { + icon: 'fas fa-window-maximize', + text: i18n.ts.openInWindow, + action: () => { + if (path) os.pageWindow(path); + close(); + } + }, null, { + icon: 'fas fa-external-link-alt', + text: i18n.ts.openInNewTab, + action: () => { + window.open(url, '_blank'); + close(); + } + }, { + icon: 'fas fa-link', + text: i18n.ts.copyLink, + action: () => { + copyToClipboard(url); + } + }], ev); +} + +defineExpose({ + navigate, + back, + close, }); </script> diff --git a/packages/client/src/ui/deck.vue b/packages/client/src/ui/deck.vue index 51a4853e9d..9accc34a88 100644 --- a/packages/client/src/ui/deck.vue +++ b/packages/client/src/ui/deck.vue @@ -104,7 +104,7 @@ export default defineComponent({ ]; const { canceled, result: column } = await os.select({ - title: i18n.locale._deck.addColumn, + title: i18n.ts._deck.addColumn, items: columns.map(column => ({ value: column, text: i18n.t('_deck._columns.' + column) })) @@ -121,7 +121,7 @@ export default defineComponent({ const onContextmenu = (ev) => { os.contextMenu([{ - text: i18n.locale._deck.addColumn, + text: i18n.ts._deck.addColumn, icon: null, action: addColumn }], ev); diff --git a/packages/client/src/ui/deck/deck-store.ts b/packages/client/src/ui/deck/deck-store.ts index 6b6b02f3f9..66db5e83ed 100644 --- a/packages/client/src/ui/deck/deck-store.ts +++ b/packages/client/src/ui/deck/deck-store.ts @@ -77,12 +77,12 @@ export const loadDeck = async () => { deckStore.set('columns', [{ id: 'a', type: 'main', - name: i18n.locale._deck._columns.main, + name: i18n.ts._deck._columns.main, width: 350, }, { id: 'b', type: 'notifications', - name: i18n.locale._deck._columns.notifications, + name: i18n.ts._deck._columns.notifications, width: 330, }]); deckStore.set('layout', [['a'], ['b']]); diff --git a/packages/client/src/ui/universal.vue b/packages/client/src/ui/universal.vue index 16cc9a4f06..b0dfc5aadc 100644 --- a/packages/client/src/ui/universal.vue +++ b/packages/client/src/ui/universal.vue @@ -20,7 +20,7 @@ </main> </div> - <XSideView v-if="isDesktop" ref="side" class="side"/> + <XSideView v-if="isDesktop" ref="sideEl" class="side"/> <div v-if="isDesktop" ref="widgetsEl" class="widgets"> <XWidgets @mounted="attachSticky"/> @@ -31,9 +31,9 @@ <div v-if="isMobile" class="buttons"> <button class="button nav _button" @click="drawerMenuShowing = true"><i class="fas fa-bars"></i><span v-if="menuIndicated" class="indicator"><i class="fas fa-circle"></i></span></button> <button class="button home _button" @click="$route.name === 'index' ? top() : $router.push('/')"><i class="fas fa-home"></i></button> - <button class="button notifications _button" @click="$router.push('/my/notifications')"><i class="fas fa-bell"></i><span v-if="$i.hasUnreadNotification" class="indicator"><i class="fas fa-circle"></i></span></button> + <button class="button notifications _button" @click="$router.push('/my/notifications')"><i class="fas fa-bell"></i><span v-if="$i?.hasUnreadNotification" class="indicator"><i class="fas fa-circle"></i></span></button> <button class="button widget _button" @click="widgetsShowing = true"><i class="fas fa-layer-group"></i></button> - <button class="button post _button" @click="post()"><i class="fas fa-pencil-alt"></i></button> + <button class="button post _button" @click="os.post()"><i class="fas fa-pencil-alt"></i></button> </div> <transition :name="$store.state.animation ? 'menuDrawer-back' : ''"> @@ -64,155 +64,133 @@ </div> </template> -<script lang="ts"> -import { defineComponent, defineAsyncComponent, provide, onMounted, computed, ref, watch } from 'vue'; +<script lang="ts" setup> +import { defineAsyncComponent, provide, onMounted, computed, ref, watch } from 'vue'; import { instanceName } from '@/config'; import { StickySidebar } from '@/scripts/sticky-sidebar'; -import XSidebar from '@/ui/_common_/sidebar.vue'; import XDrawerMenu from '@/ui/_common_/sidebar-for-mobile.vue'; import XCommon from './_common_/common.vue'; import XSideView from './classic.side.vue'; import * as os from '@/os'; import * as symbols from '@/symbols'; import { defaultStore } from '@/store'; -import * as EventEmitter from 'eventemitter3'; import { menuDef } from '@/menu'; import { useRoute } from 'vue-router'; import { i18n } from '@/i18n'; +import { $i } from '@/account'; +const XWidgets = defineAsyncComponent(() => import('./universal.widgets.vue')); +const XSidebar = defineAsyncComponent(() => import('@/ui/_common_/sidebar.vue')); const DESKTOP_THRESHOLD = 1100; const MOBILE_THRESHOLD = 500; -export default defineComponent({ - components: { - XCommon, - XSidebar, - XDrawerMenu, - XWidgets: defineAsyncComponent(() => import('./universal.widgets.vue')), - XSideView, // NOTE: dynamic importするとAsyncComponentWrapperが間に入るせいでref取得できなくて面倒になる - }, - - setup() { - const isDesktop = ref(window.innerWidth >= DESKTOP_THRESHOLD); - const isMobile = ref(window.innerWidth <= MOBILE_THRESHOLD); - window.addEventListener('resize', () => { - isMobile.value = window.innerWidth <= MOBILE_THRESHOLD; - }); - - const pageInfo = ref(); - const widgetsEl = ref<HTMLElement>(); - const widgetsShowing = ref(false); - - const sideViewController = new EventEmitter(); - - provide('sideViewHook', isDesktop.value ? (url) => { - sideViewController.emit('navigate', url); - } : null); - - const menuIndicated = computed(() => { - for (const def in menuDef) { - if (def === 'notifications') continue; // 通知は下にボタンとして表示されてるから - if (menuDef[def].indicated) return true; - } - return false; - }); - - const drawerMenuShowing = ref(false); - - const route = useRoute(); - watch(route, () => { - drawerMenuShowing.value = false; - }); - - document.documentElement.style.overflowY = 'scroll'; - - if (defaultStore.state.widgets.length === 0) { - defaultStore.set('widgets', [{ - name: 'calendar', - id: 'a', place: 'right', data: {} - }, { - name: 'notifications', - id: 'b', place: 'right', data: {} - }, { - name: 'trends', - id: 'c', place: 'right', data: {} - }]); - } - - onMounted(() => { - if (!isDesktop.value) { - window.addEventListener('resize', () => { - if (window.innerWidth >= DESKTOP_THRESHOLD) isDesktop.value = true; - }, { passive: true }); - } - }); - - const changePage = (page) => { - if (page == null) return; - if (page[symbols.PAGE_INFO]) { - pageInfo.value = page[symbols.PAGE_INFO]; - document.title = `${pageInfo.value.title} | ${instanceName}`; - } - }; - - const onContextmenu = (ev) => { - const isLink = (el: HTMLElement) => { - if (el.tagName === 'A') return true; - if (el.parentElement) { - return isLink(el.parentElement); - } - }; - if (isLink(ev.target)) return; - if (['INPUT', 'TEXTAREA', 'IMG', 'VIDEO', 'CANVAS'].includes(ev.target.tagName) || ev.target.attributes['contenteditable']) return; - if (window.getSelection().toString() !== '') return; - const path = route.path; - os.contextMenu([{ - type: 'label', - text: path, - }, { - icon: 'fas fa-columns', - text: i18n.locale.openInSideView, - action: () => { - this.$refs.side.navigate(path); - } - }, { - icon: 'fas fa-window-maximize', - text: i18n.locale.openInWindow, - action: () => { - os.pageWindow(path); - } - }], ev); - }; - - const attachSticky = (el) => { - const sticky = new StickySidebar(widgetsEl.value); - window.addEventListener('scroll', () => { - sticky.calc(window.scrollY); - }, { passive: true }); - }; - - return { - pageInfo, - isDesktop, - isMobile, - widgetsEl, - widgetsShowing, - drawerMenuShowing, - menuIndicated, - wallpaper: localStorage.getItem('wallpaper') != null, - changePage, - top: () => { - window.scroll({ top: 0, behavior: 'smooth' }); - }, - onTransition: () => { - if (window._scroll) window._scroll(); - }, - post: os.post, - onContextmenu, - attachSticky, - }; - }, +const isDesktop = ref(window.innerWidth >= DESKTOP_THRESHOLD); +const isMobile = ref(window.innerWidth <= MOBILE_THRESHOLD); +window.addEventListener('resize', () => { + isMobile.value = window.innerWidth <= MOBILE_THRESHOLD; }); + +const pageInfo = ref(); +const widgetsEl = $ref<HTMLElement>(); +const widgetsShowing = ref(false); + +let sideEl = $ref<InstanceType<typeof XSideView>>(); + +provide('sideViewHook', isDesktop.value ? (url) => { + sideEl.navigate(url); +} : null); + +const menuIndicated = computed(() => { + for (const def in menuDef) { + if (def === 'notifications') continue; // 通知は下にボタンとして表示されてるから + if (menuDef[def].indicated) return true; + } + return false; +}); + +const drawerMenuShowing = ref(false); + +const route = useRoute(); +watch(route, () => { + drawerMenuShowing.value = false; +}); + +document.documentElement.style.overflowY = 'scroll'; + +if (defaultStore.state.widgets.length === 0) { + defaultStore.set('widgets', [{ + name: 'calendar', + id: 'a', place: 'right', data: {} + }, { + name: 'notifications', + id: 'b', place: 'right', data: {} + }, { + name: 'trends', + id: 'c', place: 'right', data: {} + }]); +} + +onMounted(() => { + if (!isDesktop.value) { + window.addEventListener('resize', () => { + if (window.innerWidth >= DESKTOP_THRESHOLD) isDesktop.value = true; + }, { passive: true }); + } +}); + +const changePage = (page) => { + if (page == null) return; + if (page[symbols.PAGE_INFO]) { + pageInfo.value = page[symbols.PAGE_INFO]; + document.title = `${pageInfo.value.title} | ${instanceName}`; + } +}; + +const onContextmenu = (ev) => { + const isLink = (el: HTMLElement) => { + if (el.tagName === 'A') return true; + if (el.parentElement) { + return isLink(el.parentElement); + } + }; + if (isLink(ev.target)) return; + if (['INPUT', 'TEXTAREA', 'IMG', 'VIDEO', 'CANVAS'].includes(ev.target.tagName) || ev.target.attributes['contenteditable']) return; + if (window.getSelection()?.toString() !== '') return; + const path = route.path; + os.contextMenu([{ + type: 'label', + text: path, + }, { + icon: 'fas fa-columns', + text: i18n.ts.openInSideView, + action: () => { + sideEl.navigate(path); + } + }, { + icon: 'fas fa-window-maximize', + text: i18n.ts.openInWindow, + action: () => { + os.pageWindow(path); + } + }], ev); +}; + +const attachSticky = (el) => { + const sticky = new StickySidebar(widgetsEl); + window.addEventListener('scroll', () => { + sticky.calc(window.scrollY); + }, { passive: true }); +}; + +function top() { + window.scroll({ top: 0, behavior: 'smooth' }); +} + +function onTransition() { + if (window._scroll) window._scroll(); +} + +const wallpaper = localStorage.getItem('wallpaper') != null; </script> <style lang="scss" scoped> diff --git a/packages/client/src/widgets/calendar.vue b/packages/client/src/widgets/calendar.vue index b0e3edcb12..c6a69b3fb8 100644 --- a/packages/client/src/widgets/calendar.vue +++ b/packages/client/src/widgets/calendar.vue @@ -79,13 +79,13 @@ const tick = () => { month.value = nm + 1; day.value = nd; weekDay.value = [ - i18n.locale._weekday.sunday, - i18n.locale._weekday.monday, - i18n.locale._weekday.tuesday, - i18n.locale._weekday.wednesday, - i18n.locale._weekday.thursday, - i18n.locale._weekday.friday, - i18n.locale._weekday.saturday + i18n.ts._weekday.sunday, + i18n.ts._weekday.monday, + i18n.ts._weekday.tuesday, + i18n.ts._weekday.wednesday, + i18n.ts._weekday.thursday, + i18n.ts._weekday.friday, + i18n.ts._weekday.saturday ][now.getDay()]; const dayNumer = now.getTime() - new Date(ny, nm, nd).getTime(); diff --git a/packages/client/src/widgets/federation.vue b/packages/client/src/widgets/federation.vue index 4c43117e48..5f1131dce1 100644 --- a/packages/client/src/widgets/federation.vue +++ b/packages/client/src/widgets/federation.vue @@ -54,13 +54,13 @@ const charts = ref([]); const fetching = ref(true); const fetch = async () => { - const instances = await os.api('federation/instances', { + const fetchedInstances = await os.api('federation/instances', { sort: '+lastCommunicatedAt', limit: 5 }); - const charts = await Promise.all(instances.map(i => os.api('charts/instance', { host: i.host, limit: 16, span: 'hour' }))); - instances.value = instances; - charts.value = charts; + const fetchedCharts = await Promise.all(fetchedInstances.map(i => os.api('charts/instance', { host: i.host, limit: 16, span: 'hour' }))); + instances.value = fetchedInstances; + charts.value = fetchedCharts; fetching.value = false; }; diff --git a/packages/client/src/widgets/timeline.vue b/packages/client/src/widgets/timeline.vue index fa700cc8ee..34e3b20e36 100644 --- a/packages/client/src/widgets/timeline.vue +++ b/packages/client/src/widgets/timeline.vue @@ -101,22 +101,22 @@ const choose = async (ev) => { } })); os.popupMenu([{ - text: i18n.locale._timelines.home, + text: i18n.ts._timelines.home, icon: 'fas fa-home', action: () => { setSrc('home') } }, { - text: i18n.locale._timelines.local, + text: i18n.ts._timelines.local, icon: 'fas fa-comments', action: () => { setSrc('local') } }, { - text: i18n.locale._timelines.social, + text: i18n.ts._timelines.social, icon: 'fas fa-share-alt', action: () => { setSrc('social') } }, { - text: i18n.locale._timelines.global, + text: i18n.ts._timelines.global, icon: 'fas fa-globe', action: () => { setSrc('global') } - }, antennaItems.length > 0 ? null : undefined, ...antennaItems, listItems.length > 0 ? null : undefined, ...listItems], ev.currentTarget || ev.target).then(() => { + }, antennaItems.length > 0 ? null : undefined, ...antennaItems, listItems.length > 0 ? null : undefined, ...listItems], ev.currentTarget ?? ev.target).then(() => { menuOpened.value = false; }); }; diff --git a/packages/client/src/widgets/trends.vue b/packages/client/src/widgets/trends.vue index eb5eb4049f..a34710eae7 100644 --- a/packages/client/src/widgets/trends.vue +++ b/packages/client/src/widgets/trends.vue @@ -52,8 +52,8 @@ const stats = ref([]); const fetching = ref(true); const fetch = () => { - os.api('hashtags/trend').then(stats => { - stats.value = stats; + os.api('hashtags/trend').then(res => { + stats.value = res; fetching.value = false; }); }; diff --git a/packages/client/yarn.lock b/packages/client/yarn.lock index a45a24ce0e..76be45f145 100644 --- a/packages/client/yarn.lock +++ b/packages/client/yarn.lock @@ -4139,10 +4139,10 @@ minimist@^1.2.0, minimist@^1.2.5: resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw== -misskey-js@0.0.13: - version "0.0.13" - resolved "https://registry.yarnpkg.com/misskey-js/-/misskey-js-0.0.13.tgz#03a4e469186e28752d599dc4093519eb64647970" - integrity sha512-kBdJdfe281gtykzzsrN3IAxWUQIimzPiJGyKWf863ggWJlWYVPmP9hTFlX2z8oPOaypgVBPEPHyw/jNUdc2DbQ== +misskey-js@0.0.14: + version "0.0.14" + resolved "https://registry.yarnpkg.com/misskey-js/-/misskey-js-0.0.14.tgz#1a616bdfbe81c6ee6900219eaf425bb5c714dd4d" + integrity sha512-bvLx6U3OwQwqHfp/WKwIVwdvNYAAPk0+YblXyxmSG3dwlzCgBRRLcB8o6bNruUDyJgh3t73pLDcOz3myxcUmww== dependencies: autobind-decorator "^2.4.0" eventemitter3 "^4.0.7" diff --git a/packages/shared/.eslintrc.js b/packages/shared/.eslintrc.js index 9d7d4159ec..2d3356c3a6 100644 --- a/packages/shared/.eslintrc.js +++ b/packages/shared/.eslintrc.js @@ -37,6 +37,7 @@ module.exports = { ] }], */ + 'eqeqeq': ['error', 'always', { 'null': 'ignore' }], 'no-multi-spaces': ['error'], 'no-var': ['error'], 'prefer-arrow-callback': ['error'], @@ -56,7 +57,7 @@ module.exports = { 'object-curly-spacing': ['error', 'always'], 'space-infix-ops': ['error'], 'space-before-blocks': ['error', 'always'], - '@typescript-eslint/no-unnecessary-condition': ['error'], + '@typescript-eslint/no-unnecessary-condition': ['warn'], '@typescript-eslint/no-var-requires': ['warn'], '@typescript-eslint/no-inferrable-types': ['warn'], '@typescript-eslint/no-empty-function': ['off'],