Merge branch 'develop' of https://codeberg.org/calckey/calckey into note-improvements

This commit is contained in:
Freeplay 2023-03-15 18:40:21 -04:00
commit 9e2bcbbb2b
299 changed files with 1864 additions and 22470 deletions

View file

@ -96,6 +96,9 @@ id: 'aid'
# Max note length, should be < 8000.
#maxNoteLength: 3000
# Maximum lenght of an image caption or file comment (default 1500, max 8192)
#maxCaptionLength: 1500
# Whether disable HSTS
#disableHsts: true

View file

@ -19,6 +19,8 @@ git remote set-url origin https://codeberg.org/calckey/calckey.git
git fetch
git checkout main # or beta or develop
git pull --ff
NODE_ENV=production pnpm run migrate
# build using prefered method
```
@ -29,6 +31,8 @@ git remote set-url origin https://codeberg.org/calckey/calckey.git
git fetch
git checkout main # or beta or develop
git pull --ff
NODE_ENV=production pnpm run migrate
# build using prefered method
```
@ -48,5 +52,7 @@ git remote set-url origin https://codeberg.org/calckey/calckey.git
git fetch
git checkout main # or beta or develop
git pull --ff
NODE_ENV=production pnpm run migrate
# build using prefered method
```

View file

@ -24,10 +24,6 @@ gulp.task('copy:client:fonts', () =>
gulp.src('./packages/client/node_modules/three/examples/fonts/**/*').pipe(gulp.dest('./built/_client_dist_/fonts/'))
);
gulp.task('copy:client:phosphor', () =>
gulp.src('./node_modules/phosphor-icons/src/fonts/*').pipe(gulp.dest('./built/_client_dist_/phosphor/'))
);
gulp.task('copy:client:locales', cb => {
fs.mkdirSync('./built/_client_dist_/locales', { recursive: true });
@ -59,7 +55,7 @@ gulp.task('build:backend:style', () => {
});
gulp.task('build', gulp.parallel(
'copy:client:locales', 'copy:backend:views', 'copy:backend:custom', 'build:backend:script', 'build:backend:style', 'copy:client:fonts', 'copy:client:phosphor'
'copy:client:locales', 'copy:backend:views', 'copy:backend:custom', 'build:backend:script', 'build:backend:style', 'copy:client:fonts'
));
gulp.task('default', gulp.task('build'));

View file

@ -69,8 +69,8 @@ exportRequested: "Has sol·licitat una exportació. Això pot trigar una estona.
importRequested: "Has sol·licitat una importació. Això pot trigar una estona."
lists: "Llistes"
noLists: "No tens cap llista"
note: "Nota"
notes: "Notes"
note: "Post"
notes: "Posts"
following: "Seguint"
followers: "Seguidors"
followsYou: "Et segueix"
@ -141,7 +141,7 @@ _theme:
mention: "Menció"
renote: "Renotar"
_sfx:
note: "Notes"
note: "Posts"
notification: "Notificacions"
_2fa:
step2Url: "També pots inserir aquest enllaç i utilitzes una aplicació d'escriptori:"

View file

@ -1806,7 +1806,7 @@ _apps:
pwa: "Install PWA"
kaiteki: "Kaiteki"
milktea: "Milktea"
subwayTooter: "Subway Tooter"
kimis: "Kimis"
missLi: "MissLi"
mona: "Mona"
theDesk: "TheDesk"
lesskey: "Lesskey"

View file

@ -70,8 +70,8 @@ exportRequested: "Vous avez demandé une exportation. Lopération pourrait pr
importRequested: "Vous avez initié un import. Cela pourrait prendre un peu de temps."
lists: "Listes"
noLists: "Vous navez aucune liste"
note: "Notes"
notes: "Notes"
note: "Post"
notes: "Posts"
following: "Abonnements"
followers: "Abonné·e·s"
followsYou: "Vous suit"

View file

@ -1,7 +1,7 @@
---
_lang_: "日本語"
headlineMisskey: "ノートでつながるネットワーク"
introMisskey: "ようこそMisskeyは、オープンソースの分散型マイクロブログサービスです。\n「ート」を作成して、いま起こっていることを共有したり、あなたについて皆に発信しよう📡\n「リアクション」機能で、皆のートに素早く反応を追加することもできます👍\n新しい世界を探検しよう🚀"
headlineMisskey: "ずっと無料でオープンソースの非中央集権型ソーシャルメディアプラットフォーム🚀"
introMisskey: "ようこそCalckeyは、オープンソースの非中央集権型ソーシャルメディアプラットフォームです。\nいま起こっていることを共有したり、あなたについて皆に発信しよう📡\n「リアクション」機能で、皆の投稿に素早く反応を追加することもできます👍\n新しい世界を探検しよう🚀"
monthAndDay: "{month}月 {day}日"
search: "検索"
notifications: "通知"
@ -10,11 +10,11 @@ password: "パスワード"
forgotPassword: "パスワードを忘れた"
fetchingAsApObject: "連合に照会中"
ok: "OK"
gotIt: "わかった"
gotIt: "わかった"
cancel: "キャンセル"
enterUsername: "ユーザー名を入力"
renotedBy: "{user}がRenote"
noNotes: "ノートはありません"
renotedBy: "{user}がブースト"
noNotes: "投稿はありません"
noNotifications: "通知はありません"
instance: "インスタンス"
settings: "設定"
@ -32,6 +32,7 @@ uploading: "アップロード中"
save: "保存"
users: "ユーザー"
addUser: "ユーザーを追加"
addInstance: "インスタンスを追加"
favorite: "お気に入り"
favorites: "お気に入り"
unfavorite: "お気に入り解除"
@ -44,7 +45,7 @@ copyContent: "内容をコピー"
copyLink: "リンクをコピー"
delete: "削除"
deleteAndEdit: "削除して編集"
deleteAndEditConfirm: "このートを削除してもう一度編集しますかこのートへのリアクション、Renote、返信も全て削除されます。"
deleteAndEditConfirm: "この投稿を削除してもう一度編集しますか?この投稿へのリアクション、ブースト、返信も全て削除されます。"
addToList: "リストに追加"
sendMessage: "メッセージを送信"
copyUsername: "ユーザー名をコピー"
@ -64,14 +65,14 @@ import: "インポート"
export: "エクスポート"
files: "ファイル"
download: "ダウンロード"
driveFileDeleteConfirm: "ファイル「{name}」を削除しますか?このファイルを添付したノートも消えます。"
driveFileDeleteConfirm: "ファイル「{name}」を削除しますか?このファイルを添付した投稿も消えます。"
unfollowConfirm: "{name}のフォローを解除しますか?"
exportRequested: "エクスポートをリクエストしました。これには時間がかかる場合があります。エクスポートが終わると、「ドライブ」に追加されます。"
importRequested: "インポートをリクエストしました。これには時間がかかる場合があります。"
lists: "リスト"
noLists: "リストはありません"
note: "ノート"
notes: "ノート"
note: "投稿"
notes: "投稿"
following: "フォロー"
followers: "フォロワー"
followsYou: "フォローされています"
@ -94,13 +95,13 @@ followRequests: "フォロー申請"
unfollow: "フォロー解除"
followRequestPending: "フォロー許可待ち"
enterEmoji: "絵文字を入力"
renote: "Renote"
unrenote: "Renote解除"
renoted: "Renoteしました。"
cantRenote: "この投稿はRenoteできません。"
cantReRenote: "RenoteをRenoteすることはできません。"
renote: "ブースト"
unrenote: "ブースト解除"
renoted: "ブーストしました。"
cantRenote: "この投稿はブーストできません。"
cantReRenote: "ブーストをブーストすることはできません。"
quote: "引用"
pinnedNote: "ピン留めされたノート"
pinnedNote: "ピン留めされた投稿"
pinned: "ピン留め"
you: "あなた"
clickToShow: "クリックして表示"
@ -139,11 +140,11 @@ settingGuide: "おすすめ設定"
cacheRemoteFiles: "リモートのファイルをキャッシュする"
cacheRemoteFilesDescription: "この設定を無効にすると、リモートファイルをキャッシュせず直リンクするようになります。サーバーのストレージを節約できますが、サムネイルが生成されないので通信量が増加します。"
flagAsBot: "Botとして設定"
flagAsBotDescription: "このアカウントがプログラムによって運用される場合は、このフラグをオンにします。オンにすると、反応の連鎖を防ぐためのフラグとして他の開発者に役立ったり、Misskeyのシステム上での扱いがBotに合ったものになります。"
flagAsCat: "Catとして設定"
flagAsBotDescription: "このアカウントがプログラムによって運用される場合は、このフラグをオンにします。オンにすると、反応の連鎖を防ぐためのフラグとして他の開発者に役立ったり、Calckeyのシステム上での扱いがBotに合ったものになります。"
flagAsCat: "あなたは…猫?😺"
flagAsCatDescription: "このアカウントが猫であることを示す場合は、このフラグをオンにします。"
flagShowTimelineReplies: "タイムラインにノートへの返信を表示する"
flagShowTimelineRepliesDescription: "オンにすると、タイムラインにユーザーのノート以外にもそのユーザーの他のノートへの返信を表示します。"
flagShowTimelineReplies: "タイムラインに投稿の返信を表示する"
flagShowTimelineRepliesDescription: "オンにすると、タイムラインにユーザーの投稿以外にもそのユーザーの他の投稿への返信を表示します。"
autoAcceptFollowed: "フォロー中ユーザーからのフォロリクを自動承認"
addAccount: "アカウントを追加"
loginFailed: "ログインに失敗しました"
@ -160,6 +161,7 @@ proxyAccount: "プロキシアカウント"
proxyAccountDescription: "プロキシアカウントは、特定の条件下でユーザーのリモートフォローを代行するアカウントです。例えば、ユーザーがリモートユーザーをリストに入れたとき、リストに入れられたユーザーを誰もフォローしていないとアクティビティがインスタンスに配達されないため、代わりにプロキシアカウントがフォローするようにします。"
host: "ホスト"
selectUser: "ユーザーを選択"
selectInstance: "インスタンスを選択"
recipient: "宛先"
annotation: "注釈"
federation: "連合"
@ -197,10 +199,11 @@ muteAndBlock: "ミュートとブロック"
mutedUsers: "ミュートしたユーザー"
blockedUsers: "ブロックしたユーザー"
noUsers: "ユーザーはいません"
noInstances: "インスタンスはありません"
editProfile: "プロフィールを編集"
noteDeleteConfirm: "このノートを削除しますか?"
noteDeleteConfirm: "この投稿を削除しますか?"
pinLimitExceeded: "これ以上ピン留めできません"
intro: "Misskeyのインストールが完了しました管理者アカウントを作成しましょう。"
intro: "Calckeyのインストールが完了しました管理者アカウントを作成しましょう。"
done: "完了"
processing: "処理中"
preview: "プレビュー"
@ -325,7 +328,7 @@ connectService: "接続する"
disconnectService: "切断する"
enableLocalTimeline: "ローカルタイムラインを有効にする"
enableGlobalTimeline: "グローバルタイムラインを有効にする"
enableRecommendedTimeline: "推奨されるタイムラインを有効にする"
enableRecommendedTimeline: "おすすめタイムラインを有効にする"
disablingTimelinesInfo: "これらのタイムラインを無効化しても、利便性のため管理者およびモデレーターは引き続き利用することができます。"
registration: "登録"
enableRegistration: "誰でも新規登録できるようにする"
@ -342,7 +345,7 @@ pinnedUsersDescription: "「みつける」ページなどにピン留めした
pinnedPages: "ピン留めページ"
pinnedPagesDescription: "インスタンスのトップページにピン留めしたいページのパスを改行で区切って記述します。"
pinnedClipId: "ピン留めするクリップのID"
pinnedNotes: "ピン留めされたノート"
pinnedNotes: "ピン留めされた投稿"
hcaptcha: "hCaptcha"
enableHcaptcha: "hCaptchaを有効にする"
hcaptchaSiteKey: "サイトキー"
@ -359,10 +362,11 @@ antennaSource: "受信ソース"
antennaKeywords: "受信キーワード"
antennaExcludeKeywords: "除外キーワード"
antennaKeywordsDescription: "スペースで区切るとAND指定になり、改行で区切るとOR指定になります"
notifyAntenna: "新しいノートを通知する"
withFileAntenna: "ファイルが添付されたノートのみ"
notifyAntenna: "新しい投稿を通知する"
withFileAntenna: "ファイルが添付された投稿のみ"
enableServiceworker: "ブラウザへのプッシュ通知を有効にする"
antennaUsersDescription: "ユーザー名を改行で区切って指定します"
antennaInstancesDescription: "インスタンスを改行で区切って指定します"
caseSensitive: "大文字小文字を区別する"
withReplies: "返信を含む"
connectedTo: "次のアカウントに接続されています"
@ -393,7 +397,7 @@ securityKeyName: "キーの名前"
registerSecurityKey: "セキュリティキーを登録する"
lastUsed: "最後の使用"
unregister: "登録を解除"
passwordLessLogin: "パスワード無しログイン"
passwordLessLogin: "パスワード無しログイン"
resetPassword: "パスワードをリセット"
newPasswordIs: "新しいパスワードは「{password}」です"
reduceUiAnimation: "UIのアニメーションを減らす"
@ -422,9 +426,9 @@ messagingWithGroup: "グループでチャット"
title: "タイトル"
text: "テキスト"
enable: "有効にする"
next: "次"
next: "次"
retype: "再入力"
noteOf: "{user}のノート"
noteOf: "{user}の投稿"
inviteToGroup: "グループに招待"
quoteAttached: "引用付き"
quoteQuestion: "引用として添付しますか?"
@ -482,8 +486,8 @@ accountSettings: "アカウント設定"
promotion: "プロモーション"
promote: "プロモート"
numberOfDays: "日数"
hideThisNote: "このノートを非表示"
showFeaturedNotesInTimeline: "タイムラインにおすすめのノートを表示する"
hideThisNote: "この投稿を非表示"
showFeaturedNotesInTimeline: "タイムラインにおすすめの投稿を表示する"
objectStorage: "オブジェクトストレージ"
useObjectStorage: "オブジェクトストレージを使用"
objectStorageBaseUrl: "Base URL"
@ -504,7 +508,7 @@ objectStorageSetPublicRead: "アップロード時に'public-read'を設定す
serverLogs: "サーバーログ"
deleteAll: "全て削除"
showFixedPostForm: "タイムライン上部に投稿フォームを表示する"
newNoteRecived: "新しいノートがあります"
newNoteRecived: "新しい投稿があります"
sounds: "サウンド"
listen: "聴く"
none: "なし"
@ -519,7 +523,7 @@ recentUsed: "最近使用"
install: "インストール"
uninstall: "アンインストール"
installedApps: "インストールされたアプリ"
nothing: "ありません"
nothing: "まだ何もありません"
installedDate: "インストール日時"
lastUsedDate: "最終使用日時"
state: "状態"
@ -527,10 +531,10 @@ sort: "ソート"
ascendingOrder: "昇順"
descendingOrder: "降順"
scratchpad: "スクラッチパッド"
scratchpadDescription: "スクラッチパッドは、AiScriptの実験環境を提供します。Misskeyと対話するコードの記述、実行、結果の確認ができます。"
scratchpadDescription: "スクラッチパッドは、AiScriptの実験環境を提供します。Calckeyと対話するコードの記述、実行、結果の確認ができます。"
output: "出力"
script: "スクリプト"
disablePagesScript: "Pagesのスクリプトを無効にする"
disablePagesScript: "ページのスクリプトを無効にする"
updateRemoteUser: "リモートユーザー情報の更新"
deleteAllFiles: "すべてのファイルを削除"
deleteAllFilesConfirm: "すべてのファイルを削除しますか?"
@ -626,7 +630,7 @@ sample: "サンプル"
abuseReports: "通報"
reportAbuse: "通報"
reportAbuseOf: "{name}を通報する"
fillAbuseReportDescription: "通報理由の詳細を記入してください。対象のノートがある場合はそのURLも記入してください。"
fillAbuseReportDescription: "通報理由の詳細を記入してください。対象の投稿がある場合はそのURLも記入してください。"
abuseReported: "内容が送信されました。ご報告ありがとうございました。"
reporter: "通報者"
reporteeOrigin: "通報先"
@ -639,7 +643,7 @@ openInNewTab: "新しいタブで開く"
openInSideView: "サイドビューで開く"
defaultNavigationBehaviour: "デフォルトのナビゲーション"
editTheseSettingsMayBreakAccount: "これらの設定を編集するとアカウントが破損する可能性があります。"
instanceTicker: "ノートのインスタンス情報"
instanceTicker: "投稿のインスタンス情報"
waitingFor: "{x}を待っています"
random: "ランダム"
system: "システム"
@ -650,16 +654,16 @@ createNew: "新規作成"
optional: "任意"
createNewClip: "新しいクリップを作成"
unclip: "クリップ解除"
confirmToUnclipAlreadyClippedNote: "このノートはすでにクリップ「{name}」に含まれています。ノートをこのクリップから除外しますか?"
confirmToUnclipAlreadyClippedNote: "この投稿はすでにクリップ「{name}」に含まれています。投稿をこのクリップから除外しますか?"
public: "パブリック"
i18nInfo: "Calckeyは有志によって様々な言語に翻訳されています。{link}で翻訳に協力できます。"
manageAccessTokens: "アクセストークンの管理"
accountInfo: "アカウント情報"
notesCount: "ノートの数"
notesCount: "投稿の数"
repliesCount: "返信した数"
renotesCount: "Renoteした数"
renotesCount: "ブーストした数"
repliedCount: "返信された数"
renotedCount: "Renoteされた数"
renotedCount: "ブーストされた数"
followingCount: "フォロー数"
followersCount: "フォロワー数"
sentReactionsCount: "リアクションした数"
@ -671,17 +675,17 @@ no: "いいえ"
driveFilesCount: "ドライブのファイル数"
driveUsage: "ドライブ使用量"
noCrawle: "クローラーによるインデックスを拒否"
noCrawleDescription: "検索エンジンにあなたのユーザーページ、ート、Pagesなどのコンテンツを登録(インデックス)しないよう要請します。"
lockedAccountInfo: "フォローを承認制にしても、ノートの公開範囲を「フォロワー」にしない限り、誰でもあなたのノートを見ることができます。"
noCrawleDescription: "検索エンジンにあなたのプロフィールや投稿、ページなどのコンテンツを登録(インデックス)しないよう要請します。"
lockedAccountInfo: "フォローを承認制にしても、投稿の公開範囲を「フォロワー」にしない限り、誰でもあなたの投稿を見ることができます。"
alwaysMarkSensitive: "デフォルトでメディアを閲覧注意にする"
loadRawImages: "添付画像のサムネイルをオリジナル画質にする"
disableShowingAnimatedImages: "アニメーション画像を再生しない"
verificationEmailSent: "確認のメールを送信しました。メールに記載されたリンクにアクセスして、設定を完了してください。"
notSet: "未設定"
emailVerified: "メールアドレスが確認されました"
noteFavoritesCount: "お気に入りノートの数"
pageLikesCount: "Pageにいいねした数"
pageLikedCount: "Pageにいいねされた数"
noteFavoritesCount: "お気に入りの投稿の数"
pageLikesCount: "ページにいいねした数"
pageLikedCount: "ページにいいねされた数"
contact: "連絡先"
useSystemFont: "システムのデフォルトのフォントを使う"
clips: "クリップ"
@ -689,7 +693,7 @@ experimentalFeatures: "実験的機能"
developer: "開発者"
makeExplorable: "アカウントを見つけやすくする"
makeExplorableDescription: "オフにすると、「みつける」にアカウントが載らなくなります。"
showGapBetweenNotesInTimeline: "タイムラインのノートを離して表示"
showGapBetweenNotesInTimeline: "タイムラインの投稿を離して表示"
duplicate: "複製"
left: "左"
center: "中央"
@ -701,9 +705,9 @@ showTitlebar: "タイトルバーを表示する"
clearCache: "キャッシュをクリア"
onlineUsersCount: "{n}人がオンライン"
nUsers: "{n}ユーザー"
nNotes: "{n}ノート"
nNotes: "{n}投稿"
sendErrorReports: "エラーリポートを送信"
sendErrorReportsDescription: "オンにすると、問題が発生したときにエラーの詳細情報がMisskeyに共有され、ソフトウェアの品質向上に役立てることができます。エラー情報には、OSのバージョン、ブラウザの種類、行動履歴などが含まれます。"
sendErrorReportsDescription: "オンにすると、問題が発生したときにエラーの詳細情報がCalckeyに共有され、ソフトウェアの品質向上に役立てることができます。エラー情報には、OSのバージョン、ブラウザの種類、行動履歴などが含まれます。"
myTheme: "マイテーマ"
backgroundColor: "背景"
accentColor: "アクセント"
@ -742,7 +746,7 @@ unlikeConfirm: "いいね解除しますか?"
fullView: "フルビュー"
quitFullView: "フルビュー解除"
addDescription: "説明を追加"
userPagePinTip: "個々のノートのメニューから「ピン留め」を選択することで、ここにノートを表示しておくことができます。"
userPagePinTip: "個々の投稿のメニューから「ピン留め」を選択することで、ここに投稿を表示しておくことができます。"
notSpecifiedMentionWarning: "宛先に含まれていないメンションがあります"
info: "情報"
userInfo: "ユーザー情報"
@ -772,7 +776,7 @@ postToGallery: "ギャラリーへ投稿"
gallery: "ギャラリー"
recentPosts: "最近の投稿"
popularPosts: "人気の投稿"
shareWithNote: "ノートで共有"
shareWithNote: "投稿で共有"
ads: "広告"
expiration: "期限"
memo: "メモ"
@ -786,7 +790,7 @@ secureMode: "セキュアモード (Authorized Fetch)"
instanceSecurity: "インスタンスのセキュリティー"
secureModeInfo: "他のインスタンスからリクエストするときに、証明を付けなければ返送しません。他のインスタンスの設定ファイルでsignToActivityPubGetはtrueにしてください。"
privateMode: "非公開モード"
privateModeInfo: "有効にして、許可されているインスタンスのみがリクエストできます。すべてのノートが公開に非表示にします。"
privateModeInfo: "有効にして、許可されているインスタンスのみがリクエストできます。すべての投稿が公開に非表示にします。"
allowedInstances: "許可されたインスタンス"
allowedInstancesDescription: "許可したいインスタンスのホストを改行で区切って設定します。非公開モードだけで有効です。"
previewNoteText: "本文をプレビュー"
@ -794,7 +798,7 @@ customCss: "カスタムCSS"
customCssWarn: "この設定は必ず知識のある方が行ってください。不適切な設定を行うとクライアントが正常に使用できなくなる恐れがあります。"
global: "グローバル"
squareAvatars: "アイコンを四角形で表示"
seperateRenoteQuote: "リノートと引用ボタンを分ける"
seperateRenoteQuote: "ブーストと引用ボタンを分ける"
sent: "送信"
received: "受信"
searchResult: "検索結果"
@ -802,13 +806,13 @@ hashtags: "ハッシュタグ"
troubleshooting: "トラブルシューティング"
useBlurEffect: "UIにぼかし効果を使用"
learnMore: "詳しく"
misskeyUpdated: "Misskeyが更新されました"
misskeyUpdated: "Calckeyが更新されました"
whatIsNew: "更新情報を見る"
translate: "翻訳"
translatedFrom: "{x}から翻訳"
accountDeletionInProgress: "アカウントの削除が進行中です"
usernameInfo: "サーバー上であなたのアカウントを一意に識別するための名前。アルファベット(a~z, A~Z)、数字(0~9)、およびアンダーバー(_)が使用できます。ユーザー名は後から変更することは出来ません。"
aiChanMode: "藍モード"
aiChanMode: "藍モードクラシックUI"
enterSendsMessage: "メッセージングでReturnキーを押すと、メッセージが送信されますデフォルトはCtrl + Returnです"
keepCw: "CWを維持する"
pubSub: "Pub/Subのアカウント"
@ -912,15 +916,25 @@ customMOTDDescription: "ユーザがページをロード/リロードするた
customSplashIcons: "カスタムスプラッシュスクリーンアイコン"
customSplashIconsDescription: "ユーザがページをロード/リロードするたびにランダムに表示される、改行で区切られたカスタムスプラッシュスクリーンアイコンの URL。画像は静的なURLで、できればすべて192x192にリサイズしてください。"
showUpdates: "Calckeyの更新時にポップアップを表示する"
recommendedInstances: "推奨インスタンス"
recommendedInstancesDescription: "推奨タイムラインに表示するために改行で区切られた推奨インスタンス。`https://`を追加しないでください。ドメインのみを追加してください。"
recommendedInstances: "おすすめインスタンス"
recommendedInstancesDescription: "おすすめタイムラインに表示される、改行で区切られたインスタンス。`https://`を追加しないでください。ドメインのみを追加してください。"
caption: "自動キャプション"
splash: "スプラッシュスクリーン"
updateAvailable: "アップデートがありますよ"
updateAvailable: "アップデートがありますよ"
swipeOnDesktop: "デスクトップでモバイルスタイルのスワイプを可能にする"
logoImageUrl: "ロゴのURL"
showAdminUpdates: "新しいCalckeyのバージョンが利用可能であることを示す(管理者のみ)"
replayTutorial: "リプレイチュートリアル"
replayTutorial: "もう一度チュートリアルを見る"
migration: "アカウントの引っ越し"
moveTo: "このアカウントを新しいアカウントに引っ越す"
moveToLabel: "引っ越し先のアカウント:"
moveAccount: "引っ越し実行!"
moveAccountDescription: "この操作は取り消せません。まずは引っ越し先のアカウントでこのアカウントに対しエイリアスを作成したことを確認してください。エイリアス作成後、引っ越し先のアカウントをこのように入力してください:@person@instance.com"
moveFrom: "別のアカウントからこのアカウントに引っ越す"
moveFromLabel: "引っ越し元のアカウント:"
moveFromDescription: "別のアカウントからこのアカウントにフォロワーを引き継いで引っ越したい場合、ここでエイリアスを作成しておく必要があります。必ず引っ越しを実行する前に作成してください!引っ越し元のアカウントをこのように入力してください:@person@instance.com"
migrationConfirm: "本当にこのアカウントを {account} に引っ越しますか?一度引っ越しを行うと取り消せず、二度とこのアカウントを元の状態で使用することはできません。\nまた、引っ越し先のアカウントでエイリアスを作成したことを確認してください。"
defaultReaction: "リモートとローカルの投稿に対するデフォルトの絵文字リアクション"
_sensitiveMediaDetection:
description: "機械学習を使って自動でセンシティブなメディアを検出し、モデレーションに役立てることができます。サーバーの負荷が少し増えます。"
@ -930,24 +944,20 @@ _sensitiveMediaDetection:
setSensitiveFlagAutomaticallyDescription: "この設定をオフにしても内部的に判定結果は保持されます。"
analyzeVideos: "動画の解析を有効化"
analyzeVideosDescription: "静止画に加えて動画も解析するようにします。サーバーの負荷が少し増えます。"
_emailUnavailable:
used: "既に使用されています"
format: "形式が正しくありません"
disposable: "恒久的に使用可能なアドレスではありません"
mx: "正しいメールサーバーではありません"
smtp: "メールサーバーが応答しません"
_ffVisibility:
public: "公開"
followers: "フォロワーだけに公開"
private: "非公開"
_signup:
almostThere: "ほとんど完了です"
emailAddressInfo: "あなたが使っているメールアドレスを入力してください。メールアドレスが公開されることはありません。"
emailSent: "入力されたメールアドレス({email})宛に確認のメールが送信されました。メールに記載されたリンクにアクセスすると、アカウントの作成が完了します。"
_accountDelete:
accountDelete: "アカウントの削除"
mayTakeTime: "アカウントの削除は負荷のかかる処理であるため、作成したコンテンツの数やアップロードしたファイルの数が多いと完了までに時間がかかることがあります。"
@ -955,33 +965,27 @@ _accountDelete:
requestAccountDelete: "アカウント削除をリクエスト"
started: "削除処理が開始されました。"
inProgress: "削除が進行中"
_ad:
back: "戻る"
reduceFrequencyOfThisAd: "この広告の表示頻度を下げる"
_forgotPassword:
enterEmail: "アカウントに登録したメールアドレスを入力してください。そのアドレス宛てに、パスワードリセット用のリンクが送信されます。"
ifNoEmail: "メールアドレスを登録していない場合は、管理者までお問い合わせください。"
contactAdmin: "このインスタンスではメールがサポートされていないため、パスワードリセットを行う場合は管理者までお問い合わせください。"
_gallery:
my: "自分の投稿"
liked: "いいねした投稿"
like: "いいね!"
unlike: "いいね解除"
_email:
_follow:
title: "フォローされました"
_receiveFollowRequest:
title: "フォローリクエストを受け取りました"
_plugin:
install: "プラグインのインストール"
installWarn: "信頼できないプラグインはインストールしないでください。"
manage: "プラグインの管理"
_preferencesBackups:
list: "作成したバックアップ"
saveNew: "新規保存"
@ -1000,33 +1004,29 @@ _preferencesBackups:
updatedAt: "更新日時: {date} {time}"
cannotLoad: "読み込みできません"
invalidFile: "ファイル形式が違います。"
_registry:
scope: "スコープ"
key: "キー"
keys: "キー"
domain: "ドメイン"
createKey: "キーを作成"
_aboutMisskey:
about: "Calckeyは、2022年から開発されているThatOneCalculator社製のMisskeyのforkです。"
about: "Calckeyは、2022年に生まれたThatOneCalculatorによるMisskeyのforkです。"
contributors: "主なコントリビューター"
allContributors: "全てのコントリビューター"
source: "ソースコード"
translation: "Misskeyを翻訳"
donate: "Misskeyに寄付"
morePatrons: "他にも多くの方が支援してくれています。ありがとうございます🥰"
translation: "Calckeyを翻訳"
donate: "Calckeyに寄付"
morePatrons: "他にも多くの方が支援してくれています。ありがとうございます 🥰"
patrons: "支援者"
_nsfw:
respect: "閲覧注意のメディアは隠す"
ignore: "閲覧注意のメディアを隠さない"
force: "常にメディアを隠す"
_mfm:
cheatSheet: "MFMチートシート"
intro: "MFMは、Misskey内の様々な場所で使用できる専用のマークアップ言語です。ここでは、MFMで使用可能な構文一覧が確認できます。"
dummy: "MisskeyでFediverseの世界が広がります"
intro: "MFMは、MisskeyやCalckey、Akkomaなどの様々な場所で使用できるマークアップ言語です。ここでは、MFMで使用可能な構文一覧が確認できます。"
dummy: "CalckeyでFediverseの世界が広がります"
mention: "メンション"
mentionDescription: "アットマーク + ユーザー名で、特定のユーザーを示すことができます。"
hashtag: "ハッシュタグ"
@ -1089,18 +1089,15 @@ _mfm:
rotateDescription: "指定した角度で回転させます。"
plain: "プレーン"
plainDescription: "内側の構文を全て無効にします。"
_instanceTicker:
none: "表示しない"
remote: "リモートユーザーに表示"
always: "常に表示"
_serverDisconnectedBehavior:
reload: "自動でリロード"
dialog: "ダイアログで警告"
quiet: "控えめに警告"
nothing: "何も起こらない"
_channel:
create: "チャンネルを作成"
edit: "チャンネルを編集"
@ -1111,33 +1108,28 @@ _channel:
following: "フォロー中"
usersCount: "{n}人が参加中"
notesCount: "{n}投稿があります"
_messaging:
dms: "ディーエム"
groups: "グループ"
_menuDisplay:
sideFull: "横"
sideIcon: "横(アイコン)"
top: "上部"
hide: "隠す"
_wordMute:
muteWords: "ミュートするワード"
muteWordsDescription: "スペースで区切るとAND指定になり、改行で区切るとOR指定になります。"
muteWordsDescription2: "キーワードをスラッシュで囲むと正規表現になります。"
softDescription: "指定した条件のノートをタイムラインから隠します。"
hardDescription: "指定した条件のノートをタイムラインに追加しないようにします。追加されなかったノートは、条件を変更しても除外されたままになります。"
softDescription: "指定した条件の投稿をタイムラインから隠します。"
hardDescription: "指定した条件の投稿をタイムラインに追加しないようにします。追加されなかった投稿は、条件を変更しても除外されたままになります。"
soft: "ソフト"
hard: "ハード"
mutedNotes: "ミュートされたノート"
mutedNotes: "ミュートされた投稿"
_instanceMute:
instanceMuteDescription: "ミュートしたインスタンスのユーザーへの返信を含めて、設定したインスタンスの全てのートとRenoteをミュートします。"
instanceMuteDescription: "ミュートしたインスタンスのユーザーへの返信を含めて、設定したインスタンスの全ての投稿とブーストをミュートします。"
instanceMuteDescription2: "改行で区切って設定します"
title: "設定したインスタンスのノートを隠します。"
title: "設定したインスタンスの投稿を隠します。"
heading: "ミュートするインスタンス"
_theme:
explore: "テーマを探す"
install: "テーマのインストール"
@ -1168,7 +1160,6 @@ _theme:
inputConstantName: "定数名を入力してください"
importInfo: "ここにテーマコードを貼り付けて、エディターにインポートできます"
deleteConstantConfirm: "定数 {const} を削除しても良いですか?"
keys:
accent: "アクセント"
bg: "背景"
@ -1187,7 +1178,7 @@ _theme:
hashtag: "ハッシュタグ"
mention: "メンション"
mentionMe: "あなた宛てメンション"
renote: "Renote"
renote: "ブースト"
modalBg: "モーダルの背景"
divider: "分割線"
scrollbarHandle: "スクロールバーの取っ手"
@ -1213,16 +1204,14 @@ _theme:
accentDarken: "アクセント (暗め)"
accentLighten: "アクセント (明るめ)"
fgHighlighted: "強調された文字"
_sfx:
note: "ノート"
noteMy: "ノート(自分)"
note: "投稿"
noteMy: "投稿(自分)"
notification: "通知"
chat: "チャット"
chatBg: "チャット(バックグラウンド)"
antenna: "アンテナ受信"
channel: "チャンネル通知"
_ago:
future: "未来"
justNow: "たった今"
@ -1233,35 +1222,32 @@ _ago:
weeksAgo: "{n}週間前"
monthsAgo: "{n}ヶ月前"
yearsAgo: "{n}年前"
_time:
second: "秒"
minute: "分"
hour: "時間"
day: "日"
_tutorial:
title: "Calckeyの使い方"
step1_1: "ようこそ!"
step1_2: "設定をしてみましょう"
step2_1: "メモを書いたり、誰かをフォローする前に、プロフィールの設定を済ませましょう。"
step2_2: "あなたが誰なのか、いくつかの情報を提供することで、他の人があなたのメモを見たり、フォローしたりしたいのかがわかりやすくなります。"
step3_1: "さあ、何人かの人をフォローする時間です"
step3_2: "あなたのホームとソーシャルタイムラインは、あなたが誰をフォローしているかで決まります。 まずは、いくつかのアカウントをフォローしてみましょう。"
step4_1: "さあ、外に出てみましょう。"
step4_2: "最初の投稿は、{introduction}の投稿や、シンプルに「こんにちは、世界よ!」的な投稿をするのが好きな人もいます。"
step1_1: "ようこそ"
step1_2: "使い始める前に、いくつか設定を済ませましょう。すぐできますよ!"
step2_1: "最初に、あなたのプロフィールを作りましょう。"
step2_2: "プロフィールを設定することで、他の人があなたの投稿を見たり、フォローしたりするときの助けになります。"
step3_1: "それでは、何人かフォローしてみましょう"
step3_2: "あなたのホームとソーシャルタイムラインは、あなたが誰をフォローしているかで決まります。まずは、いくつかのアカウントをフォローしてみましょう。\nプロフィールの右上にある丸いボタンをクリックするとフォローできます。"
step4_1: "投稿してみましょう!"
step4_2: "最初は{introduction}に投稿したり、シンプルに「こんにちは、アカウント作ってみました!」などの投稿をする人もいます。"
step5_1: "タイムライン、タイムラインだらけ!"
step5_2: "あなたのインスタンスは{timelines}異なるタイムラインを有効にしています。"
step5_3: "ホーム{icon}のタイムラインは、あなたのフォロワーからの投稿を見ることができます。"
step5_4: "ローカル{icon}タイムラインは、このインスタンスのみんなの投稿を見ることができる場所です。"
step5_5: "おすすめ{icon}のタイムラインは、管理人がおすすめするインスタンスの投稿を見ることができます。"
step5_6: "ソーシャル{icon}のタイムラインは、あなたのフォロワーの友達の投稿を見ることができる場所です。"
step5_7: "グローバル{icon}タイムラインは、接続している他のすべてのインスタンスからの投稿を見ることができます。"
step6_1: "それで、ここは何なの?"
step6_2: "まあ、あなたはCalckeyに参加しただけではありません。何千ものサーバーが相互接続されたネットワークで インスタンスと呼ばれる。"
step6_3: "各サーバーは異なる方法で動作し、すべてのサーバーがCalckeyを実行するわけではありません。でも、このサーバーは動くんです"
step6_4: "さあ、探検して、楽しんでください!"
step5_2: "あなたのインスタンスでは{timelines}種類のタイムラインが有効になっています。"
step5_3: "ホーム{icon}タイムラインでは、あなたがフォローしているアカウントの投稿を見ることができます。"
step5_4: "ローカル{icon}タイムラインでは、このインスタンスのみんなの投稿を見ることができます。"
step5_5: "おすすめ{icon}タイムラインでは、管理人がおすすめするインスタンスの投稿を見ることができます。"
step5_6: "ソーシャル{icon}タイムラインでは、ホームタイムラインとローカルタイムラインの投稿を同時に見ることができます。"
step5_7: "グローバル{icon}タイムラインでは、接続している他のすべてのインスタンスからの投稿を見ることができます。"
step6_1: "じゃあ、ここはどんな場所なの?"
step6_2: "実は、あなたはただCalckeyに参加しただけではありません。ここは、何千もの相互接続されたサーバーが構成する Fediverse への入口です。各サーバーは「インスタンス」と呼ばれます。"
step6_3: "それぞれのサーバーでは必ずしもCalckeyが使われているわけではなく、異なる動作をするサーバーもあります。しかし、あなたは他のサーバーのアカウントもフォローしたり、返信・ブーストができます。一見難しそうですが大丈夫すぐ慣れます。"
step6_4: "これで完了です。お楽しみください!"
_2fa:
alreadyRegistered: "既に設定は完了しています。"
registerDevice: "デバイスを登録"
@ -1272,7 +1258,6 @@ _2fa:
step3: "アプリに表示されているトークンを入力して完了です。"
step4: "これからログインするときも、同じようにトークンを入力します。"
securityKeyInfo: "FIDO2をサポートするハードウェアセキュリティキーもしくは端末の指紋認証やPINを使用してログインするように設定できます。"
_permissions:
"read:account": "アカウントの情報を見る"
"write:account": "アカウントの情報を変更する"
@ -1288,7 +1273,7 @@ _permissions:
"write:messaging": "チャットを操作する"
"read:mutes": "ミュートを見る"
"write:mutes": "ミュートを操作する"
"write:notes": "ノートを作成・削除する"
"write:notes": "投稿を作成・削除する"
"read:notifications": "通知を見る"
"write:notifications": "通知を操作する"
"read:reactions": "リアクションを見る"
@ -1306,22 +1291,21 @@ _permissions:
"write:gallery": "ギャラリーを操作する"
"read:gallery-likes": "ギャラリーのいいねを見る"
"write:gallery-likes": "ギャラリーのいいねを操作する"
_auth:
shareAccess: "「{name}」がアカウントにアクセスすることを許可しますか?"
shareAccessAsk: "アカウントへのアクセスを許可しますか?"
permissionAsk: "このアプリは次の権限を要求しています"
pleaseGoBack: "アプリケーションに戻ってやっていってください"
permissionAsk: "このアプリケーションは次の権限を要求しています"
pleaseGoBack: "アプリケーションに戻り続行してください"
callback: "アプリケーションに戻っています"
denied: "アクセスを拒否しました"
copyAsk: "以下の認証コードをアプリケーションにコピーしてください"
_antennaSources:
all: "全てのノート"
homeTimeline: "フォローしているユーザーのノート"
users: "指定した一人または複数のユーザーのノート"
userList: "指定したリストのユーザーのノート"
userGroup: "指定したグループのユーザーのノート"
all: "全ての投稿"
homeTimeline: "フォローしているユーザーの投稿"
users: "指定した一人または複数のユーザーの投稿"
userList: "指定したリストのユーザーの投稿"
userGroup: "指定したグループのユーザーの投稿"
instances: "指定したインスタンスの全ユーザーの投稿"
_weekday:
sunday: "日曜日"
monday: "月曜日"
@ -1330,7 +1314,6 @@ _weekday:
thursday: "木曜日"
friday: "金曜日"
saturday: "土曜日"
_widgets:
memo: "付箋"
notifications: "通知"
@ -1353,14 +1336,14 @@ _widgets:
jobQueue: "ジョブキュー"
serverMetric: "サーバーメトリクス"
aiscript: "AiScriptコンソール"
aichan: "藍"
userList: "ユーザーリスト"
_userList:
chooseList: "リストを選択"
_cw:
hide: "隠す"
show: "もっと見る"
chars: "{count}文字"
files: "{count}ファイル"
_poll:
noOnlyOneChoice: "選択肢は最低2つ必要です"
choiceN: "選択肢{n}"
@ -1383,7 +1366,6 @@ _poll:
remainingHours: "終了まであと{h}時間{m}分"
remainingMinutes: "終了まであと{m}分{s}秒"
remainingSeconds: "終了まであと{s}秒"
_visibility:
public: "パブリック"
publicDescription: "全てのユーザーに公開"
@ -1395,10 +1377,9 @@ _visibility:
specifiedDescription: "指定したユーザーのみに公開"
localOnly: "ローカルのみ"
localOnlyDescription: "リモートユーザーには非公開"
_postForm:
replyPlaceholder: "このノートに返信..."
quotePlaceholder: "このノートを引用..."
replyPlaceholder: "この投稿に返信..."
quotePlaceholder: "この投稿を引用..."
channelPlaceholder: "チャンネルに投稿..."
_placeholders:
a: "いまどうしてる?"
@ -1407,7 +1388,6 @@ _postForm:
d: "言いたいことは?"
e: "ここに書いてください"
f: "あなたが書くのを待っています..."
_profile:
name: "名前"
username: "ユーザー名"
@ -1420,51 +1400,47 @@ _profile:
metadataContent: "内容"
changeAvatar: "アバター画像を変更"
changeBanner: "バナー画像を変更"
locationDescription: "正しく入力すると、あなたの現地時間が他のユーザーに表示されます。"
_exportOrImport:
allNotes: "全てのノート"
allNotes: "全ての投稿"
followingList: "フォロー"
muteList: "ミュート"
blockingList: "ブロック"
userLists: "リスト"
excludeMutingUsers: "ミュートしているユーザーを除外"
excludeInactiveUsers: "使われていないアカウントを除外"
_charts:
federation: "連合"
apRequest: "リクエスト"
usersIncDec: "ユーザーの増減"
usersTotal: "ユーザーの合計"
activeUsers: "アクティブユーザー数"
notesIncDec: "ノートの増減"
localNotesIncDec: "ローカルのノートの増減"
remoteNotesIncDec: "リモートのノートの増減"
notesTotal: "ノートの合計"
notesIncDec: "投稿の増減"
localNotesIncDec: "ローカルの投稿の増減"
remoteNotesIncDec: "リモートの投稿の増減"
notesTotal: "投稿の合計"
filesIncDec: "ファイルの増減"
filesTotal: "ファイルの合計"
storageUsageIncDec: "ストレージ使用量の増減"
storageUsageTotal: "ストレージ使用量の合計"
_instanceCharts:
requests: "リクエスト"
users: "ユーザーの増減"
usersTotal: "ユーザーの累積"
notes: "ノートの増減"
notesTotal: "ノートの累積"
notes: "投稿の増減"
notesTotal: "投稿の累積"
ff: "フォロー/フォロワーの増減"
ffTotal: "フォロー/フォロワーの累積"
cacheSize: "キャッシュサイズの増減"
cacheSizeTotal: "キャッシュサイズの累積"
files: "ファイル数の増減"
filesTotal: "ファイル数の累積"
_timelines:
home: "ホーム"
local: "ローカル"
recommended: "一押し"
recommended: "おすすめ"
social: "ソーシャル"
global: "グローバル"
_pages:
newPage: "ページの作成"
editPage: "ページの編集"
@ -1511,59 +1487,49 @@ _pages:
section: "セクション"
image: "画像"
button: "ボタン"
if: "もし"
_if:
variable: "変数"
post: "投稿フォーム"
_post:
text: "内容"
attachCanvasImage: "キャンバスの画像を添付する"
canvasId: "キャンバスID"
textInput: "テキスト入力"
_textInput:
name: "変数名"
text: "タイトル"
default: "デフォルト値"
textareaInput: "複数行テキスト入力"
_textareaInput:
name: "変数名"
text: "タイトル"
default: "デフォルト値"
numberInput: "数値入力"
_numberInput:
name: "変数名"
text: "タイトル"
default: "デフォルト値"
canvas: "キャンバス"
_canvas:
id: "キャンバスID"
width: "幅"
height: "高さ"
note: "ノート埋め込み"
note: "投稿の埋め込み"
_note:
id: "ノートID"
idDescription: "ノートURLをペーストして設定することもできます。"
id: "投稿のID"
idDescription: "投稿のURLをペーストして設定することもできます。"
detailed: "詳細な表示"
switch: "スイッチ"
_switch:
name: "変数名"
text: "タイトル"
default: "デフォルト値"
counter: "カウンター"
_counter:
name: "変数名"
text: "タイトル"
inc: "増加値"
_button:
text: "タイトル"
colored: "色付き"
@ -1582,14 +1548,12 @@ _pages:
callAiScript: "AiScript呼び出し"
_callAiScript:
functionName: "関数名"
radioButton: "選択肢"
_radioButton:
name: "変数名"
title: "タイトル"
values: "改行で区切った選択肢"
default: "デフォルト値"
script:
categories:
flow: "制御"
@ -1766,18 +1730,16 @@ _pages:
enviromentVariables: "環境変数"
pageVariables: "ページ要素"
argVariables: "入力スロット"
_relayStatus:
requesting: "承認待ち"
accepted: "承認済み"
rejected: "拒否済み"
_notification:
fileUploaded: "ファイルがアップロードされました"
youGotMention: "{name}からのメンション"
youGotReply: "{name}からのリプライ"
youGotQuote: "{name}による引用"
youRenoted: "{name}がRenoteしました"
youRenoted: "{name}がブーストしました"
youGotPoll: "{name}が投票しました"
youGotMessagingMessageFromUser: "{name}からのチャットがあります"
youGotMessagingMessageFromGroup: "{name}のチャットがあります"
@ -1787,13 +1749,12 @@ _notification:
youWereInvitedToGroup: "{userName}があなたをグループに招待しました"
pollEnded: "アンケートの結果が出ました"
emptyPushNotificationMessage: "プッシュ通知の更新をしました"
_types:
all: "すべて"
follow: "フォロー"
mention: "メンション"
reply: "リプライ"
renote: "Renote"
renote: "ブースト"
quote: "引用"
reaction: "リアクション"
pollVote: "アンケートに投票された"
@ -1802,12 +1763,10 @@ _notification:
followRequestAccepted: "フォローが受理された"
groupInvited: "グループに招待された"
app: "連携アプリからの通知"
_actions:
followBack: "フォローバック"
reply: "返信"
renote: "Renote"
renote: "ブースト"
_deck:
alwaysShowMainColumn: "常にメインカラムを表示"
columnAlign: "カラムの寄せ"
@ -1825,7 +1784,6 @@ _deck:
introduction: "カラムを組み合わせて自分だけのインターフェイスを作りましょう!"
introduction2: "画面の右にある + を押して、いつでもカラムを追加できます。"
widgetsIntroduction: "カラムのメニューから、「ウィジェットの編集」を選択してウィジェットを追加してください"
_columns:
main: "メイン"
widgets: "ウィジェット"
@ -1835,3 +1793,20 @@ _deck:
list: "リスト"
mentions: "あなた宛て"
direct: "ダイレクト"
_apps:
apps: "アプリ"
crossPlatform: "クロスプラットフォーム"
mobile: "モバイル"
firstParty: "ファーストパーティ"
firstClass: "対応度◎"
secondClass: "対応度○"
thirdClass: "対応度△"
free: "無料"
paid: "有料"
pwa: "PWAをインストール"
kaiteki: "Kaiteki"
milktea: "Milktea"
missLi: "MissLi"
mona: "Mona"
theDesk: "TheDesk"
lesskey: "Lesskey"

