Merge branch 'develop' into mkjs-n

This commit is contained in:
tamaina 2023-06-04 13:31:14 +00:00
commit f81479ad05
127 changed files with 7780 additions and 3638 deletions

View file

@ -32,15 +32,22 @@
- ハッシュタグのノート一覧ページから、そのハッシュタグで投稿するボタンを追加 - ハッシュタグのノート一覧ページから、そのハッシュタグで投稿するボタンを追加
- アカウント初期設定ウィザードに戻るボタンを追加 - アカウント初期設定ウィザードに戻るボタンを追加
- アカウントの初期設定ウィザードにあとでボタンを追加 - アカウントの初期設定ウィザードにあとでボタンを追加
- サーバーにカスタム絵文字の種類が多い場合のパフォーマンスの改善
- Fix: URLプレビューで情報が取得できなかった際の挙動を修正 - Fix: URLプレビューで情報が取得できなかった際の挙動を修正
- Fix: Safari、Firefoxでの新規登録時、パスワードマネージャーにメールアドレスが登録されていた挙動を修正 - Fix: Safari、Firefoxでの新規登録時、パスワードマネージャーにメールアドレスが登録されていた挙動を修正
- fix:ロールタイムラインが無効でも投稿が流れてしまう問題の修正 - Fix: ロールタイムラインが無効でも投稿が流れてしまう問題の修正
- fix:ロールタイムラインにて全ての投稿が流れてしまう問題の修正 - Fix: ロールタイムラインにて全ての投稿が流れてしまう問題の修正
- Fix: 「アクセストークンの管理」画面でアプリの情報が表示されない問題の修正
- Fix: Firefoxにおける絵文字ピッカーのTabキーフォーカス問題の修正
- Fix: フォローボタンがテーマのカラースキームによって視認性が悪くなる問題を修正
- 新しいプロパティ `fgOnWhite` が追加されました
### Server ### Server
- bullをbull-mqにアップグレードし、ジョブキューのパフォーマンスを改善 - bullをbull-mqにアップグレードし、ジョブキューのパフォーマンスを改善
- ストリーミングのパフォーマンスを改善 - ストリーミングのパフォーマンスを改善
- Fix: 無効化されたアンテナにアクセスがあった際に再度有効化するように
- Fix: お知らせの画像URLを空にできない問題を修正 - Fix: お知らせの画像URLを空にできない問題を修正
- Fix: i/notificationsのsinceIdが機能しない問題を修正
## 13.12.2 ## 13.12.2
@ -365,6 +372,7 @@ Meilisearchの設定に`index`が必要になりました。値はMisskeyサー
- アンテナでCWも検索対象にするように - アンテナでCWも検索対象にするように
- ノートの操作部をホバー時のみ表示するオプションを追加 - ノートの操作部をホバー時のみ表示するオプションを追加
- サウンドを追加 - サウンドを追加
- enhance(client): MFMのx2, scale, positionが含まれていたらートをたたむように
- サーバーのパフォーマンスを改善 - サーバーのパフォーマンスを改善
### Bugfixes ### Bugfixes

View file

@ -267,8 +267,8 @@ start: "البداية"
home: "الرئيسي" home: "الرئيسي"
remoteUserCaution: "هذه المعلومات قد لا تكون مكتملة بما أن المستخدم من مثيل بعيد." remoteUserCaution: "هذه المعلومات قد لا تكون مكتملة بما أن المستخدم من مثيل بعيد."
activity: "النشاط" activity: "النشاط"
images: "الصور" images: "صور"
image: "الصور" image: "صور"
birthday: "تاريخ الميلاد" birthday: "تاريخ الميلاد"
yearsOld: "{age} سنة" yearsOld: "{age} سنة"
registeredDate: "انضم في" registeredDate: "انضم في"
@ -1331,7 +1331,7 @@ _pages:
text: "نص" text: "نص"
textarea: "حقل نصي" textarea: "حقل نصي"
section: "قسم" section: "قسم"
image: "الصور" image: "صور"
button: "زرّ" button: "زرّ"
note: "ملاحظة مضمّنة" note: "ملاحظة مضمّنة"
_note: _note:

View file

@ -1060,6 +1060,8 @@ cancelReactionConfirm: "Möchtest du deine Reaktion wirklich löschen?"
changeReactionConfirm: "Möchtest du deine Reaktion wirklich ändern?" changeReactionConfirm: "Möchtest du deine Reaktion wirklich ändern?"
later: "Später" later: "Später"
goToMisskey: "Zu Misskey" goToMisskey: "Zu Misskey"
additionalEmojiDictionary: "Zusätzliche Emoji-Wörterbücher"
installed: "Installiert"
_initialAccountSetting: _initialAccountSetting:
accountCreated: "Dein Konto wurde erfolgreich erstellt!" accountCreated: "Dein Konto wurde erfolgreich erstellt!"
letsStartAccountSetup: "Lass uns nun dein Konto einrichten." letsStartAccountSetup: "Lass uns nun dein Konto einrichten."

View file

@ -1060,6 +1060,8 @@ cancelReactionConfirm: "Really delete your reaction?"
changeReactionConfirm: "Really change your reaction?" changeReactionConfirm: "Really change your reaction?"
later: "Later" later: "Later"
goToMisskey: "To Misskey" goToMisskey: "To Misskey"
additionalEmojiDictionary: "Additional emoji dictionaries"
installed: "Installed"
_initialAccountSetting: _initialAccountSetting:
accountCreated: "Your account was successfully created!" accountCreated: "Your account was successfully created!"
letsStartAccountSetup: "For starters, let's set up your profile." letsStartAccountSetup: "For starters, let's set up your profile."

2
locales/index.d.ts vendored
View file

