Merge branch 'develop' into swn
This commit is contained in:
commit
28a2297985
65 changed files with 1243 additions and 652 deletions
28
CHANGELOG.md
28
CHANGELOG.md
|
@ -10,16 +10,28 @@
|
|||
|
||||
-->
|
||||
|
||||
## 12.x.x (unreleased)
|
||||
|
||||
### Improvements
|
||||
## 12.103.1 (2022/02/02)
|
||||
|
||||
### Bugfixes
|
||||
- 投稿のNSFW画像を表示したあとにリアクションが更新されると画像が非表示になる問題を修正
|
||||
- 「クリップ」ページが開かない問題を修正
|
||||
- トレンドウィジェットが動作しないのを修正
|
||||
- リアクション設定で絵文字ピッカーが開かないのを修正
|
||||
- DMページでメンションが含まれる問題を修正
|
||||
- クライアント: ツールチップの表示位置が正しくない問題を修正
|
||||
|
||||
## 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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -176,3 +176,7 @@ describe('After user singed in', () => {
|
|||
cy.contains('Hello, Misskey!');
|
||||
});
|
||||
});
|
||||
|
||||
// TODO: 投稿フォームの公開範囲指定のテスト
|
||||
// TODO: 投稿フォームのファイル添付のテスト
|
||||
// TODO: 投稿フォームのハッシュタグ保持フィールドのテスト
|
||||
|
|
|
@ -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: "উল্লেখসমূহ"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -235,6 +235,8 @@ resetAreYouSure: "リセットしますか?"
|
|||
saved: "保存しました"
|
||||
messaging: "チャット"
|
||||
upload: "アップロード"
|
||||
keepOriginalUploading: "オリジナル画像を保持"
|
||||
keepOriginalUploadingDescription: "画像をアップロードする時にオリジナル版を保持します。オフにするとアップロード時にブラウザでWeb公開用画像を生成します。"
|
||||
fromDrive: "ドライブから"
|
||||
fromUrl: "URLから"
|
||||
uploadFromUrl: "URLアップロード"
|
||||
|
|
|
@ -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: "형식이 올바르지 않습니다"
|
||||
|
|
|
@ -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: "在新标签页中打开"
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "misskey",
|
||||
"version": "12.102.1",
|
||||
"version": "12.103.1",
|
||||
"codename": "indigo",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
@ -46,7 +46,7 @@
|
|||
"@types/fluent-ffmpeg": "2.1.20",
|
||||
"@typescript-eslint/parser": "5.10.0",
|
||||
"cross-env": "7.0.3",
|
||||
"cypress": "9.3.1",
|
||||
"cypress": "9.4.1",
|
||||
"start-server-and-test": "1.14.0",
|
||||
"typescript": "4.5.5"
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
export function isDuplicateKeyValueError(e: Error): boolean {
|
||||
return e.message.startsWith('duplicate key value');
|
||||
export function isDuplicateKeyValueError(e: unknown | Error): boolean {
|
||||
return (e as any).message && (e as Error).message.startsWith('duplicate key value');
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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',
|
||||
},
|
||||
},
|
||||
|
||||
|
|
|
@ -105,7 +105,10 @@ export interface NoteStreamTypes {
|
|||
};
|
||||
reacted: {
|
||||
reaction: string;
|
||||
emoji?: Emoji;
|
||||
emoji?: {
|
||||
name: string;
|
||||
url: string;
|
||||
} | null;
|
||||
userId: User['id'];
|
||||
};
|
||||
unreacted: {
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
|
||||
|
|
|
@ -21,6 +21,7 @@ html
|
|||
meta(name='referrer' content='origin')
|
||||
meta(name='theme-color' content='#86b300')
|
||||
meta(name='theme-color-orig' content='#86b300')
|
||||
meta(property='twitter:card' content='summary')
|
||||
meta(property='og:site_name' content= instanceName || 'Misskey')
|
||||
meta(name='viewport' content='width=device-width, initial-scale=1')
|
||||
link(rel='icon' href= icon || '/favicon.ico')
|
||||
|
@ -42,7 +43,9 @@ html
|
|||
block meta
|
||||
|
||||
block og
|
||||
meta(property='og:image' content=img)
|
||||
meta(property='og:title' content= title || 'Misskey')
|
||||
meta(property='og:description' content= desc || '✨🌎✨ A interplanetary communication platform ✨🚀✨')
|
||||
meta(property='og:image' content= img)
|
||||
|
||||
style
|
||||
include ../style.css
|
||||
|
|
|
@ -16,6 +16,3 @@ block og
|
|||
meta(property='og:description' content= channel.description)
|
||||
meta(property='og:url' content= url)
|
||||
meta(property='og:image' content= channel.bannerUrl)
|
||||
|
||||
block meta
|
||||
meta(name='twitter:card' content='summary')
|
||||
|
|
|
@ -26,8 +26,6 @@ block meta
|
|||
meta(name='misskey:user-id' content=user.id)
|
||||
meta(name='misskey:clip-id' content=clip.id)
|
||||
|
||||
meta(name='twitter:card' content='summary')
|
||||
|
||||
// todo
|
||||
if user.twitter
|
||||
meta(name='twitter:creator' content=`@${user.twitter.screenName}`)
|
||||
|
|
|
@ -25,8 +25,6 @@ block meta
|
|||
meta(name='misskey:user-username' content=user.username)
|
||||
meta(name='misskey:user-id' content=user.id)
|
||||
|
||||
meta(name='twitter:card' content='summary')
|
||||
|
||||
// todo
|
||||
if user.twitter
|
||||
meta(name='twitter:creator' content=`@${user.twitter.screenName}`)
|
||||
|
|
|
@ -27,8 +27,6 @@ block meta
|
|||
meta(name='misskey:user-id' content=user.id)
|
||||
meta(name='misskey:note-id' content=note.id)
|
||||
|
||||
meta(name='twitter:card' content='summary')
|
||||
|
||||
// todo
|
||||
if user.twitter
|
||||
meta(name='twitter:creator' content=`@${user.twitter.screenName}`)
|
||||
|
|
|
@ -26,8 +26,6 @@ block meta
|
|||
meta(name='misskey:user-id' content=user.id)
|
||||
meta(name='misskey:page-id' content=page.id)
|
||||
|
||||
meta(name='twitter:card' content='summary')
|
||||
|
||||
// todo
|
||||
if user.twitter
|
||||
meta(name='twitter:creator' content=`@${user.twitter.screenName}`)
|
||||
|
|
|
@ -25,8 +25,6 @@ block meta
|
|||
meta(name='misskey:user-username' content=user.username)
|
||||
meta(name='misskey:user-id' content=user.id)
|
||||
|
||||
meta(name='twitter:card' content='summary')
|
||||
|
||||
if profile.twitter
|
||||
meta(name='twitter:creator' content=`@${profile.twitter.screenName}`)
|
||||
|
||||
|
|
|
@ -59,7 +59,7 @@ class NotificationManager {
|
|||
|
||||
if (exist) {
|
||||
// 「メンションされているかつ返信されている」場合は、メンションとしての通知ではなく返信としての通知にする
|
||||
if (reason != 'mention') {
|
||||
if (reason !== 'mention') {
|
||||
exist.reason = reason;
|
||||
}
|
||||
} else {
|
||||
|
@ -201,7 +201,7 @@ export default async (user: { id: User['id']; username: User['username']; host:
|
|||
mentionedUsers.push(await Users.findOneOrFail(data.reply.userId));
|
||||
}
|
||||
|
||||
if (data.visibility == 'specified') {
|
||||
if (data.visibility === 'specified') {
|
||||
if (data.visibleUsers == null) throw new Error('invalid param');
|
||||
|
||||
for (const u of data.visibleUsers) {
|
||||
|
@ -301,7 +301,7 @@ export default async (user: { id: User['id']; username: User['username']; host:
|
|||
if (Users.isRemoteUser(user)) activeUsersChart.update(user);
|
||||
|
||||
// 未読通知を作成
|
||||
if (data.visibility == 'specified') {
|
||||
if (data.visibility === 'specified') {
|
||||
if (data.visibleUsers == null) throw new Error('invalid param');
|
||||
|
||||
for (const u of data.visibleUsers) {
|
||||
|
@ -439,7 +439,7 @@ export default async (user: { id: User['id']; username: User['username']; host:
|
|||
async function renderNoteOrRenoteActivity(data: Option, note: Note) {
|
||||
if (data.localOnly) return null;
|
||||
|
||||
const content = data.renote && data.text == null && data.poll == null && (data.files == null || data.files.length == 0)
|
||||
const content = data.renote && data.text == null && data.poll == null && (data.files == null || data.files.length === 0)
|
||||
? renderAnnounce(data.renote.uri ? data.renote.uri : `${config.url}/notes/${data.renote.id}`, note)
|
||||
: renderCreate(await renderNote(note, false), note);
|
||||
|
||||
|
@ -478,7 +478,7 @@ async function insertNote(user: { id: User['id']; host: User['host']; }, data: O
|
|||
userId: user.id,
|
||||
localOnly: data.localOnly!,
|
||||
visibility: data.visibility as any,
|
||||
visibleUserIds: data.visibility == 'specified'
|
||||
visibleUserIds: data.visibility === 'specified'
|
||||
? data.visibleUsers
|
||||
? data.visibleUsers.map(u => u.id)
|
||||
: []
|
||||
|
@ -502,7 +502,7 @@ async function insertNote(user: { id: User['id']; host: User['host']; }, data: O
|
|||
insert.mentions = mentionedUsers.map(u => u.id);
|
||||
const profiles = await UserProfiles.find({ userId: In(insert.mentions) });
|
||||
insert.mentionedRemoteUsers = JSON.stringify(mentionedUsers.filter(u => Users.isRemoteUser(u)).map(u => {
|
||||
const profile = profiles.find(p => p.userId == u.id);
|
||||
const profile = profiles.find(p => p.userId === u.id);
|
||||
const url = profile != null ? profile.url : null;
|
||||
return {
|
||||
uri: u.uri,
|
||||
|
|
|
@ -39,7 +39,7 @@ export default async function(user: User, note: Note, quiet = false) {
|
|||
let renote: Note | undefined;
|
||||
|
||||
// if deletd note is renote
|
||||
if (note.renoteId && note.text == null && !note.hasPoll && (note.fileIds == null || note.fileIds.length == 0)) {
|
||||
if (note.renoteId && note.text == null && !note.hasPoll && (note.fileIds == null || note.fileIds.length === 0)) {
|
||||
renote = await Notes.findOne({
|
||||
id: note.renoteId,
|
||||
});
|
||||
|
|
|
@ -76,7 +76,7 @@ export default async (user: { id: User['id']; host: User['host']; }, note: Note,
|
|||
// カスタム絵文字リアクションだったら絵文字情報も送る
|
||||
const decodedReaction = decodeReaction(reaction);
|
||||
|
||||
let emoji = await Emojis.findOne({
|
||||
const emoji = await Emojis.findOne({
|
||||
where: {
|
||||
name: decodedReaction.name,
|
||||
host: decodedReaction.host,
|
||||
|
|
|
@ -52,7 +52,7 @@ export default async function(
|
|||
|
||||
if (note.user != null) { // たぶんnullになることは無いはずだけど一応
|
||||
for (const antenna of myAntennas) {
|
||||
if (await checkHitAntenna(antenna, note, note.user as any, undefined, Array.from(following))) {
|
||||
if (await checkHitAntenna(antenna, note, note.user, undefined, Array.from(following))) {
|
||||
readAntennaNotes.push(note);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -114,9 +114,9 @@ export async function sendEmail(to: string, subject: string, html: string, text:
|
|||
</html>`,
|
||||
});
|
||||
|
||||
logger.info('Message sent: %s', info.messageId);
|
||||
} catch (e) {
|
||||
logger.error(e);
|
||||
throw e;
|
||||
logger.info(`Message sent: ${info.messageId}`);
|
||||
} catch (err) {
|
||||
logger.error(err as Error);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
}],
|
||||
|
|
|
@ -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",
|
||||
|
|
51
packages/client/src/components/chart-tooltip.vue
Normal file
51
packages/client/src/components/chart-tooltip.vue
Normal file
|
@ -0,0 +1,51 @@
|
|||
<template>
|
||||
<MkTooltip ref="tooltip" :showing="showing" :x="x" :y="y" :max-width="340" @closed="emit('closed')">
|
||||
<div v-if="title" class="qpcyisrl">
|
||||
<div class="title">{{ title }}</div>
|
||||
<div v-for="x in series" class="series">
|
||||
<span class="color" :style="{ background: x.backgroundColor, borderColor: x.borderColor }"></span>
|
||||
<span>{{ x.text }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</MkTooltip>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { } from 'vue';
|
||||
import MkTooltip from './ui/tooltip.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
showing: boolean;
|
||||
x: number;
|
||||
y: number;
|
||||
title: string;
|
||||
series: {
|
||||
backgroundColor: string;
|
||||
borderColor: string;
|
||||
text: string;
|
||||
}[];
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(ev: 'closed'): void;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.qpcyisrl {
|
||||
> .title {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
> .series {
|
||||
> .color {
|
||||
display: inline-block;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-width: 1px;
|
||||
border-style: solid;
|
||||
margin-right: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -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,6 +138,43 @@ 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();
|
||||
|
@ -212,7 +250,15 @@ export default defineComponent({
|
|||
},
|
||||
interaction: {
|
||||
intersect: false,
|
||||
mode: 'index',
|
||||
},
|
||||
elements: {
|
||||
point: {
|
||||
hoverRadius: 5,
|
||||
hoverBorderWidth: 2,
|
||||
},
|
||||
},
|
||||
animation: false,
|
||||
plugins: {
|
||||
legend: {
|
||||
display: props.detailed,
|
||||
|
@ -222,10 +268,12 @@ export default defineComponent({
|
|||
},
|
||||
},
|
||||
tooltip: {
|
||||
enabled: false,
|
||||
mode: 'index',
|
||||
animation: {
|
||||
duration: 0,
|
||||
},
|
||||
external: externalTooltipHandler,
|
||||
},
|
||||
zoom: {
|
||||
pan: {
|
||||
|
@ -684,6 +732,10 @@ export default defineComponent({
|
|||
fetchAndRender();
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
if (disposeTooltipComponent) disposeTooltipComponent();
|
||||
});
|
||||
|
||||
return {
|
||||
chartEl,
|
||||
fetching,
|
||||
|
|
|
@ -117,7 +117,7 @@ export default defineComponent({
|
|||
text: computed(() => {
|
||||
return props.textConverter(finalValue.value);
|
||||
}),
|
||||
source: thumbEl,
|
||||
targetElement: thumbEl,
|
||||
}, {}, 'closed');
|
||||
|
||||
const style = document.createElement('style');
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -157,7 +157,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');
|
||||
});
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
@ -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
|
||||
|
@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -101,7 +101,7 @@ export default defineComponent({
|
|||
emojis: props.note.emojis,
|
||||
users,
|
||||
count: props.count,
|
||||
source: buttonRef.value
|
||||
targetElement: buttonRef.value,
|
||||
}, {}, 'closed');
|
||||
});
|
||||
|
||||
|
|
|
@ -52,7 +52,7 @@ export default defineComponent({
|
|||
showing,
|
||||
users,
|
||||
count: props.count,
|
||||
source: buttonRef.value
|
||||
targetElement: buttonRef.value
|
||||
}, {}, 'closed');
|
||||
});
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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];
|
||||
const emit = defineEmits<{
|
||||
(e: 'close'): void;
|
||||
}>();
|
||||
|
||||
if (item && item.then) { // if item is Promise
|
||||
items.value[i] = { type: 'pending' };
|
||||
item.then(actualItem => {
|
||||
items.value[i] = actualItem;
|
||||
});
|
||||
}
|
||||
}
|
||||
let itemsEl = $ref<HTMLDivElement>();
|
||||
|
||||
this.items2 = items;
|
||||
},
|
||||
immediate: true
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
if (this.viaKeyboard) {
|
||||
this.$nextTick(() => {
|
||||
focusNext(this.$refs.items.children[0], true, false);
|
||||
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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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, () => {
|
||||
|
|
|
@ -14,7 +14,7 @@ if (localStorage.getItem('accounts') != null) {
|
|||
//#endregion
|
||||
|
||||
import { computed, createApp, watch, markRaw, version as vueVersion } from 'vue';
|
||||
import * as compareVersions from 'compare-versions';
|
||||
import compareVersions from 'compare-versions';
|
||||
|
||||
import widgets from '@/widgets';
|
||||
import directives from '@/directives';
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
@ -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');
|
||||
|
|
|
@ -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 } :
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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]);
|
||||
|
@ -74,6 +77,10 @@ function select(src: any, label: string | null, multiple: boolean): Promise<Driv
|
|||
text: label,
|
||||
type: 'label'
|
||||
} : undefined, {
|
||||
type: 'switch',
|
||||
text: i18n.ts.keepOriginalUploading,
|
||||
ref: keepOriginal
|
||||
}, {
|
||||
text: i18n.ts.upload,
|
||||
icon: 'fas fa-upload',
|
||||
action: chooseFileFromPc
|
||||
|
|
|
@ -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
|
||||
|
|
20
packages/client/src/types/menu.ts
Normal file
20
packages/client/src/types/menu.ts
Normal file
|
@ -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;
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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.ts.openInSideView,
|
||||
action: () => {
|
||||
this.$refs.side.navigate(path);
|
||||
}
|
||||
}, {
|
||||
icon: 'fas fa-window-maximize',
|
||||
text: i18n.ts.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>
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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'],
|
||||
|
|
30
yarn.lock
30
yarn.lock
|
@ -1117,10 +1117,10 @@ csso@~2.3.1:
|
|||
clap "^1.0.9"
|
||||
source-map "^0.5.3"
|
||||
|
||||
cypress@9.3.1:
|
||||
version "9.3.1"
|
||||
resolved "https://registry.yarnpkg.com/cypress/-/cypress-9.3.1.tgz#8116f52d49d6daf90a91e88f3eafd940234d2958"
|
||||
integrity sha512-BODdPesxX6bkVUnH8BVsV8I/jn57zQtO1FEOUTiuG2us3kslW7g0tcuwiny7CKCmJUZz8S/D587ppC+s58a+5Q==
|
||||
cypress@9.4.1:
|
||||
version "9.4.1"
|
||||
resolved "https://registry.yarnpkg.com/cypress/-/cypress-9.4.1.tgz#1a4ba706435829c24b7edf350c2b059e05da9084"
|
||||
integrity sha512-+JgMG9uT+QFx97JU9kOHE3jO3+0UdkQ9H1oCBiC7A74qme7Jkdy2sYDBCPjjGczutnWnGUTMRlwiNMP/Uq6LrQ==
|
||||
dependencies:
|
||||
"@cypress/request" "^2.88.10"
|
||||
"@cypress/xvfb" "^1.2.4"
|
||||
|
@ -1159,10 +1159,10 @@ cypress@9.3.1:
|
|||
pretty-bytes "^5.6.0"
|
||||
proxy-from-env "1.0.0"
|
||||
request-progress "^3.0.0"
|
||||
semver "^7.3.2"
|
||||
supports-color "^8.1.1"
|
||||
tmp "~0.2.1"
|
||||
untildify "^4.0.0"
|
||||
url "^0.11.0"
|
||||
yauzl "^2.10.0"
|
||||
|
||||
d@1, d@^1.0.1:
|
||||
|
@ -3422,11 +3422,6 @@ pumpify@^1.3.5:
|
|||
inherits "^2.0.3"
|
||||
pump "^2.0.0"
|
||||
|
||||
punycode@1.3.2:
|
||||
version "1.3.2"
|
||||
resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.3.2.tgz#9653a036fb7c1ee42342f2325cceefea3926c48d"
|
||||
integrity sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=
|
||||
|
||||
punycode@^2.1.0, punycode@^2.1.1:
|
||||
version "2.1.1"
|
||||
resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec"
|
||||
|
@ -3450,11 +3445,6 @@ query-string@^4.1.0:
|
|||
object-assign "^4.1.0"
|
||||
strict-uri-encode "^1.0.0"
|
||||
|
||||
querystring@0.2.0:
|
||||
version "0.2.0"
|
||||
resolved "https://registry.yarnpkg.com/querystring/-/querystring-0.2.0.tgz#b209849203bb25df820da756e747005878521620"
|
||||
integrity sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=
|
||||
|
||||
queue-microtask@^1.2.2:
|
||||
version "1.2.3"
|
||||
resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243"
|
||||
|
@ -3736,7 +3726,7 @@ semver-greatest-satisfied-range@^1.1.0:
|
|||
resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7"
|
||||
integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==
|
||||
|
||||
semver@^7.3.5:
|
||||
semver@^7.3.2, semver@^7.3.5:
|
||||
version "7.3.5"
|
||||
resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.5.tgz#0b621c879348d8998e4b0e4be94b3f12e6018ef7"
|
||||
integrity sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==
|
||||
|
@ -4353,14 +4343,6 @@ urix@^0.1.0:
|
|||
resolved "https://registry.yarnpkg.com/urix/-/urix-0.1.0.tgz#da937f7a62e21fec1fd18d49b35c2935067a6c72"
|
||||
integrity sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=
|
||||
|
||||
url@^0.11.0:
|
||||
version "0.11.0"
|
||||
resolved "https://registry.yarnpkg.com/url/-/url-0.11.0.tgz#3838e97cfc60521eb73c525a8e55bfdd9e2e28f1"
|
||||
integrity sha1-ODjpfPxgUh63PFJajlW/3Z4uKPE=
|
||||
dependencies:
|
||||
punycode "1.3.2"
|
||||
querystring "0.2.0"
|
||||
|
||||
use@^3.1.0:
|
||||
version "3.1.1"
|
||||
resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f"
|
||||
|
|
Loading…
Reference in a new issue