View file

@ -41,7 +41,6 @@
"@tensorflow/tfjs": "^3.21.0",
"calckey-js": "^0.0.22",
"js-yaml": "4.1.0",
"phosphor-icons": "^1.4.2",
"seedrandom": "^3.0.5"
},
"devDependencies": {

View file

@ -0,0 +1,10 @@
import {loadConfig} from './built/config.js';
import {createRedisConnection} from "./built/redis.js";
const config = loadConfig();
const redis = createRedisConnection(config);
redis.on('connect', () => redis.disconnect());
redis.on('error', (e) => {
throw e;
});

View file

@ -0,0 +1,11 @@
export class DriveComment1677935903517 {
name = 'DriveComment1677935903517'
async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "drive_file" ALTER "comment" TYPE character varying(8192)`);
}
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "drive_file" ALTER "comment" TYPE character varying(512)`);
}
}

View file

@ -8,6 +8,7 @@
"start:test": "NODE_ENV=test pnpm node ./built/index.js",
"migrate": "typeorm migration:run -d ormconfig.js",
"revertmigration": "typeorm migration:revert -d ormconfig.js",
"check:connect": "node ./check_connect.js",
"build": "pnpm swc src -d built -D",
"watch": "pnpm swc src -d built -D -w",
"lint": "pnpm rome check \"src/**/*.ts\"",
@ -71,6 +72,7 @@
"jsonld": "6.0.0",
"jsrsasign": "10.6.1",
"koa": "2.13.4",
"koa-remove-trailing-slashes": "2.0.3",
"koa-bodyparser": "4.3.0",
"koa-favicon": "2.1.0",
"koa-json-body": "5.3.0",
@ -97,6 +99,7 @@
"punycode": "2.1.1",
"pureimage": "0.3.15",
"qrcode": "1.5.1",
"qs": "6.9.7",
"random-seed": "0.3.0",
"ratelimiter": "3.4.1",
"re2": "1.18.0",
@ -157,6 +160,7 @@
"@types/pug": "2.0.6",
"@types/punycode": "2.1.0",
"@types/qrcode": "1.5.0",
"@types/qs": "6.9.7",
"@types/random-seed": "0.3.3",
"@types/ratelimiter": "3.4.4",
"@types/redis": "4.0.11",