@ -1063,6 +1063,8 @@ export interface Locale {
"changeReactionConfirm": string; "changeReactionConfirm": string;
"later": string; "later": string;
"goToMisskey": string; "goToMisskey": string;
"additionalEmojiDictionary": string;
"installed": string;
"_initialAccountSetting": { "_initialAccountSetting": {
"accountCreated": string; "accountCreated": string;
"letsStartAccountSetup": string; "letsStartAccountSetup": string;

View file

@ -1060,6 +1060,8 @@ cancelReactionConfirm: "リアクションを取り消しますか?"
changeReactionConfirm: "リアクションを変更しますか?" changeReactionConfirm: "リアクションを変更しますか?"
later: "あとで" later: "あとで"
goToMisskey: "Misskeyへ" goToMisskey: "Misskeyへ"
additionalEmojiDictionary: "絵文字の追加辞書"
installed: "インストール済み"
_initialAccountSetting: _initialAccountSetting:
accountCreated: "アカウントの作成が完了しました!" accountCreated: "アカウントの作成が完了しました!"

View file

@ -47,11 +47,13 @@ copyContent: "内容をコピー"
copyLink: "リンクをコピー" copyLink: "リンクをコピー"
delete: "ほかす" delete: "ほかす"
deleteAndEdit: "ほかして直す" deleteAndEdit: "ほかして直す"
deleteAndEditConfirm: "このノートをほかしてもっかい直す?このノートへのリアクション、Renote、返信も全部消えるんやけどそれでもええん" deleteAndEditConfirm: "このノートをほかしてもっかい直す?このノートへのツッコミ、Renote、返信も全部消えるんやけどそれでもええん"
addToList: "リストに入れたる" addToList: "リストに入れたる"
sendMessage: "メッセージを送る" sendMessage: "メッセージを送る"
copyRSS: "RSSをコピー" copyRSS: "RSSをコピー"
copyUsername: "ユーザー名をコピー" copyUsername: "ユーザー名をコピー"
copyUserId: "ユーザーIDをコピー"
copyNoteId: "ートIDをコピー"
searchUser: "ユーザーを検索" searchUser: "ユーザーを検索"
reply: "返事" reply: "返事"
loadMore: "まだまだあるで!" loadMore: "まだまだあるで!"
@ -1043,6 +1045,10 @@ preventAiLearning: "生成AIの学習に使わんといて"
preventAiLearningDescription: "他の文章生成AIとか画像生成AIに、投稿したートとか画像なんかを勝手に使わんように頼むで。具体的にはnoaiフラグをHTMLレスポンスに含めるんやけど、これ聞いてくれるんはAIの気分次第やから、使われる可能性もちょっとはあるな。" preventAiLearningDescription: "他の文章生成AIとか画像生成AIに、投稿したートとか画像なんかを勝手に使わんように頼むで。具体的にはnoaiフラグをHTMLレスポンスに含めるんやけど、これ聞いてくれるんはAIの気分次第やから、使われる可能性もちょっとはあるな。"
options: "オプション" options: "オプション"
specifyUser: "ユーザー指定" specifyUser: "ユーザー指定"
rolesThatCanBeUsedThisEmojiAsReaction: "ツッコミとして使えるロール"
rolesThatCanBeUsedThisEmojiAsReactionEmptyDescription: "ロールが一個も指定されてへんかったら、誰でもツッコミとして使えるで。"
cancelReactionConfirm: "ツッコむんをやっぱやめるか?"
changeReactionConfirm: "ツッコミを別のに変えるか?"
_initialAccountSetting: _initialAccountSetting:
accountCreated: "アカウント作り終わったで。" accountCreated: "アカウント作り終わったで。"
letsStartAccountSetup: "アカウントの初期設定をしよか。" letsStartAccountSetup: "アカウントの初期設定をしよか。"
@ -1614,7 +1620,7 @@ _timelineTutorial:
step2_2: "最初のノートは、自己紹介とか「{name}始めてみたんや」とかがええと思うで。" step2_2: "最初のノートは、自己紹介とか「{name}始めてみたんや」とかがええと思うで。"
step3_1: "投稿できた?" step3_1: "投稿できた?"
step3_2: "あんたのノートがタイムラインに出てきたら成功や。" step3_2: "あんたのノートがタイムラインに出てきたら成功や。"
step4_1: "ノートには、「リアクション」を付けれるで。" step4_1: "ノートには、「ツッコミ」を付けれるで。"
step4_2: "ツッコむんやったら、ノートの「+」マークを押して、好きな絵文字を選ぶで。" step4_2: "ツッコむんやったら、ノートの「+」マークを押して、好きな絵文字を選ぶで。"
_2fa: _2fa:
alreadyRegistered: "もう設定終わっとるわ。" alreadyRegistered: "もう設定終わっとるわ。"

View file

@ -52,6 +52,8 @@ addToList: "리스트에 추가"
sendMessage: "메시지 보내기" sendMessage: "메시지 보내기"
copyRSS: "RSS 복사" copyRSS: "RSS 복사"
copyUsername: "유저명 복사" copyUsername: "유저명 복사"
copyUserId: "유저 ID 복사"
copyNoteId: "노트 ID 복사"
searchUser: "사용자 검색" searchUser: "사용자 검색"
reply: "답글" reply: "답글"
loadMore: "더 보기" loadMore: "더 보기"
@ -790,6 +792,7 @@ noMaintainerInformationWarning: "관리자 정보가 설정되어 있지 않습
noBotProtectionWarning: "Bot 방어가 설정되어 있지 않습니다." noBotProtectionWarning: "Bot 방어가 설정되어 있지 않습니다."
configure: "설정하기" configure: "설정하기"
postToGallery: "갤러리에 업로드" postToGallery: "갤러리에 업로드"
postToHashtag: "이 해시태그에 게시"
gallery: "갤러리" gallery: "갤러리"
recentPosts: "최근 포스트" recentPosts: "최근 포스트"
popularPosts: "인기 포스트" popularPosts: "인기 포스트"
@ -823,6 +826,7 @@ translatedFrom: "{x}에서 번역"
accountDeletionInProgress: "계정 삭제 작업을 진행하고 있습니다" accountDeletionInProgress: "계정 삭제 작업을 진행하고 있습니다"
usernameInfo: "서버상에서 계정을 식별하기 위한 이름. 알파벳(a~z, A~Z), 숫자(0~9) 및 언더바(_)를 사용할 수 있습니다. 사용자명은 나중에 변경할 수 없습니다." usernameInfo: "서버상에서 계정을 식별하기 위한 이름. 알파벳(a~z, A~Z), 숫자(0~9) 및 언더바(_)를 사용할 수 있습니다. 사용자명은 나중에 변경할 수 없습니다."
aiChanMode: "아이 모드" aiChanMode: "아이 모드"
devMode: "개발자 모드"
keepCw: "CW 유지하기" keepCw: "CW 유지하기"
pubSub: "Pub/Sub 계정" pubSub: "Pub/Sub 계정"
lastCommunication: "마지막 통신" lastCommunication: "마지막 통신"
@ -830,8 +834,10 @@ resolved: "해결됨"
unresolved: "해결되지 않음" unresolved: "해결되지 않음"
breakFollow: "팔로워 해제" breakFollow: "팔로워 해제"
breakFollowConfirm: "팔로우를 해제하시겠습니까?" breakFollowConfirm: "팔로우를 해제하시겠습니까?"
itsOn: "켜짐" itsOn: "켜져 있습니다"
itsOff: "꺼짐" itsOff: "꺼져 있습니다"
on: "켜짐"
off: "꺼짐"
emailRequiredForSignup: "가입할 때 이메일 주소 입력을 필수로 하기" emailRequiredForSignup: "가입할 때 이메일 주소 입력을 필수로 하기"
unread: "읽지 않음" unread: "읽지 않음"
filter: "필터" filter: "필터"
@ -986,6 +992,8 @@ cannotBeChangedLater: "나중에 변경할 수 없습니다."
reactionAcceptance: "리액션 수신" reactionAcceptance: "리액션 수신"
likeOnly: "좋아요만 받기" likeOnly: "좋아요만 받기"
likeOnlyForRemote: "리모트에서는 좋아요만 받기" likeOnlyForRemote: "리모트에서는 좋아요만 받기"
nonSensitiveOnly: "열람 주의로 설정되지 않았을 때만 받기"
nonSensitiveOnlyForLocalLikeOnlyForRemote: "열람 주의로 설정되지 않았을 때만 받기 (리모트에서는 좋아요만 받기)"
rolesAssignedToMe: "나에게 할당된 역할" rolesAssignedToMe: "나에게 할당된 역할"
resetPasswordConfirm: "비밀번호를 재설정하시겠습니까?" resetPasswordConfirm: "비밀번호를 재설정하시겠습니까?"
sensitiveWords: "민감한 단어" sensitiveWords: "민감한 단어"
@ -1043,31 +1051,43 @@ preventAiLearning: "기계학습(생성형 AI)으로의 사용을 거부"
preventAiLearningDescription: "외부의 문장 생성 AI나 이미지 생성 AI에 대해 제출한 노트나 이미지 등의 콘텐츠를 학습의 대상으로 사용하지 않도록 요구합니다. 다만, 이 요구사항을 지킬 의무는 없기 때문에 학습을 완전히 방지하는 것은 아닙니다." preventAiLearningDescription: "외부의 문장 생성 AI나 이미지 생성 AI에 대해 제출한 노트나 이미지 등의 콘텐츠를 학습의 대상으로 사용하지 않도록 요구합니다. 다만, 이 요구사항을 지킬 의무는 없기 때문에 학습을 완전히 방지하는 것은 아닙니다."
options: "옵션" options: "옵션"
specifyUser: "사용자 지정" specifyUser: "사용자 지정"
failedToPreviewUrl: "미리 볼 수 없음"
update: "업데이트"
rolesThatCanBeUsedThisEmojiAsReaction: "이 이모지를 리액션으로 사용할 수 있는 역할"
rolesThatCanBeUsedThisEmojiAsReactionEmptyDescription: "역할을 지정하지 않으면, 누구나 이 이모지를 리액션으로 사용할 수 있습니다."
rolesThatCanBeUsedThisEmojiAsReactionPublicRoleWarn: "역할은 공개로 설정되어 있어야 합니다."
cancelReactionConfirm: "리액션을 취소하시겠습니까?"
changeReactionConfirm: "리액션을 변경하시겠습니까?"
later: "나중에"
goToMisskey: "Misskey로"
additionalEmojiDictionary: "이모지 추가 사전"
installed: "설치됨"
_initialAccountSetting: _initialAccountSetting:
accountCreated: "계정 생성이 완료되었습니다!" accountCreated: "계정 생성이 완료되었습니다!"
letsStartAccountSetup: "계정의 초기 설정을 진행합니다." letsStartAccountSetup: "계정의 초기 설정을 진행합니다."
letsFillYourProfile: "우선 나의 프로필을 설정해 보아요." letsFillYourProfile: "우선 나의 프로필을 설정해 보아요."
profileSetting: "프로필 설정" profileSetting: "프로필 설정"
privacySetting: "\n프라이버시설정" privacySetting: "프라이버시 설정"
theseSettingsCanEditLater: "이 설정들은 나중에도 변경할 수 있습니다." theseSettingsCanEditLater: "이 설정들은 나중에도 변경할 수 있습니다."
youCanEditMoreSettingsInSettingsPageLater: "이 외에도 '설정' 페이지에서 다양한 설정을 나의 입맛에 맛게 조절할 수 있습니다. 꼭 확인해 보세요!" youCanEditMoreSettingsInSettingsPageLater: "이 외에도 '설정' 페이지에서 다양한 설정을 나의 입맛에 게 조절할 수 있습니다. 꼭 확인해 보세요!"
followUsers: "관심사가 맞는 유저를 팔로우하여 타임라인을 가꾸어 봅시다." followUsers: "관심사가 맞는 유저를 팔로우하여 타임라인을 가꾸어 봅시다."
pushNotificationDescription: "푸시 알림을 활성화하면 {name}의 알림을 나의 기기에서 받아볼 수 있게 됩니다." pushNotificationDescription: "푸시 알림을 활성화하면 {name}의 알림을 나의 기기에서 받아볼 수 있게 됩니다."
initialAccountSettingCompleted: "초기 설정을 모두 마쳤습니다!" initialAccountSettingCompleted: "초기 설정을 모두 마쳤습니다!"
haveFun: "{name}와 함께 즐거운 시간 보내세요!" haveFun: "{name}와 함께 즐거운 시간 보내세요!"
ifYouNeedLearnMore: "{name}(Misskey)의 사용 방법에 대해 자세히 알아보려면 {link}를 참고해 주세요." ifYouNeedLearnMore: "{name}(Misskey)의 사용 방법에 대해 자세히 알아보려면 {link}를 참고해 주세요."
skipAreYouSure: "초기 설정을 넘기시겠습니까?" skipAreYouSure: "초기 설정을 중단하시겠습니까?"
laterAreYouSure: "초기 설정을 나중에 진행하시겠습니까?"
_serverRules: _serverRules:
description: "회원 가입 이전에 간단하게 표시할 서버 규칙입니다. 이용 약관의 요약으로 구성하는 것을 추천합니다." description: "회원 가입 이전에 간단하게 표시할 서버 규칙입니다. 이용 약관의 요약으로 구성하는 것을 추천합니다."
_accountMigration: _accountMigration:
moveFrom: "다른 계정에서 이 계정으로 이사" moveFrom: "다른 계정에서 이 계정으로 이사"
moveFromSub: "다른 계정에 대한 별칭을 생성" moveFromSub: "다른 계정에 대한 별칭을 생성"
moveFromLabel: "기존 계정:" moveFromLabel: "기존 계정 #{n}"
moveFromDescription: "다른 계정에서 이 계정으로 팔로워를 가져오려면, 우선 여기에서 별칭을 지정해야 합니다. 반드시 이사하기 전에 지정해야 합니다! 기존 계정을 다음과 같은 형식으로 입력해 주십시오: @person@instance.com" moveFromDescription: "다른 계정에서 이 계정으로 팔로워를 가져오려면, 우선 여기에서 별칭을 지정해야 합니다. 반드시 이사하기 전에 지정해야 합니다! 기존 계정을 다음과 같은 형식으로 입력해 주십시오: @person@instance.com"
moveTo: "이 계정에서 다른 계정으로 이사" moveTo: "이 계정에서 다른 계정으로 이사"
moveToLabel: "이사할 계정:" moveToLabel: "이사할 계정:"
moveCannotBeUndone: "한 번 이사하면, 두 번 다시 되돌릴 수 없습니다." moveCannotBeUndone: "한 번 이사하면, 두 번 다시 되돌릴 수 없습니다."
moveAccountDescription: "이 작업은 취소할 수 없습니다. 먼저 이사할 계정에서 이 계정에 대한 별칭을 지정하였는지 다시 한 번 확인해 주십시오. 별칭을 지정한 다음, 이사할 계정을 다음과 같은 형식으로 입력해 주십시오: @person@instance.com" moveAccountDescription: "새 계정으로 이전합니다.\n ・팔로워가 새 계정을 자동으로 팔로우 합니다\n ・이 계정에서 팔로우는 모두 해제됩니다\n ・이 계정으로는 노트 작성 등을 할 수 없게 됩니다\n\n팔로워는 자동으로 이전되지만, 팔로우는 수동으로 진행해야 합니다. 이전하기 전에 이 계정에서 팔로우를 내보내고, 이전 후에는 즉시 이전한 계정에서 가져오기를 진행하십시오.\n리스트・뮤트・차단에 대해서도 마찬가지이므로 수동으로 이전해야 합니다.\n\n(이 설명은 이 서버(Misskey v13.12.0 이후)의 사양입니다. Mastodon 등의 다른 ActivityPub 소프트웨어에서는 작동이 다를 수 있습니다.)"
moveAccountHowTo: "계정을 이사하려면 우선 이사갈 계정에서 이 계정에 대한 별칭을 지정해야 합니다.\n별칭을 작성한 다음, 이사갈 계정을 다음과 같이 입력하십시오:\n@username@server.example.com" moveAccountHowTo: "계정을 이사하려면 우선 이사갈 계정에서 이 계정에 대한 별칭을 지정해야 합니다.\n별칭을 작성한 다음, 이사갈 계정을 다음과 같이 입력하십시오:\n@username@server.example.com"
startMigration: "이사하기" startMigration: "이사하기"
migrationConfirm: "정말로 이 계정을 {account} 으로 이전하시겠습니까? 한 번 이전한 다음에는 취소할 수 없으며, 두 번 다시 원래 상태로 복구할 수 없습니다.\n이사할 계정에서 계정 별칭을 지정하였는지 다시 한 번 확인하십시오." migrationConfirm: "정말로 이 계정을 {account} 으로 이전하시겠습니까? 한 번 이전한 다음에는 취소할 수 없으며, 두 번 다시 원래 상태로 복구할 수 없습니다.\n이사할 계정에서 계정 별칭을 지정하였는지 다시 한 번 확인하십시오."

View file

@ -1,5 +1,7 @@
--- ---
_lang_: "Norsk Bokmål" _lang_: "Norsk Bokmål"
headlineMisskey: "Et nettverk forbundet med Notes"
introMisskey: "Velkommen! Misskey er en desentralisert mikrobloggtjeneste med åpen kildekode.\nOpprett \"Notes\" for å dele tankene dine med alle rundt deg. 📡\nMed \"reaksjoner\" kan du også raskt gi uttrykk for hva du synes om alles Notes. 👍\nLa oss utforske en ny verden! 🚀"
monthAndDay: "{day}-{month}" monthAndDay: "{day}-{month}"
search: "Søk" search: "Søk"
notifications: "Varsler" notifications: "Varsler"
@ -10,8 +12,10 @@ fetchingAsApObject: "Henter fra Fediverse..."
ok: "OK" ok: "OK"
gotIt: "Skjønner" gotIt: "Skjønner"
cancel: "Avbryt" cancel: "Avbryt"
noThankYou: "Avbryt" noThankYou: "Ikke nå"
enterUsername: "Skriv inn brukernavn" enterUsername: "Skriv inn brukernavn"
renotedBy: "Renotes av {user}"
noNotes: "Ingen Notes"
noNotifications: "Ingen varsler" noNotifications: "Ingen varsler"
instance: "Server" instance: "Server"
settings: "Innstillinger" settings: "Innstillinger"
@ -21,7 +25,7 @@ otherSettings: "Andre innstillinger"
openInWindow: "Åpne i vindu" openInWindow: "Åpne i vindu"
profile: "Profil" profile: "Profil"
timeline: "Tidslinje" timeline: "Tidslinje"
noAccountDescription: "Denne brukeren har ikke skrevet sin bio ennå." noAccountDescription: "Denne brukeren har ikke skrevet sin biografi ennå."
login: "Logg inn" login: "Logg inn"
loggingIn: "Logget inn" loggingIn: "Logget inn"
logout: "Logg ut" logout: "Logg ut"
@ -30,20 +34,21 @@ uploading: "Laster opp"
save: "Lagre" save: "Lagre"
users: "Brukere" users: "Brukere"
addUser: "Legg til bruker" addUser: "Legg til bruker"
favorite: "Favoritt" favorite: "Legg til i favoritter"
favorites: "Favoritter" favorites: "Favoritter"
unfavorite: "Fjern favoritt" unfavorite: "Fjern fra favoritter"
favorited: "Lagt til i favoritter." favorited: "Lagt til i favoritter."
alreadyFavorited: "Allerede lagt til i favoritter." alreadyFavorited: "Allerede lagt til i favoritter."
cantFavorite: "Kunne ikke legge til i favoritter." cantFavorite: "Kunne ikke legge til i favoritter."
pin: "Fest" pin: "Fest til profil"
unpin: "Opphev festing" unpin: "Fjern fra profil"
copyContent: "Kopier innhold" copyContent: "Kopier innhold"
copyLink: "Kopier lenke" copyLink: "Kopier lenke"
delete: "Slett" delete: "Slett"
deleteAndEdit: "Slett og rediger" deleteAndEdit: "Slett og rediger"
deleteAndEditConfirm: "Er du sikker på at du vil slette denne Noten og redigere den? Du vil miste alle reaksjoner, Renotes og svar på den."
addToList: "Legg til i liste" addToList: "Legg til i liste"
sendMessage: "Send melding" sendMessage: "Send en melding"
copyRSS: "Kopier RSS" copyRSS: "Kopier RSS"
copyUsername: "Kopier brukernavn" copyUsername: "Kopier brukernavn"
searchUser: "Søk brukere" searchUser: "Søk brukere"
@ -63,7 +68,9 @@ unfollowConfirm: "Er du sikker på at du vil slutte å følge {name}?"
importRequested: "Du har bedt om import. Dette kan ta en stund." importRequested: "Du har bedt om import. Dette kan ta en stund."
lists: "Lister" lists: "Lister"
noLists: "Ingen lister" noLists: "Ingen lister"
following: "Følg" note: "Note"
notes: "Notes"
following: "Følger"
followers: "Følgere" followers: "Følgere"
followsYou: "Følger deg" followsYou: "Følger deg"
createList: "Opprett liste" createList: "Opprett liste"
@ -74,14 +81,22 @@ pageLoadError: "Kunne ikke hente side."
serverIsDead: "Denne serveren svarer ikke. Vennligst vent en stund og prøv igjen." serverIsDead: "Denne serveren svarer ikke. Vennligst vent en stund og prøv igjen."
enterListName: "Skriv inn et navn på listen" enterListName: "Skriv inn et navn på listen"
privacy: "Personvern" privacy: "Personvern"
defaultNoteVisibility: "Standard synlighet"
follow: "Følg" follow: "Følg"
followRequest: "Følgeforespørsel" followRequest: "Følgeforespørsel"
followRequests: "Følgeforespørsel" followRequests: "Følgeforespørsel"
unfollow: "Avfølg" unfollow: "Avfølg"
followRequestPending: "Venter på godkjenning" followRequestPending: "Venter på godkjenning"
enterEmoji: "Skriv inn en emoji" enterEmoji: "Skriv inn en emoji"
renote: "Renote"
renoted: "Renotet."
cantRenote: "Dette innlegget kan ikke renotes."
cantReRenote: "En Renote kan ikke renotes."
quote: "Sitat" quote: "Sitat"
pinned: "Fest" inChannelRenote: "Renote kun for kanal"
inChannelQuote: "Sitat kun for kanal"
pinnedNote: "Festet Note"
pinned: "Fest til profil"
you: "Du" you: "Du"
clickToShow: "Klikk for å vise" clickToShow: "Klikk for å vise"
add: "Legg til" add: "Legg til"
@ -89,18 +104,21 @@ reaction: "Reaksjon"
reactions: "Reaksjoner" reactions: "Reaksjoner"
reactionSetting: "Reaksjoner som vises i reaksjonsvelgeren" reactionSetting: "Reaksjoner som vises i reaksjonsvelgeren"
reactionSettingDescription2: "Dra for å endre rekkefølgen, klikk for å slette, trykk \"+\" for å legge til." reactionSettingDescription2: "Dra for å endre rekkefølgen, klikk for å slette, trykk \"+\" for å legge til."
rememberNoteVisibility: "Husk innstillingene for synlighet av Notes"
attachCancel: "Fjern vedlegg" attachCancel: "Fjern vedlegg"
enterFileName: "Skriv inn filnavn" enterFileName: "Skriv inn filnavn"
mute: "Skjul" mute: "Skjul"
unmute: "Vis" unmute: "Vis"
renoteMute: "Skjul Renotes"
renoteUnmute: "Vis Renotes"
block: "Blokker" block: "Blokker"
unblock: "Opphev blokkering" unblock: "Opphev blokkering"
suspend: "Suspender" suspend: "Suspender"
blockConfirm: "Blokker?" blockConfirm: "Er du sikker på at du vil blokke denne kontoen?"
unblockConfirm: "Er du sikker på at du vil oppheve blokkeringen av denne kontoen?" unblockConfirm: "Er du sikker på at du vil oppheve blokkeringen av denne kontoen?"
suspendConfirm: "Er du sikker på at du vil suspendere denne kontoen?" suspendConfirm: "Er du sikker på at du vil suspendere denne kontoen?"
selectList: "Velg liste" selectList: "Velg en liste"
selectChannel: "Velg kanal" selectChannel: "Velg en kanal"
selectAntenna: "Velg en antenne" selectAntenna: "Velg en antenne"
selectWidget: "Velg en widget" selectWidget: "Velg en widget"
editWidgets: "Rediger widgeter" editWidgets: "Rediger widgeter"
@ -113,6 +131,7 @@ flagAsBot: "Merk denne kontoen som en bot"
flagAsBotDescription: "Aktiver dette alternativet hvis denne kontoen styres av et program. Hvis det er aktivert, vil det fungere som et flagg for andre utviklere for å forhindre endeløse interaksjonskjeder med andre roboter og justere Misskeys interne systemer til å behandle denne kontoen som en bot." flagAsBotDescription: "Aktiver dette alternativet hvis denne kontoen styres av et program. Hvis det er aktivert, vil det fungere som et flagg for andre utviklere for å forhindre endeløse interaksjonskjeder med andre roboter og justere Misskeys interne systemer til å behandle denne kontoen som en bot."
flagAsCat: "Merk denne kontoen som en katt" flagAsCat: "Merk denne kontoen som en katt"
flagAsCatDescription: "Aktiver dette alternativet for å merke denne kontoen som en katt." flagAsCatDescription: "Aktiver dette alternativet for å merke denne kontoen som en katt."
flagShowTimelineReplies: "Vis svar i tidslinje"
addAccount: "Legg til konto" addAccount: "Legg til konto"
reloadAccountsList: "Last inn kontoliste på nytt" reloadAccountsList: "Last inn kontoliste på nytt"
loginFailed: "Kunne ikke logge inn" loginFailed: "Kunne ikke logge inn"
@ -120,23 +139,30 @@ general: "Generelt"
searchWith: "Søk: {q}" searchWith: "Søk: {q}"
youHaveNoLists: "Du har ingen lister" youHaveNoLists: "Du har ingen lister"
followConfirm: "Er du sikker på at du vil følge {name}?" followConfirm: "Er du sikker på at du vil følge {name}?"
selectUser: "Velg bruker" host: "Vert"
selectUser: "Velg en bruker"
recipient: "Mottaker" recipient: "Mottaker"
annotation: "Kommentarer" annotation: "Kommentarer"
federation: "Føderasjon" federation: "Føderasjon"
instances: "Server" instances: "Servere"
registeredAt: "Registrerte seg" registeredAt: "Registrerte seg"
latestRequestReceivedAt: "Siste forespørsel mottatt"
latestStatus: "Siste status"
charts: "Diagrammer"
perHour: "Per time" perHour: "Per time"
perDay: "Per dag" perDay: "Per dag"
stopActivityDelivery: "Slutt å sende aktiviteter" stopActivityDelivery: "Slutt å sende aktiviteter"
blockThisInstance: "Blokker denne serveren" blockThisInstance: "Blokker denne serveren"
operations: "Operasjoner"
software: "Programvare" software: "Programvare"
version: "Versjon" version: "Versjon"
metadata: "Metadata"
withNFiles: "{n} fil(er)" withNFiles: "{n} fil(er)"
network: "Nettverk" network: "Nettverk"
instanceInfo: "Serverinformasjon"
statistics: "Statistikk" statistics: "Statistikk"
clearQueue: "Tøm kø" clearQueue: "Tøm kø"
clearQueueConfirmTitle: "Vil du tømme kø?" clearQueueConfirmTitle: "Er du sikker på at du vil tømme køen?"
blockedInstances: "Blokkerte severe" blockedInstances: "Blokkerte severe"
blockedInstancesDescription: "Skriv opp vertsnavnene til serverne du vil blokkere, atskilt med linjeskift. Serverne i listen vil ikke lenger kunne kommunisere med denne serveren." blockedInstancesDescription: "Skriv opp vertsnavnene til serverne du vil blokkere, atskilt med linjeskift. Serverne i listen vil ikke lenger kunne kommunisere med denne serveren."
muteAndBlock: "Skjul og blokker" muteAndBlock: "Skjul og blokker"
@ -144,9 +170,13 @@ mutedUsers: "Skjulte brukere"
blockedUsers: "Blokkerte brukere" blockedUsers: "Blokkerte brukere"
noUsers: "Det er ingen brukere" noUsers: "Det er ingen brukere"
editProfile: "Rediger profil" editProfile: "Rediger profil"
noteDeleteConfirm: "Er du sikker på at du vil slette denne Noten?"
pinLimitExceeded: "Du kan ikke feste flere." pinLimitExceeded: "Du kan ikke feste flere."
intro: "Installasjonen av Misskey er ferdig! Vennligst opprett en administratorkonto."
done: "Ferdig" done: "Ferdig"
noCustomEmojis: "Ingen emoji" default: "Standard"
defaultValueIs: "Standard: {value}"
noCustomEmojis: "Det er ingen emoji"
noJobs: "Det er ingen jobber" noJobs: "Det er ingen jobber"
blocked: "Blokkert" blocked: "Blokkert"
suspended: "Suspendert" suspended: "Suspendert"
@ -154,54 +184,72 @@ all: "Alle"
notResponding: "Svarer ikke" notResponding: "Svarer ikke"
changePassword: "Endre passord" changePassword: "Endre passord"
security: "Sikkerhet" security: "Sikkerhet"
retypedNotMatch: "Inngangene stemmer ikke overens."
currentPassword: "Nåværende passord"
newPassword: "Nytt passord" newPassword: "Nytt passord"
newPasswordRetype: "Nytt passord (gjenta)" newPasswordRetype: "Nytt passord (gjenta)"
attachFile: "Legg ved filer" attachFile: "Legg ved filer"
more: "Mer!" more: "Mer!"
noSuchUser: "Bruker ikke funnet"
announcements: "Kunngjøringer" announcements: "Kunngjøringer"
remove: "Slett" remove: "Slett"
removed: "Slettet" removed: "Vellykket slettet"
removeAreYouSure: "Er du sikker på at du vil fjerne \"{x}\"?" removeAreYouSure: "Er du sikker på at du vil fjerne \"{x}\"?"
deleteAreYouSure: "Er du sikker på at du vil slette \"{x}\"?" deleteAreYouSure: "Er du sikker på at du vil slette \"{x}\"?"
saved: "Lagret" saved: "Lagret"
upload: "Laste opp" upload: "Laste opp"
keepOriginalUploading: "Behold originalbildet"
fromUrl: "Fra URL"
uploadFromUrl: "Last opp fra en URL"
uploadFromUrlDescription: "URL til filen du vil laste opp"
explore: "Utforsk" explore: "Utforsk"
messageRead: "Lest" messageRead: "Lest"
agree: "Jeg godtar" nUsersRead: "lest av {n}"
agreeTo: "Jeg godtar {0}"
agree: "Godta"
agreeBelow: "Jeg godtar følgende"
basicNotesBeforeCreateAccount: "Viktige merknader"
termsOfService: "Vilkår for bruk" termsOfService: "Vilkår for bruk"
home: "Hjem" home: "Hjem"
activity: "Aktivitet" activity: "Aktivitet"
images: "Bilder" images: "Bilder"
image: "Bilder" image: "Bilde"
birthday: "Bursdag" birthday: "Bursdag"
yearsOld: "{age} år gammel" yearsOld: "{age} år gammel"
theme: "Temaer"
light: "Lys" light: "Lys"
dark: "Mørk" dark: "Mørk"
lightThemes: "Lyse temaer"
darkThemes: "Mørke temaer"
syncDeviceDarkMode: "Synkroniser mørkmodus med enhetens innstillinger"
fileName: "Filnavn" fileName: "Filnavn"
selectFile: "Velg fil" selectFile: "Velg en fil"
selectFiles: "Velg fil" selectFiles: "Velg filer"
selectFolder: "Velg mappe" selectFolder: "Velg en mappe"
selectFolders: "Velg mappe" selectFolders: "Velg mapper"
renameFile: "Endre filnavn" renameFile: "Endre filnavn"
folderName: "Mappenavn" folderName: "Mappenavn"
createFolder: "Opprett mappe" createFolder: "Opprett en mappe"
renameFolder: "Endre mappenavn" renameFolder: "Endre mappenavn"
deleteFolder: "Slett mappe" deleteFolder: "Slett denne mappen"
addFile: "Legg til fil" addFile: "Legg til en fil"
emptyFolder: "Denne mappen er tom" emptyFolder: "Denne mappen er tom"
unableToDelete: "Kan ikke slette" unableToDelete: "Kan ikke slette"
inputNewFileName: "Skriv inn et nytt filnavn"
inputNewDescription: "Skriv inn ny bildetekst"
inputNewFolderName: "Skriv inn et nytt mappenavn"
circularReferenceFolder: "Målmappen er en undermappe til mappen du ønsker å flytte." circularReferenceFolder: "Målmappen er en undermappe til mappen du ønsker å flytte."
hasChildFilesOrFolders: "Siden denne mappen ikke er tom, kan den ikke slettes." hasChildFilesOrFolders: "Siden denne mappen ikke er tom, kan den ikke slettes."
copyUrl: "Kopier URL" copyUrl: "Kopier URL"
rename: "Endre navn" rename: "Endre navn"
avatar: "Avatar" avatar: "Avatar"
banner: "Banner" banner: "Banner"
doNothing: "Gjør ingenting" doNothing: "Ignorer"
accept: "Tillatt" accept: "Tillatt"
reject: "Avslå" reject: "Avslå"
instanceName: "Servernavn" instanceName: "Servernavn"
instanceDescription: "Serverbeskrivelse" instanceDescription: "Serverbeskrivelse"
thisYear: "I år" thisYear: "År"
thisMonth: "Måned" thisMonth: "Måned"
today: "I dag" today: "I dag"
dayX: "{day}" dayX: "{day}"
@ -216,23 +264,35 @@ registration: "Registrer"
enableRegistration: "Aktiver registrering av nye brukere" enableRegistration: "Aktiver registrering av nye brukere"
invite: "Inviter" invite: "Inviter"
basicInfo: "Grunnleggende informasjon" basicInfo: "Grunnleggende informasjon"
pinnedUsers: "Festete brukrere" pinnedUsers: "Festede brukrere"
pinnedUsersDescription: "Liste over brukernavn atskilt med linjeskift som skal festes i \"Utforsk\" fanen." pinnedUsersDescription: "Liste over brukernavn atskilt med linjeskift som skal festes i \"Utforsk\" fanen."
pinnedPages: "Festete sider" pinnedPages: "Festede sider"
pinnedNotes: "Festet Note"
hcaptcha: "hCaptcha" hcaptcha: "hCaptcha"
enableHcaptcha: "Aktiver hCaptcha" enableHcaptcha: "Aktiver hCaptcha"
recaptcha: "reCAPTCHA" recaptcha: "reCAPTCHA"
enableRecaptcha: "Aktiver reCAPTCHA" enableRecaptcha: "Aktiver reCAPTCHA"
turnstile: "Turnstile" turnstile: "Turnstile"
enableTurnstile: "Aktiver Turnstile" enableTurnstile: "Aktiver Turnstile"
antennas: "Antenner"
name: "Navn" name: "Navn"
antennaSource: "Antennekilde"
notifyAntenna: "Varsle om nye Notes"
withFileAntenna: "Bare Notes med filer"
notesAndReplies: "Notes og svar"
popularUsers: "Populære brukere" popularUsers: "Populære brukere"
exploreUsersCount: "Det finnes {count} brukere" exploreUsersCount: "Det finnes {count} brukere"
exploreFediverse: "Utforsk Fediverse"
userList: "Lister" userList: "Lister"
about: "Infomasjon" about: "Informasjon"
aboutMisskey: "Om Misskey" aboutMisskey: "Om Misskey"
newPasswordIs: "Det nye passordet er \"{password}\"."
share: "Del" share: "Del"
notFound: "Ikke funnet"
markAsReadAllNotifications: "Merk alle varsler som lest"
markAsReadAllUnreadNotes: "Merk alle Notes som lest"
help: "Hjelp" help: "Hjelp"
inputMessageHere: "Skriv inn melding her"
close: "Lukk" close: "Lukk"
invites: "Inviter" invites: "Inviter"
members: "Medlemmer" members: "Medlemmer"
@ -240,30 +300,46 @@ title: "Tittel"
text: "Tekst" text: "Tekst"
next: "Neste" next: "Neste"
retype: "Gjenta" retype: "Gjenta"
quoteAttached: "Sitat"
noMessagesYet: "Ingen meldinger ennå"
newMessageExists: "Det er nye meldinger"
onlyOneFileCanBeAttached: "Du kan bare legge ved én fil i en melding"
invitations: "Inviter" invitations: "Inviter"
available: "Tilgjengelig" available: "Tilgjengelig"
unavailable: "Utilgjengelig" unavailable: "Utilgjengelig"
tooShort: "For kort" tooShort: "For kort"
tooLong: "For langt" tooLong: "For langt"
weakPassword: "Svakt passord"
normalPassword: "Gjennomsnittlig passord"
strongPassword: "Sterkt passord"
signinWith: "Logg inn med {x}"
signinFailed: "Kunne ikke logge inn. Det oppgitte brukernavnet eller passordet er feil."
or: "eller" or: "eller"
language: "Språk" language: "Språk"
aboutX: "Om {x}" aboutX: "Om {x}"
category: "Kategorier" category: "Kategori"
createAccount: "Opprett konto" createAccount: "Opprett konto"
openImageInNewTab: "Åpne bilder i ny fane"
clientSettings: "Klientinnstillinger"
accountSettings: "Kontoinnstillinger"
objectStorageRegion: "Region" objectStorageRegion: "Region"
objectStorageUseSSL: "Bruk SSL" objectStorageUseSSL: "Bruk SSL"
objectStorageUseProxy: "Bruk Proxy" objectStorageUseProxy: "Bruk Proxy"
deleteAll: "Slett alt" deleteAll: "Slett alt"
newNoteRecived: "Det er nye Notes"
listen: "Lytt" listen: "Lytt"
none: "Ingen" none: "Ingen"
volume: "Volum"
chooseEmoji: "Velg emoji" chooseEmoji: "Velg emoji"
recentUsed: "Sist brukte" recentUsed: "Sist brukte"
install: "Installer" install: "Installer"
uninstall: "Avinstaller"
nothing: "Ingenting" nothing: "Ingenting"
deleteAllFiles: "Slett alle filer" deleteAllFiles: "Slett alle filer"
deleteAllFilesConfirm: "Vil du slette alle filer?" deleteAllFilesConfirm: "Er du sikker på at du vil slette alle filer?"
userSuspended: "Denne brukeren har blitt suspendert."
accountDeleted: "Kontoen blir slettet" accountDeleted: "Kontoen blir slettet"
accountDeletedDescription: "Denne kontoen blir slettet" accountDeletedDescription: "Denne kontoen har blitt slettet."
menu: "Meny" menu: "Meny"
poll: "Avstemning" poll: "Avstemning"
description: "Beskrivelse" description: "Beskrivelse"
@ -274,6 +350,7 @@ small: "Liten"
notificationType: "Varseltype" notificationType: "Varseltype"
edit: "Rediger" edit: "Rediger"
email: "E-post" email: "E-post"
smtpHost: "Vert"
smtpUser: "Brukernavn" smtpUser: "Brukernavn"
smtpPass: "Passord" smtpPass: "Passord"
userSaysSomething: "{name} sa noe" userSaysSomething: "{name} sa noe"
@ -289,16 +366,25 @@ reportAbuse: "Rappoter"
send: "Send" send: "Send"
openInNewTab: "Åpne i ny fane" openInNewTab: "Åpne i ny fane"
waitingFor: "Venter på {x}" waitingFor: "Venter på {x}"
random: "Tilfeldig"
system: "System" system: "System"
desktop: "Skrivebord"
i18nInfo: "Misskey oversettes til flere språk av frivillige. Du kan hjelpe til på {link}."
followingCount: "Følger" followingCount: "Følger"
followersCount: "Følgere" followersCount: "Følgere"
yes: "Ja" yes: "Ja"
no: "Nei" no: "Nei"
contact: "Kontakt"
developer: "Utvikler"
makeExplorable: "Gjør konto synlig i \"Utforsk\""
makeExplorableDescription: "Hvis du slår av dette, vises ikke kontoen din i \"Utforsk\" delen."
left: "Venstre" left: "Venstre"
nNotes: "{n} Notes"
saveAs: "Lagre som" saveAs: "Lagre som"
value: "Verdi" value: "Verdi"
deleteConfirm: "Vil du slette?" deleteConfirm: "Vil du slette?"
invalidValue: "Verdien er ugyldig." invalidValue: "Verdien er ugyldig."
closeAccount: "Avslutt konto"
emailNotification: "E-postvarsler" emailNotification: "E-postvarsler"
inChannelSearch: "Søk i kanal" inChannelSearch: "Søk i kanal"
clear: "Tøm" clear: "Tøm"
@ -312,17 +398,23 @@ accounts: "Kontoer"
switch: "Bytt" switch: "Bytt"
gallery: "Galleri" gallery: "Galleri"
ads: "Annonser" ads: "Annonser"
memo: "Notat"
high: "Høy" high: "Høy"
low: "Lav" low: "Lav"
sent: "Send" sent: "Sendt"
received: "Mottatt"
learnMore: "Les mer" learnMore: "Les mer"
misskeyUpdated: "Misskey har blitt oppdatert!"
translate: "Oversett" translate: "Oversett"
translatedFrom: "Oversatt fra {x}"
unread: "Ulest" unread: "Ulest"
manageAccounts: "Administrer konto" manageAccounts: "Administrer konto"
classic: "Klassisk" classic: "Klassisk"
muteThread: "Skjul denne tråden" muteThread: "Skjul denne tråden"
unmuteThread: "Vis denne tråden" unmuteThread: "Vis denne tråden"
continueThread: "Vis fortsettelse av tråden"
hide: "Skjul" hide: "Skjul"
smartphone: "Smarttelefon"
tablet: "Nettbrett" tablet: "Nettbrett"
auto: "Automatisk" auto: "Automatisk"
size: "Størrelse" size: "Størrelse"
@ -338,10 +430,10 @@ check: "Sjekk"
deleteAccount: "Slett konto" deleteAccount: "Slett konto"
document: "Dokumenter" document: "Dokumenter"
logoutConfirm: "Vil du logge ut?" logoutConfirm: "Vil du logge ut?"
pleaseSelect: "Vennligst velg" pleaseSelect: "Velg et alternativ"
type: "Type" type: "Type"
beta: "Beta" beta: "Beta"
account: "Kontoer" account: "Konto"
move: "Flytt" move: "Flytt"
pushNotification: "Push-varsler" pushNotification: "Push-varsler"
tools: "Verktøy" tools: "Verktøy"
@ -357,6 +449,7 @@ role: "Rolle"
color: "Farge" color: "Farge"
youCannotCreateAnymore: "Du kan ikke opprette flere." youCannotCreateAnymore: "Du kan ikke opprette flere."
cannotPerformTemporary: "Midlertidig utilgjengelig" cannotPerformTemporary: "Midlertidig utilgjengelig"
achievements: "Prestasjoner"
thisPostMayBeAnnoyingCancel: "Avbryt" thisPostMayBeAnnoyingCancel: "Avbryt"
exploreOtherServers: "Utforsk andre severe" exploreOtherServers: "Utforsk andre severe"
letsLookAtTimeline: "La oss se på tidslinje" letsLookAtTimeline: "La oss se på tidslinje"
@ -372,6 +465,26 @@ _initialAccountSetting:
theseSettingsCanEditLater: "Du kan endre disse innstillingene senere." theseSettingsCanEditLater: "Du kan endre disse innstillingene senere."
_achievements: _achievements:
_types: _types:
_notes10:
title: "Noen Notes"
_notes100:
title: "Mange Notes"
_notes500:
title: "Dekket i Notes"
_notes1000:
title: "Et fjell av Notes"
_notes5000:
title: "Overfylte Notes"
_notes10000:
title: "Super Notes"
_notes20000:
title: "Trenger... mer... Notes..."
_notes30000:
title: "Notes Notes Notes!"
_notes40000:
title: "Note fabrikk"
_notes50000:
title: "Planet av Notes"
_notes100000: _notes100000:
flavor: "Du har jammen mye å si." flavor: "Du har jammen mye å si."
_noteFavorited1: _noteFavorited1:
@ -400,7 +513,7 @@ _achievements:
_justPlainLucky: _justPlainLucky:
title: "Rett og slett heldig" title: "Rett og slett heldig"
_setNameToSyuilo: _setNameToSyuilo:
description: "Du har satt navnet ditt til \"syuilo\"" description: "Du satte navnet ditt til \"syuilo\""
_passedSinceAccountCreated1: _passedSinceAccountCreated1:
title: "Ett års jubileum" title: "Ett års jubileum"
description: "Det har gått ett år siden kontoen din ble opprettet" description: "Det har gått ett år siden kontoen din ble opprettet"
@ -468,15 +581,17 @@ _theme:
key: "Nøkkel" key: "Nøkkel"
keys: keys:
link: "Lenke" link: "Lenke"
renote: "Renote"
_sfx: _sfx:
note: "Notes"
notification: "Varsler" notification: "Varsler"
_ago: _ago:
future: "Fremitid" future: "Fremitid"
justNow: "Akkurat nå" justNow: "Akkurat nå"
secondsAgo: "{n} sekunder siden" secondsAgo: "{n}s siden"
minutesAgo: "{n} minutter siden" minutesAgo: "{n}m siden"
hoursAgo: "{n} timer siden" hoursAgo: "{n}t siden"
daysAgo: "{n} dager siden" daysAgo: "{n}d siden"
weeksAgo: "{n} uker siden" weeksAgo: "{n} uker siden"
monthsAgo: "{n} måneder siden" monthsAgo: "{n} måneder siden"
yearsAgo: "{n} år siden" yearsAgo: "{n} år siden"
@ -488,6 +603,7 @@ _time:
day: "Dager" day: "Dager"
_timelineTutorial: _timelineTutorial:
title: "Hvordan bruke Misskey" title: "Hvordan bruke Misskey"
step2_2: "Hva med å skrive en selvpresentasjon, eller bare \"Hei {name}!\" hvis du ikke har lyst?"
_2fa: _2fa:
renewTOTPCancel: "Avbryt" renewTOTPCancel: "Avbryt"
_weekday: _weekday:
@ -500,6 +616,7 @@ _weekday:
saturday: "Lørdag" saturday: "Lørdag"
_widgets: _widgets:
profile: "Profil" profile: "Profil"
instanceInfo: "Serverinformasjon"
notifications: "Varsler" notifications: "Varsler"
timeline: "Tidslinje" timeline: "Tidslinje"
calendar: "Kalender" calendar: "Kalender"
@ -535,6 +652,7 @@ _postForm:
_profile: _profile:
name: "Navn" name: "Navn"
username: "Brukernavn" username: "Brukernavn"
description: "Biografi"
metadataContent: "Innhold" metadataContent: "Innhold"
_exportOrImport: _exportOrImport:
followingList: "Følg" followingList: "Følg"
@ -576,13 +694,17 @@ _pages:
button: "Knapp" button: "Knapp"
_notification: _notification:
youWereFollowed: "fulgte deg" youWereFollowed: "fulgte deg"
unreadAntennaNote: "Antenne {name}"
achievementEarned: "Prestasjon låst opp"
_types: _types:
follow: "Følg" follow: "Nye følgere"
reply: "Svar" reply: "Svar"
quote: "Sitat" renote: "Renotes"
reaction: "Reaksjon" quote: "Sitater"
reaction: "Reaksjoner"
_actions: _actions:
reply: "Svar" reply: "Svar"
renote: "Renote"
_deck: _deck:
swapLeft: "Flytt til venstre" swapLeft: "Flytt til venstre"
swapRight: "Flytt til høyre" swapRight: "Flytt til høyre"
@ -594,6 +716,7 @@ _deck:
_columns: _columns:
notifications: "Varsler" notifications: "Varsler"
tl: "Tidslinje" tl: "Tidslinje"
antenna: "Antenner"
list: "Lister" list: "Lister"
channel: "Kanaler" channel: "Kanaler"
direct: "Direkte" direct: "Direkte"

View file

@ -2,7 +2,7 @@
_lang_: "Русский" _lang_: "Русский"
headlineMisskey: "Сеть, сплетённая из заметок" headlineMisskey: "Сеть, сплетённая из заметок"
introMisskey: "Добро пожаловать! Misskey — это децентрализованный сервис микроблогов с открытым исходным кодом.\nПишите «заметки» — делитесь со всеми происходящим вокруг или рассказывайте о себе 📡\nСтавьте «реакции» — выражайте свои чувства и эмоции от заметок других 👍\nОткройте для себя новый мир 🚀" introMisskey: "Добро пожаловать! Misskey — это децентрализованный сервис микроблогов с открытым исходным кодом.\nПишите «заметки» — делитесь со всеми происходящим вокруг или рассказывайте о себе 📡\nСтавьте «реакции» — выражайте свои чувства и эмоции от заметок других 👍\nОткройте для себя новый мир 🚀"
poweredByMisskeyDescription: "{name} один из инстансов (также называемый экземпляром Misskey), использующий платформу с открытым исходным кодом <b>Misskey</b>." poweredByMisskeyDescription: "{name} сервис на платформе с открытым исходным кодом <b>Misskey</b>, называемый инстансом Misskey."
monthAndDay: "{day}.{month}" monthAndDay: "{day}.{month}"
search: "Поиск" search: "Поиск"
notifications: "Уведомления" notifications: "Уведомления"
@ -649,8 +649,8 @@ abuseReported: "Жалоба отправлена. Большое спасибо
reporter: "Сообщивший" reporter: "Сообщивший"
reporteeOrigin: "О ком сообщено" reporteeOrigin: "О ком сообщено"
reporterOrigin: "Кто сообщил" reporterOrigin: "Кто сообщил"
forwardReport: "Перенаправление отчета на инстант." forwardReport: "Отправить жалобу на инстанс автора."
forwardReportIsAnonymous: "Удаленный инстант не сможет увидеть вашу информацию и будет отображаться как анонимная системная учетная запись." forwardReportIsAnonymous: "Жалоба на удалённый инстанс будет отправлена анонимно. Вместо ваших данных у получателя будет отображена системная учётная запись."
send: "Отправить" send: "Отправить"
abuseMarkAsResolved: "Отметить жалобу как решённую" abuseMarkAsResolved: "Отметить жалобу как решённую"
openInNewTab: "Открыть в новой вкладке" openInNewTab: "Открыть в новой вкладке"
@ -823,6 +823,7 @@ translatedFrom: "Перевод. Язык оригинала — {x}"
accountDeletionInProgress: "В настоящее время выполняется удаление учетной записи" accountDeletionInProgress: "В настоящее время выполняется удаление учетной записи"
usernameInfo: "Имя, которое отличает вашу учетную запись от других на этом сервере. Вы можете использовать алфавит (a~z, A~Z), цифры (0~9) или символы подчеркивания (_). Имена пользователей не могут быть изменены позже." usernameInfo: "Имя, которое отличает вашу учетную запись от других на этом сервере. Вы можете использовать алфавит (a~z, A~Z), цифры (0~9) или символы подчеркивания (_). Имена пользователей не могут быть изменены позже."
aiChanMode: "Режим Ай" aiChanMode: "Режим Ай"
devMode: "Режим разработчика"
keepCw: "Сохраняйте Предупреждения о содержимом" keepCw: "Сохраняйте Предупреждения о содержимом"
pubSub: "Учётные записи Pub/Sub" pubSub: "Учётные записи Pub/Sub"
lastCommunication: "Последнее сообщение" lastCommunication: "Последнее сообщение"
@ -914,8 +915,8 @@ cannotUploadBecauseInappropriate: "Файл не может быть загру
cannotUploadBecauseNoFreeSpace: "Файл не может быть загружен, так как не осталось места на диске" cannotUploadBecauseNoFreeSpace: "Файл не может быть загружен, так как не осталось места на диске"
cannotUploadBecauseExceedsFileSizeLimit: "Файл не может быть загружен, так как он превышает лимит размера файла." cannotUploadBecauseExceedsFileSizeLimit: "Файл не может быть загружен, так как он превышает лимит размера файла."
beta: "Бета" beta: "Бета"
enableAutoSensitive: "Автоматическое определение NSFW" enableAutoSensitive: "Автоматическое определение содержимого не для всех"
enableAutoSensitiveDescription: "Если доступно, используйте машинное обучение для автоматической установки флага NSFW на носителе. Даже если эта функция отключена, она может быть установлена ​​автоматически в зависимости от инстанта." enableAutoSensitiveDescription: "Позволяет определять наличие содержимого не для всех при помощи искусственного интеллекта там, где это возможно. Даже если эту опцию отключить, она всё равно может быть включена на весь инстанс."
activeEmailValidationDescription: "Если включено, будет проводиться более строгая проверка адреса электронной почты, в том числе на то, что он действительный и не временный. Если же отключено, то проверяется только корректность написания адреса." activeEmailValidationDescription: "Если включено, будет проводиться более строгая проверка адреса электронной почты, в том числе на то, что он действительный и не временный. Если же отключено, то проверяется только корректность написания адреса."
navbar: "Панель навигации" navbar: "Панель навигации"
shuffle: "Перемешать" shuffle: "Перемешать"
@ -1006,6 +1007,7 @@ noteIdOrUrl: "ID или ссылка на заметку"
video: "Видео" video: "Видео"
videos: "Видео" videos: "Видео"
dataSaver: "Экономия трафика" dataSaver: "Экономия трафика"
renotesList: "Репосты"
horizontal: "Сбоку" horizontal: "Сбоку"
youFollowing: "Подписки" youFollowing: "Подписки"
options: "Настройки ролей" options: "Настройки ролей"
@ -1180,6 +1182,9 @@ _achievements:
_client30min: _client30min:
title: "Перерыв на обед" title: "Перерыв на обед"
description: "Прошло 30 минут с момента запуска клиента" description: "Прошло 30 минут с момента запуска клиента"
_client60min:
title: "Не наглядеться на Misskey"
description: "Misskey был открыт 60 минут подряд"
_noteDeletedWithin1min: _noteDeletedWithin1min:
title: "Ой, нет!" title: "Ой, нет!"
description: "Заметка удалена через минуту после публикации" description: "Заметка удалена через минуту после публикации"
@ -1282,6 +1287,7 @@ _role:
canInvite: "Может создавать пригласительные коды" canInvite: "Может создавать пригласительные коды"
canManageCustomEmojis: "Управлять пользовательскими эмодзи" canManageCustomEmojis: "Управлять пользовательскими эмодзи"
driveCapacity: "Доступное пространство на «диске»" driveCapacity: "Доступное пространство на «диске»"
alwaysMarkNsfw: "Всегда отмечать файлы как «не для всех»"
pinMax: "Доступное количество закреплённых заметок" pinMax: "Доступное количество закреплённых заметок"
antennaMax: "Доступное количество антенн" antennaMax: "Доступное количество антенн"
wordMuteMax: "Доступное количество знаков в списке скрытия слов" wordMuteMax: "Доступное количество знаков в списке скрытия слов"
@ -1309,7 +1315,7 @@ _sensitiveMediaDetection:
description: "Машинное обучение может быть использовано для автоматического обнаружения чувствительных медиа для модерации. Нагрузка на сервер увеличивается незначительно." description: "Машинное обучение может быть использовано для автоматического обнаружения чувствительных медиа для модерации. Нагрузка на сервер увеличивается незначительно."
sensitivity: "Чувствительность обнаружения" sensitivity: "Чувствительность обнаружения"
sensitivityDescription: "Более низкая чувствительность уменьшает количество ложных срабатываний (false positives). Повышение чувствительности уменьшает утечку при обнаружении (ложноотрицательные результаты)." sensitivityDescription: "Более низкая чувствительность уменьшает количество ложных срабатываний (false positives). Повышение чувствительности уменьшает утечку при обнаружении (ложноотрицательные результаты)."
setSensitiveFlagAutomatically: "Установить флаг NSFW" setSensitiveFlagAutomatically: "Обозначить как не для всех"
setSensitiveFlagAutomaticallyDescription: "Даже если этот параметр отключен, результат оценки сохраняется внутри системы." setSensitiveFlagAutomaticallyDescription: "Даже если этот параметр отключен, результат оценки сохраняется внутри системы."
analyzeVideos: "Анализировать видео?" analyzeVideos: "Анализировать видео?"
analyzeVideosDescription: "Анализируйте видео в дополнение к неподвижным изображениям. Нагрузка на сервер немного увеличивается." analyzeVideosDescription: "Анализируйте видео в дополнение к неподвижным изображениям. Нагрузка на сервер немного увеличивается."
@ -1528,6 +1534,16 @@ _time:
minute: "мин" minute: "мин"
hour: "ч" hour: "ч"
day: "сут" day: "сут"
_timelineTutorial:
title: "Как пользоваться Misskey"
step1_1: "Это лицо Misskey, так называемая лента. Ваш инстанс, {name}, покажет тут все опубликованные на нём заметки в хронологическом порядке."
step1_2: "Здесь есть несколько лент. К примеру «персональная» лента отображает заметки тех, на кого вы подписаны. А «местная» — заметки тех, кого приютил {name}."
step2_1: "Что ж, теперь самое время опубликовать заметку. Если нажать вверху страницы на изображение карандаша, появится форма для текста."
step2_2: "Почему бы не написать немного о себе? Ну, или хотя бы «Привет, {name}»?"
step3_1: "Справились с первой заметкой?"
step3_2: "Отлично, теперь она должна появиться в вашей ленте."
step4_1: "А ещё здесь можно делиться своими реакциями на заметки."
step4_2: "Отмечайте реакции, нажимая на символ «+» под заметкой и выбирая значок по душе."
_2fa: _2fa:
alreadyRegistered: "Двухфакторная аутентификация уже настроена." alreadyRegistered: "Двухфакторная аутентификация уже настроена."
registerTOTP: "Начните настраивать приложение-аутентификатор" registerTOTP: "Начните настраивать приложение-аутентификатор"
@ -1868,6 +1884,9 @@ _deck:
_dialog: _dialog:
charactersExceeded: "Превышено максимальное количество символов! У вас {current} / из {max}" charactersExceeded: "Превышено максимальное количество символов! У вас {current} / из {max}"
charactersBelow: "Это ниже минимального количества символов! У вас {current} / из {min}" charactersBelow: "Это ниже минимального количества символов! У вас {current} / из {min}"
_disabledTimeline:
title: "Лента отключена"
description: "Ваша текущая роль не позволяет пользоваться этой лентой."
_webhookSettings: _webhookSettings:
name: "Название" name: "Название"
active: "Вкл." active: "Вкл."

View file

@ -52,6 +52,8 @@ addToList: "添加至列表"
sendMessage: "发送" sendMessage: "发送"
copyRSS: "复制RSS" copyRSS: "复制RSS"
copyUsername: "复制用户名" copyUsername: "复制用户名"
copyUserId: "复制用户ID"
copyNoteId: "复制帖子ID"
searchUser: "搜索用户" searchUser: "搜索用户"
reply: "回复" reply: "回复"
loadMore: "查看更多" loadMore: "查看更多"
@ -790,6 +792,7 @@ noMaintainerInformationWarning: "管理人员信息未设置。"
noBotProtectionWarning: "Bot保护未设置。" noBotProtectionWarning: "Bot保护未设置。"
configure: "设置" configure: "设置"
postToGallery: "发送到图库" postToGallery: "发送到图库"
postToHashtag: "投稿到这个标签"
gallery: "图库" gallery: "图库"
recentPosts: "最新发布" recentPosts: "最新发布"
popularPosts: "热门投稿" popularPosts: "热门投稿"
@ -823,6 +826,7 @@ translatedFrom: "从 {x} 翻译"
accountDeletionInProgress: "正在删除账户" accountDeletionInProgress: "正在删除账户"
usernameInfo: "在服务器上唯一标识您的帐户的名称。您可以使用字母 (a ~ z, A ~ Z)、数字 (0 ~ 9) 和下划线 (_)。用户名以后不能更改。" usernameInfo: "在服务器上唯一标识您的帐户的名称。您可以使用字母 (a ~ z, A ~ Z)、数字 (0 ~ 9) 和下划线 (_)。用户名以后不能更改。"
aiChanMode: "小蓝模式" aiChanMode: "小蓝模式"
devMode: "开发者模式"
keepCw: "回复时维持隐藏内容" keepCw: "回复时维持隐藏内容"
pubSub: "Pub/Sub账户" pubSub: "Pub/Sub账户"
lastCommunication: "最近通信" lastCommunication: "最近通信"
@ -832,6 +836,8 @@ breakFollow: "移除关注者"
breakFollowConfirm: "你想取消关注吗?" breakFollowConfirm: "你想取消关注吗?"
itsOn: "已开启" itsOn: "已开启"
itsOff: "已关闭" itsOff: "已关闭"
on: "开启"
off: "关闭"
emailRequiredForSignup: "注册账户需要电子邮件地址" emailRequiredForSignup: "注册账户需要电子邮件地址"
unread: "未读" unread: "未读"
filter: "筛选" filter: "筛选"
@ -986,6 +992,8 @@ cannotBeChangedLater: "之后不能再更改。"
reactionAcceptance: "接受表情回应" reactionAcceptance: "接受表情回应"
likeOnly: "仅点赞" likeOnly: "仅点赞"
likeOnlyForRemote: "远程仅点赞" likeOnlyForRemote: "远程仅点赞"
nonSensitiveOnly: "仅限非敏感内容"
nonSensitiveOnlyForLocalLikeOnlyForRemote: "仅限非敏感内容(远程仅点赞)"
rolesAssignedToMe: "指派给自己的角色" rolesAssignedToMe: "指派给自己的角色"
resetPasswordConfirm: "确定重置密码?" resetPasswordConfirm: "确定重置密码?"
sensitiveWords: "敏感词" sensitiveWords: "敏感词"
@ -1043,6 +1051,16 @@ preventAiLearning: "拒绝接受生成式AI的学习"
preventAiLearningDescription: "要求文章生成AI或图像生成AI不能够以发布的帖子和图像等内容作为学习对象。这是通过在HTML响应中包含noai标志来实现的这不能完全阻止AI学习你的发布内容并不是所有AI都会遵守这类请求。" preventAiLearningDescription: "要求文章生成AI或图像生成AI不能够以发布的帖子和图像等内容作为学习对象。这是通过在HTML响应中包含noai标志来实现的这不能完全阻止AI学习你的发布内容并不是所有AI都会遵守这类请求。"
options: "选项" options: "选项"
specifyUser: "用户指定" specifyUser: "用户指定"
failedToPreviewUrl: "无法预览"
update: "更新"
rolesThatCanBeUsedThisEmojiAsReaction: "可以使用表情作为回应的角色"
rolesThatCanBeUsedThisEmojiAsReactionEmptyDescription: "在没有指定角色的情况下,任何人都可以使用表情作为回应。"
rolesThatCanBeUsedThisEmojiAsReactionPublicRoleWarn: "角色必须是公开的。"
cancelReactionConfirm: "要取消回应吗?"
changeReactionConfirm: "要更改回应吗?"
later: "一会再说"
goToMisskey: "去往Misskey"
installed: "已安装"
_initialAccountSetting: _initialAccountSetting:
accountCreated: "账户创建完成了!" accountCreated: "账户创建完成了!"
letsStartAccountSetup: "来进行帐户的初始设置吧。" letsStartAccountSetup: "来进行帐户的初始设置吧。"
@ -1057,6 +1075,7 @@ _initialAccountSetting:
haveFun: "希望{name}在这里玩得开心!" haveFun: "希望{name}在这里玩得开心!"
ifYouNeedLearnMore: "关于{name}(Misskey)的使用方法,详见{link}。" ifYouNeedLearnMore: "关于{name}(Misskey)的使用方法,详见{link}。"
skipAreYouSure: "要跳过初始设置吗?" skipAreYouSure: "要跳过初始设置吗?"
laterAreYouSure: "要稍后再进行初始设定吗?"
_serverRules: _serverRules:
description: "在新用户注册前显示服务器的简单规则。推荐显示服务条款的主要内容。" description: "在新用户注册前显示服务器的简单规则。推荐显示服务条款的主要内容。"
_accountMigration: _accountMigration:

View file

@ -1055,6 +1055,11 @@ failedToPreviewUrl: "無法預覽"
update: "更新" update: "更新"
rolesThatCanBeUsedThisEmojiAsReaction: "可以當成反應使用的角色" rolesThatCanBeUsedThisEmojiAsReaction: "可以當成反應使用的角色"
rolesThatCanBeUsedThisEmojiAsReactionEmptyDescription: "如果是未指定角色的情況,則任何人都可以被當成反應來使用。" rolesThatCanBeUsedThisEmojiAsReactionEmptyDescription: "如果是未指定角色的情況,則任何人都可以被當成反應來使用。"
rolesThatCanBeUsedThisEmojiAsReactionPublicRoleWarn: "角色必須是公開的角色。"
cancelReactionConfirm: "要取消做出的反應嗎?"
changeReactionConfirm: "要變更做出的反應嗎?"
later: "稍後再說"
goToMisskey: "往Misskey"
_initialAccountSetting: _initialAccountSetting:
accountCreated: "帳戶已建立完成!" accountCreated: "帳戶已建立完成!"
letsStartAccountSetup: "來進行帳戶的初始設定吧。" letsStartAccountSetup: "來進行帳戶的初始設定吧。"
@ -1069,6 +1074,7 @@ _initialAccountSetting:
haveFun: "盡情享受{name}吧!" haveFun: "盡情享受{name}吧!"
ifYouNeedLearnMore: "關於如何使用{name}(Misskey)的詳細資訊,請見{link}。" ifYouNeedLearnMore: "關於如何使用{name}(Misskey)的詳細資訊,請見{link}。"
skipAreYouSure: "要略過初始設定嗎?" skipAreYouSure: "要略過初始設定嗎?"
laterAreYouSure: "稍後再重新進行初始設定嗎?"
_serverRules: _serverRules:
description: "設定伺服器的簡要規則,在新的註冊之前顯示。建議的內容是使用條款的摘要。" description: "設定伺服器的簡要規則,在新的註冊之前顯示。建議的內容是使用條款的摘要。"
_accountMigration: _accountMigration:

View file

@ -1,6 +1,6 @@
{ {
"name": "misskey", "name": "misskey",
"version": "13.13.0-beta.4", "version": "13.13.0-beta.7",
"codename": "nasubi", "codename": "nasubi",
"repository": { "repository": {
"type": "git", "type": "git",
@ -51,16 +51,16 @@
"gulp-replace": "1.1.4", "gulp-replace": "1.1.4",
"gulp-terser": "2.1.0", "gulp-terser": "2.1.0",
"js-yaml": "4.1.0", "js-yaml": "4.1.0",
"typescript": "5.0.4" "typescript": "5.1.3"
}, },
"devDependencies": { "devDependencies": {
"@types/gulp": "4.0.10", "@types/gulp": "4.0.10",
"@types/gulp-rename": "2.0.1", "@types/gulp-rename": "2.0.1",
"@typescript-eslint/eslint-plugin": "5.59.5", "@typescript-eslint/eslint-plugin": "5.59.8",
"@typescript-eslint/parser": "5.59.5", "@typescript-eslint/parser": "5.59.8",
"cross-env": "7.0.3", "cross-env": "7.0.3",
"cypress": "12.13.0", "cypress": "12.13.0",
"eslint": "8.40.0", "eslint": "8.41.0",
"start-server-and-test": "2.0.0" "start-server-and-test": "2.0.0"
}, },
"optionalDependencies": { "optionalDependencies": {

View file

@ -60,27 +60,27 @@
"@discordapp/twemoji": "14.1.2", "@discordapp/twemoji": "14.1.2",
"@fastify/accepts": "4.1.0", "@fastify/accepts": "4.1.0",
"@fastify/cookie": "8.3.0", "@fastify/cookie": "8.3.0",
"@fastify/cors": "8.2.1", "@fastify/cors": "8.3.0",
"@fastify/http-proxy": "9.1.0", "@fastify/http-proxy": "9.1.0",
"@fastify/multipart": "7.6.0", "@fastify/multipart": "7.6.0",
"@fastify/static": "6.10.1", "@fastify/static": "6.10.2",
"@fastify/view": "7.4.1", "@fastify/view": "7.4.1",
"@nestjs/common": "9.4.2", "@nestjs/common": "9.4.2",
"@nestjs/core": "9.4.2", "@nestjs/core": "9.4.2",
"@nestjs/testing": "9.4.2", "@nestjs/testing": "9.4.2",
"@peertube/http-signature": "1.7.0", "@peertube/http-signature": "1.7.0",
"@sinonjs/fake-timers": "10.0.2", "@sinonjs/fake-timers": "10.2.0",
"@swc/cli": "0.1.62", "@swc/cli": "0.1.62",
"@swc/core": "1.3.59", "@swc/core": "1.3.61",
"accepts": "1.3.8", "accepts": "1.3.8",
"ajv": "8.12.0", "ajv": "8.12.0",
"archiver": "5.3.1", "archiver": "5.3.1",
"autwh": "0.1.0", "autwh": "0.1.0",
"bcryptjs": "2.4.3", "bcryptjs": "2.4.3",
"blurhash": "2.0.5", "blurhash": "2.0.5",
"bullmq": "3.14.1", "bullmq": "3.15.0",
"cacheable-lookup": "6.1.0", "cacheable-lookup": "6.1.0",
"cbor": "8.1.0", "cbor": "9.0.0",
"chalk": "5.2.0", "chalk": "5.2.0",
"chalk-template": "0.4.0", "chalk-template": "0.4.0",
"chokidar": "3.5.3", "chokidar": "3.5.3",
@ -96,24 +96,24 @@
"fluent-ffmpeg": "2.1.2", "fluent-ffmpeg": "2.1.2",
"form-data": "4.0.0", "form-data": "4.0.0",
"got": "12.6.0", "got": "12.6.0",
"happy-dom": "9.19.2", "happy-dom": "9.20.3",
"hpagent": "1.2.0", "hpagent": "1.2.0",
"ioredis": "5.3.2", "ioredis": "5.3.2",
"ip-cidr": "3.1.0", "ip-cidr": "3.1.0",
"is-svg": "4.3.2", "is-svg": "4.3.2",
"js-yaml": "4.1.0", "js-yaml": "4.1.0",
"jsdom": "21.1.1", "jsdom": "22.1.0",
"json5": "2.2.3", "json5": "2.2.3",
"jsonld": "8.1.1", "jsonld": "8.2.0",
"jsrsasign": "10.8.6", "jsrsasign": "10.8.6",
"meilisearch": "0.32.4", "meilisearch": "0.32.5",
"mfm-js": "0.23.3", "mfm-js": "0.23.3",
"mime-types": "2.1.35", "mime-types": "2.1.35",
"misskey-js": "workspace:*", "misskey-js": "workspace:*",
"ms": "3.0.0-canary.1", "ms": "3.0.0-canary.1",
"nested-property": "4.0.0", "nested-property": "4.0.0",
"node-fetch": "3.3.1", "node-fetch": "3.3.1",
"nodemailer": "6.9.2", "nodemailer": "6.9.3",
"nsfwjs": "2.4.2", "nsfwjs": "2.4.2",
"oauth": "0.10.0", "oauth": "0.10.0",
"os-utils": "0.0.14", "os-utils": "0.0.14",
@ -129,7 +129,7 @@
"qrcode": "1.5.3", "qrcode": "1.5.3",
"random-seed": "0.3.0", "random-seed": "0.3.0",
"ratelimiter": "3.4.1", "ratelimiter": "3.4.1",
"re2": "1.18.0", "re2": "1.19.0",
"redis-lock": "0.1.4", "redis-lock": "0.1.4",
"reflect-metadata": "0.1.13", "reflect-metadata": "0.1.13",
"rename": "1.0.4", "rename": "1.0.4",
@ -146,16 +146,16 @@
"strict-event-emitter-types": "2.0.0", "strict-event-emitter-types": "2.0.0",
"stringz": "2.1.0", "stringz": "2.1.0",
"summaly": "github:misskey-dev/summaly", "summaly": "github:misskey-dev/summaly",
"systeminformation": "5.17.12", "systeminformation": "5.17.16",
"tinycolor2": "1.6.0", "tinycolor2": "1.6.0",
"tmp": "0.2.1", "tmp": "0.2.1",
"tsc-alias": "1.8.6", "tsc-alias": "1.8.6",
"tsconfig-paths": "4.2.0", "tsconfig-paths": "4.2.0",
"twemoji-parser": "14.0.0", "twemoji-parser": "14.0.0",
"typeorm": "0.3.16", "typeorm": "0.3.16",
"typescript": "5.0.4", "typescript": "5.1.3",
"ulid": "2.3.0", "ulid": "2.3.0",
"unzipper": "0.10.11", "unzipper": "0.10.14",
"uuid": "9.0.0", "uuid": "9.0.0",
"vary": "1.1.2", "vary": "1.1.2",
"web-push": "3.6.1", "web-push": "3.6.1",
@ -173,13 +173,13 @@
"@types/content-disposition": "0.5.5", "@types/content-disposition": "0.5.5",
"@types/escape-regexp": "0.0.1", "@types/escape-regexp": "0.0.1",
"@types/fluent-ffmpeg": "2.1.21", "@types/fluent-ffmpeg": "2.1.21",
"@types/jest": "29.5.1", "@types/jest": "29.5.2",
"@types/js-yaml": "4.0.5", "@types/js-yaml": "4.0.5",
"@types/jsdom": "21.1.1", "@types/jsdom": "21.1.1",
"@types/jsonld": "1.5.8", "@types/jsonld": "1.5.8",
"@types/jsrsasign": "10.5.8", "@types/jsrsasign": "10.5.8",
"@types/mime-types": "2.1.1", "@types/mime-types": "2.1.1",
"@types/node": "20.2.3", "@types/node": "20.2.5",
"@types/node-fetch": "3.0.3", "@types/node-fetch": "3.0.3",
"@types/nodemailer": "6.4.8", "@types/nodemailer": "6.4.8",
"@types/oauth": "0.9.1", "@types/oauth": "0.9.1",
@ -203,11 +203,11 @@
"@types/web-push": "3.3.2", "@types/web-push": "3.3.2",
"@types/websocket": "1.0.5", "@types/websocket": "1.0.5",
"@types/ws": "8.5.4", "@types/ws": "8.5.4",
"@typescript-eslint/eslint-plugin": "5.59.5", "@typescript-eslint/eslint-plugin": "5.59.8",
"@typescript-eslint/parser": "5.59.5", "@typescript-eslint/parser": "5.59.8",
"aws-sdk-client-mock": "2.1.1", "aws-sdk-client-mock": "2.1.1",
"cross-env": "7.0.3", "cross-env": "7.0.3",
"eslint": "8.40.0", "eslint": "8.41.0",
"eslint-plugin-import": "2.27.5", "eslint-plugin-import": "2.27.5",
"execa": "6.1.0", "execa": "6.1.0",
"jest": "29.5.0", "jest": "29.5.0",

View file

@ -19,6 +19,8 @@ import type * as http from 'node:http';
@Injectable() @Injectable()
export class StreamingApiServerService { export class StreamingApiServerService {
#wss: WebSocket.WebSocketServer; #wss: WebSocket.WebSocketServer;
#connections = new Map<WebSocket.WebSocket, number>();
#cleanConnectionsIntervalId: NodeJS.Timeout | null = null;
constructor( constructor(
@Inject(DI.config) @Inject(DI.config)
@ -109,7 +111,9 @@ export class StreamingApiServerService {
await stream.listen(ev, connection); await stream.listen(ev, connection);
const intervalId = user ? setInterval(() => { this.#connections.set(connection, Date.now());
const userUpdateIntervalId = user ? setInterval(() => {
this.usersRepository.update(user.id, { this.usersRepository.update(user.id, {
lastActiveDate: new Date(), lastActiveDate: new Date(),
}); });
@ -124,19 +128,34 @@ export class StreamingApiServerService {
ev.removeAllListeners(); ev.removeAllListeners();
stream.dispose(); stream.dispose();
this.redisForSub.off('message', onRedisMessage); this.redisForSub.off('message', onRedisMessage);
if (intervalId) clearInterval(intervalId); if (userUpdateIntervalId) clearInterval(userUpdateIntervalId);
}); });
connection.on('message', async (data) => { connection.on('message', async (data) => {
this.#connections.set(connection, Date.now());
if (data.toString() === 'ping') { if (data.toString() === 'ping') {
connection.send('pong'); connection.send('pong');
} }
}); });
}); });
this.#cleanConnectionsIntervalId = setInterval(() => {
const now = Date.now();
for (const [connection, lastActive] of this.#connections.entries()) {
if (now - lastActive > 1000 * 60 * 5) {
connection.terminate();
this.#connections.delete(connection);
}
}
}, 1000 * 60 * 5);
} }
@bindThis @bindThis
public detach(): Promise<void> { public detach(): Promise<void> {
if (this.#cleanConnectionsIntervalId) {
clearInterval(this.#cleanConnectionsIntervalId);
this.#cleanConnectionsIntervalId = null;
}
return new Promise((resolve) => { return new Promise((resolve) => {
this.#wss.close(() => resolve()); this.#wss.close(() => resolve());
}); });

View file

@ -113,6 +113,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
} }
this.antennasRepository.update(antenna.id, { this.antennasRepository.update(antenna.id, {
isActive: true,
lastUsedAt: new Date(), lastUsedAt: new Date(),
}); });

View file

@ -55,7 +55,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
throw new ApiError(meta.errors.noSuchSession); throw new ApiError(meta.errors.noSuchSession);
} }
// Generate access token
const accessToken = secureRndstr(32, true); const accessToken = secureRndstr(32, true);
// Fetch exist access token // Fetch exist access token
@ -65,7 +64,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
}); });
if (exist == null) { if (exist == null) {
// Lookup app
const app = await this.appsRepository.findOneByOrFail({ id: session.appId }); const app = await this.appsRepository.findOneByOrFail({ id: session.appId });
// Generate Hash // Generate Hash
@ -75,7 +73,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
const now = new Date(); const now = new Date();
// Insert access token doc
await this.accessTokensRepository.insert({ await this.accessTokensRepository.insert({
id: this.idService.genId(), id: this.idService.genId(),
createdAt: now, createdAt: now,

View file

@ -1,6 +1,6 @@
import { promisify } from 'node:util'; import { promisify } from 'node:util';
import bcrypt from 'bcryptjs'; import bcrypt from 'bcryptjs';
import * as cbor from 'cbor'; import cbor from 'cbor';
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js'; import { Endpoint } from '@/server/api/endpoint-base.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js';

View file

@ -26,7 +26,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
) { ) {
super(meta, paramDef, async (ps, me) => { super(meta, paramDef, async (ps, me) => {
const query = this.accessTokensRepository.createQueryBuilder('token') const query = this.accessTokensRepository.createQueryBuilder('token')
.where('token.userId = :userId', { userId: me.id }); .where('token.userId = :userId', { userId: me.id })
.leftJoinAndSelect('token.app', 'app');
switch (ps.sort) { switch (ps.sort) {
case '+createdAt': query.orderBy('token.createdAt', 'DESC'); break; case '+createdAt': query.orderBy('token.createdAt', 'DESC'); break;
@ -40,7 +41,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
return await Promise.all(tokens.map(token => ({ return await Promise.all(tokens.map(token => ({
id: token.id, id: token.id,
name: token.name, name: token.name ?? token.app?.name,
createdAt: token.createdAt, createdAt: token.createdAt,
lastUsedAt: token.lastUsedAt, lastUsedAt: token.lastUsedAt,
permission: token.permission, permission: token.permission,

View file

@ -91,18 +91,18 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
const includeTypes = ps.includeTypes && ps.includeTypes.filter(type => !(obsoleteNotificationTypes).includes(type as any)) as typeof notificationTypes[number][]; const includeTypes = ps.includeTypes && ps.includeTypes.filter(type => !(obsoleteNotificationTypes).includes(type as any)) as typeof notificationTypes[number][];
const excludeTypes = ps.excludeTypes && ps.excludeTypes.filter(type => !(obsoleteNotificationTypes).includes(type as any)) as typeof notificationTypes[number][]; const excludeTypes = ps.excludeTypes && ps.excludeTypes.filter(type => !(obsoleteNotificationTypes).includes(type as any)) as typeof notificationTypes[number][];
const limit = ps.limit + (ps.untilId ? 1 : 0); // untilIdに指定したものも含まれるため+1 const limit = ps.limit + (ps.untilId ? 1 : 0) + (ps.sinceId ? 1 : 0); // untilIdに指定したものも含まれるため+1
const notificationsRes = await this.redisClient.xrevrange( const notificationsRes = await this.redisClient.xrevrange(
`notificationTimeline:${me.id}`, `notificationTimeline:${me.id}`,
ps.untilId ? this.idService.parse(ps.untilId).date.getTime() : '+', ps.untilId ? this.idService.parse(ps.untilId).date.getTime() : '+',
'-', ps.sinceId ? this.idService.parse(ps.sinceId).date.getTime() : '-',
'COUNT', limit); 'COUNT', limit);
if (notificationsRes.length === 0) { if (notificationsRes.length === 0) {
return []; return [];
} }
let notifications = notificationsRes.map(x => JSON.parse(x[1][1])).filter(x => x.id !== ps.untilId) as Notification[]; let notifications = notificationsRes.map(x => JSON.parse(x[1][1])).filter(x => x.id !== ps.untilId && x !== ps.sinceId) as Notification[];
if (includeTypes && includeTypes.length > 0) { if (includeTypes && includeTypes.length > 0) {
notifications = notifications.filter(notification => includeTypes.includes(notification.type)); notifications = notifications.filter(notification => includeTypes.includes(notification.type));

View file

@ -1,5 +1,6 @@
import { IsNull, LessThanOrEqual, MoreThan } from 'typeorm'; import { IsNull, LessThanOrEqual, MoreThan } from 'typeorm';
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import * as JSON5 from 'json5';
import type { AdsRepository, UsersRepository } from '@/models/index.js'; import type { AdsRepository, UsersRepository } from '@/models/index.js';
import { MAX_NOTE_TEXT_LENGTH } from '@/const.js'; import { MAX_NOTE_TEXT_LENGTH } from '@/const.js';
import { Endpoint } from '@/server/api/endpoint-base.js'; import { Endpoint } from '@/server/api/endpoint-base.js';
@ -291,8 +292,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
iconUrl: instance.iconUrl, iconUrl: instance.iconUrl,
backgroundImageUrl: instance.backgroundImageUrl, backgroundImageUrl: instance.backgroundImageUrl,
logoImageUrl: instance.logoImageUrl, logoImageUrl: instance.logoImageUrl,
defaultLightTheme: instance.defaultLightTheme, // クライアントの手間を減らすためあらかじめJSONに変換しておく
defaultDarkTheme: instance.defaultDarkTheme, defaultLightTheme: instance.defaultLightTheme ? JSON.stringify(JSON5.parse(instance.defaultLightTheme)) : null,
defaultDarkTheme: instance.defaultDarkTheme ? JSON.stringify(JSON5.parse(instance.defaultDarkTheme)) : null,
enableEmail: instance.enableEmail, enableEmail: instance.enableEmail,
enableServiceWorker: instance.enableServiceWorker, enableServiceWorker: instance.enableServiceWorker,
translatorAvailable: instance.deeplAuthKey != null, translatorAvailable: instance.deeplAuthKey != null,

View file

@ -35,7 +35,7 @@ html
link(rel='prefetch' href='https://xn--931a.moe/assets/not-found.jpg') link(rel='prefetch' href='https://xn--931a.moe/assets/not-found.jpg')
link(rel='prefetch' href='https://xn--931a.moe/assets/error.jpg') link(rel='prefetch' href='https://xn--931a.moe/assets/error.jpg')
//- https://github.com/misskey-dev/misskey/issues/9842 //- https://github.com/misskey-dev/misskey/issues/9842
link(rel='stylesheet' href='/assets/tabler-icons/tabler-icons.min.css?v2.17.0') link(rel='stylesheet' href='/assets/tabler-icons/tabler-icons.min.css?v2.21.0')
link(rel='modulepreload' href=`/vite/${clientEntry.file}`) link(rel='modulepreload' href=`/vite/${clientEntry.file}`)
if !config.clientManifestExists if !config.clientManifestExists

View file

@ -2,7 +2,7 @@ process.env.NODE_ENV = 'test';
import * as assert from 'assert'; import * as assert from 'assert';
import * as crypto from 'node:crypto'; import * as crypto from 'node:crypto';
import * as cbor from 'cbor'; import cbor from 'cbor';
import * as OTPAuth from 'otpauth'; import * as OTPAuth from 'otpauth';
import { loadConfig } from '../../src/config.js'; import { loadConfig } from '../../src/config.js';
import { signup, api, post, react, startServer, waitFire } from '../utils.js'; import { signup, api, post, react, startServer, waitFire } from '../utils.js';

View file

@ -1,6 +1,6 @@
<link rel="preload" href="https://github.com/misskey-dev/misskey/blob/master/packages/frontend/assets/about-icon.png?raw=true" as="image" type="image/png" crossorigin="anonymous"> <link rel="preload" href="https://github.com/misskey-dev/misskey/blob/master/packages/frontend/assets/about-icon.png?raw=true" as="image" type="image/png" crossorigin="anonymous">
<link rel="preload" href="https://github.com/misskey-dev/misskey/blob/master/packages/frontend/assets/fedi.jpg?raw=true" as="image" type="image/jpeg" crossorigin="anonymous"> <link rel="preload" href="https://github.com/misskey-dev/misskey/blob/master/packages/frontend/assets/fedi.jpg?raw=true" as="image" type="image/jpeg" crossorigin="anonymous">
<link rel="stylesheet" href="https://unpkg.com/@tabler/icons-webfont@2.12.0/tabler-icons.min.css"> <link rel="stylesheet" href="https://unpkg.com/@tabler/icons-webfont@2.21.0/tabler-icons.min.css">
<link rel="stylesheet" href="https://unpkg.com/@fontsource/m-plus-rounded-1c/index.css"> <link rel="stylesheet" href="https://unpkg.com/@fontsource/m-plus-rounded-1c/index.css">
<style> <style>
html { html {

View file

@ -0,0 +1,597 @@
import { parse } from 'acorn';
import { generate } from 'astring';
import { describe, expect, it } from 'vitest';
import { normalizeClass, unwindCssModuleClassName } from './rollup-plugin-unwind-css-module-class-name';
import type * as estree from 'estree';
function parseExpression(code: string): estree.Expression {
const program = parse(code, { ecmaVersion: 'latest', sourceType: 'module' }) as unknown as estree.Program;
const statement = program.body[0] as estree.ExpressionStatement;
return statement.expression;
}
describe(normalizeClass.name, () => {
it('should normalize string', () => {
expect(normalizeClass(parseExpression('"a b c"'))).toBe('a b c');
});
it('should trim redundant spaces', () => {
expect(normalizeClass(parseExpression('" a b c "'))).toBe('a b c');
});
it('should ignore undefined', () => {
expect(normalizeClass(parseExpression('undefined'))).toBe('');
});
it('should ignore non string literals', () => {
expect(normalizeClass(parseExpression('0'))).toBe('');
expect(normalizeClass(parseExpression('true'))).toBe('');
expect(normalizeClass(parseExpression('null'))).toBe('');
expect(normalizeClass(parseExpression('/I.D/'))).toBe('');
});
it('should not normalize identifiers', () => {
expect(normalizeClass(parseExpression('EScape'))).toBeNull();
});
it('should normalize recursively array', () => {
expect(normalizeClass(parseExpression('["from", ...["Utopia"]]'))).toBe('from Utopia');
expect(normalizeClass(parseExpression('["from", ...[Utopia]]'))).toBeNull();
});
it('should normalize recursively template literal', () => {
expect(normalizeClass(parseExpression('`name ${"shiho"} code ${33}`'))).toBe('name shiho code');
expect(normalizeClass(parseExpression('`name ${shiho.name} code ${33}`'))).toBeNull();
});
it('should normalize recursively binary expression', () => {
expect(normalizeClass(parseExpression('"mirage" + "mirror"'))).toBe('miragemirror');
expect(normalizeClass(parseExpression('"mirage" + mirror'))).toBeNull();
});
it('should normalize recursively object expression', () => {
expect(normalizeClass(parseExpression('({ a: true, b: "c" })'))).toBe('a b');
expect(normalizeClass(parseExpression('({ a: false, b: "c" })'))).toBe('b');
expect(normalizeClass(parseExpression('({ a: true, b: c })'))).toBeNull();
expect(normalizeClass(parseExpression('({ a: true, b: "c", ...({ d: true }) })'))).toBe('a b d');
expect(normalizeClass(parseExpression('({ a: true, [b]: "c" })'))).toBeNull();
expect(normalizeClass(parseExpression('({ a: true, b: false, c: !false, d: !!0 })'))).toBe('a c');
});
});
it('Composition API (standard)', () => {
const ast = parse(`
import { c as api, d as defaultStore, i as i18n, aD as notePage, bN as ImgWithBlurhash, bY as getStaticImageUrl, _ as _export_sfc } from './app-!~{001}~.js';
import { M as MkContainer } from './MkContainer-!~{03M}~.js';
import { b as defineComponent, a as ref, e as onMounted, z as resolveComponent, g as openBlock, h as createBlock, i as withCtx, K as createTextVNode, E as toDisplayString, u as unref, l as createBaseVNode, q as normalizeClass, B as createCommentVNode, k as createElementBlock, F as Fragment, C as renderList, A as createVNode } from './vue-!~{002}~.js';
import './photoswipe-!~{003}~.js';
const _hoisted_1 = /* @__PURE__ */ createBaseVNode("i", { class: "ti ti-photo" }, null, -1);
const _sfc_main = /* @__PURE__ */ defineComponent({
__name: "index.photos",
props: {
user: {}
},
setup(__props) {
const props = __props;
let fetching = ref(true);
let images = ref([]);
function thumbnail(image) {
return defaultStore.state.disableShowingAnimatedImages ? getStaticImageUrl(image.url) : image.thumbnailUrl;
}
onMounted(() => {
const image = [
"image/jpeg",
"image/webp",
"image/avif",
"image/png",
"image/gif",
"image/apng",
"image/vnd.mozilla.apng"
];
api("users/notes", {
userId: props.user.id,
fileType: image,
excludeNsfw: defaultStore.state.nsfw !== "ignore",
limit: 10
}).then((notes) => {
for (const note of notes) {
for (const file of note.files) {
images.value.push({
note,
file
});
}
}
fetching.value = false;
});
});
return (_ctx, _cache) => {
const _component_MkLoading = resolveComponent("MkLoading");
const _component_MkA = resolveComponent("MkA");
return openBlock(), createBlock(MkContainer, {
"max-height": 300,
foldable: true
}, {
icon: withCtx(() => [
_hoisted_1
]),
header: withCtx(() => [
createTextVNode(toDisplayString(unref(i18n).ts.images), 1)
]),
default: withCtx(() => [
createBaseVNode("div", {
class: normalizeClass(_ctx.$style.root)
}, [
unref(fetching) ? (openBlock(), createBlock(_component_MkLoading, { key: 0 })) : createCommentVNode("", true),
!unref(fetching) && unref(images).length > 0 ? (openBlock(), createElementBlock("div", {
key: 1,
class: normalizeClass(_ctx.$style.stream)
}, [
(openBlock(true), createElementBlock(Fragment, null, renderList(unref(images), (image) => {
return openBlock(), createBlock(_component_MkA, {
key: image.note.id + image.file.id,
class: normalizeClass(_ctx.$style.img),
to: unref(notePage)(image.note)
}, {
default: withCtx(() => [
createVNode(ImgWithBlurhash, {
hash: image.file.blurhash,
src: thumbnail(image.file),
title: image.file.name
}, null, 8, ["hash", "src", "title"])
]),
_: 2
}, 1032, ["class", "to"]);
}), 128))
], 2)) : createCommentVNode("", true),
!unref(fetching) && unref(images).length == 0 ? (openBlock(), createElementBlock("p", {
key: 2,
class: normalizeClass(_ctx.$style.empty)
}, toDisplayString(unref(i18n).ts.nothing), 3)) : createCommentVNode("", true)
], 2)
]),
_: 1
});
};
}
});
const root = "xenMW";
const stream = "xaZzf";
const img = "xtA8t";
const empty = "xhYKj";
const style0 = {
root: root,
stream: stream,
img: img,
empty: empty
};
const cssModules = {
"$style": style0
};
const index_photos = /* @__PURE__ */ _export_sfc(_sfc_main, [["__cssModules", cssModules]]);
export { index_photos as default };
`.slice(1), { ecmaVersion: 'latest', sourceType: 'module' });
unwindCssModuleClassName(ast);
expect(generate(ast)).toBe(`
import {c as api, d as defaultStore, i as i18n, aD as notePage, bN as ImgWithBlurhash, bY as getStaticImageUrl, _ as _export_sfc} from './app-!~{001}~.js';
import {M as MkContainer} from './MkContainer-!~{03M}~.js';
import {b as defineComponent, a as ref, e as onMounted, z as resolveComponent, g as openBlock, h as createBlock, i as withCtx, K as createTextVNode, E as toDisplayString, u as unref, l as createBaseVNode, q as normalizeClass, B as createCommentVNode, k as createElementBlock, F as Fragment, C as renderList, A as createVNode} from './vue-!~{002}~.js';
import './photoswipe-!~{003}~.js';
const _hoisted_1 = createBaseVNode("i", {
class: "ti ti-photo"
}, null, -1);
const _sfc_main = defineComponent({
__name: "index.photos",
props: {
user: {}
},
setup(__props) {
const props = __props;
let fetching = ref(true);
let images = ref([]);
function thumbnail(image) {
return defaultStore.state.disableShowingAnimatedImages ? getStaticImageUrl(image.url) : image.thumbnailUrl;
}
onMounted(() => {
const image = ["image/jpeg", "image/webp", "image/avif", "image/png", "image/gif", "image/apng", "image/vnd.mozilla.apng"];
api("users/notes", {
userId: props.user.id,
fileType: image,
excludeNsfw: defaultStore.state.nsfw !== "ignore",
limit: 10
}).then(notes => {
for (const note of notes) {
for (const file of note.files) {
images.value.push({
note,
file
});
}
}
fetching.value = false;
});
});
return (_ctx, _cache) => {
const _component_MkLoading = resolveComponent("MkLoading");
const _component_MkA = resolveComponent("MkA");
return (openBlock(), createBlock(MkContainer, {
"max-height": 300,
foldable: true
}, {
icon: withCtx(() => [_hoisted_1]),
header: withCtx(() => [createTextVNode(toDisplayString(unref(i18n).ts.images), 1)]),
default: withCtx(() => [createBaseVNode("div", {
class: "xenMW"
}, [unref(fetching) ? (openBlock(), createBlock(_component_MkLoading, {
key: 0
})) : createCommentVNode("", true), !unref(fetching) && unref(images).length > 0 ? (openBlock(), createElementBlock("div", {
key: 1,
class: "xaZzf"
}, [(openBlock(true), createElementBlock(Fragment, null, renderList(unref(images), image => {
return (openBlock(), createBlock(_component_MkA, {
key: image.note.id + image.file.id,
class: "xtA8t",
to: unref(notePage)(image.note)
}, {
default: withCtx(() => [createVNode(ImgWithBlurhash, {
hash: image.file.blurhash,
src: thumbnail(image.file),
title: image.file.name
}, null, 8, ["hash", "src", "title"])]),
_: 2
}, 1032, ["class", "to"]));
}), 128))], 2)) : createCommentVNode("", true), !unref(fetching) && unref(images).length == 0 ? (openBlock(), createElementBlock("p", {
key: 2,
class: "xhYKj"
}, toDisplayString(unref(i18n).ts.nothing), 3)) : createCommentVNode("", true)], 2)]),
_: 1
}));
};
}
});
const root = "xenMW";
const stream = "xaZzf";
const img = "xtA8t";
const empty = "xhYKj";
const style0 = {
root: root,
stream: stream,
img: img,
empty: empty
};
const cssModules = {
"$style": style0
};
const index_photos = _sfc_main;
export {index_photos as default};
`.slice(1));
});
it('Composition API (with `useCssModule()`)', () => {
const ast = parse(`
import { a7 as getCurrentInstance, b as defineComponent, G as useCssModule, a1 as h, H as TransitionGroup } from './!~{002}~.js';
import { d as defaultStore, aK as toast, b5 as MkAd, i as i18n, _ as _export_sfc } from './app-!~{001}~.js';
function isDebuggerEnabled(id) {
try {
return localStorage.getItem(\`DEBUG_\${id}\`) !== null;
} catch {
return false;
}
}
function stackTraceInstances() {
let instance = getCurrentInstance();
const stack = [];
while (instance) {
stack.push(instance);
instance = instance.parent;
}
return stack;
}
const _sfc_main = defineComponent({
props: {
items: {
type: Array,
required: true
},
direction: {
type: String,
required: false,
default: "down"
},
reversed: {
type: Boolean,
required: false,
default: false
},
noGap: {
type: Boolean,
required: false,
default: false
},
ad: {
type: Boolean,
required: false,
default: false
}
},
setup(props, { slots, expose }) {
const $style = useCssModule();
function getDateText(time) {
const date = new Date(time).getDate();
const month = new Date(time).getMonth() + 1;
return i18n.t("monthAndDay", {
month: month.toString(),
day: date.toString()
});
}
if (props.items.length === 0)
return;
const renderChildrenImpl = () => props.items.map((item, i) => {
if (!slots || !slots.default)
return;
const el = slots.default({
item
})[0];
if (el.key == null && item.id)
el.key = item.id;
if (i !== props.items.length - 1 && new Date(item.createdAt).getDate() !== new Date(props.items[i + 1].createdAt).getDate()) {
const separator = h("div", {
class: $style["separator"],
key: item.id + ":separator"
}, h("p", {
class: $style["date"]
}, [
h("span", {
class: $style["date-1"]
}, [
h("i", {
class: \`ti ti-chevron-up \${$style["date-1-icon"]}\`
}),
getDateText(item.createdAt)
]),
h("span", {
class: $style["date-2"]
}, [
getDateText(props.items[i + 1].createdAt),
h("i", {
class: \`ti ti-chevron-down \${$style["date-2-icon"]}\`
})
])
]));
return [el, separator];
} else {
if (props.ad && item._shouldInsertAd_) {
return [h(MkAd, {
key: item.id + ":ad",
prefer: ["horizontal", "horizontal-big"]
}), el];
} else {
return el;
}
}
});
const renderChildren = () => {
const children = renderChildrenImpl();
if (isDebuggerEnabled(6864)) {
const nodes = children.flatMap((node) => node ?? []);
const keys = new Set(nodes.map((node) => node.key));
if (keys.size !== nodes.length) {
const id = crypto.randomUUID();
const instances = stackTraceInstances();
toast(instances.reduce((a, c) => \`\${a} at \${c.type.name}\`, \`[DEBUG_6864 (\${id})]: \${nodes.length - keys.size} duplicated keys found\`));
console.warn({ id, debugId: 6864, stack: instances });
}
}
return children;
};
function onBeforeLeave(el) {
el.style.top = \`\${el.offsetTop}px\`;
el.style.left = \`\${el.offsetLeft}px\`;
}
function onLeaveCanceled(el) {
el.style.top = "";
el.style.left = "";
}
return () => h(
defaultStore.state.animation ? TransitionGroup : "div",
{
class: {
[$style["date-separated-list"]]: true,
[$style["date-separated-list-nogap"]]: props.noGap,
[$style["reversed"]]: props.reversed,
[$style["direction-down"]]: props.direction === "down",
[$style["direction-up"]]: props.direction === "up"
},
...defaultStore.state.animation ? {
name: "list",
tag: "div",
onBeforeLeave,
onLeaveCanceled
} : {}
},
{ default: renderChildren }
);
}
});
const reversed = "xxiZh";
const separator = "xxeDx";
const date = "xxawD";
const style0 = {
"date-separated-list": "xfKPa",
"date-separated-list-nogap": "xf9zr",
"direction-up": "x7AeO",
"direction-down": "xBIqc",
reversed: reversed,
separator: separator,
date: date,
"date-1": "xwtmh",
"date-1-icon": "xsNPa",
"date-2": "x1xvw",
"date-2-icon": "x9ZiG"
};
const cssModules = {
"$style": style0
};
const MkDateSeparatedList = /* @__PURE__ */ _export_sfc(_sfc_main, [["__cssModules", cssModules]]);
export { MkDateSeparatedList as M };
`.slice(1), { ecmaVersion: 'latest', sourceType: 'module' });
unwindCssModuleClassName(ast);
expect(generate(ast)).toBe(`
import {a7 as getCurrentInstance, b as defineComponent, G as useCssModule, a1 as h, H as TransitionGroup} from './!~{002}~.js';
import {d as defaultStore, aK as toast, b5 as MkAd, i as i18n, _ as _export_sfc} from './app-!~{001}~.js';
function isDebuggerEnabled(id) {
try {
return localStorage.getItem(\`DEBUG_\${id}\`) !== null;
} catch {
return false;
}
}
function stackTraceInstances() {
let instance = getCurrentInstance();
const stack = [];
while (instance) {
stack.push(instance);
instance = instance.parent;
}
return stack;
}
const _sfc_main = defineComponent({
props: {
items: {
type: Array,
required: true
},
direction: {
type: String,
required: false,
default: "down"
},
reversed: {
type: Boolean,
required: false,
default: false
},
noGap: {
type: Boolean,
required: false,
default: false
},
ad: {
type: Boolean,
required: false,
default: false
}
},
setup(props, {slots, expose}) {
const $style = useCssModule();
function getDateText(time) {
const date = new Date(time).getDate();
const month = new Date(time).getMonth() + 1;
return i18n.t("monthAndDay", {
month: month.toString(),
day: date.toString()
});
}
if (props.items.length === 0) return;
const renderChildrenImpl = () => props.items.map((item, i) => {
if (!slots || !slots.default) return;
const el = slots.default({
item
})[0];
if (el.key == null && item.id) el.key = item.id;
if (i !== props.items.length - 1 && new Date(item.createdAt).getDate() !== new Date(props.items[i + 1].createdAt).getDate()) {
const separator = h("div", {
class: $style["separator"],
key: item.id + ":separator"
}, h("p", {
class: $style["date"]
}, [h("span", {
class: $style["date-1"]
}, [h("i", {
class: \`ti ti-chevron-up \${$style["date-1-icon"]}\`
}), getDateText(item.createdAt)]), h("span", {
class: $style["date-2"]
}, [getDateText(props.items[i + 1].createdAt), h("i", {
class: \`ti ti-chevron-down \${$style["date-2-icon"]}\`
})])]));
return [el, separator];
} else {
if (props.ad && item._shouldInsertAd_) {
return [h(MkAd, {
key: item.id + ":ad",
prefer: ["horizontal", "horizontal-big"]
}), el];
} else {
return el;
}
}
});
const renderChildren = () => {
const children = renderChildrenImpl();
if (isDebuggerEnabled(6864)) {
const nodes = children.flatMap(node => node ?? []);
const keys = new Set(nodes.map(node => node.key));
if (keys.size !== nodes.length) {
const id = crypto.randomUUID();
const instances = stackTraceInstances();
toast(instances.reduce((a, c) => \`\${a} at \${c.type.name}\`, \`[DEBUG_6864 (\${id})]: \${nodes.length - keys.size} duplicated keys found\`));
console.warn({
id,
debugId: 6864,
stack: instances
});
}
}
return children;
};
function onBeforeLeave(el) {
el.style.top = \`\${el.offsetTop}px\`;
el.style.left = \`\${el.offsetLeft}px\`;
}
function onLeaveCanceled(el) {
el.style.top = "";
el.style.left = "";
}
return () => h(defaultStore.state.animation ? TransitionGroup : "div", {
class: {
[$style["date-separated-list"]]: true,
[$style["date-separated-list-nogap"]]: props.noGap,
[$style["reversed"]]: props.reversed,
[$style["direction-down"]]: props.direction === "down",
[$style["direction-up"]]: props.direction === "up"
},
...defaultStore.state.animation ? {
name: "list",
tag: "div",
onBeforeLeave,
onLeaveCanceled
} : {}
}, {
default: renderChildren
});
}
});
const reversed = "xxiZh";
const separator = "xxeDx";
const date = "xxawD";
const style0 = {
"date-separated-list": "xfKPa",
"date-separated-list-nogap": "xf9zr",
"direction-up": "x7AeO",
"direction-down": "xBIqc",
reversed: reversed,
separator: separator,
date: date,
"date-1": "xwtmh",
"date-1-icon": "xsNPa",
"date-2": "x1xvw",
"date-2-icon": "x9ZiG"
};
const cssModules = {
"$style": style0
};
const MkDateSeparatedList = _export_sfc(_sfc_main, [["__cssModules", cssModules]]);
export {MkDateSeparatedList as M};
`.slice(1));
});

View file

@ -0,0 +1,275 @@
import { generate } from 'astring';
import * as estree from 'estree';
import { walk } from '../node_modules/estree-walker/src/index.js';
import type * as estreeWalker from 'estree-walker';
import type { Plugin } from 'vite';
function isFalsyIdentifier(identifier: estree.Identifier): boolean {
return identifier.name === 'undefined' || identifier.name === 'NaN';
}
function normalizeClassWalker(tree: estree.Node): string | null {
if (tree.type === 'Identifier') return isFalsyIdentifier(tree) ? '' : null;
if (tree.type === 'Literal') return typeof tree.value === 'string' ? tree.value : '';
if (tree.type === 'BinaryExpression') {
if (tree.operator !== '+') return null;
const left = normalizeClassWalker(tree.left);
const right = normalizeClassWalker(tree.right);
if (left === null || right === null) return null;
return `${left}${right}`;
}
if (tree.type === 'TemplateLiteral') {
if (tree.expressions.some((x) => x.type !== 'Literal' && (x.type !== 'Identifier' || !isFalsyIdentifier(x)))) return null;
return tree.quasis.reduce((a, c, i) => {
const v = i === tree.quasis.length - 1 ? '' : (tree.expressions[i] as Partial<estree.Literal>).value;
return a + c.value.raw + (typeof v === 'string' ? v : '');
}, '');
}
if (tree.type === 'ArrayExpression') {
const values = tree.elements.map((treeNode) => {
if (treeNode === null) return '';
if (treeNode.type === 'SpreadElement') return normalizeClassWalker(treeNode.argument);
return normalizeClassWalker(treeNode);
});
if (values.some((x) => x === null)) return null;
return values.join(' ');
}
if (tree.type === 'ObjectExpression') {
const values = tree.properties.map((treeNode) => {
if (treeNode.type === 'SpreadElement') return normalizeClassWalker(treeNode.argument);
let x = treeNode.value;
let inveted = false;
while (x.type === 'UnaryExpression' && x.operator === '!') {
x = x.argument;
inveted = !inveted;
}
if (x.type === 'Literal') {
if (inveted === !x.value) {
return treeNode.key.type === 'Identifier' ? treeNode.computed ? null : treeNode.key.name : treeNode.key.type === 'Literal' ? treeNode.key.value : '';
} else {
return '';
}
}
if (x.type === 'Identifier') {
if (inveted !== isFalsyIdentifier(x)) {
return '';
} else {
return null;
}
}
return null;
});
if (values.some((x) => x === null)) return null;
return values.join(' ');
}
console.error(`Unexpected node type: ${tree.type}`);
return null;
}
export function normalizeClass(tree: estree.Node): string | null {
const walked = normalizeClassWalker(tree);
return walked && walked.replace(/^\s+|\s+(?=\s)|\s+$/g, '');
}
export function unwindCssModuleClassName(ast: estree.Node): void {
(walk as typeof estreeWalker.walk)(ast, {
enter(node, parent): void {
if (parent?.type !== 'Program') return;
if (node.type !== 'VariableDeclaration') return;
if (node.declarations.length !== 1) return;
if (node.declarations[0].id.type !== 'Identifier') return;
const name = node.declarations[0].id.name;
if (node.declarations[0].init?.type !== 'CallExpression') return;
if (node.declarations[0].init.callee.type !== 'Identifier') return;
if (node.declarations[0].init.callee.name !== '_export_sfc') return;
if (node.declarations[0].init.arguments.length !== 2) return;
if (node.declarations[0].init.arguments[0].type !== 'Identifier') return;
const ident = node.declarations[0].init.arguments[0].name;
if (!ident.startsWith('_sfc_main')) return;
if (node.declarations[0].init.arguments[1].type !== 'ArrayExpression') return;
if (node.declarations[0].init.arguments[1].elements.length === 0) return;
const __cssModulesIndex = node.declarations[0].init.arguments[1].elements.findIndex((x) => {
if (x?.type !== 'ArrayExpression') return false;
if (x.elements.length !== 2) return false;
if (x.elements[0]?.type !== 'Literal') return false;
if (x.elements[0].value !== '__cssModules') return false;
if (x.elements[1]?.type !== 'Identifier') return false;
return true;
});
if (!~__cssModulesIndex) return;
const cssModuleForestName = ((node.declarations[0].init.arguments[1].elements[__cssModulesIndex] as estree.ArrayExpression).elements[1] as estree.Identifier).name;
const cssModuleForestNode = parent.body.find((x) => {
if (x.type !== 'VariableDeclaration') return false;
if (x.declarations.length !== 1) return false;
if (x.declarations[0].id.type !== 'Identifier') return false;
if (x.declarations[0].id.name !== cssModuleForestName) return false;
if (x.declarations[0].init?.type !== 'ObjectExpression') return false;
return true;
}) as unknown as estree.VariableDeclaration;
const moduleForest = new Map((cssModuleForestNode.declarations[0].init as estree.ObjectExpression).properties.flatMap((property) => {
if (property.type !== 'Property') return [];
if (property.key.type !== 'Literal') return [];
if (property.value.type !== 'Identifier') return [];
return [[property.key.value as string, property.value.name as string]];
}));
const sfcMain = parent.body.find((x) => {
if (x.type !== 'VariableDeclaration') return false;
if (x.declarations.length !== 1) return false;
if (x.declarations[0].id.type !== 'Identifier') return false;
if (x.declarations[0].id.name !== ident) return false;
return true;
}) as unknown as estree.VariableDeclaration;
if (sfcMain.declarations[0].init?.type !== 'CallExpression') return;
if (sfcMain.declarations[0].init.callee.type !== 'Identifier') return;
if (sfcMain.declarations[0].init.callee.name !== 'defineComponent') return;
if (sfcMain.declarations[0].init.arguments.length !== 1) return;
if (sfcMain.declarations[0].init.arguments[0].type !== 'ObjectExpression') return;
const setup = sfcMain.declarations[0].init.arguments[0].properties.find((x) => {
if (x.type !== 'Property') return false;
if (x.key.type !== 'Identifier') return false;
if (x.key.name !== 'setup') return false;
return true;
}) as unknown as estree.Property;
if (setup.value.type !== 'FunctionExpression') return;
const render = setup.value.body.body.find((x) => {
if (x.type !== 'ReturnStatement') return false;
return true;
}) as unknown as estree.ReturnStatement;
if (render.argument?.type !== 'ArrowFunctionExpression') return;
if (render.argument.params.length !== 2) return;
const ctx = render.argument.params[0];
if (ctx.type !== 'Identifier') return;
if (ctx.name !== '_ctx') return;
if (render.argument.body.type !== 'BlockStatement') return;
for (const [key, value] of moduleForest) {
const cssModuleTreeNode = parent.body.find((x) => {
if (x.type !== 'VariableDeclaration') return false;
if (x.declarations.length !== 1) return false;
if (x.declarations[0].id.type !== 'Identifier') return false;
if (x.declarations[0].id.name !== value) return false;
return true;
}) as unknown as estree.VariableDeclaration;
if (cssModuleTreeNode.declarations[0].init?.type !== 'ObjectExpression') return;
const moduleTree = new Map(cssModuleTreeNode.declarations[0].init.properties.flatMap((property) => {
if (property.type !== 'Property') return [];
const actualKey = property.key.type === 'Identifier' ? property.key.name : property.key.type === 'Literal' ? property.key.value : null;
if (typeof actualKey !== 'string') return [];
if (property.value.type === 'Literal') return [[actualKey, property.value.value as string]];
if (property.value.type !== 'Identifier') return [];
const labelledValue = property.value.name;
const actualValue = parent.body.find((x) => {
if (x.type !== 'VariableDeclaration') return false;
if (x.declarations.length !== 1) return false;
if (x.declarations[0].id.type !== 'Identifier') return false;
if (x.declarations[0].id.name !== labelledValue) return false;
return true;
}) as unknown as estree.VariableDeclaration;
if (actualValue.declarations[0].init?.type !== 'Literal') return [];
return [[actualKey, actualValue.declarations[0].init.value as string]];
}));
(walk as typeof estreeWalker.walk)(render.argument.body, {
enter(childNode) {
if (childNode.type !== 'MemberExpression') return;
if (childNode.object.type !== 'MemberExpression') return;
if (childNode.object.object.type !== 'Identifier') return;
if (childNode.object.object.name !== ctx.name) return;
if (childNode.object.property.type !== 'Identifier') return;
if (childNode.object.property.name !== key) return;
if (childNode.property.type !== 'Identifier') return;
const actualValue = moduleTree.get(childNode.property.name);
if (actualValue === undefined) return;
this.replace({
type: 'Literal',
value: actualValue,
});
},
});
(walk as typeof estreeWalker.walk)(render.argument.body, {
enter(childNode) {
if (childNode.type !== 'MemberExpression') return;
if (childNode.object.type !== 'MemberExpression') return;
if (childNode.object.object.type !== 'Identifier') return;
if (childNode.object.object.name !== ctx.name) return;
if (childNode.object.property.type !== 'Identifier') return;
if (childNode.object.property.name !== key) return;
if (childNode.property.type !== 'Identifier') return;
console.error(`Undefined style detected: ${key}.${childNode.property.name} (in ${name})`);
this.replace({
type: 'Identifier',
name: 'undefined',
});
},
});
(walk as typeof estreeWalker.walk)(render.argument.body, {
enter(childNode) {
if (childNode.type !== 'CallExpression') return;
if (childNode.callee.type !== 'Identifier') return;
if (childNode.callee.name !== 'normalizeClass') return;
if (childNode.arguments.length !== 1) return;
const normalized = normalizeClass(childNode.arguments[0]);
if (normalized === null) return;
this.replace({
type: 'Literal',
value: normalized,
});
},
});
}
if (node.declarations[0].init.arguments[1].elements.length === 1) {
this.replace({
type: 'VariableDeclaration',
declarations: [{
type: 'VariableDeclarator',
id: {
type: 'Identifier',
name: node.declarations[0].id.name,
},
init: {
type: 'Identifier',
name: ident,
},
}],
kind: 'const',
});
} else {
this.replace({
type: 'VariableDeclaration',
declarations: [{
type: 'VariableDeclarator',
id: {
type: 'Identifier',
name: node.declarations[0].id.name,
},
init: {
type: 'CallExpression',
callee: {
type: 'Identifier',
name: '_export_sfc',
},
arguments: [{
type: 'Identifier',
name: ident,
}, {
type: 'ArrayExpression',
elements: node.declarations[0].init.arguments[1].elements.slice(0, __cssModulesIndex).concat(node.declarations[0].init.arguments[1].elements.slice(__cssModulesIndex + 1)),
}],
},
}],
kind: 'const',
});
}
},
});
}
// eslint-disable-next-line import/no-default-export
export default function pluginUnwindCssModuleClassName(): Plugin {
return {
name: 'UnwindCssModuleClassName',
renderChunk(code): { code: string } {
const ast = this.parse(code) as unknown as estree.Node;
unwindCssModuleClassName(ast);
return { code: generate(ast) };
},
};
}

View file

@ -20,12 +20,13 @@
"@rollup/plugin-replace": "5.0.2", "@rollup/plugin-replace": "5.0.2",
"@rollup/pluginutils": "5.0.2", "@rollup/pluginutils": "5.0.2",
"@syuilo/aiscript": "0.13.3", "@syuilo/aiscript": "0.13.3",
"@tabler/icons-webfont": "2.17.0", "@tabler/icons-webfont": "2.21.0",
"@vitejs/plugin-vue": "4.2.3", "@vitejs/plugin-vue": "4.2.3",
"@vue-macros/reactivity-transform": "0.3.8", "@vue-macros/reactivity-transform": "0.3.9",
"@vue/compiler-sfc": "3.3.4", "@vue/compiler-sfc": "3.3.4",
"astring": "1.8.6",
"autosize": "6.0.1", "autosize": "6.0.1",
"broadcast-channel": "4.20.2", "broadcast-channel": "5.1.0",
"browser-image-resizer": "github:misskey-dev/browser-image-resizer#v2.2.1-misskey.3", "browser-image-resizer": "github:misskey-dev/browser-image-resizer#v2.2.1-misskey.3",
"buraha": "github:misskey-dev/buraha", "buraha": "github:misskey-dev/buraha",
"canvas-confetti": "1.6.0", "canvas-confetti": "1.6.0",
@ -34,11 +35,12 @@
"chartjs-chart-matrix": "2.0.1", "chartjs-chart-matrix": "2.0.1",
"chartjs-plugin-gradient": "0.6.1", "chartjs-plugin-gradient": "0.6.1",
"chartjs-plugin-zoom": "2.0.1", "chartjs-plugin-zoom": "2.0.1",
"chromatic": "6.17.4", "chromatic": "6.18.0",
"compare-versions": "5.0.3", "compare-versions": "5.0.3",
"cropperjs": "2.0.0-beta.2", "cropperjs": "2.0.0-beta.2",
"date-fns": "2.30.0", "date-fns": "2.30.0",
"escape-regexp": "0.0.1", "escape-regexp": "0.0.1",
"estree-walker": "^3.0.3",
"eventemitter3": "5.0.1", "eventemitter3": "5.0.1",
"gsap": "3.11.5", "gsap": "3.11.5",
"idb-keyval": "6.2.1", "idb-keyval": "6.2.1",
@ -61,18 +63,17 @@
"strict-event-emitter-types": "2.0.0", "strict-event-emitter-types": "2.0.0",
"syuilo-password-strength": "0.0.1", "syuilo-password-strength": "0.0.1",
"textarea-caret": "3.1.0", "textarea-caret": "3.1.0",
"three": "0.151.3", "three": "0.153.0",
"throttle-debounce": "5.0.0", "throttle-debounce": "5.0.0",
"tinycolor2": "1.6.0", "tinycolor2": "1.6.0",
"tsc-alias": "1.8.6", "tsc-alias": "1.8.6",
"tsconfig-paths": "4.2.0", "tsconfig-paths": "4.2.0",
"twemoji-parser": "14.0.0", "twemoji-parser": "14.0.0",
"typescript": "5.0.4", "typescript": "5.1.3",
"uuid": "9.0.0", "uuid": "9.0.0",
"vanilla-tilt": "1.8.0", "vanilla-tilt": "1.8.0",
"vite": "4.3.9", "vite": "4.3.9",
"vue": "3.3.4", "vue": "3.3.4",
"vue-plyr": "7.0.0",
"vue-prism-editor": "2.0.0-alpha.2", "vue-prism-editor": "2.0.0-alpha.2",
"vuedraggable": "next" "vuedraggable": "next"
}, },
@ -113,19 +114,19 @@
"@types/uuid": "9.0.1", "@types/uuid": "9.0.1",
"@types/websocket": "1.0.5", "@types/websocket": "1.0.5",
"@types/ws": "8.5.4", "@types/ws": "8.5.4",
"@typescript-eslint/eslint-plugin": "5.59.5", "@typescript-eslint/eslint-plugin": "5.59.8",
"@typescript-eslint/parser": "5.59.5", "@typescript-eslint/parser": "5.59.8",
"@vitest/coverage-c8": "0.31.1", "@vitest/coverage-c8": "0.31.4",
"@vue/runtime-core": "3.3.4", "@vue/runtime-core": "3.3.4",
"astring": "1.8.5", "acorn": "^8.8.2",
"chokidar-cli": "3.0.0", "chokidar-cli": "3.0.0",
"cross-env": "7.0.3", "cross-env": "7.0.3",
"cypress": "12.13.0", "cypress": "12.13.0",
"eslint": "8.40.0", "eslint": "8.41.0",
"eslint-plugin-import": "2.27.5", "eslint-plugin-import": "2.27.5",
"eslint-plugin-vue": "9.14.1", "eslint-plugin-vue": "9.14.1",
"fast-glob": "3.2.12", "fast-glob": "3.2.12",
"happy-dom": "9.19.2", "happy-dom": "9.20.3",
"micromatch": "3.1.10", "micromatch": "3.1.10",
"msw": "1.2.1", "msw": "1.2.1",
"msw-storybook-addon": "1.8.0", "msw-storybook-addon": "1.8.0",
@ -137,7 +138,7 @@
"storybook-addon-misskey-theme": "github:misskey-dev/storybook-addon-misskey-theme", "storybook-addon-misskey-theme": "github:misskey-dev/storybook-addon-misskey-theme",
"summaly": "github:misskey-dev/summaly", "summaly": "github:misskey-dev/summaly",
"vite-plugin-turbosnap": "1.0.2", "vite-plugin-turbosnap": "1.0.2",
"vitest": "0.31.1", "vitest": "0.31.4",
"vitest-fetch-mock": "0.2.2", "vitest-fetch-mock": "0.2.2",
"vue-eslint-parser": "9.3.0", "vue-eslint-parser": "9.3.0",
"vue-tsc": "1.6.5" "vue-tsc": "1.6.5"

View file

@ -1,6 +1,5 @@
import { computed, createApp, watch, markRaw, version as vueVersion, defineAsyncComponent, App } from 'vue'; import { computed, createApp, watch, markRaw, version as vueVersion, defineAsyncComponent, App } from 'vue';
import { compareVersions } from 'compare-versions'; import { compareVersions } from 'compare-versions';
import JSON5 from 'json5';
import widgets from '@/widgets'; import widgets from '@/widgets';
import directives from '@/directives'; import directives from '@/directives';
import components from '@/components'; import components from '@/components';
@ -180,8 +179,8 @@ export async function common(createVue: () => App<Element>) {
fetchInstanceMetaPromise.then(() => { fetchInstanceMetaPromise.then(() => {
if (defaultStore.state.themeInitial) { if (defaultStore.state.themeInitial) {
if (instance.defaultLightTheme != null) ColdDeviceStorage.set('lightTheme', JSON5.parse(instance.defaultLightTheme)); if (instance.defaultLightTheme != null) ColdDeviceStorage.set('lightTheme', JSON.parse(instance.defaultLightTheme));
if (instance.defaultDarkTheme != null) ColdDeviceStorage.set('darkTheme', JSON5.parse(instance.defaultDarkTheme)); if (instance.defaultDarkTheme != null) ColdDeviceStorage.set('darkTheme', JSON.parse(instance.defaultDarkTheme));
defaultStore.set('themeInitial', false); defaultStore.set('themeInitial', false);
} }
}); });

View file

@ -3,7 +3,14 @@
<div v-if="achievements" :class="$style.root"> <div v-if="achievements" :class="$style.root">
<div v-for="achievement in achievements" :key="achievement" :class="$style.achievement" class="_panel"> <div v-for="achievement in achievements" :key="achievement" :class="$style.achievement" class="_panel">
<div :class="$style.icon"> <div :class="$style.icon">
<div :class="[$style.iconFrame, $style['iconFrame_' + ACHIEVEMENT_BADGES[achievement.name].frame]]"> <div
:class="[$style.iconFrame, {
[$style.iconFrame_bronze]: ACHIEVEMENT_BADGES[achievement.name].frame === 'bronze',
[$style.iconFrame_silver]: ACHIEVEMENT_BADGES[achievement.name].frame === 'silver',
[$style.iconFrame_gold]: ACHIEVEMENT_BADGES[achievement.name].frame === 'gold',
[$style.iconFrame_platinum]: ACHIEVEMENT_BADGES[achievement.name].frame === 'platinum',
}]"
>
<div :class="[$style.iconInner]" :style="{ background: ACHIEVEMENT_BADGES[achievement.name].bg }"> <div :class="[$style.iconInner]" :style="{ background: ACHIEVEMENT_BADGES[achievement.name].bg }">
<img :class="$style.iconImg" :src="ACHIEVEMENT_BADGES[achievement.name].img"> <img :class="$style.iconImg" :src="ACHIEVEMENT_BADGES[achievement.name].img">
</div> </div>

View file

@ -10,7 +10,7 @@
</li> </li>
<li tabindex="-1" :class="$style.item" @click="chooseUser()" @keydown="onKeydown">{{ i18n.ts.selectUser }}</li> <li tabindex="-1" :class="$style.item" @click="chooseUser()" @keydown="onKeydown">{{ i18n.ts.selectUser }}</li>
</ol> </ol>
<ol v-else-if="hashtags.length > 0" ref="suggests" :class="[$style.list, $style.hashtags]"> <ol v-else-if="hashtags.length > 0" ref="suggests" :class="$style.list">
<li v-for="hashtag in hashtags" tabindex="-1" :class="$style.item" @click="complete(type, hashtag)" @keydown="onKeydown"> <li v-for="hashtag in hashtags" tabindex="-1" :class="$style.item" @click="complete(type, hashtag)" @keydown="onKeydown">
<span class="name">{{ hashtag }}</span> <span class="name">{{ hashtag }}</span>
</li> </li>
@ -42,7 +42,7 @@ import { acct } from '@/filters/user';
import * as os from '@/os'; import * as os from '@/os';
import { MFM_TAGS } from '@/scripts/mfm-tags'; import { MFM_TAGS } from '@/scripts/mfm-tags';
import { defaultStore } from '@/store'; import { defaultStore } from '@/store';
import { emojilist } from '@/scripts/emojilist'; import { emojilist, getEmojiName } from '@/scripts/emojilist';
import { i18n } from '@/i18n'; import { i18n } from '@/i18n';
import { miLocalStorage } from '@/local-storage'; import { miLocalStorage } from '@/local-storage';
import { customEmojis } from '@/custom-emojis'; import { customEmojis } from '@/custom-emojis';
@ -71,14 +71,14 @@ const emojiDb = computed(() => {
url: char2path(x.char), url: char2path(x.char),
})); }));
for (const x of lib) { for (const index of Object.values(defaultStore.state.additionalUnicodeEmojiIndexes)) {
if (x.keywords) { for (const [emoji, keywords] of Object.entries(index)) {
for (const k of x.keywords) { for (const k of keywords) {
unicodeEmojiDB.push({ unicodeEmojiDB.push({
emoji: x.char, emoji: emoji,
name: k, name: k,
aliasOf: x.name, aliasOf: getEmojiName(emoji)!,
url: char2path(x.char), url: char2path(emoji),
}); });
} }
} }

View file

@ -7,7 +7,7 @@
@click="emit('click', $event)" @click="emit('click', $event)"
@mousedown="onMousedown" @mousedown="onMousedown"
> >
<div ref="ripples" :class="$style.ripples"></div> <div ref="ripples" :class="$style.ripples" :data-children-class="$style.ripple"></div>
<div :class="$style.content"> <div :class="$style.content">
<slot></slot> <slot></slot>
</div> </div>
@ -18,7 +18,7 @@
:to="to" :to="to"
@mousedown="onMousedown" @mousedown="onMousedown"
> >
<div ref="ripples" :class="$style.ripples"></div> <div ref="ripples" :class="$style.ripples" :data-children-class="$style.ripple"></div>
<div :class="$style.content"> <div :class="$style.content">
<slot></slot> <slot></slot>
</div> </div>
@ -26,9 +26,7 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { nextTick, onMounted, useCssModule } from 'vue'; import { nextTick, onMounted } from 'vue';
const $style = useCssModule();
const props = defineProps<{ const props = defineProps<{
type?: 'button' | 'submit' | 'reset'; type?: 'button' | 'submit' | 'reset';
@ -81,7 +79,7 @@ function onMousedown(evt: MouseEvent): void {
const rect = target.getBoundingClientRect(); const rect = target.getBoundingClientRect();
const ripple = document.createElement('div'); const ripple = document.createElement('div');
ripple.classList.add($style.ripple); ripple.classList.add(ripples!.dataset.childrenClass!);
ripple.style.top = (evt.clientY - rect.top - 1).toString() + 'px'; ripple.style.top = (evt.clientY - rect.top - 1).toString() + 'px';
ripple.style.left = (evt.clientX - rect.left - 1).toString() + 'px'; ripple.style.left = (evt.clientX - rect.left - 1).toString() + 'px';

View file

@ -1,20 +1,20 @@
<template> <template>
<button <button
class="hdcaacmi _button" class="_button"
:class="{ wait, active: isFollowing, full }" :class="[$style.root, { [$style.wait]: wait, [$style.active]: isFollowing, [$style.full]: full }]"
:disabled="wait" :disabled="wait"
@click="onClick" @click="onClick"
> >
<template v-if="!wait"> <template v-if="!wait">
<template v-if="isFollowing"> <template v-if="isFollowing">
<span v-if="full">{{ i18n.ts.unfollow }}</span><i class="ti ti-minus"></i> <span v-if="full" :class="$style.text">{{ i18n.ts.unfollow }}</span><i class="ti ti-minus"></i>
</template> </template>
<template v-else> <template v-else>
<span v-if="full">{{ i18n.ts.follow }}</span><i class="ti ti-plus"></i> <span v-if="full" :class="$style.text">{{ i18n.ts.follow }}</span><i class="ti ti-plus"></i>
</template> </template>
</template> </template>
<template v-else> <template v-else>
<span v-if="full">{{ i18n.ts.processing }}</span><MkLoading :em="true"/> <span v-if="full" :class="$style.text">{{ i18n.ts.processing }}</span><MkLoading :em="true"/>
</template> </template>
</button> </button>
</template> </template>
@ -57,8 +57,8 @@ async function onClick() {
} }
</script> </script>
<style lang="scss" scoped> <style lang="scss" module>
.hdcaacmi { .root {
position: relative; position: relative;
display: inline-block; display: inline-block;
font-weight: bold; font-weight: bold;
@ -103,7 +103,7 @@ async function onClick() {
} }
&.active { &.active {
color: #fff; color: var(--fgOnAccent);
background: var(--accent); background: var(--accent);
&:hover { &:hover {
@ -121,9 +121,9 @@ async function onClick() {
cursor: wait !important; cursor: wait !important;
opacity: 0.7; opacity: 0.7;
} }
}
> span { .text {
margin-right: 6px; margin-right: 6px;
} }
}
</style> </style>

View file

@ -3,7 +3,7 @@
<div v-if="game.ready" :class="$style.game"> <div v-if="game.ready" :class="$style.game">
<div :class="$style.cps" class="">{{ number(cps) }}cps</div> <div :class="$style.cps" class="">{{ number(cps) }}cps</div>
<div :class="$style.count" class=""><i class="ti ti-cookie" style="font-size: 70%;"></i> {{ number(cookies) }}</div> <div :class="$style.count" class=""><i class="ti ti-cookie" style="font-size: 70%;"></i> {{ number(cookies) }}</div>
<button v-click-anime class="_button" :class="$style.button" @click="onClick"> <button v-click-anime class="_button" @click="onClick">
<img src="/client-assets/cookie.png" :class="$style.img"> <img src="/client-assets/cookie.png" :class="$style.img">
</button> </button>
</div> </div>
@ -84,10 +84,6 @@ onUnmounted(() => {
margin-bottom: 6px; margin-bottom: 6px;
} }
.button {
}
.img { .img {
max-width: 90px; max-width: 90px;
} }

View file

@ -1,5 +1,5 @@
<template> <template>
<div ref="rootEl" class="_panel" :class="[$style.root, { [$style.naked]: naked, [$style.thin]: thin, [$style.hideHeader]: !showHeader, [$style.scrollable]: scrollable, [$style.closed]: !showBody }]"> <div ref="rootEl" class="_panel" :class="[$style.root, { [$style.naked]: naked, [$style.thin]: thin, [$style.scrollable]: scrollable }]">
<header v-if="showHeader" ref="headerEl" :class="$style.header"> <header v-if="showHeader" ref="headerEl" :class="$style.header">
<div :class="$style.title"> <div :class="$style.title">
<span :class="$style.titleIcon"><slot name="icon"></slot></span> <span :class="$style.titleIcon"><slot name="icon"></slot></span>
@ -34,7 +34,7 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { onMounted, ref, shallowRef, watch } from 'vue'; import { onMounted, onUnmounted, ref, shallowRef, watch } from 'vue';
import { defaultStore } from '@/store'; import { defaultStore } from '@/store';
import { i18n } from '@/i18n'; import { i18n } from '@/i18n';
@ -83,13 +83,19 @@ function afterLeave(el) {
const calcOmit = () => { const calcOmit = () => {
if (omitted.value || ignoreOmit.value || props.maxHeight == null) return; if (omitted.value || ignoreOmit.value || props.maxHeight == null) return;
if (!contentEl.value) return;
const height = contentEl.value.offsetHeight; const height = contentEl.value.offsetHeight;
omitted.value = height > props.maxHeight; omitted.value = height > props.maxHeight;
}; };
const omitObserver = new ResizeObserver((entries, observer) => {
calcOmit();
});
onMounted(() => { onMounted(() => {
watch(showBody, v => { watch(showBody, v => {
const headerHeight = props.showHeader ? headerEl.value.offsetHeight : 0; if (!rootEl.value) return;
const headerHeight = props.showHeader ? headerEl.value?.offsetHeight ?? 0 : 0;
rootEl.value.style.minHeight = `${headerHeight}px`; rootEl.value.style.minHeight = `${headerHeight}px`;
if (v) { if (v) {
rootEl.value.style.flexBasis = 'auto'; rootEl.value.style.flexBasis = 'auto';
@ -100,13 +106,15 @@ onMounted(() => {
immediate: true, immediate: true,
}); });
rootEl.value.style.setProperty('--maxHeight', props.maxHeight + 'px'); if (rootEl.value) rootEl.value.style.setProperty('--maxHeight', props.maxHeight + 'px');
calcOmit(); calcOmit();
new ResizeObserver((entries, observer) => { if (contentEl.value) omitObserver.observe(contentEl.value);
calcOmit(); });
}).observe(contentEl.value);
onUnmounted(() => {
omitObserver.disconnect();
}); });
</script> </script>

View file

@ -36,7 +36,7 @@ export default defineComponent({
}, },
setup(props, { slots, expose }) { setup(props, { slots, expose }) {
const $style = useCssModule(); const $style = useCssModule(); // 使
function getDateText(time: string) { function getDateText(time: string) {
const date = new Date(time).getDate(); const date = new Date(time).getDate();
const month = new Date(time).getMonth() + 1; const month = new Date(time).getMonth() + 1;

View file

@ -4,7 +4,15 @@
<div v-if="icon" :class="$style.icon"> <div v-if="icon" :class="$style.icon">
<i :class="icon"></i> <i :class="icon"></i>
</div> </div>
<div v-else-if="!input && !select" :class="[$style.icon, $style['type_' + type]]"> <div
v-else-if="!input && !select"
:class="[$style.icon, {
[$style.type_success]: type === 'success',
[$style.type_error]: type === 'error',
[$style.type_warning]: type === 'warning',
[$style.type_info]: type === 'info',
}]"
>
<i v-if="type === 'success'" :class="$style.iconInner" class="ti ti-check"></i> <i v-if="type === 'success'" :class="$style.iconInner" class="ti ti-check"></i>
<i v-else-if="type === 'error'" :class="$style.iconInner" class="ti ti-circle-x"></i> <i v-else-if="type === 'error'" :class="$style.iconInner" class="ti ti-circle-x"></i>
<i v-else-if="type === 'warning'" :class="$style.iconInner" class="ti ti-alert-triangle"></i> <i v-else-if="type === 'warning'" :class="$style.iconInner" class="ti ti-alert-triangle"></i>

View file

@ -1,7 +1,8 @@
<template> <template>
<div class="omfetrab" :class="['s' + size, 'w' + width, 'h' + height, { asDrawer, asWindow }]" :style="{ maxHeight: maxHeight ? maxHeight + 'px' : undefined }"> <div class="omfetrab" :class="['s' + size, 'w' + width, 'h' + height, { asDrawer, asWindow }]" :style="{ maxHeight: maxHeight ? maxHeight + 'px' : undefined }">
<input ref="searchEl" :value="q" class="search" data-prevent-emoji-insert :class="{ filled: q != null && q != '' }" :placeholder="i18n.ts.search" type="search" @input="input()" @paste.stop="paste" @keydown.stop.prevent.enter="onEnter"> <input ref="searchEl" :value="q" class="search" data-prevent-emoji-insert :class="{ filled: q != null && q != '' }" :placeholder="i18n.ts.search" type="search" @input="input()" @paste.stop="paste" @keydown.stop.prevent.enter="onEnter">
<div ref="emojisEl" class="emojis"> <!-- FirefoxのTabフォーカスが想定外の挙動となるためtabindex="-1"を追加 https://github.com/misskey-dev/misskey/issues/10744 -->
<div ref="emojisEl" class="emojis" tabindex="-1">
<section class="result"> <section class="result">
<div v-if="searchResultCustom.length > 0" class="body"> <div v-if="searchResultCustom.length > 0" class="body">
<button <button
@ -101,7 +102,7 @@ import { isTouchUsing } from '@/scripts/touch';
import { deviceKind } from '@/scripts/device-kind'; import { deviceKind } from '@/scripts/device-kind';
import { i18n } from '@/i18n'; import { i18n } from '@/i18n';
import { defaultStore } from '@/store'; import { defaultStore } from '@/store';
import { customEmojiCategories, customEmojis } from '@/custom-emojis'; import { customEmojiCategories, customEmojis, customEmojisMap } from '@/custom-emojis';
import { $i } from '@/account'; import { $i } from '@/account';
const props = withDefaults(defineProps<{ const props = withDefaults(defineProps<{
@ -223,7 +224,6 @@ watch(q, () => {
if (newQ.includes(' ')) { // AND if (newQ.includes(' ')) { // AND
const keywords = newQ.split(' '); const keywords = newQ.split(' ');
//
for (const emoji of emojis) { for (const emoji of emojis) {
if (keywords.every(keyword => emoji.name.includes(keyword))) { if (keywords.every(keyword => emoji.name.includes(keyword))) {
matches.add(emoji); matches.add(emoji);
@ -232,13 +232,14 @@ watch(q, () => {
} }
if (matches.size >= max) return matches; if (matches.size >= max) return matches;
// for (const index of Object.values(defaultStore.state.additionalUnicodeEmojiIndexes)) {
for (const emoji of emojis) { for (const emoji of emojis) {
if (keywords.every(keyword => emoji.name.includes(keyword) || emoji.keywords.some(alias => alias.includes(keyword)))) { if (keywords.every(keyword => index[emoji.char].some(k => k.includes(keyword)))) {
matches.add(emoji); matches.add(emoji);
if (matches.size >= max) break; if (matches.size >= max) break;
} }
} }
}
} else { } else {
for (const emoji of emojis) { for (const emoji of emojis) {
if (emoji.name.startsWith(newQ)) { if (emoji.name.startsWith(newQ)) {
@ -248,13 +249,14 @@ watch(q, () => {
} }
if (matches.size >= max) return matches; if (matches.size >= max) return matches;
for (const index of Object.values(defaultStore.state.additionalUnicodeEmojiIndexes)) {
for (const emoji of emojis) { for (const emoji of emojis) {
if (emoji.keywords.some(keyword => keyword.startsWith(newQ))) { if (index[emoji.char].some(k => k.startsWith(newQ))) {
matches.add(emoji); matches.add(emoji);
if (matches.size >= max) break; if (matches.size >= max) break;
} }
} }
if (matches.size >= max) return matches; }
for (const emoji of emojis) { for (const emoji of emojis) {
if (emoji.name.includes(newQ)) { if (emoji.name.includes(newQ)) {
@ -264,13 +266,15 @@ watch(q, () => {
} }
if (matches.size >= max) return matches; if (matches.size >= max) return matches;
for (const index of Object.values(defaultStore.state.additionalUnicodeEmojiIndexes)) {
for (const emoji of emojis) { for (const emoji of emojis) {
if (emoji.keywords.some(keyword => keyword.includes(newQ))) { if (index[emoji.char].some(k => k.includes(newQ))) {
matches.add(emoji); matches.add(emoji);
if (matches.size >= max) break; if (matches.size >= max) break;
} }
} }
} }
}
return matches; return matches;
}; };
@ -352,7 +356,7 @@ function done(query?: string): boolean | void {
if (query == null || typeof query !== 'string') return; if (query == null || typeof query !== 'string') return;
const q2 = query.replace(/:/g, ''); const q2 = query.replace(/:/g, '');
const exactMatchCustom = customEmojis.value.find(emoji => emoji.name === q2); const exactMatchCustom = customEmojisMap.get(q2);
if (exactMatchCustom) { if (exactMatchCustom) {
chosen(exactMatchCustom); chosen(exactMatchCustom);
return true; return true;

View file

@ -5,7 +5,7 @@
<div :class="[$style.header, { [$style.opened]: opened }]" class="_button" role="button" data-cy-folder-header @click="toggle"> <div :class="[$style.header, { [$style.opened]: opened }]" class="_button" role="button" data-cy-folder-header @click="toggle">
<div :class="$style.headerIcon"><slot name="icon"></slot></div> <div :class="$style.headerIcon"><slot name="icon"></slot></div>
<div :class="$style.headerText"> <div :class="$style.headerText">
<div :class="$style.headerTextMain"> <div>
<MkCondensedLine :minScale="2 / 3"><slot name="label"></slot></MkCondensedLine> <MkCondensedLine :minScale="2 / 3"><slot name="label"></slot></MkCondensedLine>
</div> </div>
<div :class="$style.headerTextSub"> <div :class="$style.headerTextSub">
@ -185,10 +185,6 @@ onMounted(() => {
padding-right: 12px; padding-right: 12px;
} }
.headerTextMain {
}
.headerTextSub { .headerTextSub {
color: var(--fgTransparentWeak); color: var(--fgTransparentWeak);
font-size: .85em; font-size: .85em;

View file

@ -1,30 +1,30 @@
<template> <template>
<button <button
class="kpoogebi _button" class="_button"
:class="{ wait, active: isFollowing || hasPendingFollowRequestFromYou, full, large }" :class="[$style.root, { [$style.wait]: wait, [$style.active]: isFollowing || hasPendingFollowRequestFromYou, [$style.full]: full, [$style.large]: large }]"
:disabled="wait" :disabled="wait"
@click="onClick" @click="onClick"
> >
<template v-if="!wait"> <template v-if="!wait">
<template v-if="hasPendingFollowRequestFromYou && user.isLocked"> <template v-if="hasPendingFollowRequestFromYou && user.isLocked">
<span v-if="full">{{ i18n.ts.followRequestPending }}</span><i class="ti ti-hourglass-empty"></i> <span v-if="full" :class="$style.text">{{ i18n.ts.followRequestPending }}</span><i class="ti ti-hourglass-empty"></i>
</template> </template>
<template v-else-if="hasPendingFollowRequestFromYou && !user.isLocked"> <template v-else-if="hasPendingFollowRequestFromYou && !user.isLocked">
<!-- つまりリモートフォローの場合 --> <!-- つまりリモートフォローの場合 -->
<span v-if="full">{{ i18n.ts.processing }}</span><MkLoading :em="true" :colored="false"/> <span v-if="full" :class="$style.text">{{ i18n.ts.processing }}</span><MkLoading :em="true" :colored="false"/>
</template> </template>
<template v-else-if="isFollowing"> <template v-else-if="isFollowing">
<span v-if="full">{{ i18n.ts.unfollow }}</span><i class="ti ti-minus"></i> <span v-if="full" :class="$style.text">{{ i18n.ts.unfollow }}</span><i class="ti ti-minus"></i>
</template> </template>
<template v-else-if="!isFollowing && user.isLocked"> <template v-else-if="!isFollowing && user.isLocked">
<span v-if="full">{{ i18n.ts.followRequest }}</span><i class="ti ti-plus"></i> <span v-if="full" :class="$style.text">{{ i18n.ts.followRequest }}</span><i class="ti ti-plus"></i>
</template> </template>
<template v-else-if="!isFollowing && !user.isLocked"> <template v-else-if="!isFollowing && !user.isLocked">
<span v-if="full">{{ i18n.ts.follow }}</span><i class="ti ti-plus"></i> <span v-if="full" :class="$style.text">{{ i18n.ts.follow }}</span><i class="ti ti-plus"></i>
</template> </template>
</template> </template>
<template v-else> <template v-else>
<span v-if="full">{{ i18n.ts.processing }}</span><MkLoading :em="true" :colored="false"/> <span v-if="full" :class="$style.text">{{ i18n.ts.processing }}</span><MkLoading :em="true" :colored="false"/>
</template> </template>
</button> </button>
</template> </template>
@ -126,13 +126,12 @@ onBeforeUnmount(() => {
}); });
</script> </script>
<style lang="scss" scoped> <style lang="scss" module>
.kpoogebi { .root {
position: relative; position: relative;
display: inline-block; display: inline-block;
font-weight: bold; font-weight: bold;
color: var(--accent); color: var(--fgOnWhite);
background: transparent;
border: solid 1px var(--accent); border: solid 1px var(--accent);
padding: 0; padding: 0;
height: 31px; height: 31px;
@ -196,9 +195,9 @@ onBeforeUnmount(() => {
cursor: wait !important; cursor: wait !important;
opacity: 0.7; opacity: 0.7;
} }
}
> span { .text {
margin-right: 6px; margin-right: 6px;
} }
}
</style> </style>

View file

@ -1,9 +1,9 @@
<template> <template>
<div ref="root" :class="[$style.root, { [$style.cover]: cover }]" :title="title ?? ''"> <div ref="root" :class="['chromatic-ignore', $style.root, { [$style.cover]: cover }]" :title="title ?? ''">
<TransitionGroup <TransitionGroup
:duration="defaultStore.state.animation && props.transition?.duration || undefined" :duration="defaultStore.state.animation && props.transition?.duration || undefined"
:enterActiveClass="defaultStore.state.animation && props.transition?.enterActiveClass || undefined" :enterActiveClass="defaultStore.state.animation && props.transition?.enterActiveClass || undefined"
:leaveActiveClass="defaultStore.state.animation && (props.transition?.leaveActiveClass ?? $style['transition_leaveActive']) || undefined" :leaveActiveClass="defaultStore.state.animation && (props.transition?.leaveActiveClass ?? $style.transition_leaveActive) || undefined"
:enterFromClass="defaultStore.state.animation && props.transition?.enterFromClass || undefined" :enterFromClass="defaultStore.state.animation && props.transition?.enterFromClass || undefined"
:leaveToClass="defaultStore.state.animation && props.transition?.leaveToClass || undefined" :leaveToClass="defaultStore.state.animation && props.transition?.leaveToClass || undefined"
:enterToClass="defaultStore.state.animation && props.transition?.enterToClass || undefined" :enterToClass="defaultStore.state.animation && props.transition?.enterToClass || undefined"
@ -23,6 +23,11 @@ import { WorkerMultiDispatch } from '@/scripts/worker-multi-dispatch';
import { extractAvgColorFromBlurhash } from '@/scripts/extract-avg-color-from-blurhash'; import { extractAvgColorFromBlurhash } from '@/scripts/extract-avg-color-from-blurhash';
const workerPromise = new Promise<WorkerMultiDispatch | null>(resolve => { const workerPromise = new Promise<WorkerMultiDispatch | null>(resolve => {
// Web Worker
if (import.meta.env.MODE === 'test') {
resolve(null);
return;
}
const testWorker = new TestWebGL2(); const testWorker = new TestWebGL2();
testWorker.addEventListener('message', event => { testWorker.addEventListener('message', event => {
if (event.data.result) { if (event.data.result) {
@ -42,11 +47,10 @@ const workerPromise = new Promise<WorkerMultiDispatch | null>(resolve => {
</script> </script>
<script lang="ts" setup> <script lang="ts" setup>
import { computed, nextTick, onMounted, onUnmounted, shallowRef, useCssModule, watch } from 'vue'; import { computed, nextTick, onMounted, onUnmounted, shallowRef, watch } from 'vue';
import { v4 as uuid } from 'uuid'; import { v4 as uuid } from 'uuid';
import { render } from 'buraha'; import { render } from 'buraha';
import { defaultStore } from '@/store'; import { defaultStore } from '@/store';
const $style = useCssModule();
const props = withDefaults(defineProps<{ const props = withDefaults(defineProps<{
transition?: { transition?: {

View file

@ -1,27 +1,27 @@
<template> <template>
<div class="mk-media-banner"> <div :class="$style.root">
<div v-if="media.isSensitive && hide" class="sensitive" @click="hide = false"> <div v-if="media.isSensitive && hide" :class="$style.sensitive" @click="hide = false">
<span class="icon"><i class="ti ti-alert-triangle"></i></span> <span style="font-size: 1.6em;"><i class="ti ti-alert-triangle"></i></span>
<b>{{ i18n.ts.sensitive }}</b> <b>{{ i18n.ts.sensitive }}</b>
<span>{{ i18n.ts.clickToShow }}</span> <span>{{ i18n.ts.clickToShow }}</span>
</div> </div>
<div v-else-if="media.type.startsWith('audio') && media.type !== 'audio/midi'" class="audio"> <div v-else-if="media.type.startsWith('audio') && media.type !== 'audio/midi'" :class="$style.audio">
<VuePlyr :options="{ volume: 0.5 }"> <audio
<audio controls preload="metadata"> ref="audioEl"
<source
:src="media.url" :src="media.url"
:type="media.type" :title="media.name"
controls
preload="metadata"
@volumechange="volumechange"
/> />
</audio>
</VuePlyr>
</div> </div>
<a <a
v-else class="download" v-else :class="$style.download"
:href="media.url" :href="media.url"
:title="media.name" :title="media.name"
:download="media.name" :download="media.name"
> >
<span class="icon"><i class="ti ti-download"></i></span> <span style="font-size: 1.6em;"><i class="ti ti-download"></i></span>
<b>{{ media.name }}</b> <b>{{ media.name }}</b>
</a> </a>
</div> </div>
@ -30,9 +30,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import { onMounted } from 'vue'; import { onMounted } from 'vue';
import * as misskey from 'misskey-js'; import * as misskey from 'misskey-js';
import VuePlyr from 'vue-plyr';
import { soundConfigStore } from '@/scripts/sound'; import { soundConfigStore } from '@/scripts/sound';
import 'vue-plyr/dist/vue-plyr.css';
import { i18n } from '@/i18n'; import { i18n } from '@/i18n';
const props = withDefaults(defineProps<{ const props = withDefaults(defineProps<{
@ -52,55 +50,34 @@ onMounted(() => {
}); });
</script> </script>
<style lang="scss" scoped> <style lang="scss" module>
.mk-media-banner { .root {
width: 100%; width: 100%;
border-radius: 4px; border-radius: 4px;
margin-top: 4px; margin-top: 4px;
// overflow: clip; overflow: clip;
}
--plyr-color-main: var(--accent); .download,
--plyr-audio-controls-background: var(--bg); .sensitive {
--plyr-audio-controls-color: var(--accentLighten);
> .download,
> .sensitive {
display: flex; display: flex;
align-items: center; align-items: center;
font-size: 12px; font-size: 12px;
padding: 8px 12px; padding: 8px 12px;
white-space: nowrap; white-space: nowrap;
> * {
display: block;
} }
> b { .download {
overflow: hidden;
text-overflow: ellipsis;
}
> *:not(:last-child) {
margin-right: .2em;
}
> .icon {
font-size: 1.6em;
}
}
> .download {
background: var(--noteAttachedFile); background: var(--noteAttachedFile);
} }
> .sensitive { .sensitive {
background: #111; background: #111;
color: #fff; color: #fff;
} }
> .audio { .audio {
border-radius: 8px; border-radius: 8px;
// overflow: clip; overflow: clip;
}
} }
</style> </style>

View file

@ -32,8 +32,7 @@
<div v-if="image.comment" :class="$style.indicator">ALT</div> <div v-if="image.comment" :class="$style.indicator">ALT</div>
<div v-if="image.isSensitive" :class="$style.indicator" style="color: var(--warn);">NSFW</div> <div v-if="image.isSensitive" :class="$style.indicator" style="color: var(--warn);">NSFW</div>
</div> </div>
<button v-tooltip="i18n.ts.hide" :class="$style.hide" class="_button" @click.stop.prevent="hide = true"><i class="ti ti-eye-off"></i></button> <button :class="$style.menu" class="_button" @click.stop="showMenu"><i class="ti ti-dots" style="vertical-align: middle;"></i></button>
<button :class="$style.menu" class="_button" @click.stop="showMenu"><i class="ti ti-dots"></i></button>
</template> </template>
</div> </div>
</template> </template>
@ -79,9 +78,16 @@ watch(() => props.image, () => {
}); });
function showMenu(ev: MouseEvent) { function showMenu(ev: MouseEvent) {
os.popupMenu([...(iAmModerator ? [{ os.popupMenu([{
text: i18n.ts.markAsSensitive, text: i18n.ts.hide,
icon: 'ti ti-eye-off', icon: 'ti ti-eye-off',
action: () => {
hide = true;
},
}, ...(iAmModerator ? [{
text: i18n.ts.markAsSensitive,
icon: 'ti ti-eye-exclamation',
danger: true,
action: () => { action: () => {
os.apiWithDialog('drive/files/update', { fileId: props.image.id, isSensitive: true }); os.apiWithDialog('drive/files/update', { fileId: props.image.id, isSensitive: true });
}, },
@ -122,34 +128,20 @@ function showMenu(ev: MouseEvent) {
background-size: 16px 16px; background-size: 16px 16px;
} }
.hide {
display: block;
position: absolute;
border-radius: 6px;
background-color: var(--accentedBg);
-webkit-backdrop-filter: var(--blur, blur(15px));
backdrop-filter: var(--blur, blur(15px));
color: var(--accent);
font-size: 0.8em;
padding: 6px 8px;
text-align: center;
top: 12px;
right: 12px;
}
.menu { .menu {
display: block; display: block;
position: absolute; position: absolute;
border-radius: 6px; border-radius: 999px;
background-color: rgba(0, 0, 0, 0.3); background-color: rgba(0, 0, 0, 0.3);
-webkit-backdrop-filter: var(--blur, blur(15px)); -webkit-backdrop-filter: var(--blur, blur(15px));
backdrop-filter: var(--blur, blur(15px)); backdrop-filter: var(--blur, blur(15px));
color: #fff; color: #fff;
font-size: 0.8em; font-size: 0.8em;
padding: 6px 8px; width: 32px;
height: 32px;
text-align: center; text-align: center;
bottom: 12px; bottom: 10px;
right: 12px; right: 10px;
} }
.imageContainer { .imageContainer {
@ -166,12 +158,10 @@ function showMenu(ev: MouseEvent) {
.indicators { .indicators {
display: inline-flex; display: inline-flex;
position: absolute; position: absolute;
top: 12px; top: 10px;
left: 12px; left: 10px;
text-align: center;
pointer-events: none; pointer-events: none;
opacity: .5; opacity: .5;
font-size: 14px;
gap: 6px; gap: 6px;
} }
@ -182,7 +172,7 @@ function showMenu(ev: MouseEvent) {
color: var(--accentLighten); color: var(--accentLighten);
display: inline-block; display: inline-block;
font-weight: bold; font-weight: bold;
font-size: 12px; font-size: 0.8em;
padding: 2px 6px; padding: 2px 5px;
} }
</style> </style>

View file

@ -6,8 +6,11 @@
ref="gallery" ref="gallery"
:class="[ :class="[
$style.medias, $style.medias,
count <= 4 ? $style['n' + count] : $style.nMany, count === 1 ? [$style.n1, {
$style[`n1${defaultStore.reactiveState.mediaListWithOneImageAppearance.value}`] [$style.n116_9]: defaultStore.reactiveState.mediaListWithOneImageAppearance.value === '16_9',
[$style.n11_1]: defaultStore.reactiveState.mediaListWithOneImageAppearance.value === '1_1',
[$style.n12_3]: defaultStore.reactiveState.mediaListWithOneImageAppearance.value === '2_3',
}] : count === 2 ? $style.n2 : count === 3 ? $style.n3 : count === 4 ? $style.n4 : $style.nMany,
]" ]"
> >
<template v-for="media in mediaList.filter(media => previewable(media))"> <template v-for="media in mediaList.filter(media => previewable(media))">
@ -20,7 +23,7 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { onMounted, ref, useCssModule, watch, shallowRef } from 'vue'; import { onMounted, watch, shallowRef } from 'vue';
import * as misskey from 'misskey-js'; import * as misskey from 'misskey-js';
import PhotoSwipeLightbox from 'photoswipe/lightbox'; import PhotoSwipeLightbox from 'photoswipe/lightbox';
import PhotoSwipe from 'photoswipe'; import PhotoSwipe from 'photoswipe';
@ -37,8 +40,6 @@ const props = defineProps<{
raw?: boolean; raw?: boolean;
}>(); }>();
const $style = useCssModule();
const gallery = shallowRef<HTMLDivElement>(); const gallery = shallowRef<HTMLDivElement>();
const pswpZIndex = os.claimZIndex('middle'); const pswpZIndex = os.claimZIndex('middle');
document.documentElement.style.setProperty('--mk-pswp-root-z-index', pswpZIndex.toString()); document.documentElement.style.setProperty('--mk-pswp-root-z-index', pswpZIndex.toString());
@ -96,7 +97,7 @@ onMounted(() => {
return item; return item;
}), }),
gallery: gallery.value, gallery: gallery.value,
mainClass: $style.pswp, mainClass: 'pswp',
children: '.image', children: '.image',
thumbSelector: '.image', thumbSelector: '.image',
loop: false, loop: false,
@ -268,7 +269,7 @@ const previewable = (file: misskey.entities.DriveFile): boolean => {
border-radius: 8px; border-radius: 8px;
} }
.pswp { :global(.pswp) {
--pswp-root-z-index: var(--mk-pswp-root-z-index, 2000700) !important; --pswp-root-z-index: var(--mk-pswp-root-z-index, 2000700) !important;
--pswp-bg: var(--modalBg) !important; --pswp-bg: var(--modalBg) !important;
} }

View file

@ -1,36 +1,36 @@
<template> <template>
<div v-if="hide" class="icozogqfvdetwohsdglrbswgrejoxbdj" @click="hide = false"> <div v-if="hide" :class="$style.hidden" @click="hide = false">
<!-- 注意dataSaverMode が有効になっている際にはhide false になるまでサムネイルや動画を読み込まないようにすること --> <!-- 注意dataSaverMode が有効になっている際にはhide false になるまでサムネイルや動画を読み込まないようにすること -->
<div> <div :class="$style.sensitive">
<b v-if="video.isSensitive"><i class="ti ti-alert-triangle"></i> {{ i18n.ts.sensitive }}{{ defaultStore.state.enableDataSaverMode ? ` (${i18n.ts.video}${video.size ? ' ' + bytes(video.size) : ''})` : '' }}</b> <b v-if="video.isSensitive" style="display: block;"><i class="ti ti-alert-triangle"></i> {{ i18n.ts.sensitive }}{{ defaultStore.state.enableDataSaverMode ? ` (${i18n.ts.video}${video.size ? ' ' + bytes(video.size) : ''})` : '' }}</b>
<b v-else><i class="ti ti-movie"></i> {{ defaultStore.state.enableDataSaverMode && video.size ? bytes(video.size) : i18n.ts.video }}</b> <b v-else style="display: block;"><i class="ti ti-movie"></i> {{ defaultStore.state.enableDataSaverMode && video.size ? bytes(video.size) : i18n.ts.video }}</b>
<span>{{ i18n.ts.clickToShow }}</span> <span>{{ i18n.ts.clickToShow }}</span>
</div> </div>
</div> </div>
<div v-else class="kkjnbbplepmiyuadieoenjgutgcmtsvu"> <div v-else :class="$style.visible">
<VuePlyr :options="{ volume: 0.5 }">
<video <video
:class="$style.video"
:poster="video.thumbnailUrl"
:title="video.comment"
:alt="video.comment"
preload="none"
controls controls
:data-poster="video.thumbnailUrl" @contextmenu.stop
> >
<source <source
size="720"
:src="video.url" :src="video.url"
:type="video.type" :type="video.type"
/> >
</video> </video>
</VuePlyr> <i class="ti ti-eye-off" :class="$style.hide" @click="hide = true"></i>
<i class="ti ti-eye-off" @click="hide = true"></i>
</div> </div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { ref } from 'vue'; import { ref } from 'vue';
import * as misskey from 'misskey-js'; import * as misskey from 'misskey-js';
import VuePlyr from 'vue-plyr';
import bytes from '@/filters/bytes'; import bytes from '@/filters/bytes';
import { defaultStore } from '@/store'; import { defaultStore } from '@/store';
import 'vue-plyr/dist/vue-plyr.css';
import { i18n } from '@/i18n'; import { i18n } from '@/i18n';
const props = defineProps<{ const props = defineProps<{
@ -40,13 +40,12 @@ const props = defineProps<{
const hide = ref((defaultStore.state.nsfw === 'force' || defaultStore.state.enableDataSaverMode) ? true : (props.video.isSensitive && defaultStore.state.nsfw !== 'ignore')); const hide = ref((defaultStore.state.nsfw === 'force' || defaultStore.state.enableDataSaverMode) ? true : (props.video.isSensitive && defaultStore.state.nsfw !== 'ignore'));
</script> </script>
<style lang="scss" scoped> <style lang="scss" module>
.kkjnbbplepmiyuadieoenjgutgcmtsvu { .visible {
position: relative; position: relative;
}
--plyr-color-main: var(--accent); .hide {
> i {
display: block; display: block;
position: absolute; position: absolute;
border-radius: 6px; border-radius: 6px;
@ -61,11 +60,10 @@ const hide = ref((defaultStore.state.nsfw === 'force' || defaultStore.state.enab
right: 12px; right: 12px;
} }
> video { .video {
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
font-size: 3.5em; font-size: 3.5em;
overflow: hidden; overflow: hidden;
background-position: center; background-position: center;
@ -73,23 +71,18 @@ const hide = ref((defaultStore.state.nsfw === 'force' || defaultStore.state.enab
width: 100%; width: 100%;
height: 100%; height: 100%;
} }
}
.icozogqfvdetwohsdglrbswgrejoxbdj { .hidden {
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
background: #111; background: #111;
color: #fff; color: #fff;
}
> div { .sensitive {
display: table-cell; display: table-cell;
text-align: center; text-align: center;
font-size: 12px; font-size: 12px;
> b {
display: block;
}
}
} }
</style> </style>

View file

@ -2,7 +2,7 @@
<MkA v-user-preview="canonical" :class="[$style.root, { [$style.isMe]: isMe }]" :to="url" :style="{ background: bgCss }"> <MkA v-user-preview="canonical" :class="[$style.root, { [$style.isMe]: isMe }]" :to="url" :style="{ background: bgCss }">
<img :class="$style.icon" :src="`/avatar/@${username}@${host}`" alt=""> <img :class="$style.icon" :src="`/avatar/@${username}@${host}`" alt="">
<span> <span>
<span :class="$style.username">@{{ username }}</span> <span>@{{ username }}</span>
<span v-if="(host != localHost) || defaultStore.state.showFullAcct" :class="$style.host">@{{ toUnicode(host) }}</span> <span v-if="(host != localHost) || defaultStore.state.showFullAcct" :class="$style.host">@{{ toUnicode(host) }}</span>
</span> </span>
</MkA> </MkA>

View file

@ -49,7 +49,7 @@
<span>{{ i18n.ts.none }}</span> <span>{{ i18n.ts.none }}</span>
</span> </span>
</div> </div>
<div v-if="childMenu" :class="$style.child"> <div v-if="childMenu">
<XChild ref="child" :items="childMenu" :targetElement="childTarget" :rootElement="itemsEl" showing @actioned="childActioned"/> <XChild ref="child" :items="childMenu" :targetElement="childTarget" :rootElement="itemsEl" showing @actioned="childActioned"/>
</div> </div>
</div> </div>

View file

@ -1,10 +1,30 @@
<template> <template>
<Transition <Transition
:name="transitionName" :name="transitionName"
:enterActiveClass="$style['transition_' + transitionName + '_enterActive']" :enterActiveClass="normalizeClass({
:leaveActiveClass="$style['transition_' + transitionName + '_leaveActive']" [$style.transition_modalDrawer_enterActive]: transitionName === 'modal-drawer',
:enterFromClass="$style['transition_' + transitionName + '_enterFrom']" [$style.transition_modalPopup_enterActive]: transitionName === 'modal-popup',
:leaveToClass="$style['transition_' + transitionName + '_leaveTo']" [$style.transition_modal_enterActive]: transitionName === 'modal',
[$style.transition_send_enterActive]: transitionName === 'send',
})"
:leaveActiveClass="normalizeClass({
[$style.transition_modalDrawer_leaveActive]: transitionName === 'modal-drawer',
[$style.transition_modalPopup_leaveActive]: transitionName === 'modal-popup',
[$style.transition_modal_leaveActive]: transitionName === 'modal',
[$style.transition_send_leaveActive]: transitionName === 'send',
})"
:enterFromClass="normalizeClass({
[$style.transition_modalDrawer_enterFrom]: transitionName === 'modal-drawer',
[$style.transition_modalPopup_enterFrom]: transitionName === 'modal-popup',
[$style.transition_modal_enterFrom]: transitionName === 'modal',
[$style.transition_send_enterFrom]: transitionName === 'send',
})"
:leaveToClass="normalizeClass({
[$style.transition_modalDrawer_leaveTo]: transitionName === 'modal-drawer',
[$style.transition_modalPopup_leaveTo]: transitionName === 'modal-popup',
[$style.transition_modal_leaveTo]: transitionName === 'modal',
[$style.transition_send_leaveTo]: transitionName === 'send',
})"
:duration="transitionDuration" appear @afterLeave="emit('closed')" @enter="emit('opening')" @afterEnter="onOpened" :duration="transitionDuration" appear @afterLeave="emit('closed')" @enter="emit('opening')" @afterEnter="onOpened"
> >
<div v-show="manualShowing != null ? manualShowing : showing" v-hotkey.global="keymap" :class="[$style.root, { [$style.drawer]: type === 'drawer', [$style.dialog]: type === 'dialog', [$style.popup]: type === 'popup' }]" :style="{ zIndex, pointerEvents: (manualShowing != null ? manualShowing : showing) ? 'auto' : 'none', '--transformOrigin': transformOrigin }"> <div v-show="manualShowing != null ? manualShowing : showing" v-hotkey.global="keymap" :class="[$style.root, { [$style.drawer]: type === 'drawer', [$style.dialog]: type === 'dialog', [$style.popup]: type === 'popup' }]" :style="{ zIndex, pointerEvents: (manualShowing != null ? manualShowing : showing) ? 'auto' : 'none', '--transformOrigin': transformOrigin }">
@ -17,7 +37,7 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { nextTick, onMounted, watch, provide } from 'vue'; import { nextTick, normalizeClass, onMounted, onUnmounted, provide, watch } from 'vue';
import * as os from '@/os'; import * as os from '@/os';
import { isTouchUsing } from '@/scripts/touch'; import { isTouchUsing } from '@/scripts/touch';
import { defaultStore } from '@/store'; import { defaultStore } from '@/store';
@ -38,7 +58,7 @@ type ModalTypes = 'popup' | 'dialog' | 'drawer';
const props = withDefaults(defineProps<{ const props = withDefaults(defineProps<{
manualShowing?: boolean | null; manualShowing?: boolean | null;
anchor?: { x: string; y: string; }; anchor?: { x: string; y: string; };
src?: HTMLElement; src?: HTMLElement | null;
preferType?: ModalTypes | 'auto'; preferType?: ModalTypes | 'auto';
zPriority?: 'low' | 'middle' | 'high'; zPriority?: 'low' | 'middle' | 'high';
noOverlap?: boolean; noOverlap?: boolean;
@ -264,6 +284,10 @@ const onOpened = () => {
}, { passive: true }); }, { passive: true });
}; };
const alignObserver = new ResizeObserver((entries, observer) => {
align();
});
onMounted(() => { onMounted(() => {
watch(() => props.src, async () => { watch(() => props.src, async () => {
if (props.src) { if (props.src) {
@ -278,12 +302,14 @@ onMounted(() => {
}, { immediate: true }); }, { immediate: true });
nextTick(() => { nextTick(() => {
new ResizeObserver((entries, observer) => { alignObserver.observe(content!);
align();
}).observe(content!);
}); });
}); });
onUnmounted(() => {
alignObserver.disconnect();
});
defineExpose({ defineExpose({
close, close,
}); });
@ -339,8 +365,8 @@ defineExpose({
} }
} }
.transition_modal-popup_enterActive, .transition_modalPopup_enterActive,
.transition_modal-popup_leaveActive { .transition_modalPopup_leaveActive {
> .bg { > .bg {
transition: opacity 0.1s !important; transition: opacity 0.1s !important;
} }
@ -350,8 +376,8 @@ defineExpose({
transition: opacity 0.1s cubic-bezier(0, 0, 0.2, 1), transform 0.1s cubic-bezier(0, 0, 0.2, 1) !important; transition: opacity 0.1s cubic-bezier(0, 0, 0.2, 1), transform 0.1s cubic-bezier(0, 0, 0.2, 1) !important;
} }
} }
.transition_modal-popup_enterFrom, .transition_modalPopup_enterFrom,
.transition_modal-popup_leaveTo { .transition_modalPopup_leaveTo {
> .bg { > .bg {
opacity: 0; opacity: 0;
} }
@ -364,7 +390,7 @@ defineExpose({
} }
} }
.transition_modal-drawer_enterActive { .transition_modalDrawer_enterActive {
> .bg { > .bg {
transition: opacity 0.2s !important; transition: opacity 0.2s !important;
} }
@ -373,7 +399,7 @@ defineExpose({
transition: transform 0.2s cubic-bezier(0,.5,0,1) !important; transition: transform 0.2s cubic-bezier(0,.5,0,1) !important;
} }
} }
.transition_modal-drawer_leaveActive { .transition_modalDrawer_leaveActive {
> .bg { > .bg {
transition: opacity 0.2s !important; transition: opacity 0.2s !important;
} }
@ -382,8 +408,8 @@ defineExpose({
transition: transform 0.2s cubic-bezier(0,.5,0,1) !important; transition: transform 0.2s cubic-bezier(0,.5,0,1) !important;
} }
} }
.transition_modal-drawer_enterFrom, .transition_modalDrawer_enterFrom,
.transition_modal-drawer_leaveTo { .transition_modalDrawer_leaveTo {
> .bg { > .bg {
opacity: 0; opacity: 0;
} }

View file

@ -44,8 +44,8 @@
<div v-if="appearNote.channel" :class="$style.colorBar" :style="{ background: appearNote.channel.color }"></div> <div v-if="appearNote.channel" :class="$style.colorBar" :style="{ background: appearNote.channel.color }"></div>
<MkAvatar :class="$style.avatar" :user="appearNote.user" link preview/> <MkAvatar :class="$style.avatar" :user="appearNote.user" link preview/>
<div :class="$style.main"> <div :class="$style.main">
<MkNoteHeader :class="$style.header" :note="appearNote" :mini="true"/> <MkNoteHeader :note="appearNote" :mini="true"/>
<MkInstanceTicker v-if="showTicker" :class="$style.ticker" :instance="appearNote.user.instance"/> <MkInstanceTicker v-if="showTicker" :instance="appearNote.user.instance"/>
<div style="container-type: inline-size;"> <div style="container-type: inline-size;">
<p v-if="appearNote.cw != null" :class="$style.cw"> <p v-if="appearNote.cw != null" :class="$style.cw">
<Mfm v-if="appearNote.cw != ''" style="margin-right: 8px;" :text="appearNote.cw" :author="appearNote.user" :i="$i"/> <Mfm v-if="appearNote.cw != ''" style="margin-right: 8px;" :text="appearNote.cw" :author="appearNote.user" :i="$i"/>
@ -58,13 +58,13 @@
<Mfm v-if="appearNote.text" :text="appearNote.text" :author="appearNote.user" :i="$i" :emojiUrls="appearNote.emojis"/> <Mfm v-if="appearNote.text" :text="appearNote.text" :author="appearNote.user" :i="$i" :emojiUrls="appearNote.emojis"/>
<div v-if="translating || translation" :class="$style.translation"> <div v-if="translating || translation" :class="$style.translation">
<MkLoading v-if="translating" mini/> <MkLoading v-if="translating" mini/>
<div v-else :class="$style.translated"> <div v-else>
<b>{{ i18n.t('translatedFrom', { x: translation.sourceLang }) }}: </b> <b>{{ i18n.t('translatedFrom', { x: translation.sourceLang }) }}: </b>
<Mfm :text="translation.text" :author="appearNote.user" :i="$i" :emojiUrls="appearNote.emojis"/> <Mfm :text="translation.text" :author="appearNote.user" :i="$i" :emojiUrls="appearNote.emojis"/>
</div> </div>
</div> </div>
</div> </div>
<div v-if="appearNote.files.length > 0" :class="$style.files"> <div v-if="appearNote.files.length > 0">
<MkMediaList :mediaList="appearNote.files"/> <MkMediaList :mediaList="appearNote.files"/>
</div> </div>
<MkPoll v-if="appearNote.poll" :note="appearNote" :class="$style.poll"/> <MkPoll v-if="appearNote.poll" :note="appearNote" :class="$style.poll"/>
@ -205,8 +205,11 @@ const isMyRenote = $i && ($i.id === note.userId);
const showContent = ref(false); const showContent = ref(false);
const urls = appearNote.text ? extractUrlFromMfm(mfm.parse(appearNote.text)) : null; const urls = appearNote.text ? extractUrlFromMfm(mfm.parse(appearNote.text)) : null;
const isLong = (appearNote.cw == null && appearNote.text != null && ( const isLong = (appearNote.cw == null && appearNote.text != null && (
(appearNote.text.includes('$[x2')) ||
(appearNote.text.includes('$[x3')) || (appearNote.text.includes('$[x3')) ||
(appearNote.text.includes('$[x4')) || (appearNote.text.includes('$[x4')) ||
(appearNote.text.includes('$[scale')) ||
(appearNote.text.includes('$[position')) ||
(appearNote.text.split('\n').length > 9) || (appearNote.text.split('\n').length > 9) ||
(appearNote.text.length > 500) || (appearNote.text.length > 500) ||
(appearNote.files.length >= 5) || (appearNote.files.length >= 5) ||
@ -379,6 +382,8 @@ function undoReact(note): void {
function onContextmenu(ev: MouseEvent): void { function onContextmenu(ev: MouseEvent): void {
const isLink = (el: HTMLElement) => { const isLink = (el: HTMLElement) => {
if (el.tagName === 'A') return true; if (el.tagName === 'A') return true;
// Audio
if (el.tagName === 'AUDIO') return true;
if (el.parentElement) { if (el.parentElement) {
return isLink(el.parentElement); return isLink(el.parentElement);
} }

View file

@ -4,7 +4,7 @@
v-show="!isDeleted" v-show="!isDeleted"
ref="el" ref="el"
v-hotkey="keymap" v-hotkey="keymap"
:class="[$style.root, { [$style.renote]: isRenote }]" :class="$style.root"
> >
<MkNoteSub v-for="note in conversation" :key="note.id" :class="$style.replyToMore" :note="note"/> <MkNoteSub v-for="note in conversation" :key="note.id" :class="$style.replyToMore" :note="note"/>
<MkNoteSub v-if="appearNote.reply" :note="appearNote.reply" :class="$style.replyTo"/> <MkNoteSub v-if="appearNote.reply" :note="appearNote.reply" :class="$style.replyTo"/>
@ -52,7 +52,7 @@
</div> </div>
</div> </div>
<div :class="$style.noteHeaderUsername"><MkAcct :user="appearNote.user"/></div> <div :class="$style.noteHeaderUsername"><MkAcct :user="appearNote.user"/></div>
<MkInstanceTicker v-if="showTicker" :class="$style.ticker" :instance="appearNote.user.instance"/> <MkInstanceTicker v-if="showTicker" :instance="appearNote.user.instance"/>
</div> </div>
</header> </header>
<div :class="$style.noteContent"> <div :class="$style.noteContent">
@ -72,7 +72,7 @@
<Mfm :text="translation.text" :author="appearNote.user" :i="$i" :emojiUrls="appearNote.emojis"/> <Mfm :text="translation.text" :author="appearNote.user" :i="$i" :emojiUrls="appearNote.emojis"/>
</div> </div>
</div> </div>
<div v-if="appearNote.files.length > 0" :class="$style.files"> <div v-if="appearNote.files.length > 0">
<MkMediaList :mediaList="appearNote.files"/> <MkMediaList :mediaList="appearNote.files"/>
</div> </div>
<MkPoll v-if="appearNote.poll" ref="pollViewer" :note="appearNote" :class="$style.poll"/> <MkPoll v-if="appearNote.poll" ref="pollViewer" :note="appearNote" :class="$style.poll"/>

View file

@ -6,7 +6,7 @@
<MkUserName :user="$i" :nowrap="true"/> <MkUserName :user="$i" :nowrap="true"/>
</div> </div>
<div> <div>
<div :class="$style.content"> <div>
<Mfm :text="text.trim()" :author="$i" :i="$i"/> <Mfm :text="text.trim()" :author="$i" :i="$i"/>
</div> </div>
</div> </div>

View file

@ -5,7 +5,19 @@
<MkAvatar v-else-if="notification.type === 'achievementEarned'" :class="$style.icon" :user="$i" link preview/> <MkAvatar v-else-if="notification.type === 'achievementEarned'" :class="$style.icon" :user="$i" link preview/>
<MkAvatar v-else-if="notification.user" :class="$style.icon" :user="notification.user" link preview/> <MkAvatar v-else-if="notification.user" :class="$style.icon" :user="notification.user" link preview/>
<img v-else-if="notification.icon" :class="$style.icon" :src="notification.icon" alt=""/> <img v-else-if="notification.icon" :class="$style.icon" :src="notification.icon" alt=""/>
<div :class="[$style.subIcon, $style['t_' + notification.type]]"> <div
:class="[$style.subIcon, {
[$style.t_follow]: notification.type === 'follow',
[$style.t_followRequestAccepted]: notification.type === 'followRequestAccepted',
[$style.t_receiveFollowRequest]: notification.type === 'receiveFollowRequest',
[$style.t_renote]: notification.type === 'renote',
[$style.t_reply]: notification.type === 'reply',
[$style.t_mention]: notification.type === 'mention',
[$style.t_quote]: notification.type === 'quote',
[$style.t_pollEnded]: notification.type === 'pollEnded',
[$style.t_achievementEarned]: notification.type === 'achievementEarned',
}]"
>
<i v-if="notification.type === 'follow'" class="ti ti-plus"></i> <i v-if="notification.type === 'follow'" class="ti ti-plus"></i>
<i v-else-if="notification.type === 'receiveFollowRequest'" class="ti ti-clock"></i> <i v-else-if="notification.type === 'receiveFollowRequest'" class="ti ti-clock"></i>
<i v-else-if="notification.type === 'followRequestAccepted'" class="ti ti-check"></i> <i v-else-if="notification.type === 'followRequestAccepted'" class="ti ti-check"></i>
@ -34,7 +46,7 @@
<span v-else>{{ notification.header }}</span> <span v-else>{{ notification.header }}</span>
<MkTime v-if="withTime" :time="notification.createdAt" :class="$style.headerTime"/> <MkTime v-if="withTime" :time="notification.createdAt" :class="$style.headerTime"/>
</header> </header>
<div :class="$style.content"> <div>
<MkA v-if="notification.type === 'reaction'" :class="$style.text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)"> <MkA v-if="notification.type === 'reaction'" :class="$style.text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)">
<i class="ti ti-quote" :class="$style.quote"></i> <i class="ti ti-quote" :class="$style.quote"></i>
<Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="true" :author="notification.note.user"/> <Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="true" :author="notification.note.user"/>
@ -243,9 +255,6 @@ useTooltip(reactionRef, (showing) => {
font-size: 0.9em; font-size: 0.9em;
} }
.content {
}
.text { .text {
display: flex; display: flex;
width: 100%; width: 100%;

View file

@ -8,7 +8,7 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { onMounted } from 'vue'; import { onMounted, onUnmounted } from 'vue';
import { i18n } from '@/i18n'; import { i18n } from '@/i18n';
const props = withDefaults(defineProps<{ const props = withDefaults(defineProps<{
@ -21,16 +21,22 @@ let content = $shallowRef<HTMLElement>();
let omitted = $ref(false); let omitted = $ref(false);
let ignoreOmit = $ref(false); let ignoreOmit = $ref(false);
onMounted(() => {
const calcOmit = () => { const calcOmit = () => {
if (omitted || ignoreOmit) return; if (omitted || ignoreOmit) return;
omitted = content.offsetHeight > props.maxHeight; omitted = content.offsetHeight > props.maxHeight;
}; };
const omitObserver = new ResizeObserver((entries, observer) => {
calcOmit(); calcOmit();
new ResizeObserver((entries, observer) => { });
onMounted(() => {
calcOmit(); calcOmit();
}).observe(content); omitObserver.observe(content);
});
onUnmounted(() => {
omitObserver.disconnect();
}); });
</script> </script>

View file

@ -1,7 +1,7 @@
<template> <template>
<div :class="{ [$style.done]: closed || isVoted }"> <div :class="{ [$style.done]: closed || isVoted }">
<ul :class="$style.choices"> <ul :class="$style.choices">
<li v-for="(choice, i) in note.poll.choices" :key="i" :class="[$style.choice, { [$style.voted]: choice.voted }]" @click="vote(i)"> <li v-for="(choice, i) in note.poll.choices" :key="i" :class="$style.choice" @click="vote(i)">
<div :class="$style.bg" :style="{ 'width': `${showResult ? (choice.votes / total * 100) : 0}%` }"></div> <div :class="$style.bg" :style="{ 'width': `${showResult ? (choice.votes / total * 100) : 0}%` }"></div>
<span :class="$style.fg"> <span :class="$style.fg">
<template v-if="choice.isVoted"><i class="ti ti-check" style="margin-right: 4px; color: var(--accent);"></i></template> <template v-if="choice.isVoted"><i class="ti ti-check" style="margin-right: 4px; color: var(--accent);"></i></template>

View file

@ -22,21 +22,21 @@
<span v-if="visibility === 'specified'"><i class="ti ti-mail"></i></span> <span v-if="visibility === 'specified'"><i class="ti ti-mail"></i></span>
<span :class="$style.headerRightButtonText">{{ i18n.ts._visibility[visibility] }}</span> <span :class="$style.headerRightButtonText">{{ i18n.ts._visibility[visibility] }}</span>
</button> </button>
<button v-else :class="['_button', $style.headerRightItem, $style.visibility]" disabled> <button v-else class="_button" :class="[$style.headerRightItem, $style.visibility]" disabled>
<span><i class="ti ti-device-tv"></i></span> <span><i class="ti ti-device-tv"></i></span>
<span :class="$style.headerRightButtonText">{{ channel.name }}</span> <span :class="$style.headerRightButtonText">{{ channel.name }}</span>
</button> </button>
</template> </template>
<button v-click-anime v-tooltip="i18n.ts._visibility.disableFederation" :class="['_button', $style.headerRightItem, $style.localOnly, { [$style.danger]: localOnly }]" :disabled="channel != null || visibility === 'specified'" @click="toggleLocalOnly"> <button v-click-anime v-tooltip="i18n.ts._visibility.disableFederation" class="_button" :class="[$style.headerRightItem, { [$style.danger]: localOnly }]" :disabled="channel != null || visibility === 'specified'" @click="toggleLocalOnly">
<span v-if="!localOnly"><i class="ti ti-rocket"></i></span> <span v-if="!localOnly"><i class="ti ti-rocket"></i></span>
<span v-else><i class="ti ti-rocket-off"></i></span> <span v-else><i class="ti ti-rocket-off"></i></span>
</button> </button>
<button v-click-anime v-tooltip="i18n.ts.reactionAcceptance" :class="['_button', $style.headerRightItem, $style.reactionAcceptance, { [$style.danger]: reactionAcceptance === 'likeOnly' }]" @click="toggleReactionAcceptance"> <button v-click-anime v-tooltip="i18n.ts.reactionAcceptance" class="_button" :class="[$style.headerRightItem, { [$style.danger]: reactionAcceptance === 'likeOnly' }]" @click="toggleReactionAcceptance">
<span v-if="reactionAcceptance === 'likeOnly'"><i class="ti ti-heart"></i></span> <span v-if="reactionAcceptance === 'likeOnly'"><i class="ti ti-heart"></i></span>
<span v-else-if="reactionAcceptance === 'likeOnlyForRemote'"><i class="ti ti-heart-plus"></i></span> <span v-else-if="reactionAcceptance === 'likeOnlyForRemote'"><i class="ti ti-heart-plus"></i></span>
<span v-else><i class="ti ti-icons"></i></span> <span v-else><i class="ti ti-icons"></i></span>
</button> </button>
<button v-click-anime class="_button" :class="[$style.submit, { [$style.submitPosting]: posting }]" :disabled="!canPost" data-cy-open-post-form-submit @click="post"> <button v-click-anime class="_button" :class="$style.submit" :disabled="!canPost" data-cy-open-post-form-submit @click="post">
<div :class="$style.submitInner"> <div :class="$style.submitInner">
<template v-if="posted"></template> <template v-if="posted"></template>
<template v-else-if="posting"><MkEllipsis/></template> <template v-else-if="posting"><MkEllipsis/></template>
@ -66,7 +66,7 @@
<div v-if="maxTextLength - textLength < 100" :class="['_acrylic', $style.textCount, { [$style.textOver]: textLength > maxTextLength }]">{{ maxTextLength - textLength }}</div> <div v-if="maxTextLength - textLength < 100" :class="['_acrylic', $style.textCount, { [$style.textOver]: textLength > maxTextLength }]">{{ maxTextLength - textLength }}</div>
</div> </div>
<input v-show="withHashtags" ref="hashtagsInputEl" v-model="hashtags" :class="$style.hashtags" :placeholder="i18n.ts.hashtags" list="hashtags"> <input v-show="withHashtags" ref="hashtagsInputEl" v-model="hashtags" :class="$style.hashtags" :placeholder="i18n.ts.hashtags" list="hashtags">
<XPostFormAttaches v-model="files" :class="$style.attaches" @detach="detachFile" @changeSensitive="updateFileSensitive" @changeName="updateFileName"/> <XPostFormAttaches v-model="files" @detach="detachFile" @changeSensitive="updateFileSensitive" @changeName="updateFileName"/>
<MkPollEditor v-if="poll" v-model="poll" @destroyed="poll = null"/> <MkPollEditor v-if="poll" v-model="poll" @destroyed="poll = null"/>
<MkNotePreview v-if="showPreview" :class="$style.preview" :text="text"/> <MkNotePreview v-if="showPreview" :class="$style.preview" :text="text"/>
<div v-if="showingOptions" style="padding: 8px 16px;"> <div v-if="showingOptions" style="padding: 8px 16px;">

View file

@ -93,7 +93,7 @@ function showFileMenu(file, ev: MouseEvent) {
action: () => { rename(file); }, action: () => { rename(file); },
}, { }, {
text: file.isSensitive ? i18n.ts.unmarkAsSensitive : i18n.ts.markAsSensitive, text: file.isSensitive ? i18n.ts.unmarkAsSensitive : i18n.ts.markAsSensitive,
icon: file.isSensitive ? 'ti ti-eye-off' : 'ti ti-eye', icon: file.isSensitive ? 'ti ti-eye-exclamation' : 'ti ti-eye',
action: () => { toggleSensitive(file); }, action: () => { toggleSensitive(file); },
}, { }, {
text: i18n.ts.describeFile, text: i18n.ts.describeFile,

View file

@ -1,6 +1,6 @@
<template> <template>
<div :class="[$style.root, { [$style.collapsed]: collapsed }]"> <div :class="[$style.root, { [$style.collapsed]: collapsed }]">
<div :class="$style.body"> <div>
<span v-if="note.isHidden" style="opacity: 0.5">({{ i18n.ts.private }})</span> <span v-if="note.isHidden" style="opacity: 0.5">({{ i18n.ts.private }})</span>
<span v-if="note.deletedAt" style="opacity: 0.5">({{ i18n.ts.deleted }})</span> <span v-if="note.deletedAt" style="opacity: 0.5">({{ i18n.ts.deleted }})</span>
<MkA v-if="note.replyId" :class="$style.reply" :to="`/notes/${note.replyId}`"><i class="ti ti-arrow-back-up"></i></MkA> <MkA v-if="note.replyId" :class="$style.reply" :to="`/notes/${note.replyId}`"><i class="ti ti-arrow-back-up"></i></MkA>
@ -76,10 +76,6 @@ const collapsed = $ref(
} }
} }
.body {
}
.reply { .reply {
margin-right: 6px; margin-right: 6px;
color: var(--accent); color: var(--accent);

View file

@ -6,7 +6,7 @@
ref="inputEl" ref="inputEl"
v-model="v" v-model="v"
v-adaptive-border v-adaptive-border
:class="[$style.textarea, { [$style.code]: code, _monospace: code }]" :class="[$style.textarea, { _monospace: code }]"
:disabled="disabled" :disabled="disabled"
:required="required" :required="required"
:readonly="readonly" :readonly="readonly"

View file

@ -22,7 +22,7 @@
</div> </div>
</template> </template>
<template v-else-if="tweetId && tweetExpanded"> <template v-else-if="tweetId && tweetExpanded">
<div ref="twitter" :class="$style.twitter"> <div ref="twitter">
<iframe ref="tweet" scrolling="no" frameborder="no" :style="{ position: 'relative', width: '100%', height: `${tweetHeight}px` }" :src="`https://platform.twitter.com/embed/index.html?embedId=${embedId}&amp;hideCard=false&amp;hideThread=false&amp;lang=en&amp;theme=${defaultStore.state.darkMode ? 'dark' : 'light'}&amp;id=${tweetId}`"></iframe> <iframe ref="tweet" scrolling="no" frameborder="no" :style="{ position: 'relative', width: '100%', height: `${tweetHeight}px` }" :src="`https://platform.twitter.com/embed/index.html?embedId=${embedId}&amp;hideCard=false&amp;hideThread=false&amp;lang=en&amp;theme=${defaultStore.state.darkMode ? 'dark' : 'light'}&amp;id=${tweetId}`"></iframe>
</div> </div>
<div :class="$style.action"> <div :class="$style.action">
@ -31,7 +31,7 @@
</MkButton> </MkButton>
</div> </div>
</template> </template>
<div v-else :class="$style.urlPreview"> <div v-else>
<component :is="self ? 'MkA' : 'a'" :class="[$style.link, { [$style.compact]: compact }]" :[attr]="self ? url.substr(local.length) : url" rel="nofollow noopener" :target="target" :title="url"> <component :is="self ? 'MkA' : 'a'" :class="[$style.link, { [$style.compact]: compact }]" :[attr]="self ? url.substr(local.length) : url" rel="nofollow noopener" :target="target" :title="url">
<div v-if="thumbnail" :class="$style.thumbnail" :style="`background-image: url('${thumbnail}')`"> <div v-if="thumbnail" :class="$style.thumbnail" :style="`background-image: url('${thumbnail}')`">
</div> </div>
@ -210,13 +210,6 @@ onUnmounted(() => {
width: 100%; width: 100%;
} }
.twitter {
}
.urlPreview {
}
.link { .link {
position: relative; position: relative;
display: block; display: block;

View file

@ -8,7 +8,7 @@
</div> </div>
<span v-if="$i && $i.id !== user.id && user.isFollowed" :class="$style.followed">{{ i18n.ts.followsYou }}</span> <span v-if="$i && $i.id !== user.id && user.isFollowed" :class="$style.followed">{{ i18n.ts.followsYou }}</span>
<div :class="$style.description"> <div :class="$style.description">
<div v-if="user.description" class="mfm"> <div v-if="user.description" :class="$style.mfm">
<Mfm :text="user.description" :author="user" :i="$i"/> <Mfm :text="user.description" :author="user" :i="$i"/>
</div> </div>
<span v-else style="opacity: 0.7;">{{ i18n.ts.noAccountDescription }}</span> <span v-else style="opacity: 0.7;">{{ i18n.ts.noAccountDescription }}</span>

View file

@ -1,5 +1,13 @@
<template> <template>
<div v-tooltip="text" :class="[$style.root, $style['status_' + user.onlineStatus]]"></div> <div
v-tooltip="text"
:class="[$style.root, {
[$style.status_online]: user.onlineStatus === 'online',
[$style.status_active]: user.onlineStatus === 'active',
[$style.status_offline]: user.onlineStatus === 'offline',
[$style.status_unknown]: user.onlineStatus === 'unknown',
}]"
></div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>

View file

@ -9,7 +9,7 @@
@closed="$emit('closed')" @closed="$emit('closed')"
> >
<template #header>{{ i18n.ts.selectUser }}</template> <template #header>{{ i18n.ts.selectUser }}</template>
<div :class="$style.root"> <div>
<div :class="$style.form"> <div :class="$style.form">
<FormSplit :minWidth="170"> <FormSplit :minWidth="170">
<MkInput v-model="username" :autofocus="true" @update:modelValue="search"> <MkInput v-model="username" :autofocus="true" @update:modelValue="search">
@ -126,8 +126,6 @@ onMounted(() => {
</script> </script>
<style lang="scss" module> <style lang="scss" module>
.root {
}
.form { .form {
padding: 0 var(--root-margin); padding: 0 var(--root-margin);

View file

@ -3,9 +3,9 @@
<div :class="$style.root"> <div :class="$style.root">
<div v-for="u in users" :key="u.id" :class="$style.user"> <div v-for="u in users" :key="u.id" :class="$style.user">
<MkAvatar :class="$style.avatar" :user="u"/> <MkAvatar :class="$style.avatar" :user="u"/>
<MkUserName :class="$style.name" :user="u" :nowrap="true"/> <MkUserName :user="u" :nowrap="true"/>
</div> </div>
<div v-if="users.length < count" :class="$style.omitted">+{{ count - users.length }}</div> <div v-if="users.length < count">+{{ count - users.length }}</div>
</div> </div>
</MkTooltip> </MkTooltip>
</template> </template>
@ -43,14 +43,6 @@ const emit = defineEmits<{
} }
} }
.name {
}
.omitted {
}
.avatar { .avatar {
width: 24px; width: 24px;
height: 24px; height: 24px;

View file

@ -39,7 +39,7 @@
<MkTimeline src="local"/> <MkTimeline src="local"/>
</div> </div>
</div> </div>
<div :class="[$style.activeUsersChart, $style.panel]"> <div :class="$style.panel">
<XActiveUsersChart/> <XActiveUsersChart/>
</div> </div>
</div> </div>
@ -220,8 +220,4 @@ function exploreOtherServers() {
height: 350px; height: 350px;
overflow: auto; overflow: auto;
} }
.activeUsersChart {
}
</style> </style>

View file

@ -1,7 +1,7 @@
<template> <template>
<div :class="$style.root"> <div :class="$style.root">
<template v-if="edit"> <template v-if="edit">
<header :class="$style['edit-header']"> <header :class="$style.editHeader">
<MkSelect v-model="widgetAdderSelected" style="margin-bottom: var(--margin)" data-cy-widget-select> <MkSelect v-model="widgetAdderSelected" style="margin-bottom: var(--margin)" data-cy-widget-select>
<template #label>{{ i18n.ts.selectWidget }}</template> <template #label>{{ i18n.ts.selectWidget }}</template>
<option v-for="widget in widgetDefs" :key="widget" :value="widget">{{ i18n.t(`_widgets.${widget}`) }}</option> <option v-for="widget in widgetDefs" :key="widget" :value="widget">{{ i18n.t(`_widgets.${widget}`) }}</option>
@ -15,15 +15,15 @@
handle=".handle" handle=".handle"
:animation="150" :animation="150"
:group="{ name: 'SortableMkWidgets' }" :group="{ name: 'SortableMkWidgets' }"
:class="$style['edit-editing']" :class="$style.editEditing"
@update:modelValue="v => emit('updateWidgets', v)" @update:modelValue="v => emit('updateWidgets', v)"
> >
<template #item="{element}"> <template #item="{element}">
<div :class="[$style.widget, $style['customize-container']]" data-cy-customize-container> <div :class="[$style.widget, $style.customizeContainer]" data-cy-customize-container>
<button :class="$style['customize-container-config']" class="_button" @click.prevent.stop="configWidget(element.id)"><i class="ti ti-settings"></i></button> <button :class="$style.customizeContainerConfig" class="_button" @click.prevent.stop="configWidget(element.id)"><i class="ti ti-settings"></i></button>
<button :class="$style['customize-container-remove']" data-cy-customize-container-remove class="_button" @click.prevent.stop="removeWidget(element)"><i class="ti ti-x"></i></button> <button :class="$style.customizeContainerRemove" data-cy-customize-container-remove class="_button" @click.prevent.stop="removeWidget(element)"><i class="ti ti-x"></i></button>
<div class="handle"> <div class="handle">
<component :is="`widget-${element.name}`" :ref="el => widgetRefs[element.id] = el" class="widget" :class="$style['customize-container-handle-widget']" :widget="element" @updateProps="updateWidget(element.id, $event)"/> <component :is="`widget-${element.name}`" :ref="el => widgetRefs[element.id] = el" class="widget" :class="$style.customizeContainerHandleWidget" :widget="element" @updateProps="updateWidget(element.id, $event)"/>
</div> </div>
</div> </div>
</template> </template>
@ -130,7 +130,7 @@ function onContextmenu(widget: Widget, ev: MouseEvent) {
} }
.edit { .edit {
&-header { &Header {
margin: 16px 0; margin: 16px 0;
> * { > * {
@ -139,17 +139,17 @@ function onContextmenu(widget: Widget, ev: MouseEvent) {
} }
} }
&-editing { &Editing {
min-height: 100px; min-height: 100px;
} }
} }
.customize-container { .customizeContainer {
position: relative; position: relative;
cursor: move; cursor: move;
&-config, &Config,
&-remove { &Remove {
position: absolute; position: absolute;
z-index: 10000; z-index: 10000;
top: 8px; top: 8px;
@ -160,17 +160,17 @@ function onContextmenu(widget: Widget, ev: MouseEvent) {
border-radius: 4px; border-radius: 4px;
} }
&-config { &Config {
right: 8px + 8px + 32px; right: 8px + 8px + 32px;
} }
&-remove { &Remove {
right: 8px; right: 8px;
} }
&-handle { &Handle {
&-widget { &Widget {
pointer-events: none; pointer-events: none;
} }
} }

View file

@ -5,7 +5,7 @@
<span :class="$style.text"><slot></slot></span> <span :class="$style.text"><slot></slot></span>
<span :class="$style.suffix"> <span :class="$style.suffix">
<span :class="$style.suffixText"><slot name="suffix"></slot></span> <span :class="$style.suffixText"><slot name="suffix"></slot></span>
<i class="ti ti-external-link" :class="$style.suffixIcon"></i> <i class="ti ti-external-link"></i>
</span> </span>
</a> </a>
<MkA v-else :class="[$style.main, { [$style.active]: active }]" class="_button" :to="to" :behavior="behavior"> <MkA v-else :class="[$style.main, { [$style.active]: active }]" class="_button" :to="to" :behavior="behavior">
@ -13,7 +13,7 @@
<span :class="$style.text"><slot></slot></span> <span :class="$style.text"><slot></slot></span>
<span :class="$style.suffix"> <span :class="$style.suffix">
<span :class="$style.suffixText"><slot name="suffix"></slot></span> <span :class="$style.suffixText"><slot name="suffix"></slot></span>
<i class="ti ti-chevron-right" :class="$style.suffixIcon"></i> <i class="ti ti-chevron-right"></i>
</span> </span>
</MkA> </MkA>
</div> </div>

View file

@ -1,7 +1,7 @@
<template> <template>
<div> <div>
<div :class="$style.label" @click="focus"><slot name="label"></slot></div> <div :class="$style.label" @click="focus"><slot name="label"></slot></div>
<div :class="$style.content"> <div>
<slot></slot> <slot></slot>
</div> </div>
<div :class="$style.caption"><slot name="caption"></slot></div> <div :class="$style.caption"><slot name="caption"></slot></div>

View file

@ -1,6 +1,14 @@
<template> <template>
<div v-if="chosen && !shouldHide" :class="$style.root"> <div v-if="chosen && !shouldHide" :class="$style.root">
<div v-if="!showMenu" :class="[$style.main, $style['form_' + chosen.place]]"> <div
v-if="!showMenu"
:class="[$style.main, {
[$style.form_square]: chosen.place === 'square',
[$style.form_horizontal]: chosen.place === 'horizontal',
[$style.form_horizontalBig]: chosen.place === 'horizontal-big',
[$style.form_vertical]: chosen.place === 'vertical',
}]"
>
<a :href="chosen.url" target="_blank" :class="$style.link"> <a :href="chosen.url" target="_blank" :class="$style.link">
<img :src="chosen.imageUrl" :class="$style.img"> <img :src="chosen.imageUrl" :class="$style.img">
<button class="_button" :class="$style.i" @click.prevent.stop="toggleMenu"><i :class="$style.iIcon" class="ti ti-info-circle"></i></button> <button class="_button" :class="$style.i" @click.prevent.stop="toggleMenu"><i :class="$style.iIcon" class="ti ti-info-circle"></i></button>
@ -122,7 +130,7 @@ function reduceFrequency(): void {
} }
} }
&.form_horizontal-big { &.form_horizontalBig {
padding: 8px; padding: 8px;
> .link, > .link,

View file

@ -7,7 +7,7 @@
import { computed } from 'vue'; import { computed } from 'vue';
import { getProxiedImageUrl, getStaticImageUrl } from '@/scripts/media-proxy'; import { getProxiedImageUrl, getStaticImageUrl } from '@/scripts/media-proxy';
import { defaultStore } from '@/store'; import { defaultStore } from '@/store';
import { customEmojis } from '@/custom-emojis'; import { customEmojisMap } from '@/custom-emojis';
const props = defineProps<{ const props = defineProps<{
name: string; name: string;
@ -26,7 +26,7 @@ const rawUrl = computed(() => {
return props.url; return props.url;
} }
if (isLocal.value) { if (isLocal.value) {
return customEmojis.value.find(x => x.name === customEmojiName.value)?.url ?? null; return customEmojisMap.get(customEmojiName.value)?.url ?? null;
} }
return props.host ? `/emoji/${customEmojiName.value}@${props.host}.webp` : `/emoji/${customEmojiName.value}.webp`; return props.host ? `/emoji/${customEmojiName.value}@${props.host}.webp` : `/emoji/${customEmojiName.value}.webp`;
}); });

View file

@ -14,6 +14,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import { onMounted, onUnmounted, provide, inject, Ref, ref, watch } from 'vue'; import { onMounted, onUnmounted, provide, inject, Ref, ref, watch } from 'vue';
import { $$ } from 'vue/macros';
import { CURRENT_STICKY_BOTTOM, CURRENT_STICKY_TOP } from '@/const'; import { CURRENT_STICKY_BOTTOM, CURRENT_STICKY_TOP } from '@/const';
const rootEl = $shallowRef<HTMLElement>(); const rootEl = $shallowRef<HTMLElement>();
@ -83,8 +84,8 @@ onMounted(() => {
onUnmounted(() => { onUnmounted(() => {
observer.disconnect(); observer.disconnect();
}); });
defineExpose({
rootEl: $$(rootEl),
});
</script> </script>
<style lang="scss" module>
</style>

View file

@ -6,7 +6,7 @@
<template v-if="!self"> <template v-if="!self">
<span :class="$style.schema">{{ schema }}//</span> <span :class="$style.schema">{{ schema }}//</span>
<span :class="$style.hostname">{{ hostname }}</span> <span :class="$style.hostname">{{ hostname }}</span>
<span v-if="port != ''" :class="$style.port">:{{ port }}</span> <span v-if="port != ''">:{{ port }}</span>
</template> </template>
<template v-if="pathname === '/' && self"> <template v-if="pathname === '/' && self">
<span :class="$style.self">{{ hostname }}</span> <span :class="$style.self">{{ hostname }}</span>

View file

@ -1,5 +1,5 @@
<template> <template>
<component :is="'x-' + block.type" :key="block.id" :page="page" :block="block" :h="h"/> <component :is="getComponent(block.type)" :key="block.id" :page="page" :block="block" :h="h"/>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
@ -11,6 +11,16 @@ import XImage from './page.image.vue';
import XNote from './page.note.vue'; import XNote from './page.note.vue';
import { Block } from './block.type'; import { Block } from './block.type';
function getComponent(type: string) {
switch (type) {
case 'text': return XText;
case 'section': return XSection;
case 'image': return XImage;
case 'note': return XNote;
default: return null;
}
}
defineProps<{ defineProps<{
block: Block, block: Block,
h: number, h: number,

View file

@ -1,6 +1,15 @@
<template> <template>
<section> <section>
<component :is="'h' + h" :class="h < 5 ? $style['h' + h] : null">{{ block.title }}</component> <component
:is="'h' + h"
:class="{
'h2': h === 2,
'h3': h === 3,
'h4': h === 4,
}"
>
{{ block.title }}
</component>
<div class="_gaps"> <div class="_gaps">
<XBlock v-for="child in block.children" :key="child.id" :page="page" :block="child" :h="h + 1"/> <XBlock v-for="child in block.children" :key="child.id" :page="page" :block="child" :h="h + 1"/>

View file

@ -1,6 +1,6 @@
<template> <template>
<div :class="{ [$style.center]: page.alignCenter, [$style.serif]: page.font === 'serif' }"> <div :class="{ [$style.center]: page.alignCenter, [$style.serif]: page.font === 'serif' }">
<XBlock v-for="child in page.content" :key="child.id" :block="child" :h="2"/> <XBlock v-for="child in page.content" :key="child.id" :page="page" :block="child" :h="2"/>
</div> </div>
</template> </template>

View file

@ -1,4 +1,4 @@
import { shallowRef, computed, markRaw } from 'vue'; import { shallowRef, computed, markRaw, watch } from 'vue';
import * as Misskey from 'misskey-js'; import * as Misskey from 'misskey-js';
import { api, apiGet } from './os'; import { api, apiGet } from './os';
import { useStream } from '@/stream'; import { useStream } from '@/stream';
@ -16,6 +16,14 @@ export const customEmojiCategories = computed<[ ...string[], null ]>(() => {
return markRaw([...Array.from(categories), null]); return markRaw([...Array.from(categories), null]);
}); });
export const customEmojisMap = new Map<string, Misskey.entities.CustomEmoji>();
watch(customEmojis, emojis => {
customEmojisMap.clear();
for (const emoji of emojis) {
customEmojisMap.set(emoji.name, emoji);
}
}, { immediate: true });
// TODO: ここら辺副作用なのでいい感じにする // TODO: ここら辺副作用なのでいい感じにする
const stream = useStream(); const stream = useStream();

File diff suppressed because it is too large Load diff

View file

@ -149,6 +149,12 @@ const patronsWithIcon = [{
}, { }, {
name: 'かみらえっと', name: 'かみらえっと',
icon: 'https://misskey-hub.net/patrons/be1326bda7d940a482f3758ffd9ffaf6.jpg', icon: 'https://misskey-hub.net/patrons/be1326bda7d940a482f3758ffd9ffaf6.jpg',
}, {
name: 'へてて',
icon: 'https://misskey-hub.net/patrons/0431eacd7c6843d09de8ea9984307e86.jpg',
}, {
name: 'spinlock',
icon: 'https://misskey-hub.net/patrons/6a1cebc819d540a78bf20e9e3115baa8.jpg',
}]; }];
const patrons = [ const patrons = [

View file

@ -1,5 +1,5 @@
<template> <template>
<div :class="$style.root" class="_gaps"> <div class="_gaps">
<div :class="$style.header"> <div :class="$style.header">
<MkSelect v-model="type" :class="$style.typeSelect"> <MkSelect v-model="type" :class="$style.typeSelect">
<option value="isLocal">{{ i18n.ts._role._condition.isLocal }}</option> <option value="isLocal">{{ i18n.ts._role._condition.isLocal }}</option>
@ -24,7 +24,7 @@
</button> </button>
</div> </div>
<div v-if="type === 'and' || type === 'or'" :class="$style.values" class="_gaps"> <div v-if="type === 'and' || type === 'or'" class="_gaps">
<Sortable v-model="v.values" tag="div" class="_gaps" itemKey="id" handle=".drag-handle" :group="{ name: 'roleFormula' }" :animation="150" :swapThreshold="0.5"> <Sortable v-model="v.values" tag="div" class="_gaps" itemKey="id" handle=".drag-handle" :group="{ name: 'roleFormula' }" :animation="150" :swapThreshold="0.5">
<template #item="{element}"> <template #item="{element}">
<div :class="$style.item"> <div :class="$style.item">
@ -118,10 +118,6 @@ function removeSelf() {
</script> </script>
<style lang="scss" module> <style lang="scss" module>
.root {
}
.header { .header {
display: flex; display: flex;
} }
@ -148,8 +144,4 @@ function removeSelf() {
border-color: var(--accent); border-color: var(--accent);
} }
} }
.values {
}
</style> </style>

View file

@ -29,7 +29,7 @@
<template #label>Errored instances</template> <template #label>Errored instances</template>
<template #suffix>({{ number(jobs.reduce((a, b) => a + b[1], 0)) }} jobs)</template> <template #suffix>({{ number(jobs.reduce((a, b) => a + b[1], 0)) }} jobs)</template>
<div :class="$style.jobs"> <div>
<div v-if="jobs.length > 0"> <div v-if="jobs.length > 0">
<div v-for="job in jobs" :key="job[0]"> <div v-for="job in jobs" :key="job[0]">
<MkA :to="`/instance-info/${job[0]}`" behavior="window">{{ job[0] }}</MkA> <MkA :to="`/instance-info/${job[0]}`" behavior="window">{{ job[0] }}</MkA>
@ -150,7 +150,4 @@ onUnmounted(() => {
font-size: 80%; font-size: 80%;
opacity: 0.6; opacity: 0.6;
} }
.jobs {
}
</style> </style>

View file

@ -27,7 +27,7 @@
</Sortable> </Sortable>
<div :class="$style.commands"> <div :class="$style.commands">
<MkButton rounded @click="serverRules.push('')"><i class="ti ti-plus"></i> {{ i18n.ts.add }}</MkButton> <MkButton rounded @click="serverRules.push('')"><i class="ti ti-plus"></i> {{ i18n.ts.add }}</MkButton>
<MkButton primary rounded :class="$style.buttonSave" @click="save"><i class="ti ti-check"></i> {{ i18n.ts.save }}</MkButton> <MkButton primary rounded @click="save"><i class="ti ti-check"></i> {{ i18n.ts.save }}</MkButton>
</div> </div>
</div> </div>
</MkSpacer> </MkSpacer>

View file

@ -3,7 +3,7 @@
<template #item="{element}"> <template #item="{element}">
<div :class="$style.item"> <div :class="$style.item">
<!-- divが無いとエラーになる https://github.com/SortableJS/vue.draggable.next/issues/189 --> <!-- divが無いとエラーになる https://github.com/SortableJS/vue.draggable.next/issues/189 -->
<component :is="'x-' + element.type" :modelValue="element" @update:modelValue="updateItem" @remove="() => removeItem(element)"/> <component :is="getComponent(element.type)" :modelValue="element" @update:modelValue="updateItem" @remove="() => removeItem(element)"/>
</div> </div>
</template> </template>
</Sortable> </Sortable>
@ -16,6 +16,16 @@ import XText from './els/page-editor.el.text.vue';
import XImage from './els/page-editor.el.image.vue'; import XImage from './els/page-editor.el.image.vue';
import XNote from './els/page-editor.el.note.vue'; import XNote from './els/page-editor.el.note.vue';
function getComponent(type: string) {
switch (type) {
case 'section': return XSection;
case 'text': return XText;
case 'image': return XImage;
case 'note': return XNote;
default: return null;
}
}
const Sortable = defineAsyncComponent(() => import('vuedraggable').then(x => x.default)); const Sortable = defineAsyncComponent(() => import('vuedraggable').then(x => x.default));
const props = defineProps<{ const props = defineProps<{

View file

@ -4,8 +4,8 @@
<template #label>{{ i18n.ts.usageAmount }}</template> <template #label>{{ i18n.ts.usageAmount }}</template>
<div class="_gaps_m"> <div class="_gaps_m">
<div class="uawsfosz"> <div>
<div class="meter"><div :style="meterStyle"></div></div> <div :class="$style.meter"><div :class="$style.meterValue" :style="meterStyle"></div></div>
</div> </div>
<FormSplit> <FormSplit>
<MkKeyValue> <MkKeyValue>
@ -139,22 +139,16 @@ definePageMetadata({
}); });
</script> </script>
<style lang="scss" scoped> <style lang="scss" module>
.meter {
@use "sass:math"; height: 10px;
.uawsfosz {
> .meter {
$size: 12px;
background: rgba(0, 0, 0, 0.1); background: rgba(0, 0, 0, 0.1);
border-radius: math.div($size, 2); border-radius: 999px;
overflow: hidden; overflow: clip;
}
> div { .meterValue {
height: $size; height: 100%;
border-radius: math.div($size, 2); border-radius: 999px;
}
}
} }
</style> </style>

View file

@ -24,6 +24,7 @@
<div class="_gaps_s"> <div class="_gaps_s">
<MkSwitch v-model="showFixedPostForm">{{ i18n.ts.showFixedPostForm }}</MkSwitch> <MkSwitch v-model="showFixedPostForm">{{ i18n.ts.showFixedPostForm }}</MkSwitch>
<MkSwitch v-model="showFixedPostFormInChannel">{{ i18n.ts.showFixedPostFormInChannel }}</MkSwitch> <MkSwitch v-model="showFixedPostFormInChannel">{{ i18n.ts.showFixedPostFormInChannel }}</MkSwitch>
<MkSwitch v-model="showTimelineReplies">{{ i18n.ts.flagShowTimelineReplies }}<template #caption>{{ i18n.ts.flagShowTimelineRepliesDescription }} {{ i18n.ts.reflectMayTakeTime }}</template></MkSwitch>
</div> </div>
</FormSection> </FormSection>
@ -147,7 +148,13 @@
<template #label>{{ i18n.ts.other }}</template> <template #label>{{ i18n.ts.other }}</template>
<div class="_gaps"> <div class="_gaps">
<MkSwitch v-model="showTimelineReplies">{{ i18n.ts.flagShowTimelineReplies }}<template #caption>{{ i18n.ts.flagShowTimelineRepliesDescription }} {{ i18n.ts.reflectMayTakeTime }}</template></MkSwitch> <MkFolder>
<template #label>{{ i18n.ts.additionalEmojiDictionary }}</template>
<div v-for="lang in emojiIndexLangs" class="_buttons">
<MkButton @click="downloadEmojiIndex(lang)"><i class="ti ti-download"></i> {{ lang }}{{ defaultStore.reactiveState.additionalUnicodeEmojiIndexes.value[lang] ? ` (${ i18n.ts.installed })` : '' }}</MkButton>
<MkButton v-if="defaultStore.reactiveState.additionalUnicodeEmojiIndexes.value[lang]" danger @click="removeEmojiIndex(lang)"><i class="ti ti-trash"></i> {{ i18n.ts.remove }}</MkButton>
</div>
</MkFolder>
<FormLink to="/settings/deck">{{ i18n.ts.deck }}</FormLink> <FormLink to="/settings/deck">{{ i18n.ts.deck }}</FormLink>
<FormLink to="/settings/custom-css"><template #icon><i class="ti ti-code"></i></template>{{ i18n.ts.customCss }}</FormLink> <FormLink to="/settings/custom-css"><template #icon><i class="ti ti-code"></i></template>{{ i18n.ts.customCss }}</FormLink>
</div> </div>
@ -161,6 +168,8 @@ import MkSwitch from '@/components/MkSwitch.vue';
import MkSelect from '@/components/MkSelect.vue'; import MkSelect from '@/components/MkSelect.vue';
import MkRadios from '@/components/MkRadios.vue'; import MkRadios from '@/components/MkRadios.vue';
import MkRange from '@/components/MkRange.vue'; import MkRange from '@/components/MkRange.vue';
import MkFolder from '@/components/MkFolder.vue';
import MkButton from '@/components/MkButton.vue';
import FormSection from '@/components/form/section.vue'; import FormSection from '@/components/form/section.vue';
import FormLink from '@/components/form/link.vue'; import FormLink from '@/components/form/link.vue';
import MkLink from '@/components/MkLink.vue'; import MkLink from '@/components/MkLink.vue';
@ -253,6 +262,34 @@ watch([
await reloadAsk(); await reloadAsk();
}); });
const emojiIndexLangs = ['en-US'];
function downloadEmojiIndex(lang: string) {
async function main() {
const currentIndexes = defaultStore.state.additionalUnicodeEmojiIndexes;
function download() {
switch (lang) {
case 'en-US': return import('../../unicode-emoji-indexes/en-US.json').then(x => x.default);
default: throw new Error('unrecognized lang: ' + lang);
}
}
currentIndexes[lang] = await download();
await defaultStore.set('additionalUnicodeEmojiIndexes', currentIndexes);
}
os.promiseDialog(main());
}
function removeEmojiIndex(lang: string) {
async function main() {
const currentIndexes = defaultStore.state.additionalUnicodeEmojiIndexes;
delete currentIndexes[lang];
await defaultStore.set('additionalUnicodeEmojiIndexes', currentIndexes);
}
os.promiseDialog(main());
}
const headerActions = $computed(() => []); const headerActions = $computed(() => []);
const headerTabs = $computed(() => []); const headerTabs = $computed(() => []);

View file

@ -32,7 +32,7 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { computed, onMounted, onUnmounted, useCssModule } from 'vue'; import { computed, onMounted, onUnmounted } from 'vue';
import { v4 as uuid } from 'uuid'; import { v4 as uuid } from 'uuid';
import FormSection from '@/components/form/section.vue'; import FormSection from '@/components/form/section.vue';
import MkButton from '@/components/MkButton.vue'; import MkButton from '@/components/MkButton.vue';
@ -48,8 +48,6 @@ import { definePageMetadata } from '@/scripts/page-metadata';
import { miLocalStorage } from '@/local-storage'; import { miLocalStorage } from '@/local-storage';
const { t, ts } = i18n; const { t, ts } = i18n;
useCssModule();
const defaultStoreSaveKeys: (keyof typeof defaultStore['state'])[] = [ const defaultStoreSaveKeys: (keyof typeof defaultStore['state'])[] = [
'menu', 'menu',
'visibility', 'visibility',

View file

@ -3,7 +3,7 @@
<div :class="$style.avatarAndBanner" :style="{ backgroundImage: $i.bannerUrl ? `url(${ $i.bannerUrl })` : null }"> <div :class="$style.avatarAndBanner" :style="{ backgroundImage: $i.bannerUrl ? `url(${ $i.bannerUrl })` : null }">
<div :class="$style.avatarContainer"> <div :class="$style.avatarContainer">
<MkAvatar :class="$style.avatar" :user="$i" @click="changeAvatar"/> <MkAvatar :class="$style.avatar" :user="$i" @click="changeAvatar"/>
<MkButton primary rounded :class="$style.avatarEdit" @click="changeAvatar">{{ i18n.ts._profile.changeAvatar }}</MkButton> <MkButton primary rounded @click="changeAvatar">{{ i18n.ts._profile.changeAvatar }}</MkButton>
</div> </div>
<MkButton primary rounded :class="$style.bannerEdit" @click="changeBanner">{{ i18n.ts._profile.changeBanner }}</MkButton> <MkButton primary rounded :class="$style.bannerEdit" @click="changeBanner">{{ i18n.ts._profile.changeBanner }}</MkButton>
</div> </div>
@ -271,10 +271,6 @@ definePageMetadata({
margin: 0 auto 16px auto; margin: 0 auto 16px auto;
} }
.avatarEdit {
}
.bannerEdit { .bannerEdit {
position: absolute; position: absolute;
top: 16px; top: 16px;

View file

@ -1,5 +1,5 @@
<template> <template>
<div :class="$style.root"> <div>
<MkAnimBg style="position: fixed; top: 0;"/> <MkAnimBg style="position: fixed; top: 0;"/>
<div :class="$style.formContainer"> <div :class="$style.formContainer">
<form :class="$style.form" class="_panel" @submit.prevent="submit()"> <form :class="$style.form" class="_panel" @submit.prevent="submit()">
@ -53,9 +53,6 @@ function submit() {
</script> </script>
<style lang="scss" module> <style lang="scss" module>
.root {
}
.formContainer { .formContainer {
min-height: 100svh; min-height: 100svh;
padding: 32px 32px 64px 32px; padding: 32px 32px 64px 32px;

View file

@ -1,5 +1,5 @@
<template> <template>
<div :class="$style.root"> <div>
<MkAnimBg style="position: fixed; top: 0;"/> <MkAnimBg style="position: fixed; top: 0;"/>
<div :class="$style.formContainer"> <div :class="$style.formContainer">
<form :class="$style.form" class="_panel" @submit.prevent="submit()"> <form :class="$style.form" class="_panel" @submit.prevent="submit()">
@ -64,9 +64,6 @@ function submit() {
</script> </script>
<style lang="scss" module> <style lang="scss" module>
.root {
}
.formContainer { .formContainer {
min-height: 100svh; min-height: 100svh;
padding: 32px 32px 64px 32px; padding: 32px 32px 64px 32px;

View file

@ -3,7 +3,7 @@
<div ref="scrollEl" :class="[$style.scrollbox, { [$style.scroll]: isScrolling }]"> <div ref="scrollEl" :class="[$style.scrollbox, { [$style.scroll]: isScrolling }]">
<div v-for="note in notes" :key="note.id" :class="$style.note"> <div v-for="note in notes" :key="note.id" :class="$style.note">
<div class="_panel" :class="$style.content"> <div class="_panel" :class="$style.content">
<div :class="$style.body"> <div>
<MkA v-if="note.replyId" class="reply" :to="`/notes/${note.replyId}`"><i class="ti ti-arrow-back-up"></i></MkA> <MkA v-if="note.replyId" class="reply" :to="`/notes/${note.replyId}`"><i class="ti ti-arrow-back-up"></i></MkA>
<Mfm v-if="note.text" :text="note.text" :author="note.user" :i="$i"/> <Mfm v-if="note.text" :text="note.text" :author="note.user" :i="$i"/>
<MkA v-if="note.renoteId" class="rp" :to="`/notes/${note.renoteId}`">RN: ...</MkA> <MkA v-if="note.renoteId" class="rp" :to="`/notes/${note.renoteId}`">RN: ...</MkA>

View file

@ -2,7 +2,6 @@ export const unicodeEmojiCategories = ['face', 'people', 'animals_and_nature', '
export type UnicodeEmojiDef = { export type UnicodeEmojiDef = {
name: string; name: string;
keywords: string[];
char: string; char: string;
category: typeof unicodeEmojiCategories[number]; category: typeof unicodeEmojiCategories[number];
} }
@ -10,11 +9,16 @@ export type UnicodeEmojiDef = {
// initial converted from https://github.com/muan/emojilib/commit/242fe68be86ed6536843b83f7e32f376468b38fb // initial converted from https://github.com/muan/emojilib/commit/242fe68be86ed6536843b83f7e32f376468b38fb
import _emojilist from '../emojilist.json'; import _emojilist from '../emojilist.json';
export const emojilist = _emojilist as UnicodeEmojiDef[]; export const emojilist: UnicodeEmojiDef[] = _emojilist.map(x => ({
name: x[1] as string,
char: x[0] as string,
category: unicodeEmojiCategories[x[2]],
}));
const _indexByChar = new Map<string, number>(); const _indexByChar = new Map<string, number>();
const _charGroupByCategory = new Map<string, string[]>(); const _charGroupByCategory = new Map<string, string[]>();
emojilist.forEach((emo, i) => { for (let i = 0; i < emojilist.length; i++) {
const emo = emojilist[i];
_indexByChar.set(emo.char, i); _indexByChar.set(emo.char, i);
if (_charGroupByCategory.has(emo.category)) { if (_charGroupByCategory.has(emo.category)) {
@ -22,14 +26,14 @@ emojilist.forEach((emo, i) => {
} else { } else {
_charGroupByCategory.set(emo.category, [emo.char]); _charGroupByCategory.set(emo.category, [emo.char]);
} }
}); }
export const emojiCharByCategory = _charGroupByCategory; export const emojiCharByCategory = _charGroupByCategory;
export function getEmojiName(char: string): string | undefined { export function getEmojiName(char: string): string | null {
const idx = _indexByChar.get(char); const idx = _indexByChar.get(char);
if (typeof idx === 'undefined') { if (idx == null) {
return undefined; return null;
} else { } else {
return emojilist[idx].name; return emojilist[idx].name;
} }

View file

@ -73,7 +73,7 @@ export function getDriveFileMenu(file: Misskey.entities.DriveFile) {
action: () => rename(file), action: () => rename(file),
}, { }, {
text: file.isSensitive ? i18n.ts.unmarkAsSensitive : i18n.ts.markAsSensitive, text: file.isSensitive ? i18n.ts.unmarkAsSensitive : i18n.ts.markAsSensitive,
icon: file.isSensitive ? 'ti ti-eye' : 'ti ti-eye-off', icon: file.isSensitive ? 'ti ti-eye' : 'ti ti-eye-exclamation',
action: () => toggleSensitive(file), action: () => toggleSensitive(file),
}, { }, {
text: i18n.ts.describeFile, text: i18n.ts.describeFile,

View file

@ -336,7 +336,11 @@ export const defaultStore = markRaw(new Storage('base', {
}, },
enableCondensedLineForAcct: { enableCondensedLineForAcct: {
where: 'device', where: 'device',
default: true, default: false,
},
additionalUnicodeEmojiIndexes: {
where: 'device',
default: {} as Record<string, Record<string, string[]>>,
}, },
})); }));

View file

@ -22,11 +22,7 @@
} }
html { html {
touch-action: manipulation;
background-color: var(--bg); background-color: var(--bg);
background-attachment: fixed;
background-size: cover;
background-position: center;
color: var(--fg); color: var(--fg);
accent-color: var(--accent); accent-color: var(--accent);
overflow: auto; overflow: auto;
@ -38,7 +34,7 @@ html {
tab-size: 2; tab-size: 2;
&, * { &, * {
scrollbar-color: var(--scrollbarHandle) inherit; scrollbar-color: var(--scrollbarHandle) transparent;
scrollbar-width: thin; scrollbar-width: thin;
&::-webkit-scrollbar { &::-webkit-scrollbar {
@ -87,6 +83,7 @@ html._themeChanging_ {
} }
html, body { html, body {
touch-action: manipulation;
margin: 0; margin: 0;
padding: 0; padding: 0;
scroll-behavior: smooth; scroll-behavior: smooth;

View file

@ -21,6 +21,7 @@
fgTransparent: ':alpha<0.5<@fg', fgTransparent: ':alpha<0.5<@fg',
fgHighlighted: ':lighten<3<@fg', fgHighlighted: ':lighten<3<@fg',
fgOnAccent: '#fff', fgOnAccent: '#fff',
fgOnWhite: '#333',
divider: 'rgba(255, 255, 255, 0.1)', divider: 'rgba(255, 255, 255, 0.1)',
indicator: '@accent', indicator: '@accent',
panel: ':lighten<3<@bg', panel: ':lighten<3<@bg',

View file

@ -21,6 +21,7 @@
fgTransparent: ':alpha<0.5<@fg', fgTransparent: ':alpha<0.5<@fg',
fgHighlighted: ':darken<3<@fg', fgHighlighted: ':darken<3<@fg',
fgOnAccent: '#fff', fgOnAccent: '#fff',
fgOnWhite: '#333',
divider: 'rgba(0, 0, 0, 0.1)', divider: 'rgba(0, 0, 0, 0.1)',
indicator: '@accent', indicator: '@accent',
panel: ':lighten<3<@bg', panel: ':lighten<3<@bg',

View file

@ -53,6 +53,7 @@
panelHeaderBg: ':lighten<3<@panel', panelHeaderBg: ':lighten<3<@panel',
panelHeaderFg: '@fg', panelHeaderFg: '@fg',
htmlThemeColor: '@bg', htmlThemeColor: '@bg',
fgOnWhite: '@accent',
panelHighlight: ':lighten<3<@panel', panelHighlight: ':lighten<3<@panel',
listItemHoverBg: 'rgba(255, 255, 255, 0.03)', listItemHoverBg: 'rgba(255, 255, 255, 0.03)',
scrollbarHandle: 'rgba(255, 255, 255, 0.2)', scrollbarHandle: 'rgba(255, 255, 255, 0.2)',

View file

@ -11,6 +11,7 @@
bg: 'rgb(37, 38, 36)', bg: 'rgb(37, 38, 36)',
fg: 'rgb(216, 212, 199)', fg: 'rgb(216, 212, 199)',
fgHighlighted: '#fff', fgHighlighted: '#fff',
fgOnWhite: '@accent',
divider: 'rgba(255, 255, 255, 0.14)', divider: 'rgba(255, 255, 255, 0.14)',
panel: 'rgb(47, 47, 44)', panel: 'rgb(47, 47, 44)',
panelHeaderDivider: 'rgba(0, 0, 0, 0)', panelHeaderDivider: 'rgba(0, 0, 0, 0)',

View file

@ -10,6 +10,7 @@
accent: 'rgb(255, 89, 117)', accent: 'rgb(255, 89, 117)',
bg: 'rgb(28, 28, 37)', bg: 'rgb(28, 28, 37)',
fg: 'rgb(236, 239, 244)', fg: 'rgb(236, 239, 244)',
fgOnWhite: '@accent',
panel: 'rgb(35, 35, 47)', panel: 'rgb(35, 35, 47)',
renote: '@accent', renote: '@accent',
link: '@accent', link: '@accent',

View file

@ -11,6 +11,7 @@
bg: '#232323', bg: '#232323',
fg: 'rgb(199, 209, 216)', fg: 'rgb(199, 209, 216)',
fgHighlighted: '#fff', fgHighlighted: '#fff',
fgOnWhite: '@accent',
divider: 'rgba(255, 255, 255, 0.14)', divider: 'rgba(255, 255, 255, 0.14)',
panel: '#2d2d2d', panel: '#2d2d2d',
panelHeaderDivider: 'rgba(0, 0, 0, 0)', panelHeaderDivider: 'rgba(0, 0, 0, 0)',

View file

@ -12,6 +12,7 @@
fg: '#D5D5D6', fg: '#D5D5D6',
fgHighlighted: '#fff', fgHighlighted: '#fff',
fgOnAccent: '#000', fgOnAccent: '#000',
fgOnWhite: '@accent',
divider: 'rgba(255, 255, 255, 0.1)', divider: 'rgba(255, 255, 255, 0.1)',
panel: '#18181c', panel: '#18181c',
panelHeaderDivider: 'rgba(0, 0, 0, 0)', panelHeaderDivider: 'rgba(0, 0, 0, 0)',

View file

@ -12,6 +12,7 @@
fg: '#dee7e4', fg: '#dee7e4',
fgHighlighted: '#fff', fgHighlighted: '#fff',
fgOnAccent: '#192320', fgOnAccent: '#192320',
fgOnWhite: '@accent',
divider: '#e7fffb24', divider: '#e7fffb24',
panel: '#192320', panel: '#192320',
panelHeaderDivider: 'rgba(0, 0, 0, 0)', panelHeaderDivider: 'rgba(0, 0, 0, 0)',

View file

@ -12,6 +12,7 @@
fg: '#dee7e4', fg: '#dee7e4',
fgHighlighted: '#fff', fgHighlighted: '#fff',
fgOnAccent: '#192320', fgOnAccent: '#192320',
fgOnWhite: '@accent',
divider: '#e7fffb24', divider: '#e7fffb24',
panel: '#192320', panel: '#192320',
panelHeaderDivider: 'rgba(0, 0, 0, 0)', panelHeaderDivider: 'rgba(0, 0, 0, 0)',

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