View file

@ -0,0 +1 @@
declare module 'koa-remove-trailing-slashes';

View file

@ -74,6 +74,7 @@ export type Source = {
maxUserSignups?: number;
isManagedHosting?: boolean;
maxNoteLength?: number;
maxCaptionLength?: number;
deepl: {
managed?: boolean;
authKey?: string;

View file

@ -1,7 +1,12 @@
import config from "@/config/index.js";
import { DB_MAX_IMAGE_COMMENT_LENGTH } from "@/misc/hard-limits.js";
export const MAX_NOTE_TEXT_LENGTH =
config.maxNoteLength != null ? config.maxNoteLength : 3000; // <- should we increase this?
export const MAX_CAPTION_TEXT_LENGTH = Math.min(
config.maxCaptionLength ?? 1500,
DB_MAX_IMAGE_COMMENT_LENGTH,
);
export const SECOND = 1000;
export const SEC = 1000; // why do we need this duplicate here?

View file

@ -10,4 +10,4 @@ export const DB_MAX_NOTE_TEXT_LENGTH = 8192;
* Maximum image description length that can be stored in DB.
* Surrogate pairs count as one
*/
export const DB_MAX_IMAGE_COMMENT_LENGTH = 512;
export const DB_MAX_IMAGE_COMMENT_LENGTH = 8192;

View file

@ -9,6 +9,7 @@ import {
import { id } from "../id.js";
import { User } from "./user.js";
import { DriveFolder } from "./drive-folder.js";
import { DB_MAX_IMAGE_COMMENT_LENGTH } from "@/misc/hard-limits.js";
@Entity()
@Index(['userId', 'folderId', 'id'])
@ -69,7 +70,8 @@ export class DriveFile {
public size: number;
@Column('varchar', {
length: 512, nullable: true,
length: DB_MAX_IMAGE_COMMENT_LENGTH,
nullable: true,
comment: 'The comment of the DriveFile.',
})
public comment: string | null;

View file

@ -20,6 +20,7 @@ export default async (job: Bull.Job<WebhookDeliverJobData>) => {
"X-Calckey-Host": config.host,
"X-Calckey-Hook-Id": job.data.webhookId,
"X-Calckey-Hook-Secret": job.data.secret,
'Content-Type': 'application/json'
},
body: JSON.stringify({
hookId: job.data.webhookId,

View file

@ -413,8 +413,8 @@ export async function updatePerson(
isBot: getApType(object) === "Service",
isCat: (person as any).isCat === true,
isLocked: !!person.manuallyApprovesFollowers,
movedToUri: person.movedTo,
alsoKnownAs: person.alsoKnownAs,
movedToUri: person.movedTo || null,
alsoKnownAs: person.alsoKnownAs || null,
isExplorable: !!person.discoverable,
} as Partial<User>;

View file

@ -1,6 +1,6 @@
import config from "@/config/index.js";
import { fetchMeta } from "@/misc/fetch-meta.js";
import { MAX_NOTE_TEXT_LENGTH } from "@/const.js";
import { MAX_NOTE_TEXT_LENGTH, MAX_CAPTION_TEXT_LENGTH } from "@/const.js";
import define from "../../define.js";
export const meta = {
@ -86,6 +86,11 @@ export const meta = {
optional: false,
nullable: false,
},
maxCaptionTextLength: {
type: "number",
optional: false,
nullable: false,
},
emojis: {
type: "array",
optional: false,
@ -499,6 +504,7 @@ export default define(meta, paramDef, async (ps, me) => {
backgroundImageUrl: instance.backgroundImageUrl,
logoImageUrl: instance.logoImageUrl,
maxNoteTextLength: MAX_NOTE_TEXT_LENGTH, // 後方互換性のため
maxCaptionTextLength: MAX_CAPTION_TEXT_LENGTH,
defaultLightTheme: instance.defaultLightTheme,
defaultDarkTheme: instance.defaultDarkTheme,
enableEmail: instance.enableEmail,

View file

@ -2,8 +2,7 @@ import { IsNull, MoreThan } from "typeorm";
import config from "@/config/index.js";
import { fetchMeta } from "@/misc/fetch-meta.js";
import { Ads, Emojis, Users } from "@/models/index.js";
import { DB_MAX_NOTE_TEXT_LENGTH } from "@/misc/hard-limits.js";
import { MAX_NOTE_TEXT_LENGTH } from "@/const.js";
import { MAX_NOTE_TEXT_LENGTH, MAX_CAPTION_TEXT_LENGTH } from "@/const.js";
import define from "../define.js";
export const meta = {
@ -178,6 +177,11 @@ export const meta = {
optional: false,
nullable: false,
},
maxCaptionTextLength: {
type: "number",
optional: false,
nullable: false,
},
emojis: {
type: "array",
optional: false,
@ -456,6 +460,7 @@ export default define(meta, paramDef, async (ps, me) => {
backgroundImageUrl: instance.backgroundImageUrl,
logoImageUrl: instance.logoImageUrl,
maxNoteTextLength: MAX_NOTE_TEXT_LENGTH, // 後方互換性のため
maxCaptionTextLength: MAX_CAPTION_TEXT_LENGTH,
emojis: instance.privateMode && !me ? [] : await Emojis.packMany(emojis),
defaultLightTheme: instance.defaultLightTheme,
defaultDarkTheme: instance.defaultDarkTheme,

View file

@ -22,6 +22,35 @@ import github from "./service/github.js";
import twitter from "./service/twitter.js";
import { koaBody } from "koa-body";
export enum IdType {
CalckeyId,
MastodonId
};
export function convertId(idIn: string, idConvertTo: IdType ) {
let idArray = []
switch (idConvertTo) {
case IdType.MastodonId:
idArray = [...idIn].map(item => item.charCodeAt(0));
idArray = idArray.map(item => {
if (item.toString().length < 3) {
return `0${item.toString()}`
}
else return item.toString()
});
return idArray.join('');
case IdType.CalckeyId:
for (let i = 0; i < idIn.length; i += 3) {
if ((idIn.length % 3) !== 0) {
idIn = `0${idIn}`
}
idArray.push(idIn.slice(i, i+3));
}
idArray = idArray.map(item => String.fromCharCode(item));
return idArray.join('');
}
};
// Init app
const app = new Koa();
@ -82,7 +111,7 @@ mastoFileRouter.post("/v1/media", upload.single("file"), async (ctx) => {
ctx.status = 401;
return;
}
const data = await client.uploadMedia(multipartData.buffer);
const data = await client.uploadMedia(multipartData);
ctx.body = data.data;
} catch (e: any) {
console.error(e);
@ -101,7 +130,7 @@ mastoFileRouter.post("/v2/media", upload.single("file"), async (ctx) => {
ctx.status = 401;
return;
}
const data = await client.uploadMedia(multipartData.buffer);
const data = await client.uploadMedia(multipartData);
ctx.body = data.data;
} catch (e: any) {
console.error(e);

View file

@ -4,8 +4,9 @@ import Router from "@koa/router";
import { FindOptionsWhere, IsNull } from "typeorm";
import { getClient } from "../ApiMastodonCompatibleService.js";
import { argsToBools, limitToInt } from "./timeline.js";
import { convertId, IdType } from "../../index.js";
const relationshopModel = {
const relationshipModel = {
id: "",
following: false,
followed_by: false,
@ -29,7 +30,8 @@ export function apiAccountMastodon(router: Router): void {
const client = getClient(BASE_URL, accessTokens);
try {
const data = await client.verifyAccountCredentials();
const acct = data.data;
let acct = data.data;
acct.id = convertId(acct.id, IdType.MastodonId);
acct.url = `${BASE_URL}/@${acct.url}`;
acct.note = "";
acct.avatar_static = acct.avatar;
@ -42,6 +44,7 @@ export function apiAccountMastodon(router: Router): void {
sensitive: false,
language: "",
};
console.log(acct);
ctx.body = acct;
} catch (e: any) {
console.error(e);
@ -58,7 +61,9 @@ export function apiAccountMastodon(router: Router): void {
const data = await client.updateCredentials(
(ctx.request as any).body as any,
);
ctx.body = data.data;
let resp = data.data;
resp.id = convertId(resp.id, IdType.MastodonId);
ctx.body = resp;
} catch (e: any) {
console.error(e);
console.error(e.response.data);
@ -71,26 +76,10 @@ export function apiAccountMastodon(router: Router): void {
const accessTokens = ctx.headers.authorization;
const client = getClient(BASE_URL, accessTokens);
try {
let userArray = ctx.query.acct?.toString().split("@");
let userid;
if (userArray === undefined) {
ctx.status = 401;
ctx.body = { error: "no user specified" };
return;
}
if (userArray.length === 1) {
const q: FindOptionsWhere<User> = {
usernameLower: userArray[0].toLowerCase(),
host: IsNull(),
};
const user = await Users.findOneBy(q);
userid = user?.id;
} else {
userid = (await resolveUser(userArray[0], userArray[1])).id;
}
const data = await client.getAccount(userid ? userid : "");
ctx.body = data.data;
const data = await client.search((ctx.request.query as any).acct, 'accounts');
let resp = data.data.accounts[0];
resp.id = convertId(resp.id, IdType.MastodonId);
ctx.body = resp;
} catch (e: any) {
console.error(e);
console.error(e.response.data);
@ -105,8 +94,11 @@ export function apiAccountMastodon(router: Router): void {
const accessTokens = ctx.headers.authorization;
const client = getClient(BASE_URL, accessTokens);
try {
const data = await client.getAccount(ctx.params.id);
ctx.body = data.data;
const calcId = convertId(ctx.params.id, IdType.CalckeyId);
const data = await client.getAccount(calcId);
let resp = data.data;
resp.id = convertId(resp.id, IdType.MastodonId);
ctx.body = resp;
} catch (e: any) {
console.error(e);
console.error(e.response.data);
@ -123,10 +115,20 @@ export function apiAccountMastodon(router: Router): void {
const client = getClient(BASE_URL, accessTokens);
try {
const data = await client.getAccountStatuses(
ctx.params.id,
convertId(ctx.params.id, IdType.CalckeyId),
argsToBools(limitToInt(ctx.query as any)),
);
ctx.body = data.data;
let resp = data.data;
for (let statIdx = 0; statIdx < resp.length; statIdx++) {
resp[statIdx].id = convertId(resp[statIdx].id, IdType.MastodonId);
resp[statIdx].in_reply_to_account_id = resp[statIdx].in_reply_to_account_id ? convertId(resp[statIdx].in_reply_to_account_id, IdType.MastodonId) : null;
resp[statIdx].in_reply_to_id = resp[statIdx].in_reply_to_id ? convertId(resp[statIdx].in_reply_to_id, IdType.MastodonId) : null;
let mentions = resp[statIdx].mentions
for (let mtnIdx = 0; mtnIdx < mentions.length; mtnIdx++) {
resp[statIdx].mentions[mtnIdx].id = convertId(mentions[mtnIdx].id, IdType.MastodonId);
}
}
ctx.body = resp;
} catch (e: any) {
console.error(e);
console.error(e.response.data);
@ -143,10 +145,14 @@ export function apiAccountMastodon(router: Router): void {
const client = getClient(BASE_URL, accessTokens);
try {
const data = await client.getAccountFollowers(
ctx.params.id,
ctx.query as any,
convertId(ctx.params.id, IdType.CalckeyId),
limitToInt(ctx.query as any),
);
ctx.body = data.data;
let resp = data.data;
for (let acctIdx = 0; acctIdx < resp.length; acctIdx++) {
resp[acctIdx].id = convertId(resp[acctIdx].id, IdType.MastodonId);
}
ctx.body = resp;
} catch (e: any) {
console.error(e);
console.error(e.response.data);
@ -163,10 +169,14 @@ export function apiAccountMastodon(router: Router): void {
const client = getClient(BASE_URL, accessTokens);
try {
const data = await client.getAccountFollowing(
ctx.params.id,
ctx.query as any,
convertId(ctx.params.id, IdType.CalckeyId),
limitToInt(ctx.query as any),
);
ctx.body = data.data;
let resp = data.data;
for (let acctIdx = 0; acctIdx < resp.length; acctIdx++) {
resp[acctIdx].id = convertId(resp[acctIdx].id, IdType.MastodonId);
}
ctx.body = resp;
} catch (e: any) {
console.error(e);
console.error(e.response.data);
@ -199,10 +209,11 @@ export function apiAccountMastodon(router: Router): void {
const accessTokens = ctx.headers.authorization;
const client = getClient(BASE_URL, accessTokens);
try {
const data = await client.followAccount(ctx.params.id);
const acct = data.data;
const data = await client.followAccount(convertId(ctx.params.id, IdType.CalckeyId));
let acct = data.data;
acct.following = true;
ctx.body = data.data;
acct.id = convertId(acct.id, IdType.MastodonId);
ctx.body = acct;
} catch (e: any) {
console.error(e);
console.error(e.response.data);
@ -218,10 +229,11 @@ export function apiAccountMastodon(router: Router): void {
const accessTokens = ctx.headers.authorization;
const client = getClient(BASE_URL, accessTokens);
try {
const data = await client.unfollowAccount(ctx.params.id);
const acct = data.data;
const data = await client.unfollowAccount(convertId(ctx.params.id, IdType.CalckeyId));
let acct = data.data;
acct.id = convertId(acct.id, IdType.MastodonId);
acct.following = false;
ctx.body = data.data;
ctx.body = acct;
} catch (e: any) {
console.error(e);
console.error(e.response.data);
@ -237,8 +249,10 @@ export function apiAccountMastodon(router: Router): void {
const accessTokens = ctx.headers.authorization;
const client = getClient(BASE_URL, accessTokens);
try {
const data = await client.blockAccount(ctx.params.id);
ctx.body = data.data;
const data = await client.blockAccount(convertId(ctx.params.id, IdType.CalckeyId));
let resp = data.data;
resp.id = convertId(resp.id, IdType.MastodonId);
ctx.body = resp;
} catch (e: any) {
console.error(e);
console.error(e.response.data);
@ -254,8 +268,10 @@ export function apiAccountMastodon(router: Router): void {
const accessTokens = ctx.headers.authorization;
const client = getClient(BASE_URL, accessTokens);
try {
const data = await client.unblockAccount(ctx.params.id);
ctx.body = data.data;
const data = await client.unblockAccount(convertId(ctx.params.id, IdType.MastodonId));
let resp = data.data;
resp.id = convertId(resp.id, IdType.MastodonId);
ctx.body = resp;
} catch (e: any) {
console.error(e);
console.error(e.response.data);
@ -272,10 +288,12 @@ export function apiAccountMastodon(router: Router): void {
const client = getClient(BASE_URL, accessTokens);
try {
const data = await client.muteAccount(
ctx.params.id,
convertId(ctx.params.id, IdType.CalckeyId),
(ctx.request as any).body as any,
);
ctx.body = data.data;
let resp = data.data;
resp.id = convertId(resp.id, IdType.MastodonId);
ctx.body = resp;
} catch (e: any) {
console.error(e);
console.error(e.response.data);
@ -291,8 +309,10 @@ export function apiAccountMastodon(router: Router): void {
const accessTokens = ctx.headers.authorization;
const client = getClient(BASE_URL, accessTokens);
try {
const data = await client.unmuteAccount(ctx.params.id);
ctx.body = data.data;
const data = await client.unmuteAccount(convertId(ctx.params.id, IdType.CalckeyId));
let resp = data.data;
resp.id = convertId(resp.id, IdType.MastodonId);
ctx.body = resp;
} catch (e: any) {
console.error(e);
console.error(e.response.data);
@ -308,16 +328,23 @@ export function apiAccountMastodon(router: Router): void {
let users;
try {
// TODO: this should be body
const idsRaw = ctx.request.query ? ctx.request.query["id[]"] : null;
const ids = typeof idsRaw === "string" ? [idsRaw] : idsRaw;
let ids = ctx.request.query ? ctx.request.query["id[]"] : null;
if (typeof ids === "string") {
ids = [ids];
}
users = ids;
relationshopModel.id = idsRaw?.toString() || "1";
if (!idsRaw) {
ctx.body = [relationshopModel];
relationshipModel.id = ids?.toString() || "1";
if (!ids) {
ctx.body = [relationshipModel];
return;
}
const data = await client.getRelationships(ids);
ctx.body = data.data;
let resp = data.data;
for (let acctIdx = 0; acctIdx < resp.length; acctIdx++) {
resp[acctIdx].id = convertId(resp[acctIdx].id, IdType.MastodonId);
}
ctx.body = resp;
} catch (e: any) {
console.error(e);
let data = e.response.data;
@ -333,7 +360,17 @@ export function apiAccountMastodon(router: Router): void {
const client = getClient(BASE_URL, accessTokens);
try {
const data = (await client.getBookmarks(ctx.query as any)) as any;
ctx.body = data.data;
let resp = data.data;
for (let statIdx = 0; statIdx < resp.length; statIdx++) {
resp[statIdx].id = convertId(resp[statIdx].id, IdType.MastodonId);
resp[statIdx].in_reply_to_account_id = resp[statIdx].in_reply_to_account_id ? convertId(resp[statIdx].in_reply_to_account_id, IdType.MastodonId) : null;
resp[statIdx].in_reply_to_id = resp[statIdx].in_reply_to_id ? convertId(resp[statIdx].in_reply_to_id, IdType.MastodonId) : null;
let mentions = resp[statIdx].mentions
for (let mtnIdx = 0; mtnIdx < mentions.length; mtnIdx++) {
resp[statIdx].mentions[mtnIdx].id = convertId(mentions[mtnIdx].id, IdType.MastodonId);
}
}
ctx.body = resp;
} catch (e: any) {
console.error(e);
console.error(e.response.data);
@ -347,7 +384,17 @@ export function apiAccountMastodon(router: Router): void {
const client = getClient(BASE_URL, accessTokens);
try {
const data = await client.getFavourites(ctx.query as any);
ctx.body = data.data;
let resp = data.data;
for (let statIdx = 0; statIdx < resp.length; statIdx++) {
resp[statIdx].id = convertId(resp[statIdx].id, IdType.MastodonId);
resp[statIdx].in_reply_to_account_id = resp[statIdx].in_reply_to_account_id ? convertId(resp[statIdx].in_reply_to_account_id, IdType.MastodonId) : null;
resp[statIdx].in_reply_to_id = resp[statIdx].in_reply_to_id ? convertId(resp[statIdx].in_reply_to_id, IdType.MastodonId) : null;
let mentions = resp[statIdx].mentions
for (let mtnIdx = 0; mtnIdx < mentions.length; mtnIdx++) {
resp[statIdx].mentions[mtnIdx].id = convertId(mentions[mtnIdx].id, IdType.MastodonId);
}
}
ctx.body = resp;
} catch (e: any) {
console.error(e);
console.error(e.response.data);
@ -361,7 +408,11 @@ export function apiAccountMastodon(router: Router): void {
const client = getClient(BASE_URL, accessTokens);
try {
const data = await client.getMutes(ctx.query as any);
ctx.body = data.data;
let resp = data.data;
for (let acctIdx = 0; acctIdx < resp.length; acctIdx++) {
resp[acctIdx].id = convertId(resp[acctIdx].id, IdType.MastodonId);
}
ctx.body = resp;
} catch (e: any) {
console.error(e);
console.error(e.response.data);
@ -375,7 +426,11 @@ export function apiAccountMastodon(router: Router): void {
const client = getClient(BASE_URL, accessTokens);
try {
const data = await client.getBlocks(ctx.query as any);
ctx.body = data.data;
let resp = data.data;
for (let acctIdx = 0; acctIdx < resp.length; acctIdx++) {
resp[acctIdx].id = convertId(resp[acctIdx].id, IdType.MastodonId);
}
ctx.body = resp;
} catch (e: any) {
console.error(e);
console.error(e.response.data);
@ -383,7 +438,7 @@ export function apiAccountMastodon(router: Router): void {
ctx.body = e.response.data;
}
});
router.get("/v1/follow_ctxs", async (ctx) => {
router.get("/v1/follow_requests", async (ctx) => {
const BASE_URL = `${ctx.protocol}://${ctx.hostname}`;
const accessTokens = ctx.headers.authorization;
const client = getClient(BASE_URL, accessTokens);
@ -391,7 +446,11 @@ export function apiAccountMastodon(router: Router): void {
const data = await client.getFollowRequests(
((ctx.query as any) || { limit: 20 }).limit,
);
ctx.body = data.data;
let resp = data.data;
for (let acctIdx = 0; acctIdx < resp.length; acctIdx++) {
resp[acctIdx].id = convertId(resp[acctIdx].id, IdType.MastodonId);
}
ctx.body = resp;
} catch (e: any) {
console.error(e);
console.error(e.response.data);
@ -400,14 +459,16 @@ export function apiAccountMastodon(router: Router): void {
}
});
router.post<{ Params: { id: string } }>(
"/v1/follow_ctxs/:id/authorize",
"/v1/follow_requests/:id/authorize",
async (ctx) => {
const BASE_URL = `${ctx.protocol}://${ctx.hostname}`;
const accessTokens = ctx.headers.authorization;
const client = getClient(BASE_URL, accessTokens);
try {
const data = await client.acceptFollowRequest(ctx.params.id);
ctx.body = data.data;
const data = await client.acceptFollowRequest(convertId(ctx.params.id, IdType.CalckeyId));
let resp = data.data;
resp.id = convertId(resp.id, IdType.MastodonId);
ctx.body = resp;
} catch (e: any) {
console.error(e);
console.error(e.response.data);
@ -417,14 +478,16 @@ export function apiAccountMastodon(router: Router): void {
},
);
router.post<{ Params: { id: string } }>(
"/v1/follow_ctxs/:id/reject",
"/v1/follow_requests/:id/reject",
async (ctx) => {
const BASE_URL = `${ctx.protocol}://${ctx.hostname}`;
const accessTokens = ctx.headers.authorization;
const client = getClient(BASE_URL, accessTokens);
try {
const data = await client.rejectFollowRequest(ctx.params.id);
ctx.body = data.data;
const data = await client.rejectFollowRequest(convertId(ctx.params.id, IdType.CalckeyId));
let resp = data.data;
resp.id = convertId(resp.id, IdType.MastodonId);
ctx.body = resp;
} catch (e: any) {
console.error(e);
console.error(e.response.data);

View file

@ -44,12 +44,10 @@ const writeScope = [
export function apiAuthMastodon(router: Router): void {
router.post("/v1/apps", async (ctx) => {
const BASE_URL = `${ctx.request.protocol}://${ctx.request.hostname}`;
const accessTokens = ctx.request.headers.authorization;
const client = getClient(BASE_URL, accessTokens);
const body: any = ctx.request.body;
const client = getClient(BASE_URL, '');
const body: any = ctx.request.body || ctx.request.query;
try {
let scope = body.scopes;
console.log(body);
if (typeof scope === "string") scope = scope.split(" ");
const pushScope = new Set<string>();
for (const s of scope) {
@ -64,14 +62,16 @@ export function apiAuthMastodon(router: Router): void {
redirect_uris: red,
website: body.website,
});
ctx.body = {
id: appData.id,
const returns = {
id: Math.floor(Math.random() * 100).toString(),
name: appData.name,
website: appData.website,
website: body.website,
redirect_uri: red,
client_id: Buffer.from(appData.url || "").toString("base64"),
client_secret: appData.clientSecret,
client_secret: appData.clientSecret
};
console.log(returns)
ctx.body = returns;
} catch (e: any) {
console.error(e);
ctx.status = 401;

View file

@ -10,18 +10,18 @@ export async function getInstance(response: Entity.Instance) {
const totalStatuses = Notes.count({ where: { userHost: IsNull() } });
return {
uri: response.uri,
title: response.title || "",
short_description: response.description || "",
description: response.description || "",
title: response.title || "Calckey",
short_description: response.description.substring(0, 50) || "See real server website",
description: response.description || "This is a vanilla Calckey Instance. It doesnt seem to have a description. BTW you are using the Mastodon api to access this server :)",
email: response.email || "",
version: "3.0.0 compatible (Calckey)",
version: "3.0.0 compatible (3.5+ Calckey)", //I hope this version string is correct, we will need to test it.
urls: response.urls,
stats: {
user_count: (await totalUsers),
status_count: (await totalStatuses),
domain_count: response.stats.domain_count
},
thumbnail: response.thumbnail || "",
thumbnail: response.thumbnail || 'https://http.cat/404',
languages: meta.langs,
registrations: !meta.disableRegistration || response.registrations,
approval_required: !response.registrations,

View file

@ -1,6 +1,9 @@
import megalodon, { MegalodonInterface } from "@calckey/megalodon";
import Router from "@koa/router";
import { getClient } from "../ApiMastodonCompatibleService.js";
import axios from "axios";
import { Converter } from "@calckey/megalodon";
import { limitToInt } from "./timeline.js";
export function apiSearchMastodon(router: Router): void {
router.get("/v1/search", async (ctx) => {
@ -9,7 +12,7 @@ export function apiSearchMastodon(router: Router): void {
const client = getClient(BASE_URL, accessTokens);
const body: any = ctx.request.body;
try {
const query: any = ctx.query;
const query: any = limitToInt(ctx.query);
const type = query.type || "";
const data = await client.search(query.q, type, query);
ctx.body = data.data;
@ -19,4 +22,110 @@ export function apiSearchMastodon(router: Router): void {
ctx.body = e.response.data;
}
});
router.get("/v2/search", async (ctx) => {
const BASE_URL = `${ctx.request.protocol}://${ctx.request.hostname}`;
const accessTokens = ctx.headers.authorization;
const client = getClient(BASE_URL, accessTokens);
try {
const query: any = limitToInt(ctx.query);
const type = query.type;
if (type) {
const data = await client.search(query.q, type, query);
ctx.body = data.data;
} else {
const acct = await client.search(query.q, "accounts", query);
const stat = await client.search(query.q, "statuses", query);
const tags = await client.search(query.q, "hashtags", query);
ctx.body = {
accounts: acct.data.accounts,
statuses: stat.data.statuses,
hashtags: tags.data.hashtags,
};
}
} catch (e: any) {
console.error(e);
ctx.status = (401);
ctx.body = e.response.data;
}
});
router.get("/v1/trends/statuses", async (ctx) => {
const BASE_URL = `${ctx.request.protocol}://${ctx.request.hostname}`;
const accessTokens = ctx.headers.authorization;
try {
const data = await getHighlight(BASE_URL, ctx.request.hostname, accessTokens);
ctx.body = data;
} catch (e: any) {
console.error(e);
ctx.status = (401);
ctx.body = e.response.data;
}
});
router.get("/v2/suggestions", async (ctx) => {
const BASE_URL = `${ctx.request.protocol}://${ctx.request.hostname}`;
const accessTokens = ctx.headers.authorization;
try {
const query: any = ctx.query;
const data = await getFeaturedUser(
BASE_URL,
ctx.request.hostname,
accessTokens,
query.limit || 20,
);
console.log(data);
ctx.body = data;
} catch (e: any) {
console.error(e);
ctx.status = (401);
ctx.body = e.response.data;
}
});
}
async function getHighlight(
BASE_URL: string,
domain: string,
accessTokens: string | undefined,
) {
const accessTokenArr = accessTokens?.split(" ") ?? [null];
const accessToken = accessTokenArr[accessTokenArr.length - 1];
try {
const api = await axios.post(`${BASE_URL}/api/notes/featured`, {
i: accessToken,
});
const data: MisskeyEntity.Note[] = api.data;
return data.map((note) => Converter.note(note, domain));
} catch (e: any) {
console.log(e);
console.log(e.response.data);
return [];
}
}
async function getFeaturedUser(
BASE_URL: string,
host: string,
accessTokens: string | undefined,
limit: number,
) {
const accessTokenArr = accessTokens?.split(" ") ?? [null];
const accessToken = accessTokenArr[accessTokenArr.length - 1];
try {
const api = await axios.post(`${BASE_URL}/api/users`, {
i: accessToken,
limit,
origin: "local",
sort: "+follower",
state: "alive",
});
const data: MisskeyEntity.UserDetail[] = api.data;
console.log(data);
return data.map((u) => {
return {
source: "past_interactions",
account: Converter.userDetail(u, host),
};
});
} catch (e: any) {
console.log(e);
console.log(e.response.data);
return [];
}
}

View file

@ -2,6 +2,12 @@ import Router from "@koa/router";
import { getClient } from "../ApiMastodonCompatibleService.js";
import { emojiRegexAtStartToEnd } from "@/misc/emoji-regex.js";
import axios from "axios";
import querystring from 'node:querystring'
import qs from 'qs'
function normalizeQuery(data: any) {
const str = querystring.stringify(data);
return qs.parse(str);
}
export function apiStatusMastodon(router: Router): void {
router.post("/v1/statuses", async (ctx) => {
@ -9,9 +15,12 @@ export function apiStatusMastodon(router: Router): void {
const accessTokens = ctx.headers.authorization;
const client = getClient(BASE_URL, accessTokens);
try {
const body: any = ctx.request.body;
let body: any = ctx.request.body;
if ((!body.poll && body['poll[options][]']) || (!body.media_ids && body['media_ids[]'])) {
body = normalizeQuery(body)
}
const text = body.status;
const removed = text.replace(/@\S+/g, "").replaceAll(" ", "");
const removed = text.replace(/@\S+/g, "").replace(/\s|/g, '')
const isDefaultEmoji = emojiRegexAtStartToEnd.test(removed);
const isCustomEmoji = /^:[a-zA-Z0-9@_]+:$/.test(removed);
if ((body.in_reply_to_id && isDefaultEmoji) || isCustomEmoji) {
@ -36,6 +45,8 @@ export function apiStatusMastodon(router: Router): void {
}
if (!body.media_ids) body.media_ids = undefined;
if (body.media_ids && !body.media_ids.length) body.media_ids = undefined;
const { sensitive } = body
body.sensitive = typeof sensitive === 'string' ? sensitive === 'true' : sensitive
const data = await client.postStatus(text, body);
ctx.body = data.data;
} catch (e: any) {
@ -70,7 +81,7 @@ export function apiStatusMastodon(router: Router): void {
const data = await client.deleteStatus(ctx.params.id);
ctx.body = data.data;
} catch (e: any) {
console.error(e);
console.error(e.response.data, request.params.id);
ctx.status = 401;
ctx.body = e.response.data;
}
@ -430,6 +441,6 @@ export function statusModel(
pinned: false,
emoji_reactions: [],
bookmarked: false,
quote: false,
quote: null,
};
}

View file

@ -9,7 +9,9 @@ export function limitToInt(q: ParsedUrlQuery) {
let object: any = q;
if (q.limit)
if (typeof q.limit === "string") object.limit = parseInt(q.limit, 10);
return q;
if (q.offset)
if (typeof q.offset === "string") object.offset = parseInt(q.offset, 10);
return object;
}
export function argsToBools(q: ParsedUrlQuery) {
@ -26,12 +28,29 @@ export function argsToBools(q: ParsedUrlQuery) {
export function toTextWithReaction(status: Entity.Status[], host: string) {
return status.map((t) => {
if (!t) return statusModel(null, null, [], "no content");
t.quote = null as any;
if (!t.emoji_reactions) return t;
if (t.reblog) t.reblog = toTextWithReaction([t.reblog], host)[0];
const reactions = t.emoji_reactions.map(
(r) => `${r.name.replace("@.", "")} (${r.count}${r.me ? "* " : ""})`,
);
//t.emojis = getEmoji(t.content, host)
const reactions = t.emoji_reactions.map((r) => {
const emojiNotation = r.url ? `:${r.name.replace('@.', '')}:` : r.name
return `${emojiNotation} (${r.count}${r.me ? `* ` : ''})`
});
const reaction = t.emoji_reactions as Entity.Reaction[];
const emoji = t.emojis || []
for (const r of reaction) {
if (!r.url) continue
emoji.push({
'shortcode': r.name,
'url': r.url,
'static_url': r.url,
'visible_in_picker': true,
},)
}
const isMe = reaction.findIndex((r) => r.me) > -1;
const total = reaction.reduce((sum, reaction) => sum + reaction.count, 0);
t.favourited = isMe;
t.favourites_count = total;
t.emojis = emoji
t.content = `<p>${autoLinker(t.content, host)}</p><p>${reactions.join(
", ",
)}</p>`;
@ -103,7 +122,7 @@ export function apiTimelineMastodon(router: Router): void {
}
},
);
router.get<{ Params: { hashtag: string } }>(
router.get(
"/v1/timelines/home",
async (ctx, reply) => {
const BASE_URL = `${ctx.protocol}://${ctx.hostname}`;

View file

@ -414,12 +414,13 @@ export default class Connection {
const client = getClient(this.host, this.accessToken);
client.getStatus(payload.id).then((data) => {
const newPost = toTextWithReaction([data.data], this.host);
const targetPost = newPost[0]
for (const stream of this.currentSubscribe) {
this.wsConnection.send(
JSON.stringify({
stream,
event: "status.update",
payload: JSON.stringify(newPost[0]),
payload: JSON.stringify(targetPost),
}),
);
}

View file

@ -30,6 +30,8 @@ import proxyServer from "./proxy/index.js";
import webServer from "./web/index.js";
import { initializeStreamingServer } from "./api/streaming.js";
import { koaBody } from "koa-body";
import removeTrailingSlash from "koa-remove-trailing-slashes";
import {v4 as uuid} from "uuid";
export const serverLogger = new Logger("server", "gray", false);
@ -37,6 +39,8 @@ export const serverLogger = new Logger("server", "gray", false);
const app = new Koa();
app.proxy = true;
app.use(removeTrailingSlash());
if (!["production", "test"].includes(process.env.NODE_ENV || "")) {
// Logger
app.use(
@ -75,6 +79,7 @@ const mastoRouter = new Router();
mastoRouter.use(
koaBody({
urlencoded: true,
multipart: true,
}),
);
@ -154,24 +159,46 @@ router.get("/verify-email/:code", async (ctx) => {
});
mastoRouter.get("/oauth/authorize", async (ctx) => {
const client_id = ctx.request.query.client_id;
const { client_id, state, redirect_uri } = ctx.request.query;
console.log(ctx.request.req);
ctx.redirect(Buffer.from(client_id?.toString() || "", "base64").toString());
let param = "mastodon=true";
if (state)
param += `&state=${state}`;
if (redirect_uri)
param += `&redirect_uri=${redirect_uri}`;
const client = client_id? client_id : "";
ctx.redirect(`${Buffer.from(client.toString(), 'base64').toString()}?${param}`);
});
mastoRouter.post("/oauth/token", async (ctx) => {
const body: any = ctx.request.body;
let client_id: any = ctx.request.query.client_id;
const body: any = ctx.request.body || ctx.request.query;
console.log('token-request', body);
console.log('token-query', ctx.request.query);
if (body.redirect_uri.startsWith('com.tapbots') && body.grant_type === 'client_credentials') {
const ret = {
access_token: uuid(),
token_type: "Bearer",
scope: "read",
created_at: Math.floor(new Date().getTime() / 1000),
};
ctx.body = ret;
return;
}
let client_id: any = body.client_id;
const BASE_URL = `${ctx.request.protocol}://${ctx.request.hostname}`;
const generator = (megalodon as any).default;
const client = generator("misskey", BASE_URL, null) as MegalodonInterface;
let m = null;
let token = null;
if (body.code) {
m = body.code.match(/^[a-zA-Z0-9-]+/);
if (!m.length) {
ctx.body = { error: "Invalid code" };
return;
}
//m = body.code.match(/^([a-zA-Z0-9]{8})([a-zA-Z0-9]{4})([a-zA-Z0-9]{4})([a-zA-Z0-9]{4})([a-zA-Z0-9]{12})/);
//if (!m.length) {
// ctx.body = { error: "Invalid code" };
// return;
//}
//token = `${m[1]}-${m[2]}-${m[3]}-${m[4]}-${m[5]}`
console.log(body.code, token)
token = body.code
}
if (client_id instanceof Array) {
client_id = client_id.toString();
@ -182,14 +209,16 @@ mastoRouter.post("/oauth/token", async (ctx) => {
const atData = await client.fetchAccessToken(
client_id,
body.client_secret,
m ? m[0] : "",
token ? token : "",
);
ctx.body = {
const ret = {
access_token: atData.accessToken,
token_type: "Bearer",
scope: "read write follow",
scope: body.scope || 'read write follow push',
created_at: Math.floor(new Date().getTime() / 1000),
};
console.log('token-response', ret)
ctx.body = ret;
} catch (err: any) {
console.error(err);
ctx.status = 401;

View file

@ -3,7 +3,7 @@ import config from "@/config/index.js";
import { fetchMeta } from "@/misc/fetch-meta.js";
import { Users, Notes } from "@/models/index.js";
import { IsNull, MoreThan } from "typeorm";
import { MAX_NOTE_TEXT_LENGTH } from "@/const.js";
import { MAX_NOTE_TEXT_LENGTH, MAX_CAPTION_TEXT_LENGTH } from "@/const.js";
import { Cache } from "@/misc/cache.js";
const router = new Router();
@ -85,6 +85,7 @@ const nodeinfo2 = async () => {
enableHcaptcha: meta.enableHcaptcha,
enableRecaptcha: meta.enableRecaptcha,
maxNoteTextLength: MAX_NOTE_TEXT_LENGTH,
maxCaptionTextLength: MAX_CAPTION_TEXT_LENGTH,
enableTwitterIntegration: meta.enableTwitterIntegration,
enableGithubIntegration: meta.enableGithubIntegration,
enableDiscordIntegration: meta.enableDiscordIntegration,

View file

@ -9,6 +9,7 @@
"devDependencies": {
"@discordapp/twemoji": "14.0.2",
"@khmyznikov/pwa-install": "^0.2.0",
"@phosphor-icons/web": "^2.0.3",
"@rollup/plugin-alias": "3.1.9",
"@rollup/plugin-json": "4.1.0",
"@rollup/pluginutils": "^4.2.1",
@ -34,9 +35,9 @@
"calckey-js": "^0.0.22",
"chart.js": "4.1.1",
"chartjs-adapter-date-fns": "2.0.1",
"chartjs-chart-matrix": "^2.0.1",
"chartjs-plugin-gradient": "0.5.1",
"chartjs-plugin-zoom": "1.2.1",
"chartjs-chart-matrix": "^2.0.1",
"city-timezones": "^1.2.1",
"compare-versions": "5.0.3",
"cropperjs": "2.0.0-beta.2",

View file

@ -242,7 +242,7 @@ export async function openAccountMenu(
...accountItemPromises,
{
type: "parent",
icon: "ph-plus-bold ph-lg",
icon: "ph-plus ph-bold ph-lg",
text: i18n.ts.addAccount,
children: [
{
@ -261,13 +261,13 @@ export async function openAccountMenu(
},
{
type: "link",
icon: "ph-users-bold ph-lg",
icon: "ph-users ph-bold ph-lg",
text: i18n.ts.manageAccounts,
to: "/settings/accounts",
},
{
type: "button",
icon: "ph-sign-out-bold ph-lg",
icon: "ph-sign-out ph-bold ph-lg",
text: i18n.ts.logout,
action: () => {
signout();

View file

@ -1,7 +1,7 @@
<template>
<XWindow ref="uiWindow" :initial-width="400" :initial-height="500" :can-resize="true" @closed="emit('closed')">
<template #header>
<i class="ph-warning-circle-bold ph-lg" style="margin-right: 0.5em;"></i>
<i class="ph-warning-circle ph-bold ph-lg" style="margin-right: 0.5em;"></i>
<I18n :src="i18n.ts.reportAbuseOf" tag="span">
<template #name>
<b><MkAcct :user="user"/></b>

View file

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

View file

@ -2,10 +2,10 @@
<MkA :to="`/channels/${channel.id}`" class="eftoefju _panel" tabindex="-1">
<div class="banner" :style="bannerStyle">
<div class="fade"></div>
<div class="name"><i class="ph-television-bold ph-lg"></i> {{ channel.name }}</div>
<div class="name"><i class="ph-television ph-bold ph-lg"></i> {{ channel.name }}</div>
<div class="status">
<div>
<i class="ph-users-bold ph-lg ph-fw ph-lg"></i>
<i class="ph-users ph-bold ph-lg ph-fw ph-lg"></i>
<I18n :src="i18n.ts._channel.usersCount" tag="span" style="margin-left: 4px;">
<template #n>
<b>{{ channel.usersCount }}</b>
@ -13,7 +13,7 @@
</I18n>
</div>
<div>
<i class="ph-pencil-bold ph-lg ph-fw ph-lg"></i>
<i class="ph-pencil ph-bold ph-lg ph-fw ph-lg"></i>
<I18n :src="i18n.ts._channel.notesCount" tag="span" style="margin-left: 4px;">
<template #n>
<b>{{ channel.notesCount }}</b>

View file

@ -666,7 +666,7 @@ const fetchInstanceNotesChart = async (total: boolean): Promise<typeof chartData
const raw = await os.apiGet('charts/instance', { host: props.args.host, limit: props.limit, span: props.span });
return {
series: [{
name: 'Notes',
name: 'Posts',
type: 'area',
color: '#31748f',
data: format(total

View file

@ -5,8 +5,8 @@
<div class="sub">
<slot name="func"></slot>
<button v-if="foldable" class="_button" @click="() => showBody = !showBody">
<template v-if="showBody"><i class="ph-caret-up-bold ph-lg"></i></template>
<template v-else><i class="ph-caret-down-bold ph-lg"></i></template>
<template v-if="showBody"><i class="ph-caret-up ph-bold ph-lg"></i></template>
<template v-else><i class="ph-caret-down ph-bold ph-lg"></i></template>
</button>
</div>
</header>

View file

@ -64,14 +64,14 @@ export default defineComponent({
}, [
h('span', [
h('i', {
class: 'ph-caret-up-bold ph-lg icon',
class: 'ph-caret-up ph-bold ph-lg icon',
}),
getDateText(item.createdAt),
]),
h('span', [
getDateText(props.items[i + 1].createdAt),
h('i', {
class: 'ph-caret-down-bold ph-lg icon',
class: 'ph-caret-down ph-bold ph-lg icon',
}),
]),
]));

View file

@ -5,17 +5,17 @@
<i :class="icon"></i>
</div>
<div v-else-if="!input && !select" :class="[$style.icon, $style['type_' + type]]">
<i v-if="type === 'success'" :class="$style.iconInner" class="ph-check-bold ph-lg"></i>
<i v-else-if="type === 'error'" :class="$style.iconInner" class="ph-circle-wavy-warning-bold ph-lg"></i>
<i v-else-if="type === 'warning'" :class="$style.iconInner" class="ph-warning-bold ph-lg"></i>
<i v-else-if="type === 'info'" :class="$style.iconInner" class="ph-info-bold ph-lg"></i>
<i v-else-if="type === 'question'" :class="$style.iconInner" class="ph-circle-question-bold ph-lg"></i>
<i v-if="type === 'success'" :class="$style.iconInner" class="ph-check ph-bold ph-lg"></i>
<i v-else-if="type === 'error'" :class="$style.iconInner" class="ph-circle-wavy-warning ph-bold ph-lg"></i>
<i v-else-if="type === 'warning'" :class="$style.iconInner" class="ph-warning ph-bold ph-lg"></i>
<i v-else-if="type === 'info'" :class="$style.iconInner" class="ph-info ph-bold ph-lg"></i>
<i v-else-if="type === 'question'" :class="$style.iconInner" class="ph-circle-question ph-bold ph-lg"></i>
<MkLoading v-else-if="type === 'waiting'" :class="$style.iconInner" :em="true"/>
</div>
<header v-if="title" :class="$style.title"><Mfm :text="title"/></header>
<div v-if="text" :class="$style.text"><Mfm :text="text"/></div>
<MkInput v-if="input" v-model="inputValue" autofocus :type="input.type || 'text'" :placeholder="input.placeholder || undefined" @keydown="onInputKeydown">
<template v-if="input.type === 'password'" #prefix><i class="ph-password-bold ph-lg"></i></template>
<template v-if="input.type === 'password'" #prefix><i class="ph-password ph-bold ph-lg"></i></template>
</MkInput>
<MkSelect v-if="select" v-model="selectedValue" autofocus>
<template v-if="select.items">

View file

@ -63,30 +63,30 @@ const title = computed(() => `${props.file.name}\n${props.file.type} ${bytes(pro
function getMenu() {
return [{
text: i18n.ts.rename,
icon: 'ph-cursor-text-bold ph-lg',
icon: 'ph-cursor-text ph-bold ph-lg',
action: rename,
}, {
text: props.file.isSensitive ? i18n.ts.unmarkAsSensitive : i18n.ts.markAsSensitive,
icon: props.file.isSensitive ? 'ph-eye-bold ph-lg' : 'ph-eye-slash-bold ph-lg',
icon: props.file.isSensitive ? 'ph-eye ph-bold ph-lg' : 'ph-eye-slash ph-bold ph-lg',
action: toggleSensitive,
}, {
text: i18n.ts.describeFile,
icon: 'ph-cursor-text-bold ph-lg',
icon: 'ph-cursor-text ph-bold ph-lg',
action: describe,
}, null, {
text: i18n.ts.copyUrl,
icon: 'ph-link-simple-bold ph-lg',
icon: 'ph-link-simple ph-bold ph-lg',
action: copyUrl,
}, {
type: 'a',
href: props.file.url,
target: '_blank',
text: i18n.ts.download,
icon: 'ph-download-simple-bold ph-lg',
icon: 'ph-download-simple ph-bold ph-lg',
download: props.file.name,
}, null, {
text: i18n.ts.delete,
icon: 'ph-trash-bold ph-lg',
icon: 'ph-trash ph-bold ph-lg',
danger: true,
action: deleteFile,
}];

View file

@ -16,8 +16,8 @@
@dragend="onDragend"
>
<p class="name">
<template v-if="hover"><i class="ph-folder-notch-open-bold ph-lg ph-fw ph-lg"></i></template>
<template v-if="!hover"><i class="ph-folder-notch-bold ph-lg ph-fw ph-lg"></i></template>
<template v-if="hover"><i class="ph-folder-notch-open ph-bold ph-lg ph-fw ph-lg"></i></template>
<template v-if="!hover"><i class="ph-folder-notch ph-bold ph-lg ph-fw ph-lg"></i></template>
{{ folder.name }}
</p>
<p v-if="defaultStore.state.uploadFolder == folder.id" class="upload">
@ -229,7 +229,7 @@ function setAsUploadFolder() {
function onContextmenu(ev: MouseEvent) {
os.contextMenu([{
text: i18n.ts.openInWindow,
icon: 'ph-copy-bold ph-lg',
icon: 'ph-copy ph-bold ph-lg',
action: () => {
os.popup(defineAsyncComponent(() => import('@/components/MkDriveWindow.vue')), {
initialFolder: props.folder,
@ -238,11 +238,11 @@ function onContextmenu(ev: MouseEvent) {
},
}, null, {
text: i18n.ts.rename,
icon: 'ph-cursor-text-bold ph-lg',
icon: 'ph-cursor-text ph-bold ph-lg',
action: rename,
}, null, {
text: i18n.ts.delete,
icon: 'ph-trash-bold ph-lg',
icon: 'ph-trash ph-bold ph-lg',
danger: true,
action: deleteFolder,
}], ev);

View file

@ -7,7 +7,7 @@
@dragleave="onDragleave"
@drop.stop="onDrop"
>
<i v-if="folder == null" class="ph-cloud-bold ph-lg"></i>
<i v-if="folder == null" class="ph-cloud ph-bold ph-lg"></i>
<span>{{ folder == null ? i18n.ts.drive : folder.name }}</span>
</div>
</template>

View file

@ -11,7 +11,7 @@
@removeFolder="removeFolder"
/>
<template v-for="f in hierarchyFolders">
<span class="separator"><i class="ph-caret-right-bold ph-lg"></i></span>
<span class="separator"><i class="ph-caret-right ph-bold ph-lg"></i></span>
<XNavFolder
:folder="f"
:parent-folder="folder"
@ -21,10 +21,10 @@
@removeFolder="removeFolder"
/>
</template>
<span v-if="folder != null" class="separator"><i class="ph-caret-right-bold ph-lg"></i></span>
<span v-if="folder != null" class="separator"><i class="ph-caret-right ph-bold ph-lg"></i></span>
<span v-if="folder != null" class="folder current">{{ folder.name }}</span>
</div>
<button class="menu _button" @click="showMenu"><i class="ph-dots-three-outline-bold ph-lg"></i></button>
<button class="menu _button" @click="showMenu"><i class="ph-dots-three-outline ph-bold ph-lg"></i></button>
</nav>
<div
ref="main" class="main"
@ -573,26 +573,26 @@ function getMenu() {
type: 'label',
}, {
text: i18n.ts.upload,
icon: 'ph-upload-simple-bold ph-lg',
icon: 'ph-upload-simple ph-bold ph-lg',
action: () => { selectLocalFile(); },
}, {
text: i18n.ts.fromUrl,
icon: 'ph-link-simple-bold ph-lg',
icon: 'ph-link-simple ph-bold ph-lg',
action: () => { urlUpload(); },
}, null, {
text: folder.value ? folder.value.name : i18n.ts.drive,
type: 'label',
}, folder.value ? {
text: i18n.ts.renameFolder,
icon: 'ph-cursor-text-bold ph-lg',
icon: 'ph-cursor-text ph-bold ph-lg',
action: () => { renameFolder(folder.value); },
} : undefined, folder.value ? {
text: i18n.ts.deleteFolder,
icon: 'ph-trash-bold ph-lg',
icon: 'ph-trash ph-bold ph-lg',
action: () => { deleteFolder(folder.value as Misskey.entities.DriveFolder); },
} : undefined, {
text: i18n.ts.createFolder,
icon: 'ph-folder-notch-plus-bold ph-lg',
icon: 'ph-folder-notch-plus ph-bold ph-lg',
action: () => { createFolder(); },
}];
}

View file

@ -1,16 +1,16 @@
<template>
<div ref="thumbnail" class="zdjebgpv">
<ImgWithBlurhash v-if="isThumbnailAvailable" :hash="file.blurhash" :src="file.thumbnailUrl" :alt="file.name" :title="file.name" :cover="fit !== 'contain'"/>
<i v-else-if="is === 'image'" class="ph-file-image-bold ph-lg icon"></i>
<i v-else-if="is === 'video'" class="ph-file-video-bold ph-lg icon"></i>
<i v-else-if="is === 'audio' || is === 'midi'" class="ph-file-audio-bold ph-lg icon"></i>
<i v-else-if="is === 'csv'" class="ph-file-csv-bold ph-lg icon"></i>
<i v-else-if="is === 'pdf'" class="ph-file-pdf-bold ph-lg icon"></i>
<i v-else-if="is === 'textfile'" class="ph-file-text-bold ph-lg icon"></i>
<i v-else-if="is === 'archive'" class="ph-file-zip-bold ph-lg icon"></i>
<i v-else class="ph-file-bold ph-lg icon"></i>
<i v-else-if="is === 'image'" class="ph-file-image ph-bold ph-lg icon"></i>
<i v-else-if="is === 'video'" class="ph-file-video ph-bold ph-lg icon"></i>
<i v-else-if="is === 'audio' || is === 'midi'" class="ph-file-audio ph-bold ph-lg icon"></i>
<i v-else-if="is === 'csv'" class="ph-file-csv ph-bold ph-lg icon"></i>
<i v-else-if="is === 'pdf'" class="ph-file-pdf ph-bold ph-lg icon"></i>
<i v-else-if="is === 'textfile'" class="ph-file-text ph-bold ph-lg icon"></i>
<i v-else-if="is === 'archive'" class="ph-file-zip ph-bold ph-lg icon"></i>
<i v-else class="ph-file ph-bold ph-lg icon"></i>
<i v-if="isThumbnailAvailable && is === 'video'" class="ph-file-video-bold ph-lg icon-sub"></i>
<i v-if="isThumbnailAvailable && is === 'video'" class="ph-file-video ph-bold ph-lg icon-sub"></i>
</div>
</template>

View file

@ -2,7 +2,7 @@
<!-- このコンポーネントの要素のclassは親から利用されるのでむやみに弄らないこと -->
<section>
<header class="_acrylic" @click="shown = !shown">
<i class="toggle ph-fw ph-lg" :class="shown ? 'ph-caret-down-bold ph-lg' : 'ph-caret-up-bold ph-lg'"></i> <slot></slot> ({{ emojis.length }})
<i class="toggle ph-fw ph-lg" :class="shown ? 'ph-caret-down-bold ph-lg' : 'ph-caret-up ph-bold ph-lg'"></i> <slot></slot> ({{ emojis.length }})
</header>
<div v-if="shown" class="body">
<button

View file

@ -46,7 +46,7 @@
</section>
<section>
<header><i class="ph-alarm-bold ph-fw ph-lg"></i> {{ i18n.ts.recentUsed }}</header>
<header><i class="ph-alarm ph-bold ph-fw ph-lg"></i> {{ i18n.ts.recentUsed }}</header>
<div class="body">
<button
v-for="emoji in recentlyUsedEmojis"
@ -69,10 +69,10 @@
</div>
</div>
<div class="tabs">
<button class="_button tab" :class="{ active: tab === 'index' }" @click="tab = 'index'"><i class="ph-asterisk-bold ph-lg ph-fw ph-lg"></i></button>
<button class="_button tab" :class="{ active: tab === 'custom' }" @click="tab = 'custom'"><i class="ph-smiley-bold ph-lg ph-fw ph-lg"></i></button>
<button class="_button tab" :class="{ active: tab === 'unicode' }" @click="tab = 'unicode'"><i class="ph-leaf-bold ph-lg ph-fw ph-lg"></i></button>
<button class="_button tab" :class="{ active: tab === 'tags' }" @click="tab = 'tags'"><i class="ph-hash-bold ph-lg ph-fw ph-lg"></i></button>
<button class="_button tab" :class="{ active: tab === 'index' }" @click="tab = 'index'"><i class="ph-asterisk ph-bold ph-lg ph-fw ph-lg"></i></button>
<button class="_button tab" :class="{ active: tab === 'custom' }" @click="tab = 'custom'"><i class="ph-smiley ph-bold ph-lg ph-fw ph-lg"></i></button>
<button class="_button tab" :class="{ active: tab === 'unicode' }" @click="tab = 'unicode'"><i class="ph-leaf ph-bold ph-lg ph-fw ph-lg"></i></button>
<button class="_button tab" :class="{ active: tab === 'tags' }" @click="tab = 'tags'"><i class="ph-hash ph-bold ph-lg ph-fw ph-lg"></i></button>
</div>
</div>
</template>

View file

@ -1,6 +1,6 @@
<template>
<span class="mk-file-type-icon">
<template v-if="kind == 'image'"><i class="ph-file-image-bold ph-lg"></i></template>
<template v-if="kind == 'image'"><i class="ph-file-image ph-bold ph-lg"></i></template>
</span>
</template>

View file

@ -4,8 +4,8 @@
<div class="title"><slot name="header"></slot></div>
<div class="divider"></div>
<button class="_button">
<template v-if="showBody"><i class="ph-caret-up-bold ph-lg"></i></template>
<template v-else><i class="ph-caret-down-bold ph-lg"></i></template>
<template v-if="showBody"><i class="ph-caret-up ph-bold ph-lg"></i></template>
<template v-else><i class="ph-caret-down ph-bold ph-lg"></i></template>
</button>
</header>
<transition

View file

@ -13,27 +13,27 @@
>
<template v-if="!wait">
<template v-if="isBlocking">
<span v-if="full">{{ i18n.ts.blocked }}</span><i class="ph-prohibit-bold ph-lg"></i>
<span v-if="full">{{ i18n.ts.blocked }}</span><i class="ph-prohibit ph-bold ph-lg"></i>
</template>
<template v-else-if="hasPendingFollowRequestFromYou && user.isLocked">
<span v-if="full">{{ i18n.ts.followRequestPending }}</span><i class="ph-hourglass-medium-bold ph-lg"></i>
<span v-if="full">{{ i18n.ts.followRequestPending }}</span><i class="ph-hourglass-medium ph-bold ph-lg"></i>
</template>
<template v-else-if="hasPendingFollowRequestFromYou && !user.isLocked">
<!-- つまりリモートフォローの場合 -->
<span v-if="full">{{ i18n.ts.processing }}</span><i class="ph-circle-notch-bold ph-lg fa-pulse"></i>
<span v-if="full">{{ i18n.ts.processing }}</span><i class="ph-circle-notch ph-bold ph-lg fa-pulse"></i>
</template>
<template v-else-if="isFollowing">
<span v-if="full">{{ i18n.ts.unfollow }}</span><i class="ph-minus-bold ph-lg"></i>
<span v-if="full">{{ i18n.ts.unfollow }}</span><i class="ph-minus ph-bold ph-lg"></i>
</template>
<template v-else-if="!isFollowing && user.isLocked">
<span v-if="full">{{ i18n.ts.followRequest }}</span><i class="ph-plus-bold ph-lg"></i>
<span v-if="full">{{ i18n.ts.followRequest }}</span><i class="ph-plus ph-bold ph-lg"></i>
</template>
<template v-else-if="!isFollowing && !user.isLocked">
<span v-if="full">{{ i18n.ts.follow }}</span><i class="ph-plus-bold ph-lg"></i>
<span v-if="full">{{ i18n.ts.follow }}</span><i class="ph-plus ph-bold ph-lg"></i>
</template>
</template>
<template v-else>
<span v-if="full">{{ i18n.ts.processing }}</span><i class="ph-circle-notch-bold ph-lg fa-pulse ph-fw ph-lg"></i>
<span v-if="full">{{ i18n.ts.processing }}</span><i class="ph-circle-notch ph-bold ph-lg fa-pulse ph-fw ph-lg"></i>
</template>
</button>
</template>

View file

@ -1,7 +1,7 @@
<template>
<div class="mk-google">
<input v-model="query" type="search" :placeholder="q">
<button @click="search"><i class="ph-magnifying-glass-bold ph-lg"></i> {{ i18n.ts.searchByGoogle }}</button>
<button @click="search"><i class="ph-magnifying-glass ph-bold ph-lg"></i> {{ i18n.ts.searchByGoogle }}</button>
</div>
</template>

View file

@ -1,7 +1,7 @@
<template>
<div class="fpezltsf" :class="{ warn }">
<i v-if="warn" class="ph-warning-bold ph-lg"></i>
<i v-else class="ph-info-bold ph-lg"></i>
<i v-if="warn" class="ph-warning ph-bold ph-lg"></i>
<i v-else class="ph-info ph-bold ph-lg"></i>
<slot></slot>
</div>
</template>

View file

@ -5,7 +5,7 @@
</div>
<div class="value">
<slot name="value"></slot>
<button v-if="copy" v-tooltip="i18n.ts.copy" class="_textButton" style="margin-left: 0.5em;" @click="copy_"><i class="ph-clipboard-text-bold"></i></button>
<button v-if="copy" v-tooltip="i18n.ts.copy" class="_textButton" style="margin-left: 0.5em;" @click="copy_"><i class="ph-clipboard-text ph-bold"></i></button>
</div>
</div>
</template>

View file

@ -6,12 +6,12 @@
<button v-if="item.action" v-click-anime class="_button" @click="$event => { item.action($event); close(); }">
<i class="icon" :class="item.icon"></i>
<div class="text">{{ item.text }}</div>
<span v-if="item.indicate" class="indicator"><i class="ph-circle-fill"></i></span>
<span v-if="item.indicate" class="indicator"><i class="ph-circle ph-fill"></i></span>
</button>
<MkA v-else v-click-anime :to="item.to" @click.passive="close()">
<i class="icon" :class="item.icon"></i>
<div class="text">{{ item.text }}</div>
<span v-if="item.indicate" class="indicator"><i class="ph-circle-fill"></i></span>
<span v-if="item.indicate" class="indicator"><i class="ph-circle ph-fill"></i></span>
</MkA>
</template>
</div>

View file

@ -3,7 +3,7 @@
:title="url"
>
<slot></slot>
<i v-if="target === '_blank'" class="ph-arrow-square-out-bold ph-lg icon"></i>
<i v-if="target === '_blank'" class="ph-arrow-square-out ph-bold ph-lg icon"></i>
</component>
</template>

View file

@ -1,7 +1,7 @@
<template>
<div class="mk-media-banner">
<div v-if="media.isSensitive && hide" class="sensitive" @click="hide = false">
<span class="icon"><i class="ph-warning-bold ph-lg"></i></span>
<span class="icon"><i class="ph-warning ph-bold ph-lg"></i></span>
<b>{{ i18n.ts.sensitive }}</b>
<span>{{ i18n.ts.clickToShow }}</span>
</div>
@ -37,7 +37,7 @@
:title="media.name"
:download="media.name"
>
<span class="icon"><i class="ph-download-simple-bold ph-lg"></i></span>
<span class="icon"><i class="ph-download-simple ph-bold ph-lg"></i></span>
<b>{{ media.name }}</b>
</a>
</div>

View file

@ -39,6 +39,7 @@ import MkButton from '@/components/MkButton.vue';
import bytes from '@/filters/bytes';
import number from '@/filters/number';
import { i18n } from '@/i18n';
import { instance } from '@/instance';
export default defineComponent({
components: {
@ -87,8 +88,9 @@ export default defineComponent({
computed: {
remainingLength(): number {
if (typeof this.inputValue !== "string") return 512;
return 512 - length(this.inputValue);
const maxCaptionLength = instance.maxCaptionTextLength ?? 512;
if (typeof this.inputValue !== "string") return maxCaptionLength;
return maxCaptionLength - length(this.inputValue);
}
},

View file

@ -3,7 +3,7 @@
<ImgWithBlurhash class="bg" :hash="image.blurhash" :title="image.comment" :alt="image.comment"/>
<div class="text">
<div class="wrapper">
<b style="display: block;"><i class="ph-warning-bold ph-lg"></i> {{ i18n.ts.sensitive }}</b>
<b style="display: block;"><i class="ph-warning ph-bold ph-lg"></i> {{ i18n.ts.sensitive }}</b>
<span style="display: block;">{{ i18n.ts.clickToShow }}</span>
</div>
</div>
@ -16,7 +16,7 @@
<ImgWithBlurhash :hash="image.blurhash" :src="url" :alt="image.comment" :type="image.type" :title="image.comment" :cover="false"/>
<div v-if="image.type === 'image/gif'" class="gif">GIF</div>
</a>
<button v-tooltip="i18n.ts.hide" class="_button hide" @click="hide = true"><i class="ph-eye-slash-bold ph-lg"></i></button>
<button v-tooltip="i18n.ts.hide" class="_button hide" @click="hide = true"><i class="ph-eye-slash ph-bold ph-lg"></i></button>
</div>
</template>

View file

@ -1,7 +1,7 @@
<template>
<div v-if="hide" class="icozogqfvdetwohsdglrbswgrejoxbdj" @click="hide = false">
<div>
<b><i class="ph-warning-bold ph-lg"></i> {{ i18n.ts.sensitive }}</b>
<b><i class="ph-warning ph-bold ph-lg"></i> {{ i18n.ts.sensitive }}</b>
<span>{{ i18n.ts.clickToShow }}</span>
</div>
</div>
@ -36,7 +36,7 @@
>
</video>
</VuePlyr>
<i class="ph-eye-slash-bold ph-lg" @click="hide = true"></i>
<i class="ph-eye-slash ph-bold ph-lg" @click="hide = true"></i>
</div>
</template>

View file

@ -19,16 +19,16 @@
<i v-if="item.icon" class="ph-fw ph-lg" :class="item.icon"></i>
<MkAvatar v-if="item.avatar" :user="item.avatar" class="avatar"/>
<span>{{ item.text }}</span>
<span v-if="item.indicate" class="indicator"><i class="ph-circle-fill"></i></span>
<span v-if="item.indicate" class="indicator"><i class="ph-circle ph-fill"></i></span>
</MkA>
<a v-else-if="item.type === 'a'" :href="item.href" :target="item.target" :download="item.download" :tabindex="i" class="_button item" @click="close(true)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
<i v-if="item.icon" class="ph-fw ph-lg" :class="item.icon"></i>
<span>{{ item.text }}</span>
<span v-if="item.indicate" class="indicator"><i class="ph-circle-fill"></i></span>
<span v-if="item.indicate" class="indicator"><i class="ph-circle ph-fill"></i></span>
</a>
<button v-else-if="item.type === 'user' && !items.hidden" :tabindex="i" class="_button item" :class="{ active: item.active }" :disabled="item.active" @click="clicked(item.action, $event)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
<MkAvatar :user="item.user" class="avatar"/><MkUserName :user="item.user"/>
<span v-if="item.indicate" class="indicator"><i class="ph-circle-fill"></i></span>
<span v-if="item.indicate" class="indicator"><i class="ph-circle ph-fill"></i></span>
</button>
<span v-else-if="item.type === 'switch'" :tabindex="i" class="item" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
<FormSwitch v-model="item.ref" :disabled="item.disabled" class="form-switch">{{ item.text }}</FormSwitch>
@ -36,13 +36,13 @@
<button v-else-if="item.type === 'parent'" :tabindex="i" class="_button item parent" :class="{ childShowing: childShowingItem === item }" @mouseenter="showChildren(item, $event)">
<i v-if="item.icon" class="ph-fw ph-lg" :class="item.icon"></i>
<span>{{ item.text }}</span>
<span class="caret"><i class="ph-caret-right-bold ph-lg ph-fw ph-lg"></i></span>
<span class="caret"><i class="ph-caret-right ph-bold ph-lg ph-fw ph-lg"></i></span>
</button>
<button v-else-if="!item.hidden" :tabindex="i" class="_button item" :class="{ danger: item.danger, active: item.active }" :disabled="item.active" @click="clicked(item.action, $event)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
<i v-if="item.icon" class="ph-fw ph-lg" :class="item.icon"></i>
<MkAvatar v-if="item.avatar" :user="item.avatar" class="avatar"/>
<span>{{ item.text }}</span>
<span v-if="item.indicate" class="indicator"><i class="ph-circle-fill"></i></span>
<span v-if="item.indicate" class="indicator"><i class="ph-circle ph-fill"></i></span>
</button>
</template>
<span v-if="items2.length === 0" class="none item">

View file

@ -2,13 +2,13 @@
<MkModal ref="modal" @click="$emit('click')" @closed="$emit('closed')">
<div ref="rootEl" class="hrmcaedk _narrow_" :style="{ width: `${width}px`, height: (height ? `min(${height}px, 100%)` : '100%') }">
<div class="header" @contextmenu="onContextmenu">
<button v-if="history.length > 0" v-tooltip="i18n.ts.goBack" class="_button" @click="back()"><i class="ph-caret-left-bold ph-lg"></i></button>
<button v-if="history.length > 0" v-tooltip="i18n.ts.goBack" class="_button" @click="back()"><i class="ph-caret-left ph-bold ph-lg"></i></button>
<span v-else style="display: inline-block; width: 20px"></span>
<span v-if="pageMetadata?.value" class="title">
<i v-if="pageMetadata?.value.icon" class="icon" :class="pageMetadata?.value.icon"></i>
<span>{{ pageMetadata?.value.title }}</span>
</span>
<button class="_button" @click="$refs.modal.close()"><i class="ph-x-bold ph-lg"></i></button>
<button class="_button" @click="$refs.modal.close()"><i class="ph-x ph-bold ph-lg"></i></button>
</div>
<div class="body">
<MkStickyContainer>
@ -68,22 +68,22 @@ const contextmenu = $computed(() => {
type: 'label',
text: path,
}, {
icon: 'ph-arrows-out-simple-bold ph-lg',
icon: 'ph-arrows-out-simple ph-bold ph-lg',
text: i18n.ts.showInPage,
action: expand,
}, {
icon: 'ph-arrow-square-out-bold ph-lg',
icon: 'ph-arrow-square-out ph-bold ph-lg',
text: i18n.ts.popout,
action: popout,
}, null, {
icon: 'ph-arrow-square-out-bold ph-lg',
icon: 'ph-arrow-square-out ph-bold ph-lg',
text: i18n.ts.openInNewTab,
action: () => {
window.open(pageUrl, '_blank');
modal.close();
},
}, {
icon: 'ph-link-simple-bold ph-lg',
icon: 'ph-link-simple ph-bold ph-lg',
text: i18n.ts.copyLink,
action: () => {
copyToClipboard(pageUrl);

View file

@ -2,12 +2,12 @@
<MkModal ref="modal" :prefer-type="'dialog'" @click="onBgClick" @closed="$emit('closed')">
<div ref="rootEl" class="ebkgoccj" :style="{ width: `${width}px`, height: scroll ? (height ? `${height}px` : null) : (height ? `min(${height}px, 100%)` : '100%') }" @keydown="onKeydown">
<div ref="headerEl" class="header">
<button v-if="withOkButton" class="_button" @click="$emit('close')"><i class="ph-x-bold ph-lg"></i></button>
<button v-if="withOkButton" class="_button" @click="$emit('close')"><i class="ph-x ph-bold ph-lg"></i></button>
<span class="title">
<slot name="header"></slot>
</span>
<button v-if="!withOkButton" class="_button" @click="$emit('close')"><i class="ph-x-bold ph-lg"></i></button>
<button v-if="withOkButton" class="_button" :disabled="okButtonDisabled" @click="$emit('ok')"><i class="ph-check-bold ph-lg"></i></button>
<button v-if="!withOkButton" class="_button" @click="$emit('close')"><i class="ph-x ph-bold ph-lg"></i></button>
<button v-if="withOkButton" class="_button" :disabled="okButtonDisabled" @click="$emit('ok')"><i class="ph-check ph-bold ph-lg"></i></button>
</div>
<div class="body">
<slot :width="bodyWidth" :height="bodyHeight"></slot>

View file

@ -1,6 +1,6 @@
<template>
<div class="msjugskd _block">
<i class="ph-airplane-takeoff-bold ph-lg" style="margin-right: 8px;"/>
<i class="ph-airplane-takeoff ph-bold ph-lg" style="margin-right: 8px;"/>
{{ i18n.ts.accountMoved }}
<MkMention class="link" :username="acct" :host="host"/>
</div>

View file

@ -12,11 +12,11 @@
<MkNoteSub v-if="appearNote.reply" :note="appearNote.reply" class="reply-to"/>
<div class="note-context">
<div class="line"></div>
<div v-if="appearNote._prId_" class="info"><i class="ph-megaphone-simple-bold ph-lg"></i> {{ i18n.ts.promotion }}<button class="_textButton hide" @click="readPromo()">{{ i18n.ts.hideThisNote }} <i class="ph-x-bold ph-lg"></i></button></div>
<div v-if="appearNote._featuredId_" class="info"><i class="ph-lightning-bold ph-lg"></i> {{ i18n.ts.featured }}</div>
<div v-if="pinned" class="info"><i class="ph-push-pin-bold ph-lg"></i>{{ i18n.ts.pinnedNote }}</div>
<div v-if="appearNote._prId_" class="info"><i class="ph-megaphone-simple-bold ph-lg"></i> {{ i18n.ts.promotion }}<button class="_textButton hide" @click="readPromo()">{{ i18n.ts.hideThisNote }} <i class="ph-x ph-bold ph-lg"></i></button></div>
<div v-if="appearNote._featuredId_" class="info"><i class="ph-lightning ph-bold ph-lg"></i> {{ i18n.ts.featured }}</div>
<div v-if="pinned" class="info"><i class="ph-push-pin ph-bold ph-lg"></i>{{ i18n.ts.pinnedNote }}</div>
<div v-if="isRenote" class="renote">
<i class="ph-repeat-bold ph-lg"></i>
<i class="ph-repeat ph-bold ph-lg"></i>
<I18n :src="i18n.ts.renotedBy" tag="span">
<template #user>
<MkA v-user-preview="note.userId" class="name" :to="userPage(note.user)">
@ -26,7 +26,7 @@
</I18n>
<div class="info">
<button ref="renoteTime" class="_button time" @click.stop="showRenoteMenu()">
<i v-if="isMyRenote" class="ph-dots-three-outline-bold ph-lg dropdownIcon"></i>
<i v-if="isMyRenote" class="ph-dots-three-outline ph-bold ph-lg dropdownIcon"></i>
<MkTime :time="note.createdAt"/>
</button>
<MkVisibility :note="note"/>
@ -69,7 +69,7 @@
<span>{{ i18n.ts.showLess }}</span>
</button>
</div>
<MkA v-if="appearNote.channel && !inChannel" class="channel" :to="`/channels/${appearNote.channel.id}`" @click.stop><i class="ph-television-bold ph-lg"></i> {{ appearNote.channel.name }}</MkA>
<MkA v-if="appearNote.channel && !inChannel" class="channel" :to="`/channels/${appearNote.channel.id}`" @click.stop><i class="ph-television ph-bold ph-lg"></i> {{ appearNote.channel.name }}</MkA>
</div>
<MkNoteFooter :note="appearNote"></MkNoteFooter>
</div>
@ -235,7 +235,7 @@ function showRenoteMenu(viaKeyboard = false): void {
if (!isMyRenote) return;
os.popupMenu([{
text: i18n.ts.unrenote,
icon: 'ph-trash-bold ph-lg',
icon: 'ph-trash ph-bold ph-lg',
danger: true,
action: () => {
os.api('notes/delete', {

View file

@ -13,7 +13,7 @@
<MkNoteSub v-if="appearNote.reply" :note="appearNote.reply" class="reply-to" @click.self="router.push(notePage(appearNote))"/>
<div v-if="isRenote" class="renote">
<MkAvatar class="avatar" :user="note.user"/>
<i class="ph-repeat-bold ph-lg"></i>
<i class="ph-repeat ph-bold ph-lg"></i>
<I18n :src="i18n.ts.renotedBy" tag="span">
<template #user>
<MkA v-user-preview="note.userId" class="name" :to="userPage(note.user)">
@ -23,7 +23,7 @@
</I18n>
<div class="info">
<button ref="renoteTime" class="_button time" @click="showRenoteMenu()">
<i v-if="isMyRenote" class="ph-dots-three-outline-bold ph-lg dropdownIcon"></i>
<i v-if="isMyRenote" class="ph-dots-three-outline ph-bold ph-lg dropdownIcon"></i>
<MkTime :time="note.createdAt"/>
</button>
<MkVisibility :note="note"/>
@ -70,7 +70,7 @@
<MkUrlPreview v-for="url in urls" :key="url" :url="url" :compact="true" :detail="true" class="url-preview"/>
<div v-if="appearNote.renote" class="renote"><XNoteSimple :note="appearNote.renote" @click.stop="router.push(notePage(appearNote.renote))"/></div>
</div>
<MkA v-if="appearNote.channel && !inChannel" class="channel" :to="`/channels/${appearNote.channel.id}`"><i class="ph-television-bold ph-lg"></i> {{ appearNote.channel.name }}</MkA>
<MkA v-if="appearNote.channel && !inChannel" class="channel" :to="`/channels/${appearNote.channel.id}`"><i class="ph-television ph-bold ph-lg"></i> {{ appearNote.channel.name }}</MkA>
</div>
<footer class="footer">
<div class="info">
@ -80,21 +80,21 @@
</div>
<XReactionsViewer ref="reactionsViewer" :note="appearNote"/>
<button v-tooltip.noDelay.bottom="i18n.ts.reply" class="button _button" @click="reply()">
<template v-if="appearNote.reply"><i class="ph-arrow-u-up-left-bold ph-lg"></i></template>
<template v-else><i class="ph-arrow-bend-up-left-bold ph-lg"></i></template>
<template v-if="appearNote.reply"><i class="ph-arrow-u-up-left ph-bold ph-lg"></i></template>
<template v-else><i class="ph-arrow-bend-up-left ph-bold ph-lg"></i></template>
<p v-if="appearNote.repliesCount > 0" class="count">{{ appearNote.repliesCount }}</p>
</button>
<XRenoteButton ref="renoteButton" class="button" :note="appearNote" :count="appearNote.renoteCount"/>
<XStarButton v-if="appearNote.myReaction == null" ref="starButton" class="button" :note="appearNote"/>
<button v-if="appearNote.myReaction == null" ref="reactButton" v-tooltip.noDelay.bottom="i18n.ts.reaction" class="button _button" @click="react()">
<i class="ph-smiley-bold ph-lg"></i>
<i class="ph-smiley ph-bold ph-lg"></i>
</button>
<button v-if="appearNote.myReaction != null" ref="reactButton" class="button _button reacted" @click="undoReact(appearNote)">
<i class="ph-minus-bold ph-lg"></i>
<i class="ph-minus ph-bold ph-lg"></i>
</button>
<XQuoteButton class="button" :note="appearNote"/>
<button ref="menuButton" v-tooltip.noDelay.bottom="i18n.ts.more" class="button _button" @click="menu()">
<i class="ph-dots-three-outline-bold ph-lg"></i>
<i class="ph-dots-three-outline ph-bold ph-lg"></i>
</button>
</footer>
</div>
@ -267,7 +267,7 @@ function showRenoteMenu(viaKeyboard = false): void {
if (!isMyRenote) return;
os.popupMenu([{
text: i18n.ts.unrenote,
icon: 'ph-trash-bold ph-lg',
icon: 'ph-trash ph-bold ph-lg',
danger: true,
action: () => {
os.api('notes/delete', {

View file

@ -29,7 +29,7 @@
</template>
<div v-else-if="replies.length > 0" class="more">
<div class="line"></div>
<MkA class="text _link" :to="notePage(note)">{{ i18n.ts.continueThread }} <i class="ph-caret-double-right-bold ph-lg"></i></MkA>
<MkA class="text _link" :to="notePage(note)">{{ i18n.ts.continueThread }} <i class="ph-caret-double-right ph-bold ph-lg"></i></MkA>
</div>
</template>
</div>

View file

@ -5,16 +5,16 @@
<MkAvatar v-else-if="notification.user" class="icon" :user="notification.user"/>
<img v-else-if="notification.icon" class="icon" :src="notification.icon" alt=""/>
<div class="sub-icon" :class="notification.type">
<i v-if="notification.type === 'follow'" class="ph-hand-waving-bold"></i>
<i v-else-if="notification.type === 'receiveFollowRequest'" class="ph-clock-bold"></i>
<i v-else-if="notification.type === 'followRequestAccepted'" class="ph-check-bold"></i>
<i v-else-if="notification.type === 'groupInvited'" class="ph-identification-card-bold"></i>
<i v-else-if="notification.type === 'renote'" class="ph-repeat-bold"></i>
<i v-else-if="notification.type === 'reply'" class="ph-arrow-bend-up-left-bold"></i>
<i v-else-if="notification.type === 'mention'" class="ph-at-bold"></i>
<i v-else-if="notification.type === 'quote'" class="ph-quotes-bold"></i>
<i v-else-if="notification.type === 'pollVote'" class="ph-microphone-stage-bold"></i>
<i v-else-if="notification.type === 'pollEnded'" class="ph-microphone-stage-bold"></i>
<i v-if="notification.type === 'follow'" class="ph-hand-waving ph-bold"></i>
<i v-else-if="notification.type === 'receiveFollowRequest'" class="ph-clock ph-bold"></i>
<i v-else-if="notification.type === 'followRequestAccepted'" class="ph-check ph-bold"></i>
<i v-else-if="notification.type === 'groupInvited'" class="ph-identification-card ph-bold"></i>
<i v-else-if="notification.type === 'renote'" class="ph-repeat ph-bold"></i>
<i v-else-if="notification.type === 'reply'" class="ph-arrow-bend-up-left ph-bold"></i>
<i v-else-if="notification.type === 'mention'" class="ph-at ph-bold"></i>
<i v-else-if="notification.type === 'quote'" class="ph-quotes ph-bold"></i>
<i v-else-if="notification.type === 'pollVote'" class="ph-microphone-stage ph-bold"></i>
<i v-else-if="notification.type === 'pollEnded'" class="ph-microphone-stage ph-bold"></i>
<!-- notification.reaction null になることはまずないがここでoptional chaining使うと一部ブラウザで刺さるので念の為 -->
<XReactionIcon
v-else-if="notification.type === 'reaction'"
@ -33,14 +33,14 @@
<MkTime v-if="withTime" :time="notification.createdAt" class="time"/>
</header>
<MkA v-if="notification.type === 'reaction'" class="text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)">
<i class="ph-quotes-fill ph-lg"></i>
<i class="ph-quotes ph-fill ph-lg"></i>
<Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="!full" :custom-emojis="notification.note.emojis"/>
<i class="ph-quotes-fill ph-lg"></i>
<i class="ph-quotes ph-fill ph-lg"></i>
</MkA>
<MkA v-if="notification.type === 'renote'" class="text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note.renote)">
<i class="ph-quotes-fill ph-lg"></i>
<i class="ph-quotes ph-fill ph-lg"></i>
<Mfm :text="getNoteSummary(notification.note.renote)" :plain="true" :nowrap="!full" :custom-emojis="notification.note.renote.emojis"/>
<i class="ph-quotes-fill ph-lg"></i>
<i class="ph-quotes ph-fill ph-lg"></i>
</MkA>
<MkA v-if="notification.type === 'reply'" class="text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)">
<Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="!full" :custom-emojis="notification.note.emojis"/>
@ -52,14 +52,14 @@
<Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="!full" :custom-emojis="notification.note.emojis"/>
</MkA>
<MkA v-if="notification.type === 'pollVote'" class="text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)">
<i class="ph-quotes-fill ph-lg"></i>
<i class="ph-quotes ph-fill ph-lg"></i>
<Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="!full" :custom-emojis="notification.note.emojis"/>
<i class="ph-quotes-fill ph-lg"></i>
<i class="ph-quotes ph-fill ph-lg"></i>
</MkA>
<MkA v-if="notification.type === 'pollEnded'" class="text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)">
<i class="ph-quotes-fill ph-lg"></i>
<i class="ph-quotes ph-fill ph-lg"></i>
<Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="!full" :custom-emojis="notification.note.emojis"/>
<i class="ph-quotes-fill ph-lg"></i>
<i class="ph-quotes ph-fill ph-lg"></i>
</MkA>
<span v-if="notification.type === 'follow'" class="text" style="opacity: 0.6;">{{ i18n.ts.youGotNewFollower }}<div v-if="full"><MkFollowButton :user="notification.user" :full="true"/></div></span>
<span v-if="notification.type === 'followRequestAccepted'" class="text" style="opacity: 0.6;">{{ i18n.ts.followRequestAccepted }}</span>

View file

@ -57,7 +57,7 @@ const buttonsLeft = $computed(() => {
if (history.length > 1) {
buttons.push({
icon: 'ph-caret-left-bold ph-lg',
icon: 'ph-caret-left ph-bold ph-lg',
onClick: back,
});
}
@ -66,7 +66,7 @@ const buttonsLeft = $computed(() => {
});
const buttonsRight = $computed(() => {
const buttons = [{
icon: 'ph-arrows-out-simple-bold ph-lg',
icon: 'ph-arrows-out-simple ph-bold ph-lg',
title: i18n.ts.showInPage,
onClick: expand,
}];
@ -86,22 +86,22 @@ provide('shouldOmitHeaderTitle', true);
provide('shouldHeaderThin', true);
const contextmenu = $computed(() => ([{
icon: 'ph-arrows-out-simple-bold ph-lg',
icon: 'ph-arrows-out-simple ph-bold ph-lg',
text: i18n.ts.showInPage,
action: expand,
}, {
icon: 'ph-arrow-square-out-bold ph-lg',
icon: 'ph-arrow-square-out ph-bold ph-lg',
text: i18n.ts.popout,
action: popout,
}, {
icon: 'ph-arrow-square-out-bold ph-lg',
icon: 'ph-arrow-square-out ph-bold ph-lg',
text: i18n.ts.openInNewTab,
action: () => {
window.open(url + router.getCurrentPath(), '_blank');
windowEl.close();
},
}, {
icon: 'ph-link-simple-bold ph-lg',
icon: 'ph-link-simple ph-bold ph-lg',
text: i18n.ts.copyLink,
action: () => {
copyToClipboard(url + router.getCurrentPath());

View file

@ -4,7 +4,7 @@
<li v-for="(choice, i) in note.poll.choices" :key="i" :class="{ voted: choice.voted }" @click.stop="vote(i)">
<div class="backdrop" :style="{ 'width': `${showResult ? (choice.votes / total * 100) : 0}%` }"></div>
<span>
<template v-if="choice.isVoted"><i class="ph-check-bold ph-lg"></i></template>
<template v-if="choice.isVoted"><i class="ph-check ph-bold ph-lg"></i></template>
<Mfm :text="choice.text" :plain="true" :custom-emojis="note.emojis"/>
<span v-if="showResult" class="votes">({{ i18n.t('_poll.votesCount', { n: choice.votes }) }})</span>
</span>

View file

@ -1,14 +1,14 @@
<template>
<div class="zmdxowus">
<p v-if="choices.length < 2" class="caution">
<i class="ph-warning-bold ph-lg"></i>{{ i18n.ts._poll.noOnlyOneChoice }}
<i class="ph-warning ph-bold ph-lg"></i>{{ i18n.ts._poll.noOnlyOneChoice }}
</p>
<ul>
<li v-for="(choice, i) in choices" :key="i">
<MkInput class="input" small :model-value="choice" :placeholder="i18n.t('_poll.choiceN', { n: i + 1 })" @update:modelValue="onInput(i, $event)">
</MkInput>
<button class="_button" @click="remove(i)">
<i class="ph-x-bold ph-lg"></i>
<i class="ph-x ph-bold ph-lg"></i>
</button>
</li>
</ul>

View file

@ -8,35 +8,35 @@
@drop.stop="onDrop"
>
<header>
<button v-if="!fixed" class="cancel _button" @click="cancel"><i class="ph-x-bold ph-lg"></i></button>
<button v-if="!fixed" class="cancel _button" @click="cancel"><i class="ph-x ph-bold ph-lg"></i></button>
<button v-click-anime v-tooltip="i18n.ts.switchAccount" class="account _button" @click="openAccountMenu">
<MkAvatar :user="postAccount ?? $i" class="avatar"/>
</button>
<div class="right">
<span class="text-count" :class="{ over: textLength > maxTextLength }">{{ maxTextLength - textLength }}</span>
<span v-if="localOnly" class="local-only"><i class="ph-hand-fist-bold ph-lg"></i></span>
<span v-if="localOnly" class="local-only"><i class="ph-hand-fist ph-bold ph-lg"></i></span>
<button ref="visibilityButton" v-tooltip="i18n.ts.visibility" class="_button visibility" :disabled="channel != null" @click="setVisibility">
<span v-if="visibility === 'public'"><i class="ph-planet-bold ph-lg"></i></span>
<span v-if="visibility === 'home'"><i class="ph-house-bold ph-lg"></i></span>
<span v-if="visibility === 'followers'"><i class="ph-lock-simple-open-bold ph-lg"></i></span>
<span v-if="visibility === 'specified'"><i class="ph-envelope-simple-open-bold ph-lg"></i></span>
<span v-if="visibility === 'public'"><i class="ph-planet ph-bold ph-lg"></i></span>
<span v-if="visibility === 'home'"><i class="ph-house ph-bold ph-lg"></i></span>
<span v-if="visibility === 'followers'"><i class="ph-lock-simple-open ph-bold ph-lg"></i></span>
<span v-if="visibility === 'specified'"><i class="ph-envelope-simple-open ph-bold ph-lg"></i></span>
</button>
<button v-tooltip="i18n.ts.previewNoteText" class="_button preview" :class="{ active: showPreview }" @click="showPreview = !showPreview"><i class="ph-file-code-bold ph-lg"></i></button>
<button class="submit _buttonGradate" :disabled="!canPost" data-cy-open-post-form-submit @click="post">{{ submitText }}<i :class="reply ? 'ph-arrow-bend-up-left-bold ph-lg' : renote ? 'ph-quotes-bold ph-lg' : 'ph-paper-plane-tilt-bold ph-lg'"></i></button>
<button v-tooltip="i18n.ts.previewNoteText" class="_button preview" :class="{ active: showPreview }" @click="showPreview = !showPreview"><i class="ph-file-code ph-bold ph-lg"></i></button>
<button class="submit _buttonGradate" :disabled="!canPost" data-cy-open-post-form-submit @click="post">{{ submitText }}<i :class="reply ? 'ph-arrow-bend-up-left ph-bold ph-lg' : renote ? 'ph-quotes ph-bold ph-lg' : 'ph-paper-plane-tilt ph-bold ph-lg'"></i></button>
</div>
</header>
<div class="form" :class="{ fixed }">
<XNoteSimple v-if="reply" class="preview" :note="reply"/>
<XNoteSimple v-if="renote" class="preview" :note="renote"/>
<div v-if="quoteId" class="with-quote"><i class="ph-quotes-bold ph-lg"></i> {{ i18n.ts.quoteAttached }}<button @click="quoteId = null"><i class="ph-x-bold ph-lg"></i></button></div>
<div v-if="quoteId" class="with-quote"><i class="ph-quotes ph-bold ph-lg"></i> {{ i18n.ts.quoteAttached }}<button @click="quoteId = null"><i class="ph-x ph-bold ph-lg"></i></button></div>
<div v-if="visibility === 'specified'" class="to-specified">
<span style="margin-right: 8px;">{{ i18n.ts.recipient }}</span>
<div class="visibleUsers">
<span v-for="u in visibleUsers" :key="u.id">
<MkAcct :user="u"/>
<button class="_button" @click="removeVisibleUser(u)"><i class="ph-x-bold ph-lg"></i></button>
<button class="_button" @click="removeVisibleUser(u)"><i class="ph-x ph-bold ph-lg"></i></button>
</span>
<button class="_button" @click="addVisibleUser"><i class="ph-plus-bold ph-md ph-fw ph-lg"></i></button>
<button class="_button" @click="addVisibleUser"><i class="ph-plus ph-bold ph-md ph-fw ph-lg"></i></button>
</div>
</div>
<MkInfo v-if="hasNotSpecifiedMentions" warn class="hasNotSpecifiedMentions">{{ i18n.ts.notSpecifiedMentionWarning }} - <button class="_textButton" @click="addMissingMention()">{{ i18n.ts.add }}</button></MkInfo>
@ -47,14 +47,14 @@
<XPollEditor v-if="poll" v-model="poll" @destroyed="poll = null"/>
<XNotePreview v-if="showPreview" class="preview" :text="text"/>
<footer>
<button v-tooltip="i18n.ts.attachFile" class="_button" @click="chooseFileFrom"><i class="ph-upload-bold ph-lg"></i></button>
<button v-tooltip="i18n.ts.poll" class="_button" :class="{ active: poll }" @click="togglePoll"><i class="ph-microphone-stage-bold ph-lg"></i></button>
<button v-tooltip="i18n.ts.useCw" class="_button" :class="{ active: useCw }" @click="useCw = !useCw"><i class="ph-eye-slash-bold ph-lg"></i></button>
<button v-tooltip="i18n.ts.mention" class="_button" @click="insertMention"><i class="ph-at-bold ph-lg"></i></button>
<button v-tooltip="i18n.ts.hashtags" class="_button" :class="{ active: withHashtags }" @click="withHashtags = !withHashtags"><i class="ph-hash-bold ph-lg"></i></button>
<button v-tooltip="i18n.ts.emoji" class="_button" @click="insertEmoji"><i class="ph-smiley-bold ph-lg"></i></button>
<button v-if="postFormActions.length > 0" v-tooltip="i18n.ts.plugin" class="_button" @click="showActions"><i class="ph-plug-bold ph-lg"></i></button>
<button v-tooltip="i18n.ts._mfm.cheatSheet" class="_button right" @click="openCheatSheet"><i class="ph-question-bold ph-lg"></i></button>
<button v-tooltip="i18n.ts.attachFile" class="_button" @click="chooseFileFrom"><i class="ph-upload ph-bold ph-lg"></i></button>
<button v-tooltip="i18n.ts.poll" class="_button" :class="{ active: poll }" @click="togglePoll"><i class="ph-microphone-stage ph-bold ph-lg"></i></button>
<button v-tooltip="i18n.ts.useCw" class="_button" :class="{ active: useCw }" @click="useCw = !useCw"><i class="ph-eye-slash ph-bold ph-lg"></i></button>
<button v-tooltip="i18n.ts.mention" class="_button" @click="insertMention"><i class="ph-at ph-bold ph-lg"></i></button>
<button v-tooltip="i18n.ts.hashtags" class="_button" :class="{ active: withHashtags }" @click="withHashtags = !withHashtags"><i class="ph-hash ph-bold ph-lg"></i></button>
<button v-tooltip="i18n.ts.emoji" class="_button" @click="insertEmoji"><i class="ph-smiley ph-bold ph-lg"></i></button>
<button v-if="postFormActions.length > 0" v-tooltip="i18n.ts.plugin" class="_button" @click="showActions"><i class="ph-plug ph-bold ph-lg"></i></button>
<button v-tooltip="i18n.ts._mfm.cheatSheet" class="_button right" @click="openCheatSheet"><i class="ph-question ph-bold ph-lg"></i></button>
</footer>
<datalist id="hashtags">
<option v-for="hashtag in recentHashtags" :key="hashtag" :value="hashtag"/>

View file

@ -5,7 +5,7 @@
<div class="file" @click="showFileMenu(element, $event)" @contextmenu.prevent="showFileMenu(element, $event)">
<MkDriveFileThumbnail :data-id="element.id" class="thumbnail" :file="element" fit="cover"/>
<div v-if="element.isSensitive" class="sensitive">
<i class="ph-warning-bold ph-lg icon"></i>
<i class="ph-warning ph-bold ph-lg icon"></i>
</div>
</div>
</template>
@ -115,19 +115,19 @@ export default defineComponent({
if (this.menu) return;
this.menu = os.popupMenu([{
text: i18n.ts.renameFile,
icon: 'ph-cursor-text-bold ph-lg',
icon: 'ph-cursor-text ph-bold ph-lg',
action: () => { this.rename(file); },
}, {
text: file.isSensitive ? i18n.ts.unmarkAsSensitive : i18n.ts.markAsSensitive,
icon: file.isSensitive ? 'ph-eye-slash-bold ph-lg' : 'ph-eye-bold ph-lg',
icon: file.isSensitive ? 'ph-eye-slash ph-bold ph-lg' : 'ph-eye ph-bold ph-lg',
action: () => { this.toggleSensitive(file); },
}, {
text: i18n.ts.describeFile,
icon: 'ph-cursor-text-bold ph-lg',
icon: 'ph-cursor-text ph-bold ph-lg',
action: () => { this.describe(file); },
}, {
text: i18n.ts.attachCancel,
icon: 'ph-circle-wavy-warning-bold ph-lg',
icon: 'ph-circle-wavy-warning ph-bold ph-lg',
action: () => { this.detachMedia(file.id); },
}], ev.currentTarget ?? ev.target).then(() => this.menu = null);
},

View file

@ -5,7 +5,7 @@
class="eddddedb _button"
@click="quote()"
>
<i class="ph-quotes-bold ph-lg"></i>
<i class="ph-quotes ph-bold ph-lg"></i>
</button>
</template>

View file

@ -6,11 +6,11 @@
class="eddddedb _button canRenote"
@click="renote(false, $event)"
>
<i class="ph-repeat-bold ph-lg"></i>
<i class="ph-repeat ph-bold ph-lg"></i>
<p v-if="count > 0" class="count">{{ count }}</p>
</button>
<button v-else class="eddddedb _button">
<i class="ph-prohibit-bold ph-lg"></i>
<i class="ph-prohibit ph-bold ph-lg"></i>
</button>
</template>
@ -66,7 +66,7 @@ const renote = async (viaKeyboard = false, ev?: MouseEvent) => {
let buttonActions = [{
text: i18n.ts.renote,
icon: 'ph-repeat-bold ph-lg',
icon: 'ph-repeat ph-bold ph-lg',
danger: false,
action: () => {
os.api('notes/create', {
@ -86,7 +86,7 @@ const renote = async (viaKeyboard = false, ev?: MouseEvent) => {
if (!defaultStore.state.seperateRenoteQuote) {
buttonActions.push({
text: i18n.ts.quote,
icon: 'ph-quotes-bold ph-lg',
icon: 'ph-quotes ph-bold ph-lg',
danger: false,
action: () => {
os.post({
@ -99,7 +99,7 @@ const renote = async (viaKeyboard = false, ev?: MouseEvent) => {
if (hasRenotedBefore) {
buttonActions.push({
text: i18n.ts.unrenote,
icon: 'ph-trash-bold ph-lg',
icon: 'ph-trash ph-bold ph-lg',
danger: true,
action: () => {
os.api('notes/unrenote', {

View file

@ -11,7 +11,7 @@
<template #suffix>@{{ host }}</template>
</MkInput>
<MkInput v-if="!user || user && !user.usePasswordLessLogin" v-model="password" class="_formBlock" :placeholder="i18n.ts.password" type="password" :with-password-toggle="true" required data-cy-signin-password>
<template #prefix><i class="ph-lock-bold ph-lg"></i></template>
<template #prefix><i class="ph-lock ph-bold ph-lg"></i></template>
<template #caption><button class="_textButton" type="button" @click="resetPassword">{{ i18n.ts.forgotPassword }}</button></template>
</MkInput>
<MkButton class="_formBlock" type="submit" primary :disabled="signing" style="margin: 0 auto;">{{ signing ? i18n.ts.loggingIn : i18n.ts.login }}</MkButton>
@ -30,20 +30,20 @@
<p style="margin-bottom:0;">{{ i18n.ts.twoStepAuthentication }}</p>
<MkInput v-if="user && user.usePasswordLessLogin" v-model="password" type="password" :with-password-toggle="true" required>
<template #label>{{ i18n.ts.password }}</template>
<template #prefix><i class="ph-lock-bold ph-lg"></i></template>
<template #prefix><i class="ph-lock ph-bold ph-lg"></i></template>
</MkInput>
<MkInput v-model="token" type="text" pattern="^[0-9]{6}$" autocomplete="off" :spellcheck="false" required>
<template #label>{{ i18n.ts.token }}</template>
<template #prefix><i class="ph-poker-chip-bold ph-lg"></i></template>
<template #prefix><i class="ph-poker-chip ph-bold ph-lg"></i></template>
</MkInput>
<MkButton type="submit" :disabled="signing" primary style="margin: 0 auto;">{{ signing ? i18n.ts.loggingIn : i18n.ts.login }}</MkButton>
</div>
</div>
</div>
<div class="social _section">
<a v-if="meta && meta.enableTwitterIntegration" class="_borderButton _gap" :href="`${apiUrl}/signin/twitter`"><i class="ph-twitter-logo-bold ph-lg" style="margin-right: 4px;"></i>{{ i18n.t('signinWith', { x: 'Twitter' }) }}</a>
<a v-if="meta && meta.enableGithubIntegration" class="_borderButton _gap" :href="`${apiUrl}/signin/github`"><i class="ph-github-logo-bold ph-lg" style="margin-right: 4px;"></i>{{ i18n.t('signinWith', { x: 'GitHub' }) }}</a>
<a v-if="meta && meta.enableDiscordIntegration" class="_borderButton _gap" :href="`${apiUrl}/signin/discord`"><i class="ph-discord-logo-bold ph-lg" style="margin-right: 4px;"></i>{{ i18n.t('signinWith', { x: 'Discord' }) }}</a>
<a v-if="meta && meta.enableTwitterIntegration" class="_borderButton _gap" :href="`${apiUrl}/signin/twitter`"><i class="ph-twitter-logo ph-bold ph-lg" style="margin-right: 4px;"></i>{{ i18n.t('signinWith', { x: 'Twitter' }) }}</a>
<a v-if="meta && meta.enableGithubIntegration" class="_borderButton _gap" :href="`${apiUrl}/signin/github`"><i class="ph-github-logo ph-bold ph-lg" style="margin-right: 4px;"></i>{{ i18n.t('signinWith', { x: 'GitHub' }) }}</a>
<a v-if="meta && meta.enableDiscordIntegration" class="_borderButton _gap" :href="`${apiUrl}/signin/discord`"><i class="ph-discord-logo ph-bold ph-lg" style="margin-right: 4px;"></i>{{ i18n.t('signinWith', { x: 'Discord' }) }}</a>
</div>
</form>
</template>

View file

@ -2,53 +2,53 @@
<form class="qlvuhzng _formRoot" autocomplete="new-password" @submit.prevent="onSubmit">
<MkInput v-if="instance.disableRegistration" v-model="invitationCode" class="_formBlock" type="text" :spellcheck="false" required data-cy-signup-invitation-code @update:modelValue="onChangeInvitationCode">
<template #label>{{ i18n.ts.invitationCode }}</template>
<template #prefix><i class="ph-key-bold ph-lg"></i></template>
<template #prefix><i class="ph-key ph-bold ph-lg"></i></template>
</MkInput>
<div v-if="!instance.disableRegistration || (instance.disableRegistration && invitationState === 'entered')">
<MkInput v-model="username" class="_formBlock" type="text" pattern="^[a-zA-Z0-9_]{1,20}$" :spellcheck="false" required data-cy-signup-username @update:modelValue="onChangeUsername">
<template #label>{{ i18n.ts.username }} <div v-tooltip:dialog="i18n.ts.usernameInfo" class="_button _help"><i class="ph-question-bold"></i></div></template>
<template #label>{{ i18n.ts.username }} <div v-tooltip:dialog="i18n.ts.usernameInfo" class="_button _help"><i class="ph-question ph-bold"></i></div></template>
<template #prefix>@</template>
<template #suffix>@{{ host }}</template>
<template #caption>
<span v-if="usernameState === 'wait'" style="color:#6e6a86"><i class="ph-circle-notch-bold ph-lg fa-pulse ph-fw ph-lg"></i> {{ i18n.ts.checking }}</span>
<span v-else-if="usernameState === 'ok'" style="color: var(--success)"><i class="ph-check-bold ph-lg ph-fw ph-lg"></i> {{ i18n.ts.available }}</span>
<span v-else-if="usernameState === 'unavailable'" style="color: var(--error)"><i class="ph-warning-bold ph-lg ph-fw ph-lg"></i> {{ i18n.ts.unavailable }}</span>
<span v-else-if="usernameState === 'error'" style="color: var(--error)"><i class="ph-warning-bold ph-lg ph-fw ph-lg"></i> {{ i18n.ts.error }}</span>
<span v-else-if="usernameState === 'invalid-format'" style="color: var(--error)"><i class="ph-warning-bold ph-lg ph-fw ph-lg"></i> {{ i18n.ts.usernameInvalidFormat }}</span>
<span v-else-if="usernameState === 'min-range'" style="color: var(--error)"><i class="ph-warning-bold ph-lg ph-fw ph-lg"></i> {{ i18n.ts.tooShort }}</span>
<span v-else-if="usernameState === 'max-range'" style="color: var(--error)"><i class="ph-warning-bold ph-lg ph-fw ph-lg"></i> {{ i18n.ts.tooLong }}</span>
<span v-if="usernameState === 'wait'" style="color:#6e6a86"><i class="ph-circle-notch ph-bold ph-lg fa-pulse ph-fw ph-lg"></i> {{ i18n.ts.checking }}</span>
<span v-else-if="usernameState === 'ok'" style="color: var(--success)"><i class="ph-check ph-bold ph-lg ph-fw ph-lg"></i> {{ i18n.ts.available }}</span>
<span v-else-if="usernameState === 'unavailable'" style="color: var(--error)"><i class="ph-warning ph-bold ph-lg ph-fw ph-lg"></i> {{ i18n.ts.unavailable }}</span>
<span v-else-if="usernameState === 'error'" style="color: var(--error)"><i class="ph-warning ph-bold ph-lg ph-fw ph-lg"></i> {{ i18n.ts.error }}</span>
<span v-else-if="usernameState === 'invalid-format'" style="color: var(--error)"><i class="ph-warning ph-bold ph-lg ph-fw ph-lg"></i> {{ i18n.ts.usernameInvalidFormat }}</span>
<span v-else-if="usernameState === 'min-range'" style="color: var(--error)"><i class="ph-warning ph-bold ph-lg ph-fw ph-lg"></i> {{ i18n.ts.tooShort }}</span>
<span v-else-if="usernameState === 'max-range'" style="color: var(--error)"><i class="ph-warning ph-bold ph-lg ph-fw ph-lg"></i> {{ i18n.ts.tooLong }}</span>
</template>
</MkInput>
<MkInput v-if="instance.emailRequiredForSignup" v-model="email" class="_formBlock" :debounce="true" type="email" :spellcheck="false" required data-cy-signup-email @update:modelValue="onChangeEmail">
<template #label>{{ i18n.ts.emailAddress }} <div v-tooltip:dialog="i18n.ts._signup.emailAddressInfo" class="_button _help"><i class="ph-question-bold"></i></div></template>
<template #prefix><i class="ph-envelope-simple-open-bold ph-lg"></i></template>
<template #label>{{ i18n.ts.emailAddress }} <div v-tooltip:dialog="i18n.ts._signup.emailAddressInfo" class="_button _help"><i class="ph-question ph-bold"></i></div></template>
<template #prefix><i class="ph-envelope-simple-open ph-bold ph-lg"></i></template>
<template #caption>
<span v-if="emailState === 'wait'" style="color:#6e6a86"><i class="ph-circle-notch-bold ph-lg fa-pulse ph-fw ph-lg"></i> {{ i18n.ts.checking }}</span>
<span v-else-if="emailState === 'ok'" style="color: var(--success)"><i class="ph-check-bold ph-lg ph-fw ph-lg"></i> {{ i18n.ts.available }}</span>
<span v-else-if="emailState === 'unavailable:used'" style="color: var(--error)"><i class="ph-warning-bold ph-lg ph-fw ph-lg"></i> {{ i18n.ts._emailUnavailable.used }}</span>
<span v-else-if="emailState === 'unavailable:format'" style="color: var(--error)"><i class="ph-warning-bold ph-lg ph-fw ph-lg"></i> {{ i18n.ts._emailUnavailable.format }}</span>
<span v-else-if="emailState === 'unavailable:disposable'" style="color: var(--error)"><i class="ph-warning-bold ph-lg ph-fw ph-lg"></i> {{ i18n.ts._emailUnavailable.disposable }}</span>
<span v-else-if="emailState === 'unavailable:mx'" style="color: var(--error)"><i class="ph-warning-bold ph-lg ph-fw ph-lg"></i> {{ i18n.ts._emailUnavailable.mx }}</span>
<span v-else-if="emailState === 'unavailable:smtp'" style="color: var(--error)"><i class="ph-warning-bold ph-lg ph-fw ph-lg"></i> {{ i18n.ts._emailUnavailable.smtp }}</span>
<span v-else-if="emailState === 'unavailable'" style="color: var(--error)"><i class="ph-warning-bold ph-lg ph-fw ph-lg"></i> {{ i18n.ts.unavailable }}</span>
<span v-else-if="emailState === 'error'" style="color: var(--error)"><i class="ph-warning-bold ph-lg ph-fw ph-lg"></i> {{ i18n.ts.error }}</span>
<span v-if="emailState === 'wait'" style="color:#6e6a86"><i class="ph-circle-notch ph-bold ph-lg fa-pulse ph-fw ph-lg"></i> {{ i18n.ts.checking }}</span>
<span v-else-if="emailState === 'ok'" style="color: var(--success)"><i class="ph-check ph-bold ph-lg ph-fw ph-lg"></i> {{ i18n.ts.available }}</span>
<span v-else-if="emailState === 'unavailable:used'" style="color: var(--error)"><i class="ph-warning ph-bold ph-lg ph-fw ph-lg"></i> {{ i18n.ts._emailUnavailable.used }}</span>
<span v-else-if="emailState === 'unavailable:format'" style="color: var(--error)"><i class="ph-warning ph-bold ph-lg ph-fw ph-lg"></i> {{ i18n.ts._emailUnavailable.format }}</span>
<span v-else-if="emailState === 'unavailable:disposable'" style="color: var(--error)"><i class="ph-warning ph-bold ph-lg ph-fw ph-lg"></i> {{ i18n.ts._emailUnavailable.disposable }}</span>
<span v-else-if="emailState === 'unavailable:mx'" style="color: var(--error)"><i class="ph-warning ph-bold ph-lg ph-fw ph-lg"></i> {{ i18n.ts._emailUnavailable.mx }}</span>
<span v-else-if="emailState === 'unavailable:smtp'" style="color: var(--error)"><i class="ph-warning ph-bold ph-lg ph-fw ph-lg"></i> {{ i18n.ts._emailUnavailable.smtp }}</span>
<span v-else-if="emailState === 'unavailable'" style="color: var(--error)"><i class="ph-warning ph-bold ph-lg ph-fw ph-lg"></i> {{ i18n.ts.unavailable }}</span>
<span v-else-if="emailState === 'error'" style="color: var(--error)"><i class="ph-warning ph-bold ph-lg ph-fw ph-lg"></i> {{ i18n.ts.error }}</span>
</template>
</MkInput>
<MkInput v-model="password" class="_formBlock" type="password" autocomplete="new-password" required data-cy-signup-password @update:modelValue="onChangePassword">
<template #label>{{ i18n.ts.password }}</template>
<template #prefix><i class="ph-lock-bold ph-lg"></i></template>
<template #prefix><i class="ph-lock ph-bold ph-lg"></i></template>
<template #caption>
<span v-if="passwordStrength == 'low'" style="color: var(--error)"><i class="ph-warning-bold ph-lg ph-fw ph-lg"></i> {{ i18n.ts.weakPassword }}</span>
<span v-if="passwordStrength == 'medium'" style="color: var(--warn)"><i class="ph-check-bold ph-lg ph-fw ph-lg"></i> {{ i18n.ts.normalPassword }}</span>
<span v-if="passwordStrength == 'high'" style="color: var(--success)"><i class="ph-check-bold ph-lg ph-fw ph-lg"></i> {{ i18n.ts.strongPassword }}</span>
<span v-if="passwordStrength == 'low'" style="color: var(--error)"><i class="ph-warning ph-bold ph-lg ph-fw ph-lg"></i> {{ i18n.ts.weakPassword }}</span>
<span v-if="passwordStrength == 'medium'" style="color: var(--warn)"><i class="ph-check ph-bold ph-lg ph-fw ph-lg"></i> {{ i18n.ts.normalPassword }}</span>
<span v-if="passwordStrength == 'high'" style="color: var(--success)"><i class="ph-check ph-bold ph-lg ph-fw ph-lg"></i> {{ i18n.ts.strongPassword }}</span>
</template>
</MkInput>
<MkInput v-model="retypedPassword" class="_formBlock" type="password" autocomplete="new-password" required data-cy-signup-password-retype @update:modelValue="onChangePasswordRetype">
<template #label>{{ i18n.ts.password }} ({{ i18n.ts.retype }})</template>
<template #prefix><i class="ph-lock-bold ph-lg"></i></template>
<template #prefix><i class="ph-lock ph-bold ph-lg"></i></template>
<template #caption>
<span v-if="passwordRetypeState == 'match'" style="color: var(--success)"><i class="ph-check-bold ph-lg ph-fw ph-lg"></i> {{ i18n.ts.passwordMatched }}</span>
<span v-if="passwordRetypeState == 'not-match'" style="color: var(--error)"><i class="ph-warning-bold ph-lg ph-fw ph-lg"></i> {{ i18n.ts.passwordNotMatched }}</span>
<span v-if="passwordRetypeState == 'match'" style="color: var(--success)"><i class="ph-check ph-bold ph-lg ph-fw ph-lg"></i> {{ i18n.ts.passwordMatched }}</span>
<span v-if="passwordRetypeState == 'not-match'" style="color: var(--error)"><i class="ph-warning ph-bold ph-lg ph-fw ph-lg"></i> {{ i18n.ts.passwordNotMatched }}</span>
</template>
</MkInput>
<MkSwitch v-if="instance.tosUrl" v-model="ToSAgreement" class="_formBlock tou">

View file

@ -6,9 +6,9 @@
<path d="m23.186 29.526c-0.993 0-1.952-0.455-2.788-1.339-2.816-2.985-3.569-2.333-4.817-1.251-0.781 0.679-1.754 1.523-3.205 1.523-2.351 0-3.969-2.302-4.036-2.4-0.314-0.454-0.2-1.077 0.254-1.391 0.451-0.312 1.074-0.2 1.39 0.251 0.301 0.429 1.317 1.54 2.393 1.54 0.704 0 1.256-0.479 1.895-1.033 1.816-1.578 3.764-2.655 7.583 1.388 0.823 0.873 1.452 0.774 1.908 0.592 1.659-0.665 3.205-3.698 3.197-5.15-3e-3 -0.552 0.442-1.002 0.994-1.005h6e-3c0.55 0 0.997 0.444 1 0.995 0.012 2.103-1.854 5.976-4.454 7.017-0.443 0.175-0.885 0.262-1.32 0.263z"/>
<path d="m14.815 15.375c-0.584 2.114-1.642 3.083-3.152 2.666-1.509-0.417-2.343-1.909-1.76-4.023 0.583-2.112 2.175-3.363 3.684-2.946 1.511 0.417 1.812 2.19 1.228 4.303zm11.416-0.755c0.473 2.141-0.675 4.838-2.204 5.176s-3.28-1.719-3.753-3.86c-0.473-2.14 0.419-3.971 1.948-4.309s3.536 0.853 4.009 2.993z"/>
</g></svg>
<i v-else-if="instance.defaultReaction === '👍'" class="ph-thumbs-up-bold ph-lg"></i>
<i v-else-if="instance.defaultReaction === ''" class="ph-heart-bold ph-lg"></i>
<i v-else class="ph-star-bold ph-lg"></i>
<i v-else-if="instance.defaultReaction === '👍'" class="ph-thumbs-up ph-bold ph-lg"></i>
<i v-else-if="instance.defaultReaction === ''" class="ph-heart ph-bold ph-lg"></i>
<i v-else class="ph-star ph-bold ph-lg"></i>
</button>
</template>

View file

@ -2,7 +2,7 @@
<div class="wrmlmaau" :class="{ collapsed, isLong }">
<div class="body">
<span v-if="note.deletedAt" style="opacity: 0.5">({{ i18n.ts.deleted }})</span>
<MkA v-if="note.replyId" class="reply" :to="`/notes/${note.replyId}`"><i class="ph-arrow-bend-up-left-bold ph-lg"></i></MkA>
<MkA v-if="note.replyId" class="reply" :to="`/notes/${note.replyId}`"><i class="ph-arrow-bend-up-left ph-bold ph-lg"></i></MkA>
<Mfm v-if="note.text" :text="note.text" :author="note.user" :i="$i" :custom-emojis="note.emojis"/>
<MkA v-if="note.renoteId" class="rp" :to="`/notes/${note.renoteId}`">{{ i18n.ts.quoteAttached }}: ...</MkA>
</div>

View file

@ -13,17 +13,17 @@
<div class="_footer navigation">
<div class="step">
<button class="arrow _button" :disabled="tutorial === 0" @click="tutorial--">
<i class="ph-caret-left-bold ph-lg"></i>
<i class="ph-caret-left ph-bold ph-lg"></i>
</button>
<span>{{ tutorial + 1 }} / 6</span>
<button class="arrow _button" :disabled="tutorial === 5" @click="tutorial++">
<i class="ph-caret-right-bold ph-lg"></i>
<i class="ph-caret-right ph-bold ph-lg"></i>
</button>
</div>
<MkButton v-if="tutorial === 5" class="ok" primary @click="close"><i class="ph-check-bold ph-lg"></i> {{ i18n.ts.gotIt }}</MkButton>
<MkButton v-else class="ok" primary @click="tutorial++"><i class="ph-check-bold ph-lg"></i> {{ i18n.ts.next }}</MkButton>
<MkButton v-if="tutorial === 5" class="ok" primary @click="close"><i class="ph-check ph-bold ph-lg"></i> {{ i18n.ts.gotIt }}</MkButton>
<MkButton v-else class="ok" primary @click="tutorial++"><i class="ph-check ph-bold ph-lg"></i> {{ i18n.ts.next }}</MkButton>
</div>
<h2 class="_title title"><i class="ph-info-bold ph-lg"></i> {{ i18n.ts._tutorial.title }}</h2>
<h2 class="_title title"><i class="ph-info ph-bold ph-lg"></i> {{ i18n.ts._tutorial.title }}</h2>
<Transition name="fade">
<div v-if="tutorial === 0" key="1" class="_content">
<h3>{{ i18n.ts._tutorial.step1_1 }}</h3>
@ -41,7 +41,7 @@
<div>{{ i18n.ts._tutorial.step3_2 }}</div>
<XFeaturedUsers/>
<br/>
<MkButton class="ok" primary @click="tutorial++"><i class="ph-check-bold ph-lg"></i> {{ i18n.ts.next }}</MkButton>
<MkButton class="ok" primary @click="tutorial++"><i class="ph-check ph-bold ph-lg"></i> {{ i18n.ts.next }}</MkButton>
</div>
<div v-else-if="tutorial === 3" key="4" class="_content">
<h3>{{ i18n.ts._tutorial.step4_1 }}</h3>
@ -64,35 +64,35 @@
<li>
<I18n :src="i18n.ts._tutorial.step5_3" tag="div">
<template #icon>
<i class="ph-house-bold ph-lg"/>
<i class="ph-house ph-bold ph-lg"/>
</template>
</I18n>
</li>
<li v-if="timelines.includes('local')">
<I18n :src="i18n.ts._tutorial.step5_4" tag="div">
<template #icon>
<i class="ph-users-bold ph-lg"/>
<i class="ph-users ph-bold ph-lg"/>
</template>
</I18n>
</li>
<li v-if="timelines.includes('recommended')">
<I18n :src="i18n.ts._tutorial.step5_5" tag="div">
<template #icon>
<i class="ph-thumbs-up-bold ph-lg"/>
<i class="ph-thumbs-up ph-bold ph-lg"/>
</template>
</I18n>
</li>
<li v-if="timelines.includes('social')">
<I18n :src="i18n.ts._tutorial.step5_6" tag="div">
<template #icon>
<i class="ph-handshake-bold ph-lg"/>
<i class="ph-handshake ph-bold ph-lg"/>
</template>
</I18n>
</li>
<li v-if="timelines.includes('global')">
<I18n :src="i18n.ts._tutorial.step5_7" tag="div">
<template #icon>
<i class="ph-planet-bold ph-lg"/>
<i class="ph-planet ph-bold ph-lg"/>
</template>
</I18n>
</li>

View file

@ -1,6 +1,6 @@
<template>
<div v-if="playerEnabled" class="player" :style="`padding: ${(player.height || 0) / (player.width || 1) * 100}% 0 0`" @click.stop>
<button class="disablePlayer" :title="i18n.ts.disablePlayer" @click="playerEnabled = false"><i class="ph-x-bold ph-lg"></i></button>
<button class="disablePlayer" :title="i18n.ts.disablePlayer" @click="playerEnabled = false"><i class="ph-x ph-bold ph-lg"></i></button>
<iframe :src="player.url + (player.url.match(/\?/) ? '&autoplay=1&auto_play=1' : '?autoplay=1&auto_play=1')" :width="player.width || '100%'" :heigth="player.height || 250" frameborder="0" allow="autoplay; encrypted-media" allowfullscreen/>
</div>
<div v-else-if="tweetId && tweetExpanded" ref="twitter" class="twitter" @click.stop>
@ -10,7 +10,7 @@
<transition :name="$store.state.animation ? 'zoom' : ''" mode="out-in">
<component :is="self ? 'MkA' : 'a'" v-if="!fetching" class="link" :class="{ compact }" :[attr]="self ? url.substr(local.length) : url" rel="nofollow noopener" :target="target" :title="url">
<div v-if="thumbnail" class="thumbnail" :style="`background-image: url('${thumbnail}')`">
<button v-if="!playerEnabled && player.url" class="_button" :title="i18n.ts.enablePlayer" @click.prevent="playerEnabled = true"><i class="ph-play-circle-bold ph-lg"></i></button>
<button v-if="!playerEnabled && player.url" class="_button" :title="i18n.ts.enablePlayer" @click.prevent="playerEnabled = true"><i class="ph-play-circle ph-bold ph-lg"></i></button>
</div>
<article>
<header>
@ -26,7 +26,7 @@
</transition>
<div v-if="tweetId" class="expandTweet">
<a @click="tweetExpanded = true">
<i class="ph-twitter-logo-bold ph-lg"></i> {{ i18n.ts.expandTweet }}
<i class="ph-twitter-logo ph-bold ph-lg"></i> {{ i18n.ts.expandTweet }}
</a>
</div>
</div>

View file

@ -1,10 +1,10 @@
<template>
<span v-if="note.visibility !== 'public'" :class="$style.visibility">
<i v-if="note.visibility === 'home'" class="ph-house-bold ph-lg"></i>
<i v-else-if="note.visibility === 'followers'" class="ph-lock-simple-open-bold ph-lg"></i>
<i v-else-if="note.visibility === 'specified'" ref="specified" class="ph-envelope-simple-open-bold ph-lg"></i>
<i v-if="note.visibility === 'home'" class="ph-house ph-bold ph-lg"></i>
<i v-else-if="note.visibility === 'followers'" class="ph-lock-simple-open ph-bold ph-lg"></i>
<i v-else-if="note.visibility === 'specified'" ref="specified" class="ph-envelope-simple-open ph-bold ph-lg"></i>
</span>
<span v-if="note.localOnly" :class="$style.localOnly"><i class="ph-hand-fist-bold ph-lg"></i></span>
<span v-if="note.localOnly" :class="$style.localOnly"><i class="ph-hand-fist ph-bold ph-lg"></i></span>
</template>
<script lang="ts" setup>

View file

@ -2,28 +2,28 @@
<MkModal ref="modal" :z-priority="'high'" :src="src" @click="modal.close()" @closed="emit('closed')">
<div class="_popup" :class="$style.root">
<button key="public" class="_button" :class="[$style.item, { [$style.active]: v === 'public' }]" data-index="1" @click="choose('public')">
<div :class="$style.icon"><i class="ph-planet-bold ph-lg"></i></div>
<div :class="$style.icon"><i class="ph-planet ph-bold ph-lg"></i></div>
<div :class="$style.body">
<span :class="$style.itemTitle">{{ i18n.ts._visibility.public }}</span>
<span :class="$style.itemDescription">{{ i18n.ts._visibility.publicDescription }}</span>
</div>
</button>
<button key="home" class="_button" :class="[$style.item, { [$style.active]: v === 'home' }]" data-index="2" @click="choose('home')">
<div :class="$style.icon"><i class="ph-house-bold ph-lg"></i></div>
<div :class="$style.icon"><i class="ph-house ph-bold ph-lg"></i></div>
<div :class="$style.body">
<span :class="$style.itemTitle">{{ i18n.ts._visibility.home }}</span>
<span :class="$style.itemDescription">{{ i18n.ts._visibility.homeDescription }}</span>
</div>
</button>
<button key="followers" class="_button" :class="[$style.item, { [$style.active]: v === 'followers' }]" data-index="3" @click="choose('followers')">
<div :class="$style.icon"><i class="ph-lock-simple-open-bold ph-lg"></i></div>
<div :class="$style.icon"><i class="ph-lock-simple-open ph-bold ph-lg"></i></div>
<div :class="$style.body">
<span :class="$style.itemTitle">{{ i18n.ts._visibility.followers }}</span>
<span :class="$style.itemDescription">{{ i18n.ts._visibility.followersDescription }}</span>
</div>
</button>
<button key="specified" :disabled="localOnly" class="_button" :class="[$style.item, { [$style.active]: v === 'specified' }]" data-index="4" @click="choose('specified')">
<div :class="$style.icon"><i class="ph-envelope-simple-open-bold ph-lg"></i></div>
<div :class="$style.icon"><i class="ph-envelope-simple-open ph-bold ph-lg"></i></div>
<div :class="$style.body">
<span :class="$style.itemTitle">{{ i18n.ts._visibility.specified }}</span>
<span :class="$style.itemDescription">{{ i18n.ts._visibility.specifiedDescription }}</span>
@ -31,12 +31,12 @@
</button>
<div :class="$style.divider"></div>
<button key="localOnly" class="_button" :class="[$style.item, $style.localOnly, { [$style.active]: localOnly }]" data-index="5" @click="localOnly = !localOnly">
<div :class="$style.icon"><i class="ph-hand-fist-bold ph-lg"></i></div>
<div :class="$style.icon"><i class="ph-hand-fist ph-bold ph-lg"></i></div>
<div :class="$style.body">
<span :class="$style.itemTitle">{{ i18n.ts._visibility.localOnly }}</span>
<span :class="$style.itemDescription">{{ i18n.ts._visibility.localOnlyDescription }}</span>
</div>
<div :class="$style.toggle"><i :class="localOnly ? 'ph-toggle-right-bold ph-lg' : 'ph-toggle-left-bold ph-lg'"></i></div>
<div :class="$style.toggle"><i :class="localOnly ? 'ph-toggle-right ph-bold ph-lg' : 'ph-toggle-left ph-bold ph-lg'"></i></div>
</button>
</div>
</MkModal>

View file

@ -1,7 +1,7 @@
<template>
<MkModal ref="modal" :prefer-type="'dialog'" :z-priority="'high'" @click="success ? done() : () => {}" @closed="emit('closed')">
<div :class="[$style.root, { [$style.iconOnly]: (text == null) || success }]">
<i v-if="success" :class="[$style.icon, $style.success]" class="ph-check-bold ph-lg"></i>
<i v-if="success" :class="[$style.icon, $style.success]" class="ph-check ph-bold ph-lg"></i>
<MkLoading v-else :class="[$style.icon, $style.waiting]" :em="true"/>
<div v-if="text && !success" :class="$style.text">{{ text }}<MkEllipsis/></div>
</div>

View file

@ -6,7 +6,7 @@
<template #label>{{ i18n.ts.selectWidget }}</template>
<option v-for="widget in widgetDefs" :key="widget" :value="widget">{{ i18n.t(`_widgets.${widget}`) }}</option>
</MkSelect>
<MkButton inline primary class="mk-widget-add" @click="addWidget"><i class="ph-plus-bold ph-lg"></i> {{ i18n.ts.add }}</MkButton>
<MkButton inline primary class="mk-widget-add" @click="addWidget"><i class="ph-plus ph-bold ph-lg"></i> {{ i18n.ts.add }}</MkButton>
<MkButton inline @click="$emit('exit')">{{ i18n.ts.close }}</MkButton>
</header>
<XDraggable
@ -17,8 +17,8 @@
>
<template #item="{element}">
<div class="customize-container">
<button class="config _button" @click.prevent.stop="configWidget(element.id)"><i class="ph-gear-six-bold ph-lg"></i></button>
<button class="remove _button" @click.prevent.stop="removeWidget(element)"><i class="ph-x-bold ph-lg"></i></button>
<button class="config _button" @click.prevent.stop="configWidget(element.id)"><i class="ph-gear-six ph-bold ph-lg"></i></button>
<button class="remove _button" @click.prevent.stop="removeWidget(element)"><i class="ph-x ph-bold ph-lg"></i></button>
<div class="handle">
<component :is="`mkw-${element.name}`" :ref="el => widgetRefs[element.id] = el" class="widget" :widget="element" @updateProps="updateWidget(element.id, $event)"/>
</div>
@ -104,7 +104,7 @@ function onContextmenu(widget: Widget, ev: MouseEvent) {
type: 'label',
text: i18n.t(`_widgets.${widget.name}`),
}, {
icon: 'ph-gear-six-bold ph-lg',
icon: 'ph-gear-six ph-bold ph-lg',
text: i18n.ts.settings,
action: () => {
configWidget(widget.id);

View file

@ -11,9 +11,9 @@
</span>
<span class="right">
<button v-for="button in buttonsRight" v-tooltip="button.title" class="button _button" :class="{ highlighted: button.highlighted }" @click="button.onClick"><i :class="button.icon"></i></button>
<button v-if="canResize && maximized" class="button _button" @click="unMaximize()"><i class="ph-copy-bold ph-lg"></i></button>
<button v-else-if="canResize && !maximized" class="button _button" @click="maximize()"><i class="ph-browser-bold ph-lg"></i></button>
<button v-if="closeButton" class="button _button" @click="close()"><i class="ph-x-bold ph-lg"></i></button>
<button v-if="canResize && maximized" class="button _button" @click="unMaximize()"><i class="ph-copy ph-bold ph-lg"></i></button>
<button v-else-if="canResize && !maximized" class="button _button" @click="maximize()"><i class="ph-browser ph-bold ph-lg"></i></button>
<button v-if="closeButton" class="button _button" @click="close()"><i class="ph-x ph-bold ph-lg"></i></button>
</span>
</div>
<div class="body">

View file

@ -10,7 +10,7 @@
@keydown.enter="toggle"
>
<span ref="button" v-adaptive-border v-tooltip="checked ? i18n.ts.itsOn : i18n.ts.itsOff" class="button" @click.prevent="toggle">
<i class="check ph-check-bold ph-lg"></i>
<i class="check ph-check ph-bold ph-lg"></i>
</span>
<span class="label">
<!-- TODO: 無名slotの方は廃止 -->

View file

@ -5,8 +5,8 @@
<span class="text"><slot name="label"></slot></span>
<span class="right">
<span class="text"><slot name="suffix"></slot></span>
<i v-if="opened" class="ph-caret-up-bold ph-lg icon"></i>
<i v-else class="ph-caret-down-bold ph-lg icon"></i>
<i v-if="opened" class="ph-caret-up ph-bold ph-lg icon"></i>
<i v-else class="ph-caret-down ph-bold ph-lg icon"></i>
</span>
</div>
<KeepAlive>

View file

@ -29,7 +29,7 @@
</div>
<div class="caption"><slot name="caption"></slot></div>
<MkButton v-if="manualSave && changed" primary class="save" @click="updated"><i class="ph-check-bold ph-lg"></i> {{ i18n.ts.save }}</MkButton>
<MkButton v-if="manualSave && changed" primary class="save" @click="updated"><i class="ph-check ph-bold ph-lg"></i> {{ i18n.ts.save }}</MkButton>
</div>
</template>

View file

@ -5,7 +5,7 @@
<span class="text"><slot></slot></span>
<span class="right">
<span class="text"><slot name="suffix"></slot></span>
<i class="ph-arrow-square-out-bold ph-lg icon"></i>
<i class="ph-arrow-square-out ph-bold ph-lg icon"></i>
</span>
</a>
<MkA v-else class="main _button" :class="{ active }" :to="to" :behavior="behavior">
@ -13,7 +13,7 @@
<span class="text"><slot></slot></span>
<span class="right">
<span class="text"><slot name="suffix"></slot></span>
<i class="ph-caret-right-bold ph-lg icon"></i>
<i class="ph-caret-right ph-bold ph-lg icon"></i>
</span>
</MkA>
</div>

View file

@ -18,11 +18,11 @@
>
<slot></slot>
</select>
<div ref="suffixEl" class="suffix"><i class="ph-caret-down-bold ph-lg"></i></div>
<div ref="suffixEl" class="suffix"><i class="ph-caret-down ph-bold ph-lg"></i></div>
</div>
<div class="caption"><slot name="caption"></slot></div>
<MkButton v-if="manualSave && changed" primary @click="updated"><i class="ph-floppy-disk-back-bold ph-lg"></i> {{ i18n.ts.save }}</MkButton>
<MkButton v-if="manualSave && changed" primary @click="updated"><i class="ph-floppy-disk-back ph-bold ph-lg"></i> {{ i18n.ts.save }}</MkButton>
</div>
</template>

View file

@ -8,8 +8,8 @@
</div>
<div v-else>
<div class="wszdbhzo">
<div><i class="ph-warning-bold ph-lg"></i> {{ i18n.ts.somethingHappened }}</div>
<MkButton inline class="retry" @click="retry"><i class="ph-arrow-clockwise-bold ph-lg"></i> {{ i18n.ts.retry }}</MkButton>
<div><i class="ph-warning ph-bold ph-lg"></i> {{ i18n.ts.somethingHappened }}</div>
<MkButton inline class="retry" @click="retry"><i class="ph-arrow-clockwise ph-bold ph-lg"></i> {{ i18n.ts.retry }}</MkButton>
</div>
</div>
</transition>

View file

@ -22,7 +22,7 @@
</div>
<div class="caption"><slot name="caption"></slot></div>
<MkButton v-if="manualSave && changed" primary class="save" @click="updated"><i class="ph-floppy-disk-back-bold ph-lg"></i> {{ i18n.ts.save }}</MkButton>
<MkButton v-if="manualSave && changed" primary class="save" @click="updated"><i class="ph-floppy-disk-back ph-bold ph-lg"></i> {{ i18n.ts.save }}</MkButton>
</div>
</template>

View file

@ -41,25 +41,25 @@ function onContextmenu(ev) {
type: 'label',
text: props.to,
}, {
icon: 'ph-browser-bold ph-lg',
icon: 'ph-browser ph-bold ph-lg',
text: i18n.ts.openInWindow,
action: () => {
os.pageWindow(props.to);
},
}, {
icon: 'ph-arrows-out-simple-bold ph-lg',
icon: 'ph-arrows-out-simple ph-bold ph-lg',
text: i18n.ts.showInPage,
action: () => {
router.push(props.to, 'forcePage');
},
}, null, {
icon: 'ph-arrow-square-out-bold ph-lg',
icon: 'ph-arrow-square-out ph-bold ph-lg',
text: i18n.ts.openInNewTab,
action: () => {
window.open(props.to, '_blank');
},
}, {
icon: 'ph-link-simple-bold ph-lg',
icon: 'ph-link-simple ph-bold ph-lg',
text: i18n.ts.copyLink,
action: () => {
copyToClipboard(`${url}${props.to}`);

View file

@ -3,7 +3,7 @@
<div v-if="!showMenu" class="main" :class="chosen.place">
<a :href="chosen.url" target="_blank">
<img :src="chosen.imageUrl">
<button class="_button menu" @click.prevent.stop="toggleMenu"><span class="ph-info-bold ph-lg info-circle"></span></button>
<button class="_button menu" @click.prevent.stop="toggleMenu"><span class="ph-info ph-bold ph-lg info-circle"></span></button>
</a>
</div>
<div v-else class="menu">
@ -56,9 +56,12 @@ const choseAd = (): Ad | null => {
}
const lowPriorityAds = ads.filter(ad => ad.ratio === 0);
const widgetAds = ads.filter(ad => ad.place === 'widget');
ads = ads.filter(ad => ad.ratio !== 0);
if (ads.length === 0) {
if (widgetAds.length !== 0) {
return widgetAds;
} else if (ads.length === 0) {
if (lowPriorityAds.length !== 0) {
return lowPriorityAds[Math.floor(Math.random() * lowPriorityAds.length)];
} else {
@ -132,7 +135,7 @@ function reduceFrequency(): void {
}
}
&.square {
&.widget {
> a ,
> a > img {
max-width: min(300px, 100%);
@ -140,7 +143,7 @@ function reduceFrequency(): void {
}
}
&.horizontal {
&.inline {
padding: 8px;
> a ,
@ -150,7 +153,7 @@ function reduceFrequency(): void {
}
}
&.horizontal-big {
&.inline-big {
padding: 8px;
> a ,

View file

@ -2,7 +2,7 @@
<transition :name="$store.state.animation ? 'zoom' : ''" appear>
<div class="mjndxjcg">
<img src="/static-assets/badges/error.png" class="_ghost" alt="Error"/>
<p><i class="ph-warning-bold ph-lg"></i> {{ i18n.ts.somethingHappened }}</p>
<p><i class="ph-warning ph-bold ph-lg"></i> {{ i18n.ts.somethingHappened }}</p>
<MkButton class="button" @click="() => $emit('retry')">{{ i18n.ts.retry }}</MkButton>
</div>
</transition>

View file

@ -220,9 +220,14 @@ onUnmounted(() => {
}
> .tabs {
padding-inline: 12px;
mask: linear-gradient(to right, black 80%, transparent);
-webkit-mask: linear-gradient(to right, black 80%, transparent);
mask: linear-gradient(to right, transparent, black 10px 80%, transparent);
-webkit-mask: linear-gradient(to right, transparent, black 10px 80%, transparent);
margin-left: -10px;
padding-left: 22px;
scrollbar-width: none;
&::before {
content: unset;
}
&::-webkit-scrollbar {
display: none;
}
@ -353,6 +358,16 @@ onUnmounted(() => {
white-space: nowrap;
contain: strict;
&::before {
content: "";
display: inline-block;
height: 40%;
border-left: 1px solid var(--divider);
margin-right: 1em;
margin-left: 10px;
vertical-align: -1px;
}
> .tab {
display: inline-flex;
align-items: center;

View file

@ -14,7 +14,7 @@
<span v-if="pathname != ''" class="pathname">{{ self ? pathname.substr(1) : pathname }}</span>
<span class="query">{{ query }}</span>
<span class="hash">{{ hash }}</span>
<i v-if="target === '_blank'" class="ph-arrow-square-out-bold ph-lg icon"></i>
<i v-if="target === '_blank'" class="ph-arrow-square-out ph-bold ph-lg icon"></i>
</component>
</template>

View file

@ -1,5 +1,5 @@
<template>
<Mfm :text="user.name || user.username" :plain="true" :nowrap="nowrap" :custom-emojis="user.emojis"/>
<Mfm :class="$style.root" :text="user.name || user.username" :plain="true" :nowrap="nowrap" :custom-emojis="user.emojis"/>
</template>
<script lang="ts" setup>
@ -13,3 +13,9 @@ const props = withDefaults(defineProps<{
nowrap: true,
});
</script>
<style lang="scss" module>
.root {
unicode-bidi: isolate;
}
</style>

View file

@ -2,8 +2,8 @@
<div class="ngbfujlo">
<MkTextarea :model-value="text" readonly style="margin: 0;"></MkTextarea>
<MkButton class="button" primary :disabled="posting || posted" @click="post()">
<i v-if="posted" class="ph-check-bold ph-lg"></i>
<i v-else class="ph-paper-plane-tilt-bold ph-lg"></i>
<i v-if="posted" class="ph-check ph-bold ph-lg"></i>
<i v-else class="ph-paper-plane-tilt ph-bold ph-lg"></i>
</MkButton>
</div>
</template>

File diff suppressed because it is too large Load diff

View file

@ -6,7 +6,8 @@
import "vite/modulepreload-polyfill";
import "@/style.scss";
import "@/icons.scss";
import "@phosphor-icons/web/bold";
import "@phosphor-icons/web/fill";
//#region account indexedDB migration
import { set } from "@/scripts/idb-proxy";

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