Merge branch 'develop' into firefish-language

This commit is contained in:
naskya 2023-10-17 12:44:55 +09:00
commit f8db9119c6
No known key found for this signature in database
GPG key ID: 164DFF24E2D40139
357 changed files with 2223 additions and 3425 deletions

View file

@ -9,7 +9,7 @@ By submitting this merge request, you agree to follow our [Contribution Guidelin
- [ ] I have made sure to run `pnpm run format` before submitting this pull request
If this merge request makes changes to the Firefish API, please update `docs/api-change.md`
- [ ] I updated the documentation
- [ ] I updated the document / This merge request doesn't include API changes
<!-- Uncomment if your merge request has multiple authors -->
<!-- Co-authored-by: Name <email@email.com> -->

View file

@ -122,7 +122,7 @@ db:
port: 5432
{{- else }}
host: {{ .Values.postgresql.postgresqlHostname }}
port: {{ .Values.postgresql.postgresqlPort | default "5432" | quote }}
port: {{ .Values.postgresql.postgresqlPort | default 5432 }}
{{- end }}
# Database name
@ -217,8 +217,7 @@ id: 'aid'
#maxCaptionLength: 1500
# Reserved usernames that only the administrator can register with
reservedUsernames:
{{ .Values.firefish.reservedUsernames | toYaml }}
reservedUsernames: {{ .Values.firefish.reservedUsernames | toJson }}
# Whether disable HSTS
#disableHsts: true
@ -265,8 +264,7 @@ reservedUsernames:
# Proxy remote files (default: false)
#proxyRemoteFiles: true
allowedPrivateNetworks:
{{ .Values.firefish.allowedPrivateNetworks | toYaml }}
allowedPrivateNetworks: {{ .Values.firefish.allowedPrivateNetworks | toJson }}
# TWA
#twa:

View file

@ -4,6 +4,24 @@ Breaking changes are indicated by the :warning: icon.
## v1.0.5 (unreleased)
### dev18
- :warning: response of `meta` no longer includes the following:
- `enableTwitterIntegration`
- `enableGithubIntegration`
- `enableDiscordIntegration`
- :warning: parameter of `admin/update-meta` and response of `admin/meta` no longer include the following:
- `enableTwitterIntegration`
- `enableGithubIntegration`
- `enableDiscordIntegration`
- `twitterConsumerKey`
- `twitterConsumerSecret`
- `githubClientId`
- `githubClientSecret`
- `discordClientId`
- `discordClientSecret`
- :warning: response of `admin/show-user` no longer includes `integrations`.
### dev17
- Added `lang` parameter to `notes/create` and `notes/edit`.

View file

@ -312,9 +312,6 @@ dayX: "{day}"
monthX: "{month}"
yearX: "{year}"
pages: "الصفحات"
integration: "التكامل"
connectService: "اتصل"
disconnectService: "اقطع الاتصال"
enableLocalTimeline: "تفعيل الخيط المحلي"
enableGlobalTimeline: "تفعيل الخيط الزمني الشامل"
disablingTimelinesInfo: "سيتمكن المديرون والمشرفون من الوصول إلى كل الخيوط الزمنية حتى وإن لم تفعّل."

View file

@ -316,9 +316,6 @@ dayX: "{day}"
monthX: "{month}"
yearX: "{year}"
pages: "পৃষ্ঠা"
integration: "ইন্টিগ্রেশন"
connectService: "সংযুক্ত করুন"
disconnectService: "সংযোগ বিচ্ছিন্ন করুন"
enableLocalTimeline: "স্থানীয় টাইমলাইন চালু করুন"
enableGlobalTimeline: "গ্লোবাল টাইমলাইন চালু করুন"
disablingTimelinesInfo: "আপনি এই টাইমলাইনগুলি বন্ধ করলেও প্রশাসক এবং মডারেটররা এই টাইমলাইনগুলি ব্যাবহার করতে পারবে"

View file

@ -944,7 +944,6 @@ dayX: '{day}'
tosUrl: URL de les Condicions d'ús
thisYear: Any
thisMonth: Mes
integration: Integracions
driveCapacityPerRemoteAccount: Capacitat del Disc per usuari remot
inMb: En megabytes
iconUrl: Adreça URL de la icona
@ -1036,8 +1035,6 @@ accept: Accepta
reject: Rebutja
yearX: '{year}'
pages: Pàgines
disconnectService: Desconnectar
connectService: Connectar
enableLocalTimeline: Activa la línea de temps local
enableRecommendedTimeline: Activa la línea de temps de recomanacions
pinnedClipId: ID del clip que vols fixar

View file

@ -310,9 +310,6 @@ dayX: "{day}"
monthX: "{month}"
yearX: "{year}"
pages: "Stránky"
integration: "Integrace"
connectService: "Připojit"
disconnectService: "Odpojit"
enableLocalTimeline: "Povolit lokální čas"
enableGlobalTimeline: "Povolit globální čas"
registration: "Registrace"

View file

@ -350,9 +350,6 @@ dayX: "{day}"
monthX: "{month}"
yearX: "{year}"
pages: "Nutzer-Seiten"
integration: "Integration"
connectService: "Verbinden"
disconnectService: "Trennen"
enableLocalTimeline: "Local-Timeline aktivieren"
enableGlobalTimeline: "Global-Timeline aktivieren"
disablingTimelinesInfo: "Administratoren und Moderatoren haben immer Zugriff auf alle
@ -529,12 +526,12 @@ objectStorageBaseUrl: "Basis-URL"
objectStorageBaseUrlDesc: "Die als Referenz verwendete URL. Verwendest du einen CDN
oder Proxy, gib dessen URL an. \nFür S3 verwende 'https://<bucket>.s3.amazonaws.com'.
Für GCS o.ä. verwende 'https://storage.googleapis.com/<bucket>'."
objectStorageBucket: "Eimer"
objectStorageBucket: "Bucket"
objectStorageBucketDesc: "Bitte gib den Namen des Buckets an, der bei deinem Anbieter
verwendet wird."
objectStoragePrefix: "Prefix"
objectStoragePrefixDesc: "Dateien werden in Ordnern unter diesem Prefix gespeichert."
objectStorageEndpoint: "Limit"
objectStorageEndpoint: "Endpunkt"
objectStorageEndpointDesc: "Im Falle von S3 leerlassen, für andere Anbieter den relevanten
Endpoint im Format „<host>“ oder „<host>:<port>“ angeben."
objectStorageRegion: "Region"
@ -1037,6 +1034,7 @@ _accountDelete:
_ad:
back: "Zurück"
reduceFrequencyOfThisAd: "Diese Werbeanzeige weniger anzeigen"
adsBy: Community-Banner von {by}
_forgotPassword:
enterEmail: "Gib die Email-Adresse ein, mit der du dich registriert hast. An diese
wird ein Link gesendet, mit dem du dein Passwort zurücksetzen kannst."
@ -1243,6 +1241,7 @@ _wordMute:
soft: "Leicht"
hard: "Schwer"
mutedNotes: "Stummgeschaltete Beiträge"
muteLangs: Stummgeschaltete Sprachen
_instanceMute:
instanceMuteDescription: "Schaltet alle Beiträge/Boosts stumm, die von den gelisteten
Servern stammen, inklusive Antworten von Nutzern an einen Nutzer eines stummgeschalteten
@ -2215,3 +2214,5 @@ indexable: Indexierbar
languageForTranslation: Übersetzungssprache veröffentlichen
openServerInfo: Anzeigen von Serverinformationen durch Anklicken des Server-Tickers
in einem Beitrag
vibrate: Vibrationen abspielen
clickToShowPatterns: Klicken um Modul-Muster anzuzeigen

View file

@ -215,8 +215,6 @@ thisMonth: "Μήνας"
today: "Σήμερα"
dayX: "{day}"
pages: "Σελίδες"
connectService: "Σύνδεση"
disconnectService: "Αποσύνδεση"
registration: "Εγγραφή"
pinnedPages: "Καρφιτσωμένες Σελίδες"
pinnedNotes: "Καρφιτσωμένες δημοσιεύσεις"
@ -730,7 +728,6 @@ lightThemes: Φωτεινά θέματα
darkThemes: Σκοτεινά θέματα
inputNewFolderName: Πληκτρολογήστε ένα νέο όνομα φακέλου
hasChildFilesOrFolders: Εφόσον αυτός ο φάκελος δεν είναι άδειος, δεν μπορεί να διαγραφεί.
integration: Ενσωματώσεις
enableRecommendedTimeline: Ενεργοποίηση χρονολογίου προτεινόμενων
driveCapacityPerLocalAccount: Μέγεθος Αποθηκευτικού Χώρου ανά τοπικό μέλος
driveCapacityPerRemoteAccount: Μέγεθος Αποθηκευτικού Χώρου ανά απομακρυσμένο μέλος

View file

@ -58,6 +58,7 @@ sendMessage: "Send a message"
copyUsername: "Copy username"
searchUser: "Search for a user"
reply: "Reply"
replies: "Replies"
jumpToPrevious: "Jump to previous"
loadMore: "Load more"
showMore: "Show more"
@ -112,18 +113,21 @@ unfollow: "Unfollow"
followRequestPending: "Follow request pending"
enterEmoji: "Enter an emoji"
renote: "Boost"
renotes: "Boosts"
unrenote: "Take back boost"
renoted: "Boosted."
cantRenote: "This post can't be boosted."
cantReRenote: "A boost can't be boosted."
quote: "Quote"
quotes: "Quotes"
pinnedNote: "Pinned post"
pinned: "Pin to profile"
you: "You"
clickToShow: "Click to show"
sensitive: "NSFW"
add: "Add"
reaction: "Reactions"
reaction: "Reaction"
reactions: "Reactions"
removeReaction: "Remove your reaction"
enableEmojiReactions: "Enable emoji reactions"
showEmojisInReactionNotifications: "Show emojis in reaction notifications"
@ -371,9 +375,6 @@ dayX: "{day}"
monthX: "{month}"
yearX: "{year}"
pages: "Pages"
integration: "Integrations"
connectService: "Connect"
disconnectService: "Disconnect"
enableLocalTimeline: "Enable local timeline"
enableGlobalTimeline: "Enable global timeline"
enableRecommendedTimeline: "Enable recommended timeline"
@ -739,6 +740,7 @@ system: "System"
switchUi: "Layout"
desktop: "Desktop"
clip: "Clip"
clips: "Clips"
createNew: "Create new"
optional: "Optional"
createNewClip: "Create new clip"
@ -781,7 +783,6 @@ pageLikesCount: "Number of liked Pages"
pageLikedCount: "Number of received Page likes"
contact: "Contact"
useSystemFont: "Use the system's default font"
clips: "Clips"
clipsDesc: "Clips are like share-able categorized bookmarks. You can create clips
from the menu of individual posts."
experimentalFeatures: "Experimental features"
@ -1153,6 +1154,7 @@ detectPostLanguage: "Automatically detect the language and show a translate butt
for posts in foreign languages"
vibrate: "Play vibrations"
openServerInfo: "Show server information by clicking the server ticker on a post"
iconSet: "Icon set"
_sensitiveMediaDetection:
description: "Reduces the effort of server moderation through automatically recognizing
@ -2152,3 +2154,9 @@ _feeds:
rss: "RSS"
atom: "Atom"
jsonFeed: "JSON feed"
_iconSets:
bold: "Bold"
light: "Light"
regular: "Regular"
fill: "Filled"
duotone: "Duotone"

View file

@ -340,9 +340,6 @@ dayX: "Día {day}"
monthX: "Mes {month}"
yearX: "Año {year}"
pages: "Páginas"
integration: "Integraciones"
connectService: "Conectar"
disconnectService: "Desconectar"
enableLocalTimeline: "Habilitar linea de tiempo local"
enableGlobalTimeline: "Habilitar linea de tiempo global"
disablingTimelinesInfo: "Aunque se desactiven estas lineas de tiempo, por conveniencia

View file

@ -339,8 +339,6 @@ instanceName: Instanssin nimi
thisMonth: Kuukausi
today: Tänään
monthX: '{month}'
connectService: Yhdistä
disconnectService: Katkaise yhteys
enableLocalTimeline: Ota käyttöön paikallinen aikajana
enableGlobalTimeline: Ota käyttöön globaali aikajana
enableRecommendedTimeline: Ota käyttöön suositellut -aikajana
@ -385,7 +383,6 @@ disablingTimelinesInfo: Järjestelmänvalvojilla ja moderaattoreilla on aina pä
dayX: '{day}'
yearX: '{year}'
pages: Sivut
integration: Integraatiot
instanceDescription: Instanssin kuvaus
invite: Kutsu
iconUrl: Ikoni URL-linkki

View file

@ -341,9 +341,6 @@ dayX: "{day}"
monthX: "{month}"
yearX: "{year}"
pages: "Pages"
integration: "Intégrations"
connectService: "Connexion"
disconnectService: "Déconnexion"
enableLocalTimeline: "Activer le fil local"
enableGlobalTimeline: "Activer le fil global"
disablingTimelinesInfo: "Même si vous désactivez ces fils, les administrateur·rice·s
@ -646,7 +643,7 @@ emptyToDisableSmtpAuth: "Laisser le nom dutilisateur et le mot de passe vides
smtpSecure: "Utiliser SSL/TLS implicitement dans les connexions SMTP"
smtpSecureInfo: "Désactiver cette option lorsque STARTTLS est utilisé"
testEmail: "Tester la distribution de courriel"
wordMute: "Filtre de mots"
wordMute: "Filtre de mots et langages"
regexpError: "Erreur dexpression régulière"
instanceMute: "Serveur masqué"
userSaysSomething: "{name} a dit quelque chose"
@ -960,7 +957,8 @@ _accountDelete:
inProgress: "Suppression en cours"
_ad:
back: "Retour"
reduceFrequencyOfThisAd: "Voir cette publicité moins souvent"
reduceFrequencyOfThisAd: "Voir cette bannière moins souvent"
adsBy: Bannière communautaire par {by}
_forgotPassword:
enterEmail: "Entrez ici l'adresse e-mail que vous avez enregistrée pour votre compte.
Un lien vous permettant de réinitialiser votre mot de passe sera envoyé à cette
@ -1145,6 +1143,13 @@ _wordMute:
soft: "Doux"
hard: "Strict"
mutedNotes: "Publications masquées"
muteLangsDescription2: Utiliser les code de langage (i.e en, fr, ja, zh).
lang: Langage
langDescription: Cacher du fil de publication les publications qui correspondent
à ces langues.
muteLangs: Langages filtrés
muteLangsDescription: Séparer avec des espaces or des retours à la ligne pour une
condition OU (OR).
_instanceMute:
instanceMuteDescription2: "Séparer avec des sauts de lignes"
title: "Masque les publications provenant des serveurs listés."
@ -2218,3 +2223,5 @@ openServerInfo: Afficher les informations du serveur en cliquant sur le bandeau
serveur dune publication
indexable: Indexable
languageForTranslation: Langage post-traduction
vibrate: Jouer les vibrations
clickToShowPatterns: Cliquer pour montrer les patrons de modules

View file

@ -342,9 +342,6 @@ dayX: "{day}"
monthX: "{month}"
yearX: "{year}"
pages: "Halaman"
integration: "Integrasi"
connectService: "Sambungkan"
disconnectService: "Putuskan"
enableLocalTimeline: "Nyalakan linimasa lokal"
enableGlobalTimeline: "Nyalakan linimasa global"
disablingTimelinesInfo: "Admin dan Moderator akan selalu memiliki akses ke semua linimasa
@ -1269,8 +1266,8 @@ _tutorial:
{introduction} atau \"Halo dunia!\" yang sederhana."
step5_1: "Linimasa, linimasa di mana-mana!"
step5_2: "Servermu memiliki {timelines} lini masa berbeda yang diaktifkan."
step5_3: "Lini masa Beranda {icon} adalah tempat di mana kamu bisa melihat postingan
dari akun yang kamu ikuti."
step5_3: "Lini masa Beranda {icon} adalah tempat kamu bisa melihat postingan dari
akun yang kamu ikuti."
step5_4: "Linimasa Lokal {icon} adalah tempat kamu dapat melihat postingan dari
siapa pun di server ini."
step6_1: "Jadi, tempat apa ini?"

View file

@ -331,9 +331,6 @@ dayX: "{day}"
monthX: "{month}"
yearX: "{year}"
pages: "Pagine"
integration: "Integrazioni"
connectService: "Connetti"
disconnectService: "Disconnetti"
enableLocalTimeline: "Abilita timeline locale"
enableGlobalTimeline: "Abilita timeline federata"
disablingTimelinesInfo: "Anche se disabiliti queste timeline, gli amministratori e

View file

@ -52,6 +52,7 @@ sendMessage: "メッセージを送信"
copyUsername: "ユーザー名をコピー"
searchUser: "ユーザーを検索"
reply: "返信"
replies: "返信"
loadMore: "もっと読み込む"
showMore: "もっと見る"
showLess: "閉じる"
@ -97,11 +98,13 @@ unfollow: "フォロー解除"
followRequestPending: "フォロー許可待ち"
enterEmoji: "絵文字を入力"
renote: "ブースト"
renotes: "ブースト"
unrenote: "ブースト解除"
renoted: "ブーストしました。"
cantRenote: "この投稿はブーストできません。"
cantReRenote: "ブーストをブーストすることはできません。"
quote: "引用"
quotes: "引用"
pinnedNote: "ピン留めされた投稿"
pinned: "ピン留め"
you: "あなた"
@ -109,6 +112,7 @@ clickToShow: "クリックして表示"
sensitive: "閲覧注意"
add: "追加"
reaction: "リアクション"
reactions: "リアクション"
enableEmojiReactions: "絵文字リアクションを有効にする"
showEmojisInReactionNotifications: "自分の投稿に対するリアクションの通知で絵文字を表示する"
reactionSetting: "ピッカーに表示するリアクション"
@ -334,9 +338,6 @@ dayX: "{day}日"
monthX: "{month}月"
yearX: "{year}年"
pages: "ページ"
integration: "連携"
connectService: "接続する"
disconnectService: "切断する"
enableLocalTimeline: "ローカルタイムラインを有効にする"
enableGlobalTimeline: "グローバルタイムラインを有効にする"
enableRecommendedTimeline: "おすすめタイムラインを有効にする"
@ -615,7 +616,7 @@ emptyToDisableSmtpAuth: "ユーザー名とパスワードを空欄にするこ
smtpSecure: "SMTP 接続に暗黙的なSSL/TLSを使用する"
smtpSecureInfo: "STARTTLS使用時はオフにします。"
testEmail: "配信テスト"
wordMute: "ワードミュート"
wordMute: "単語または言語のミュート"
regexpError: "正規表現エラー"
regexpErrorDescription: "{tab}ワードミュートの{line}行目の正規表現にエラーが発生しました:"
instanceMute: "サーバーミュート"
@ -693,7 +694,7 @@ no: "いいえ"
driveFilesCount: "ドライブのファイル数"
driveUsage: "ドライブ使用量"
noCrawle: "クローラーによるインデックスを拒否"
noCrawleDescription: "検索エンジンにあなたのプロフィールや投稿、ページなどのコンテンツを登録(インデックス)しないよう要請します。"
noCrawleDescription: "Web検索にあなたのプロフィールや投稿、ページなどのコンテンツを登録(インデックス)しないよう要請します。"
lockedAccountInfo: "フォローを承認制にしても、投稿の公開範囲を「フォロワー」にしない限り、誰でもあなたの投稿を見られます。"
alwaysMarkSensitive: "デフォルトでメディアを閲覧注意にする"
loadRawImages: "添付画像のサムネイルをオリジナル画質にする"
@ -810,7 +811,7 @@ instanceSecurity: "サーバーのセキュリティー"
secureModeInfo: "認証情報の無いリモートサーバーからのリクエストに応えません。"
privateMode: "非公開モード"
privateModeInfo: "有効にすると、許可したサーバーのみからリクエストを受け付けます。"
allowedInstances: "許可されたサーバー"
allowedInstances: "許可するサーバー"
allowedInstancesDescription: "許可したいサーバーのホストを改行で区切って設定します。非公開モードだけで有効です。"
previewNoteText: "本文をプレビュー"
customCss: "カスタムCSS"
@ -990,6 +991,7 @@ remindMeLater: "また後で"
addRe: "閲覧注意の投稿への返信で、注釈の先頭に\"re:\"を追加する"
languageForTranslation: "投稿翻訳に使用する言語"
detectPostLanguage: "投稿の言語を自動検出し、外国語の投稿に翻訳ボタンを表示する"
iconSet: "アイコンのスタイル"
_sensitiveMediaDetection:
description: "機械学習を使って自動でセンシティブなメディアを検出し、モデレーションに役立てられます。サーバーの負荷が少し増えます。"
@ -1504,7 +1506,7 @@ _profile:
youCanIncludeHashtags: "ハッシュタグを含められます。"
metadata: "追加情報"
metadataEdit: "追加情報を編集"
metadataDescription: "プロフィールに表として追加情報を表示できます。{a}タグまたは{l}タグを{rel}とともに追加すると、プロフィールのリンクを確認できます。"
metadataDescription: "プロフィールに追加情報を表示できます。{rel}属性をつけた{a}タグや{l}タグを含むページをリンクすることで、リンクの本人確認もできます!"
metadataLabel: "ラベル"
metadataContent: "内容"
changeAvatar: "アバター画像を変更"
@ -1990,3 +1992,12 @@ confirm: 確認
exportZip: ZIPをエクスポート
openServerInfo: "投稿内のサーバー名をクリックでサーバー情報を開く"
indexableDescription: MastodonやFirefishなどの検索機能に、あなたの投稿が表示されるのを許可します。
clickToShowPatterns: クリックしてトラックを表示
vibrate: 振動を有効にする
indexable: 投稿検索に登録
_iconSets:
bold: "太め"
light: "細め"
regular: "標準"
fill: "塗りつぶし"
duotone: "2色"

View file

@ -138,9 +138,9 @@ addEmoji: "絵文字を追加"
settingGuide: "ええ感じの設定"
cacheRemoteFiles: "リモートのファイルをキャッシュする"
cacheRemoteFilesDescription: "この設定を切っとくと、リモートファイルをキャッシュせず直リンクするようになるで。サーバーの容量は節約できるけど、サムネイルが作られんくなるから通信量が増えるで。"
flagAsBot: "ワイはBotや 🤖"
flagAsBot: "ワイはBotや🤖"
flagAsBotDescription: "もしこのアカウントがプログラムによって運用されるんやったら、このフラグをオンにしてたのむで。オンにすると、反応の連鎖を防ぐためのフラグとして他の開発者に役立ったり、Firefishのシステム上での扱いがBotに合ったもんになったりするんやで。"
flagAsCat: "ワイはCatや 🐯"
flagAsCat: "ワイはCatや🐯"
flagAsCatDescription: "自分、猫ちゃんならこのフラグつけてみ?"
flagShowTimelineReplies: "タイムラインに返信を表示させたる"
flagShowTimelineRepliesDescription: "有効にすると、タイムラインに他のユーザー宛ての投稿も表示したるで。"
@ -246,7 +246,7 @@ uploadFromUrl: "URLアップロード"
uploadFromUrlDescription: "このURLのファイルをアップロードしたいねん"
uploadFromUrlRequested: "アップロードしたい言うといたで"
uploadFromUrlMayTakeTime: "アップロード終わるんにちょい時間かかるかもしれへんわ。"
explore: "みける"
explore: "みける"
messageRead: "もう読まはった"
noMoreHistory: "これより過去の履歴はあらへんで"
startMessaging: "チャットやるで"
@ -317,9 +317,6 @@ dayX: "{day}日"
monthX: "{month}月"
yearX: "{year}年"
pages: "ページ"
integration: "連携"
connectService: "つなげるで"
disconnectService: "切るで"
enableLocalTimeline: "ローカルタイムラインを使えるようにする"
enableGlobalTimeline: "グローバルタイムラインを使えるようにする"
disablingTimelinesInfo: "ここらへんのタイムラインを使えんようにしてしもても、管理者とモデレーターは使えるままになってるで、そうやなかったら不便やからな。"
@ -334,7 +331,7 @@ bannerUrl: "バナー画像のURL"
backgroundImageUrl: "背景画像のURL"
basicInfo: "基本情報"
pinnedUsers: "ピン留めしたユーザー"
pinnedUsersDescription: "「みつける」ページとかにピン留めしたいユーザーをここに書けばええんやで。他ん人との名前は改行で区切ればええんやで。"
pinnedUsersDescription: "「みっける」ページとかにピン留めしときたい兄ちゃんらをここに書いといたらええわ。名前は改行で区切ればええで。"
pinnedPages: "ピン留めページ"
pinnedPagesDescription: "サーバーのいっちゃん上にピン留めしたいページのパスを、改行で区切って記述してな。"
pinnedClipId: "ピン留めするクリップのID"
@ -684,7 +681,7 @@ clips: "クリップ"
experimentalFeatures: "実験的機能やで"
developer: "開発者やで"
makeExplorable: "アカウントを見つけやすくするで"
makeExplorableDescription: "オフにすると、「みつける」にアカウントが載らんくなるで。"
makeExplorableDescription: "オフにすると、「みっける」ページに名前が載らんくなるで。"
showGapBetweenNotesInTimeline: "タイムライン上の投稿を離して表示するで"
duplicate: "複製"
left: "左"
@ -872,7 +869,7 @@ _registry:
domain: "ドメイン"
createKey: "キーを作る"
_aboutFirefish:
about: "Firefishは、ThatOneCalculatorが2022年にMisskeyをいじって作った、オープンなソースのソフトウアーや。"
about: "Firefishは、ThatOneCalculatorが2022年にMisskeyをいじって作った、オープンなソースのソフトウアーや。"
contributors: "ごっつい貢献者"
allContributors: "全ての貢献者"
source: "ソースコード"
@ -1442,6 +1439,12 @@ _tutorial:
step1_2: 使い始める前に、いくつか設定を済ませまひょ。すぐできますえ。
step2_1: 最初に、あんさんのプロフィールを作りまひょ
step2_2: プロフィールを設定しはることで、他ん人があんさんの投稿を見たり、フォローしたりするときの助けになってます。
step3_2: "あんさんのホームとソーシャルタイムラインは、どなたはんをフォローしはるかで決まります。ほな、いくつかアカウントをフォローしてみまひょ。\n\
プロフィールの右上にある、まあるい+ボタンをクリックしはるとフォローできますえ。"
step4_1: 投稿しとーみ
step5_1: タイムライン! 文字と写真の宝石箱や~
step5_2: うちのサーバーでは{timelines}種類のタイムラインをご用意しとります。
step4_2: 最初は{introduction}に投稿したり、シンプルに「ここは賑やかどすなぁ。うちはそこまで喋れまへんが、どうぞよろしゅうに」などと投稿しはる方もいてます。
_postForm:
_placeholders:
b: なんかおましたか?

View file

@ -36,7 +36,6 @@ selectList: "Fren tabdart"
youHaveNoLists: "Ulac ɣur-k·m ula d yiwet n tabdart"
security: "Taɣellist"
remove: "Kkes"
connectService: "Qqen"
userList: "Tibdarin"
securityKey: "Tasarutt n tɣellist"
securityKeyName: "Isem n tsarutt"

View file

@ -323,9 +323,6 @@ dayX: "{day}일"
monthX: "{month}월"
yearX: "{year}년"
pages: "페이지"
integration: "연동"
connectService: "계정 연동"
disconnectService: "계정 연동 해제"
enableLocalTimeline: "로컬 타임라인 활성화"
enableGlobalTimeline: "글로벌 타임라인 활성화"
disablingTimelinesInfo: "특정 타임라인을 비활성화하더라도 관리자 및 모더레이터는 계속 사용할 수 있습니다."

View file

@ -483,8 +483,6 @@ accept: Accepteren
reject: Afwijzen
normal: Normaal
pages: Pagina's
integration: Integraties
connectService: Koppelen
monthX: '{month}'
yearX: '{year}'
instanceName: Servernaam
@ -492,7 +490,6 @@ instanceDescription: Server omschrijving
maintainerName: Onderhouder
maintainerEmail: Onderhouder email
tosUrl: Algemene Voorwaarden URL
disconnectService: Ontkoppelen
unread: Ongelezen
manageGroups: Beheer groepen
subscribePushNotification: Pushmeldingen inschakelen

View file

@ -225,8 +225,6 @@ instanceDescription: Tjenerbeskrivelse
maintainerName: Administrator
maintainerEmail: Administrator-epost
monthX: '{month}'
connectService: Koble til
disconnectService: Koble fra
enableLocalTimeline: Aktiver lokal tidslinje
enableRegistration: Tillat registrering av nye brukere
invite: Inviter
@ -449,7 +447,6 @@ watch: Følg med på
thisMonth: Måned
today: I dag
dayX: '{day}'
integration: Integrasjoner
yearX: '{year}'
pages: Sider
enableRecaptcha: Slå på reCAPTCHA

View file

@ -331,9 +331,6 @@ dayX: "{day}"
monthX: "{month}"
yearX: "{year}"
pages: "Strony"
integration: "Integracje"
connectService: "Połącz"
disconnectService: "Rozłącz"
enableLocalTimeline: "Włącz lokalną oś czasu"
enableGlobalTimeline: "Włącz globalną oś czasu"
disablingTimelinesInfo: "Administratorzy i moderatorzy będą zawsze mieć dostęp do

View file

@ -339,9 +339,6 @@ dayX: " Dia {day}"
monthX: "mês de {month}"
yearX: "Ano {year}"
pages: "Páginas"
integration: "Integração"
connectService: "Conectar"
disconnectService: "Desconectar"
enableLocalTimeline: "Ativar linha do tempo local"
enableGlobalTimeline: "Ativar linha do tempo global"
disablingTimelinesInfo: "Se você desabilitar essas linhas do tempo, administradores

View file

@ -316,9 +316,6 @@ dayX: "{day}"
monthX: "{month}"
yearX: "{year}"
pages: "Pagini"
integration: "Integrare"
connectService: "Conectează"
disconnectService: "Deconectează"
enableLocalTimeline: "Activează cronologia locală"
enableGlobalTimeline: "Activeaza cronologia globală"
disablingTimelinesInfo: "Administratorii și Moderatorii vor avea mereu access la toate cronologiile, chiar dacă nu sunt activate."

View file

@ -335,9 +335,6 @@ dayX: "{day} день"
monthX: "{month} месяц"
yearX: "{year} год"
pages: "Страницы"
integration: "Интеграции"
connectService: "Подключиться"
disconnectService: "Отключиться"
enableLocalTimeline: "Включить локальную ленту"
enableGlobalTimeline: "Включить глобальную ленту"
disablingTimelinesInfo: "У администраторов и модераторов есть доступ ко всем лентам,

View file

@ -317,9 +317,6 @@ dayX: "{day}"
monthX: "{month}"
yearX: "{year}"
pages: "Stránky"
integration: "Integrácia"
connectService: "Pripojiť"
disconnectService: "Odpojiť"
enableLocalTimeline: "Povoliť lokálnu časovú os"
enableGlobalTimeline: "Povoliť globálnu časovú os"
disablingTimelinesInfo: "Administrátori a moderátori majú vždy prístup ku všetkým časovým osiam, aj keď sú vypnuté."

View file

@ -365,8 +365,6 @@ today: Idag
dayX: '{day}'
monthX: '{month}'
yearX: '{year}'
connectService: Anslut
disconnectService: Bortkoppla
enableLocalTimeline: Anslut till lokal tidslinje
invite: Bjud in
driveCapacityPerLocalAccount: Enhetens kapacitet per lokal användare
@ -779,7 +777,6 @@ serverLogs: Serverloggar
deleteAll: Radera alla
removeAllFollowing: Sluta följa alla följda användare
medium: Mellan
integration: Integreringar
xl: XL
desktop: Skrivbord
createNew: Skapa nya

View file

@ -327,9 +327,6 @@ dayX: "{วัน}"
monthX: "{เดือน}"
yearX: "{ปี}"
pages: "หน้า"
integration: "รวบรวม"
connectService: "เชื่อมต่อ"
disconnectService: "ตัดการเชื่อมต่อ"
enableLocalTimeline: "เปิดใช้งานไทม์ไลน์ในพื้นที่"
enableGlobalTimeline: "เปิดใช้งานไทม์ไลน์ทั่วโลก"
disablingTimelinesInfo: "ผู้ดูแลระบบและผู้ควบคุมจะสามารถเข้าถึงไทม์ไลน์ทั้งหมด ถึงแม้ว่าจะไม่ได้เปิดใช้งานก็ตาม"

View file

@ -16,10 +16,10 @@ noNotifications: "Bildirim bulunmuyor"
settings: "Ayarlar"
basicSettings: "Temel Ayarlar"
otherSettings: "Diğer Ayarlar"
openInWindow: "Bir pencere ile aç"
openInWindow: "ılır pencerede aç"
profile: "Profil"
timeline: "Zaman çizelgesi"
noAccountDescription: "Bu kullanıcı henüz kendi hakkında kısmını yazmadı."
timeline: "Akış"
noAccountDescription: "Bu kullanıcı henüz \"hakkında\" kısmını yazmadı."
login: "Giriş Yap"
logout: ıkış Yap"
signup: "Kayıt Ol"
@ -29,7 +29,7 @@ addUser: "Kullanıcı Ekle"
favorite: "Favorilere ekle"
favorites: "Favoriler"
unfavorite: "Favorilerden Kaldır"
favorited: "Favorilerime eklendi."
favorited: "Favorilere eklendi."
alreadyFavorited: "Zaten favorilerinizde kayıtlı."
pin: "Sabitlenmiş"
unpin: "Sabitlemeyi kaldır"
@ -41,9 +41,9 @@ deleteAndEditConfirm: "Bu gönderiyi silip yeniden düzenlemek istiyor musunuz?
ilişkin tüm tepkiler, destekler ve yanıtlar silinecektir."
addToList: "Listeye ekle"
sendMessage: "Mesaj Gönder"
copyUsername: "Kullanıcı Adını Kopyala"
copyUsername: "Kullanıcı Adını kopyala"
searchUser: "Kullanıcıları ara"
pinned: "Sabitlenmiş"
pinned: "Profile sabitle"
remove: "Sil"
smtpUser: "Kullanıcı Adı"
smtpPass: "Şifre"
@ -240,7 +240,7 @@ instance: Sunucu
fetchingAsApObject: Fediverse'den çekiliyor
removeReaction: Tepkini sil
rememberNoteVisibility: Gönderi görünürlüğü ayarlarını hatırla
attachCancel: Eklentiyi kaldır
attachCancel: Ek'i kaldır
suspend: Askıya Al
unsuspend: Askıya Almayı Kaldır
unmute: Susturmayı Kaldır
@ -248,13 +248,13 @@ blockConfirm: Bu hesabı engellemek istediğinize emin misiniz?
unblockConfirm: Bu hesabın engelini kaldırmak istediğinize emin misiniz?
settingGuide: Tavsiye edilen ayarlar
cacheRemoteFilesDescription: Bu ayar devre dışı bırakıldığında, uzak dosyalar doğrudan
uzak sunucudan yüklenir. Bunun devre dışı bırakılması depolama kullanımını azaltacak,
ancak küçük resimler oluşturulmayacağından trafiği artıracaktır.
dosyanın bulunduğu sunucudan yüklenir. Bunun devre dışı bırakılması depolama kullanımını
azaltacak, ancak küçük resimler oluşturulmayacağından trafiği artıracaktır.
flagAsCatDescription: Kedi kulaklarına sahip olacak ve bir kedi gibi konuşacaksın!
flagSpeakAsCat: Kedi gibi konuş
setWallpaper: Arkaplanı ayarla
removeWallpaper: Arkaplanı sil
operations: Operasyonlar
operations: İşlemler
clearCachedFiles: Ön belleği temizle
clearCachedFilesConfirm: Önbelleğe alınan tüm uzak dosyaları silmek istediğinizden
emin misiniz?
@ -357,13 +357,13 @@ whatIsNew: Değişiklikleri göster
translate: Çevir
breakFollow: Takipçiyi sil
breakFollowConfirm: Takipçiyi kaldırmak istediğinizden emin misiniz?
unfollowConfirm: "{name}'i takibi bırakmak istediğinizden emin misiniz?"
unfollowConfirm: "{name} kullanıcısını takip etmeyi bırakmak istediğinizden emin misiniz?"
importRequested: Bir içe aktarma isteğinde bulundunuz. Bu biraz zaman alabilir.
somethingHappened: Bir hata ile karşılaşıldı
retry: Tekrar Dene
youShouldUpgradeClient: Bu sayfayı görüntülemek için, lütfen istemcinizi yenileyin.
reactionSetting: Tepki seçicide gösterilecek tepkiler
unmarkAsSensitive: NSFW işaretini kaldır
unmarkAsSensitive: NSFW (Müstehcen İçerik) işaretini kaldır
enterFileName: Dosya adı gir
noJobs: Hiçbir iş yok
instanceFollowing: Sunucuda takip ediliyor
@ -481,8 +481,8 @@ mention: Bahset
download: İndir
lists: Listeler
noLists: Hiç listen yok
cantRenote: Bu gönderi yükseltilemez.
cantReRenote: Bir yükseltme tekrar yükseltilemez.
cantRenote: Bu gönderi desteklenemez.
cantReRenote: Bir destek tekrardan desteklenemez.
mute: Sustur
block: Engelle
editWidgetsExit: Tamamlandı
@ -636,10 +636,10 @@ reactionSettingDescription2: Yeniden sıralamak için sürükleyin, silmek için
eklemek için "+"ya basın.
you: Sen
clickToShow: Görmek için tıkla
sensitive: NSFW
sensitive: NSFW (Müstehcen İçerik)
add: Ekle
reaction: Tepkiler
markAsSensitive: NSFW olarak işaretle
markAsSensitive: NSFW (Müstehcen İçerik) olarak işaretle
unblock: Engeli Kaldır
addAccount: Hesap ekle
network: İnternet
@ -722,11 +722,11 @@ moveAccountDescription: Bu süreç geri döndürülemez. Taşımadan önce yeni
şeklinde biçimlendirilmiş hesabın etiketini girin
emojis: Emoji
flagAsCat: Kedi misin? 😺
selectChannel: Kanal seç
selectChannel: Bir kanal seç
emojiName: Emoji adı
showOnRemote: Orijinal sayfayı
flagSpeakAsCatDescription: Gönderileriniz kedi modundayken miyavdirilecektir
flagShowTimelineReplies: Yanıtları zaman çizelgesinde göster
flagShowTimelineReplies: Yanıtları akışta göster
silenceThisInstance: Bu sunucuyu sustur
proxyAccountDescription: Vekil hesabı, belirli koşullar altında kullanıcılar için
uzaktan takipçi işlevi gören bir hesaptır. Örneğin, bir kullanıcı listeye bir uzak
@ -760,7 +760,6 @@ banner: Banner
nsfw: NSFW
doNothing: Görmezden Gel
watch: İzle
connectService: Bağlan
registration: Kayıt
hcaptcha: hCaptcha
pinnedNotes: Sabitlenmiş gönderiler
@ -845,8 +844,8 @@ pageLoadErrorDescription: Bu problem genelde ağ hataları veya tarayıcının
kaynaklanır. Önbelleği temizlemeyi deneyin ve biraz bekledikten sonra tekrar deneyin.
quote: Alıntıla
pinnedNote: Sabitlenmiş gönderi
renote: Yükselt
unrenote: Yükseltmeyi geri al
renote: Destekle
unrenote: Desteklemeyi geri al
emojiUrl: Emoji URL'si
suspendConfirm: Bu hesabı askıya almak istediğinize emin misiniz?
addEmoji: Ekle
@ -858,7 +857,7 @@ wallpaper: Arkaplan
searchWith: 'Arat: {q}'
youHaveNoLists: Hiçbir listen yok
followConfirm: '{name} kullanıcısını takip etmek istediğine emin misin?'
metadata: Metadata
metadata: Üstveri
monitor: Monitör
jobQueue: İş Sırası
noUsers: Kullanıcılar bulunamadı
@ -885,7 +884,6 @@ deleteFolder: Bu klasörü sil
addFile: Dosya ekle
dayX: '{day}'
enableLocalTimeline: Yerel zaman çizgisini aktif et
disconnectService: Bağlantıyı kes
enableGlobalTimeline: Global zaman çizgisini aktif et
enableRegistration: Yeni kullanıcı kaydını aktif et
invite: Davet et
@ -1013,7 +1011,7 @@ incorrectPassword: Yanlış şifre.
voteConfirm: '"{choice}" için oyunuzu onaylıyor musunuz?'
failedToFetchAccountInformation: Hesap bilgileri getirilemedi
rateLimitExceeded: Hız limiti aşıldı
renotedBy: '{user} Yükseltti'
renotedBy: '{user} destekledi'
host: Host
objectStorage: Nesne Depolaması
objectStorageUseSSLDesc: API bağlantıları için HTTPS kullanmayacaksanız bunu kapatın
@ -1026,8 +1024,8 @@ verificationEmailSent: Bir doğrulama maili gönderildi. Doğrulamayı tamamlama
lütfen verilen bağlantıyı takip edin.
hashtags: Etiketler
resolved: Çözüldü
flagShowTimelineRepliesDescription: ıksa, kullanıcıların zaman çizelgesindeki diğer
kullanıcıların gönderilerine verdiği yanıtları gösterir.
flagShowTimelineRepliesDescription: ıksa, kullanıcıların akıştaki diğer kullanıcıların
gönderilerine verdiği yanıtları gösterir.
clearQueueConfirmText: Kuyrukta kalan teslim edilmemiş gönderiler birleştirilmeyecektir.
Genellikle bu işleme gerek yoktur.
image: Resim
@ -1040,8 +1038,8 @@ unsuspendConfirm: Bu hesabın askıya almasını kaldırmak istediğinize emin m
selectList: Liste seç
editWidgets: Widget'ları düzenle
showEmojisInReactionNotifications: Tepki bildirimlerinde emojileri göster
renoteMute: Yükseltmeleri sustur
renoteUnmute: Yükseltmeleri susturmayı kaldır
renoteMute: Desteklemeleri sustur
renoteUnmute: Desteklemelerde ki susturmayı kaldır
loginFailed: Giriş yapılamadı
proxyAccount: Vekil Hesap
selectUser: Kullanıcı seç
@ -1068,7 +1066,7 @@ hideThisNote: Bu gönderiyi gizle
file: Dosya
enableEmojiReactions: Emoji tepkilerini aç
cw: İçerik uyarısı
makeFollowManuallyApprove: Onay gerektiren takip istekleri
makeFollowManuallyApprove: Onayınızı gerektiren takip istekleri
today: Bugün
enableRecommendedTimeline: Tavsiye edilen zaman çizgisini aktive et
state: Durum
@ -1165,7 +1163,7 @@ indexFromDescription: Her gönderiyi dizine eklemek için boş bırakın
indexNotice: Şimdi indeksleniyor. Bu muhtemelen biraz zaman alacaktır, lütfen sunucunuzu
en az bir saat yeniden başlatmayın.
customKaTeXMacro: Özel KaTeX makroları
directNotes: Direkt Mesajlar
directNotes: Özel Mesajlar
import: İçeri Aktar
export: Dışarı Aktar
mentions: Bahsetmeler
@ -1173,8 +1171,8 @@ files: Dosyalar
driveFileDeleteConfirm: '"{name}" dosyasını silmek istediğinizden emin misiniz? Dosyayı
"Ek" olarak içeren tüm gönderilerden kaldırılacaktır.'
createList: Liste oluştur
listsDesc: Listeler, belirtilen kullanıcılarla zaman çizelgesi oluşturmanıza olanak
tanır. Zaman Çizelgesi sayfasından erişilebilirler.
listsDesc: Listeler, belirtilen kullanıcıların içeriklerini içeren akışlar oluşturmanıza
olanak tanır. Akış sayfasından erişilebilirler.
note: Gönder
enterListName: Liste için isim gir
unfollow: Takipten Çık
@ -1183,14 +1181,14 @@ followRequestPending: Takip isteği bekleniyor
enterEmoji: Bir emoji gir
followRequest: Takip İsteği
followRequests: Takip istekleri
renoted: Yükseldi.
renoted: Desteklendi.
emoji: Emoji
cacheRemoteFiles: Uzak dosyaları önbelleğe al
flagAsBot: Bu hesabı robot olarak işaretle
flagAsBotDescription: Bu hesap bir program tarafından kontrol ediliyorsa bu seçeneği
etkinleştirin. Etkinleştirilirse, diğer geliştiricilerin botlarıyla sonsuz etkileşim
zincirlerinin önlemesi ve Firefish'in dahili sistemlerinin bu hesabı bir bot olarak
ele alacak şekilde ayarlaması için bir bayrak görevi görür.
ele alacak şekilde ayarlaması için bir işaret görevi görür.
clearQueue: Sırayı Temizle
hiddenTags: Gizlenmiş Etiketler
done: Tamamlandı
@ -1206,7 +1204,6 @@ location: Konum
registeredDate: Katılım tarihi
yearX: '{year}'
pages: Sayfalar
integration: Entegrasyonlar
antennasDesc: "Antenler, belirlediğiniz kriterlere uyan yeni gönderiler görüntüler!\n
 Zaman çizelgeleri sayfasından erişilebilirler."
notesAndReplies: Gönderiler ve yanıtlar
@ -2156,3 +2153,4 @@ importZip: ZIP içe aktar
indexable: Endekslenebilir
languageForTranslation: Çeviri sonrası dili
confirm: Onayla
clickToShowPatterns: Modülün örüntülerini göstermek için tıklayın

View file

@ -336,9 +336,6 @@ dayX: "{day}"
monthX: "{month}"
yearX: "{year}"
pages: "Сторінки"
integration: "Інтеграції"
connectService: "Під’єднати"
disconnectService: "Відключитися"
enableLocalTimeline: "Увімкнути локальну стрічку"
enableGlobalTimeline: "Увімкнути глобальну стрічку"
disablingTimelinesInfo: "Адміністратори та модератори завжди мають доступ до всіх

View file

@ -338,9 +338,6 @@ dayX: "{day}"
monthX: "{month}"
yearX: "{year}"
pages: "Trang"
integration: "Tương tác"
connectService: "Kết nối"
disconnectService: "Ngắt kết nối"
enableLocalTimeline: "Bật bảng tin máy chủ"
enableGlobalTimeline: "Bật bảng tin liên hợp"
disablingTimelinesInfo: "Quản trị viên và Kiểm duyệt viên luôn có quyền truy cập mọi

View file

@ -49,6 +49,7 @@ sendMessage: "发送"
copyUsername: "复制用户名"
searchUser: "搜索用户"
reply: "回复"
replies: "回复"
loadMore: "加载更多"
showMore: "查看更多"
showLess: "关闭"
@ -94,11 +95,13 @@ unfollow: "取消关注"
followRequestPending: "关注请求待批准"
enterEmoji: "输入表情符号"
renote: "转发"
renotes: "转发"
unrenote: "取消转发"
renoted: "已转发。"
cantRenote: "此帖子无法被转发。"
cantReRenote: "转发无法被再次转发。"
quote: "引用"
quotes: "引用"
pinnedNote: "已置顶的帖子"
pinned: "置顶"
you: "您"
@ -106,6 +109,7 @@ clickToShow: "点击以显示"
sensitive: "敏感内容"
add: "添加"
reaction: "回应"
reactions: "回应"
enableEmojiReaction: "启用表情符号回应"
showEmojisInReactionNotifications: "在回应通知中显示表情符号"
reactionSetting: "在回应选择器中显示的回应"
@ -321,9 +325,6 @@ dayX: "{day} 日"
monthX: "{month} 月"
yearX: "{year} 年"
pages: "页面"
integration: "整合"
connectService: "连接"
disconnectService: "断开连接"
enableLocalTimeline: "启用本地时间线功能"
enableGlobalTimeline: "启用全局时间线"
disablingTimelinesInfo: "管理员和监察员将始终拥有对所有时间线的访问权,即使它们没有被启用。"
@ -936,7 +937,8 @@ _accountDelete:
inProgress: "正在删除"
_ad:
back: "返回"
reduceFrequencyOfThisAd: "减少此广告的频率"
reduceFrequencyOfThisAd: "减少此横幅的频率"
adsBy: 社区横幅(作者:{by}
_forgotPassword:
enterEmail: "请输入您注册账号时用的电子邮箱地址,密码重置链接将发送至该邮箱上。"
ifNoEmail: "如果您在注册时没有输入电子邮件地址,请联系服务器管理员。"
@ -1992,3 +1994,4 @@ indexable: 可索引的
languageForTranslation: 帖子翻译语言
vibrate: 播放振动
openServerInfo: 点击帖子上的服务器滚动条时显示服务器信息
clickToShowPatterns: 点击显示模块模式

View file

@ -21,7 +21,7 @@ basicSettings: "基本設定"
otherSettings: "其他設定"
openInWindow: "在新視窗開啟"
profile: "個人檔案"
timeline: "時間"
timeline: "時間"
noAccountDescription: "此用戶還沒有自我介紹。"
login: "登入"
loggingIn: "登入中"
@ -49,6 +49,7 @@ sendMessage: "發送訊息"
copyUsername: "複製使用者名稱"
searchUser: "搜尋使用者"
reply: "回覆"
replies: "回覆"
loadMore: "載入更多"
showMore: "載入更多"
showLess: "關閉"
@ -94,11 +95,13 @@ unfollow: "取消追隨"
followRequestPending: "追隨許可批准中"
enterEmoji: "輸入表情符號"
renote: "轉發"
renotes: "轉發"
unrenote: "取消轉發"
renoted: "已轉發。"
cantRenote: "無法轉發此貼文。"
cantReRenote: "無法轉發之前已經轉發過的內容。"
quote: "引用"
quotes: "引用"
pinnedNote: "已置頂的貼文"
pinned: "置頂"
you: "您"
@ -106,6 +109,7 @@ clickToShow: "按一下以顯示"
sensitive: "敏感內容"
add: "新增"
reaction: "反應"
reactions: "反應"
enableEmojiReaction: "啟用表情符號反應"
showEmojisInReactionNotifications: "在反應通知中顯示表情符號"
reactionSetting: "在選擇器中顯示反應"
@ -145,12 +149,12 @@ flagAsBot: "標記此帳號是機器人"
flagAsBotDescription: "如果本帳戶是由程式控制請啟用此選項。啟用後會作為標示幫助其他開發者防止機器人之間產生無限互動的行為並會調整Firefish內部系統將本帳戶識別為機器人。"
flagAsCat: "你是喵咪嗎w😺"
flagAsCatDescription: "如果想將本帳戶標示為一隻貓,請開啟此標示!"
flagShowTimelineReplies: "在時間上顯示貼文的回覆"
flagShowTimelineRepliesDescription: "啟用時,時間除了顯示用戶的貼文以外,還會顯示用戶對其他貼文的回覆。"
flagShowTimelineReplies: "在時間上顯示貼文的回覆"
flagShowTimelineRepliesDescription: "啟用時,時間除了顯示用戶的貼文以外,還會顯示用戶對其他貼文的回覆。"
autoAcceptFollowed: "自動准予追隨中使用者的追隨請求"
addAccount: "添加帳戶"
loginFailed: "登入失敗"
showOnRemote: "轉到所在伺服器顯示"
showOnRemote: "開啟來源頁面"
general: "一般"
wallpaper: "桌布"
setWallpaper: "設定桌布"
@ -320,12 +324,9 @@ dayX: "{day}日"
monthX: "{month}月"
yearX: "{year}年"
pages: "頁面"
integration: "整合"
connectService: "己連結"
disconnectService: "己斷開"
enableLocalTimeline: "開啟本地時間線"
enableGlobalTimeline: "啟用公開時間線"
disablingTimelinesInfo: "即使您關閉了時間線功能,管理員和板主仍可訪問所有的時間線。"
enableLocalTimeline: "開啟本地時間軸"
enableGlobalTimeline: "啟用公開時間軸"
disablingTimelinesInfo: "即使您關閉了時間軸功能,管理員和板主仍可訪問所有的時間軸。"
registration: "註冊"
enableRegistration: "開啟新使用者註冊"
invite: "邀請"
@ -385,7 +386,7 @@ administrator: "管理員"
token: "權杖"
twoStepAuthentication: "兩階段驗證"
moderator: "板主"
moderation: "言論調節"
moderation: "管理"
nUsersMentioned: "提到了{n}"
securityKey: "安全金鑰"
securityKeyName: "金鑰名稱"
@ -458,7 +459,7 @@ youHaveNoGroups: "找不到群組"
joinOrCreateGroup: "請加入現有群組,或創建新群組。"
noHistory: "沒有歷史紀錄"
signinHistory: "登入歷史"
disableAnimatedMfm: "用MFM動畫"
disableAnimatedMfm: "用MFM動畫"
doing: "正在處理..."
category: "類別"
tags: "標籤"
@ -482,7 +483,7 @@ promotion: "推廣"
promote: "推廣"
numberOfDays: "有效天數"
hideThisNote: "隱藏此貼文"
showFeaturedNotesInTimeline: "在時間上顯示熱門推薦"
showFeaturedNotesInTimeline: "在時間上顯示熱門推薦"
objectStorage: "Object Storage (物件儲存)"
useObjectStorage: "使用Object Storage"
objectStorageBaseUrl: "根URL"
@ -502,7 +503,7 @@ objectStorageUseProxyDesc: "如果不使用代理進行API連接請關閉"
objectStorageSetPublicRead: "上傳時設定為\"public-read\""
serverLogs: "伺服器日誌"
deleteAll: "刪除所有記錄"
showFixedPostForm: "於時間頁頂顯示「發送貼文」方框"
showFixedPostForm: "於時間頁頂顯示「發送貼文」方框"
newNoteRecived: "發現新的貼文"
sounds: "音效"
listen: "聆聽"
@ -596,7 +597,7 @@ emptyToDisableSmtpAuth: "留空使用者名稱及密碼以關閉SMTP驗證"
smtpSecure: "在 SMTP 連接中使用隱式 SSL/TLS"
smtpSecureInfo: "如使用STARTTLS請關閉"
testEmail: "測試郵件發送"
wordMute: "被靜音的文字"
wordMute: "被靜音的文字及語言"
regexpError: "正規表達式錯誤"
regexpErrorDescription: "{tab} 靜音文字的第 {line} 行的正規表達式有錯誤:"
instanceMute: "伺服器的靜音"
@ -670,7 +671,7 @@ no: "取消"
driveFilesCount: "雲端硬碟檔案數量"
driveUsage: "雲端硬碟使用量"
noCrawle: "拒絕搜尋引擎索引"
noCrawleDescription: "要求網路搜尋引擎不要索引你的個人資料頁、貼文及頁面等。"
noCrawleDescription: "要求外部搜尋引擎不要收錄(索引)你的內容(個人檔案、貼文、頁面等)。"
lockedAccountInfo: "即使你通過了追隨者請求,除非你將貼文的可見性設定為 「追隨者」,否則任何人都能看見你的貼文。"
alwaysMarkSensitive: "默認將圖像/影像標記為敏感內容"
loadRawImages: "以原始圖檔顯示附件圖檔的縮圖"
@ -688,7 +689,7 @@ experimentalFeatures: "實驗中的功能"
developer: "開發者"
makeExplorable: "使自己的帳戶能夠在“探索”頁面中顯示"
makeExplorableDescription: "如果關閉,帳戶將不會被顯示在\"探索\"頁面中。"
showGapBetweenNotesInTimeline: "分開顯示時間上的貼文"
showGapBetweenNotesInTimeline: "分開顯示時間上的貼文"
duplicate: "複製"
left: "左"
center: "置中"
@ -734,7 +735,7 @@ inChannelSearch: "頻道内搜尋"
useReactionPickerForContextMenu: "點擊右鍵開啟反應工具欄"
typingUsers: "{users}輸入中"
jumpToSpecifiedDate: "跳轉到特定日期"
showingPastTimeline: "顯示過往的時間"
showingPastTimeline: "顯示過往的時間"
clear: "清除"
markAllAsRead: "全部標示為已讀"
goBack: "返回"
@ -765,7 +766,7 @@ user: "使用者"
administration: "管理"
accounts: "帳戶"
switch: "切換"
noMaintainerInformationWarning: "尚未設定管理員信息。"
noMaintainerInformationWarning: "尚未設定管理員資訊。"
noBotProtectionWarning: "尚未設定Bot防護。"
configure: "設定"
postToGallery: "發佈到相簿"
@ -786,7 +787,7 @@ previewNoteText: "預覽文本"
customCss: "自定義 CSS"
customCssWarn: "這個設定必須由具備相關知識的人員操作,不當的設定可能导致客戶端無法正常使用。"
global: "公開"
squareAvatars: "頭像以方形顯示"
squareAvatars: "大頭貼以方形顯示"
sent: "發送"
received: "收取"
searchResult: "搜尋結果"
@ -822,7 +823,7 @@ unmuteThread: "將貼文串的靜音解除"
ffVisibility: "連接的公開範圍"
ffVisibilityDescription: "您可以設定您的關注/關注者資訊的公開範圍。"
continueThread: "查看更多貼文"
deleteAccountConfirm: "將要刪除帳戶。是否確定"
deleteAccountConfirm: "此帳戶將被刪除,是否繼續"
incorrectPassword: "密碼錯誤。"
voteConfirm: "確定投給「{choice}」?"
hide: "隱藏"
@ -899,7 +900,7 @@ customKaTeXMacro: "自訂KaTeX巨集"
customKaTeXMacroDescription: "使用巨集來輕鬆輸入數學表達式吧!巨集的用法與 LaTeX 中的命令定義相同。你可以使用 \\newcommand{\\
name}{content} 或 \\newcommand{\\name}[number of arguments]{content} 來輸入數學表達式。舉例來說,\\
newcommand{\\add}[2]{#1 + #2} 會將 \\add{3}{foo} 展開為 3 + foo。巨集名稱除了可用大括號 {} 括起來之外,也可使用小括號
() 和中括號 [],但使用於巨集參數的括號會有所變更。每行只能夠定義一個巨集,巨集中間無法間換。無效的行將被忽略。只支援簡單字串的替換功能,不支援條件分歧的高級語法。"
() 和中括號 [],但使用於巨集參數的括號會有所變更。每行只能夠定義一個巨集,巨集中間無法間換。無效的行將被忽略。只支援簡單字串的替換功能,不支援條件分歧的進階語法。"
enableCustomKaTeXMacro: "啟用自定義 KaTeX 宏"
_sensitiveMediaDetection:
description: "您可以使用機器學習自動檢測敏感媒體並將其用於審核。 伺服器的負荷會稍微增加。"
@ -932,7 +933,8 @@ _accountDelete:
inProgress: "正在刪除"
_ad:
back: "返回"
reduceFrequencyOfThisAd: "降低此廣告的頻率"
reduceFrequencyOfThisAd: "降低此橫幅的頻率"
adsBy: 社群橫幅(作者:{by}
_forgotPassword:
enterEmail: "請輸入您的帳戶註冊的電子郵件地址。 密碼重置連結將被發送到該電子郵件地址。"
ifNoEmail: "如果您還沒有註冊您的電子郵件地址,請聯繫管理員。"
@ -1026,7 +1028,7 @@ _mfm:
emoji: "自訂表情符號"
emojiDescription: "您可以通過將自定義表情符號名稱括在冒號中來顯示自定義表情符號。"
search: "搜尋"
searchDescription: "您可以顯示所輸入的搜索框。"
searchDescription: "顯示含有指定文字的搜尋欄。"
flip: "翻轉"
flipDescription: "將內容上下或左右翻轉。"
jelly: "動畫(果凍)"
@ -1068,7 +1070,7 @@ _mfm:
alwaysPlay: 自動播放所有MFM動畫
positionDescription: 按指定數量移動內容。
advancedDescription: 如果停用僅顯示基礎MFM及正在播放的MFM動畫
advanced: 高級MFM
advanced: 進階MFM
fade: 淡出
foreground: 文字顏色
crop: 裁切
@ -1109,14 +1111,14 @@ _wordMute:
muteWords: "加入靜音文字"
muteWordsDescription: "用空格分隔指定AND用換行分隔指定OR。"
muteWordsDescription2: "將關鍵字用斜線括起來表示正規表達式。"
softDescription: "隱藏時間中指定條件的貼文。"
hardDescription: "具有指定條件的貼文將不添加到時間線。 即使您更改條件,未被添加的貼文也會被排除在外。"
softDescription: "隱藏時間中指定條件的貼文。"
hardDescription: "符合指定條件的貼文將不添加到時間軸。 即使您更改條件,未被添加的貼文也會被排除在外。"
soft: "軟性靜音"
hard: "硬性靜音"
mutedNotes: "已靜音的貼文"
muteLangsDescription2: '使用語言代碼。例: en, fr, ja, zh.'
lang: 語言
langDescription: 將指定語言的貼文從時間中隱藏。
langDescription: 將指定語言的貼文從時間中隱藏。
muteLangs: 被靜音的語言
muteLangsDescription: OR條件以空格或換行進行分隔。
_instanceMute:
@ -1228,16 +1230,16 @@ _tutorial:
step2_1: "首先,請完成你的個人資料。"
step2_2: "通過提供一些關於你自己的資料,其他人會更容易了解他們是否想看到你的貼文或關注你。"
step3_1: "現在是時候追隨一些人了!"
step3_2: "你的主頁和社交時間是基於你所追蹤的人,所以試著先追蹤幾個帳戶。\n點擊個人資料右上角的加號就可以關注它。"
step3_2: "你的主頁和社交時間是基於你所追蹤的人,所以試著先追蹤幾個帳戶。\n點擊個人資料右上角的加號按鈕就可以關注它。"
step4_1: "讓我們出去找你。"
step4_2: "對於他們的第一條信息,有些人喜歡做 {introduction} 或一個簡單的 \"hello world!\""
step5_1: "時間線,到處都是時間線"
step5_2: "您的伺服器已啟用了{timelines}個時間。"
step5_3: "首頁 {icon} 時間是顯示你追蹤的帳號的貼文。"
step5_4: "本地 {icon} 時間線是你可以看到伺服器中所有其他用戶的貼文的時間線。"
step5_5: "社交 {icon} 時間線是你的 首頁時間線 和 本地時間線 的結合體。"
step5_6: "推薦 {icon} 時間是顯示你的伺服器管理員推薦的貼文。"
step5_7: "全球 {icon} 時間是顯示來自所有其他連接的伺服器的貼文。"
step4_2: "作為第一則貼文,有些人喜歡發 {introduction} 或單純發一個 \"hello world!\""
step5_1: "時間軸,到處都是時間軸"
step5_2: "您的伺服器已啟用了{timelines}個時間。"
step5_3: "首頁 {icon} 時間是顯示你追蹤的帳號的貼文。"
step5_4: "本地 {icon} 時間軸是你可以看到伺服器中所有其他用戶的貼文的時間軸。"
step5_5: "社交 {icon} 時間軸是你的 首頁時間軸 和 本地時間軸 的結合體。"
step5_6: "推薦 {icon} 時間是顯示你的伺服器管理員推薦的貼文。"
step5_7: "全球 {icon} 時間是顯示來自所有其他連接的伺服器的貼文。"
step6_1: "那麼,這裡是什麼地方?"
step6_2: "你不只是加入Firefish。你已經加入了Fediverse的一個門戶這是一個由成千上萬台服務器組成的互聯網絡。"
step6_3: "每個服務器也有不同而並不是所有的服務器都運行Firefish。但這個服務器確實是運行Firefish的! 你可能會覺得有點複雜,但你很快就會明白的。"
@ -1326,7 +1328,7 @@ _weekday:
_widgets:
memo: "備忘錄"
notifications: "通知"
timeline: "時間"
timeline: "時間"
calendar: "行事曆"
trends: "發燒貼文"
clock: "時鐘"
@ -1382,9 +1384,9 @@ _poll:
remainingSeconds: "{s}秒後截止"
_visibility:
public: "公開"
publicDescription: "發佈至公開時間"
publicDescription: "發佈至公開時間"
home: "不在主頁顯示"
homeDescription: "僅發送至首頁的時間"
homeDescription: "僅發送至首頁的時間"
followers: "追隨者"
followersDescription: "僅發佈至關注者"
specified: "指定使用者"
@ -1783,6 +1785,7 @@ _notification:
renote: "轉發"
reacted: 對您的貼文做出了反應
renoted: 轉發了您的貼文
voted: 投了票
_deck:
alwaysShowMainColumn: "總是顯示主欄"
columnAlign: "對齊欄位"
@ -1806,7 +1809,7 @@ _deck:
main: "主列"
widgets: "小工具"
notifications: "通知"
tl: "時間"
tl: "時間"
antenna: "天線"
list: "清單"
mentions: "提及"
@ -1815,7 +1818,7 @@ _deck:
secureMode: 安全模式(授權獲取)
instanceSecurity: 伺服器安全性
privateMode: 私人模式
allowedInstances: 列入名單的伺服器
allowedInstances: 列入允許名單的伺服器
secureModeInfo: 當從其他伺服器請求時,不要在沒有證據的情況下發回。
_messaging:
dms: 私訊
@ -1823,8 +1826,8 @@ _messaging:
manageGroups: 管理群組
replayTutorial: 重新播放教程
moveFromLabel: '您想遷移的舊帳戶:'
customMOTDDescription: 每次用戶加載/重新加載頁面時,由換行符號分隔的 MOTD啟動畫面的自定信息將隨機顯示。
privateModeInfo: 啟用後,只有列入名單的伺服器才能與你的伺服器聯合。所有貼文都將對公眾隱藏。
customMOTDDescription: 自訂MOTD(啟動畫面)訊息,一行一個。每次用戶載入/重新整理頁面時將會隨機顯示。
privateModeInfo: 啟用後,只有列入允許名單的伺服器才能與你的伺服器聯合。所有貼文都將對公眾隱藏。
adminCustomCssWarn: 除非你知道它的作用,否則請不要使用此設定。 輸入不正確的值可能會導致每個人的客戶端無法正常運行。你可在你的的用戶設定中測試,確保你的
CSS 正常工作。
showUpdates: Firefish 更新時顯示彈出視窗
@ -1838,7 +1841,7 @@ accountMoved: '該使用者已遷移至新帳戶:'
showAds: 顯示社群橫幅
noThankYou: 不用了,謝謝
selectInstance: 選擇伺服器
enableRecommendedTimeline: 啟用推薦時間
enableRecommendedTimeline: 啟用推薦時間
antennaInstancesDescription: 分行列出一個伺服器
moveTo: 遷移此帳戶到新帳戶
moveToLabel: '請輸入你將會遷移到的帳戶:'
@ -1852,7 +1855,7 @@ enableEmojiReactions: 啟用表情符號反應
breakFollowConfirm: 您確定要移除該關注者嗎?
socialTimeline: 社交時間軸
cannotUploadBecauseExceedsFileSizeLimit: 因檔案太大而無法上傳。
customMOTD: 自定義MOTD (網頁載入時顯示的息)
customMOTD: 自定義MOTD (網頁載入時顯示的息)
customSplashIcons: 啟動畫面圖標 (網址)
splash: 啟動畫面
updateAvailable: 可能有可用的更新!
@ -1870,8 +1873,9 @@ silenced: 已靜音
_experiments:
title: 試驗功能
enablePostImports: 啟用匯入貼文的功能
postImportsCaption: 允許用戶從舊有的Firefish・Misskey・Mastodon・Akkoma・Pleroma帳號匯入貼文。在伺服器佇列堵塞時匯入貼文可能會導致載入速度變慢。
findOtherInstance: 找找另一個伺服器
noGraze: 瀏覽器擴展 "Graze for Mastodon" 會與Firefish發生衝突請停用該擴展
noGraze: 瀏覽器擴充元件 "Graze for Mastodon" 會與Firefish發生衝突請停用該擴充元件
userSaysSomethingReasonRenote: '{name} 轉發了包含 {reason} 的貼文'
pushNotificationNotSupported: 你的瀏覽器或伺服器不支援推送通知
accessibility: 輔助功能
@ -1883,15 +1887,15 @@ deleted: 已刪除
editNote: 編輯貼文
edited: '於 {date} {time} 編輯'
userSaysSomethingReason: '{name} 說了 {reason}'
allowedInstancesDescription: 要加入聯邦白名單的服務器,每台伺服器用新行分隔(僅適用於私有模式)。
allowedInstancesDescription: 允許聯邦的伺服器名單,一行一個(僅適用於私人模式)。
defaultReaction: 默認的表情符號反應
license: 授權
apps: 應用
pushNotification: 推送通知
subscribePushNotification: 啟用推送通知
unsubscribePushNotification: 用推送通知
unsubscribePushNotification: 用推送通知
pushNotificationAlreadySubscribed: 推送通知已經啟用
recommendedInstancesDescription: 以每行分隔的推薦伺服器出現在推薦的時間線中
recommendedInstancesDescription: 推薦的伺服器(將顯示在推薦時間軸中),一行一個
searchPlaceholder: 在 Firefish 上搜尋
cw: 內容警告
selectChannel: 選擇一個頻道
@ -1899,9 +1903,9 @@ newer: 較新
older: 較舊
jumpToPrevious: 跳到上一個
removeReaction: 移除你的反應
listsDesc: 清單可以創建一個只有您指定用戶的時間線。 可以從時間線頁面訪問它們。
listsDesc: 清單可以創建一個只有您指定用戶的時間軸。 可以從時間軸頁面訪問它們。
flagSpeakAsCatDescription: 在喵咪模式下你的貼文會被喵化ヾ(•ω•`)o
antennasDesc: "天線會顯示符合您設置條件的新貼文!\n 可以從時間訪問它們。"
antennasDesc: "天線會顯示符合您設置條件的新貼文!\n 可以從時間訪問它們。"
expandOnNoteClick: 點擊以打開貼文
expandOnNoteClickDesc: 即使停用,您仍然可以從右鍵選單或單擊發文時間來打開貼文。
hiddenTagsDescription: '列出您希望隱藏趨勢和探索的主題標籤(不帶 #)。 隱藏的主題標籤仍然可以通過其他方式發現。'
@ -1920,7 +1924,7 @@ seperateRenoteQuote: 分開轉發及引用的按鈕
clipsDesc: 摘錄就像一個可以分享的書籤。 你可以從每個貼文的菜單創建新摘錄或將貼文加入已有的摘錄。
noteId: 貼文 ID
sendModMail: 發送審核通知
enableIdenticonGeneration: 啟用碎片生成
enableIdenticonGeneration: 啟用Identicon生成
enableServerMachineStats: 啟用伺服器硬體統計資訊
reactionPickerSkinTone: 首選表情符號膚色
indexFromDescription: 留空以索引每個貼文
@ -1932,7 +1936,7 @@ isModerator: 板主
isAdmin: 管理員
isPatron: Firefish 項目贊助者
silencedWarning: 顯示此頁面是因為這些使用者來自您伺服器管理員已靜音的伺服器,因此他們可能是垃圾訊息。
signupsDisabled: 該伺服器上的註冊當前已被禁用,但您隨時可以在另一台伺服器上註冊!或是您有該伺服器的邀請碼,請在下面輸入。
signupsDisabled: 此伺服器目前停止註冊,但您隨時可以在另一台伺服器上註冊!如果您有此伺服器的邀請碼,請在下面輸入。
showPopup: 通過彈出式視窗通知用戶
showWithSparkles: 讓標題閃閃發光
youHaveUnreadAnnouncements: 您有未讀的公告
@ -1961,10 +1965,17 @@ _dialog:
charactersExceeded: 超過字數限制! 當前 {current} / 限制 {max}
_skinTones:
yellow: 黃色
medium: 中等
dark: 深色
mediumDark: 中等偏深
light: 淺色
mediumLight: 中等偏淺
exportZip: 匯出ZIP
_feeds:
atom: Atom
rss: RSS
copyFeed: 複製訂閱URL
jsonFeed: JSON Feed
emojiPackCreator: 表情包的作者
importZip: 匯入ZIP
delete2fa: 停用二階段認證(2FA)
@ -1977,3 +1988,9 @@ addRe: 在回覆有內容警告的貼文時,在標題前面加上 "re:"
vibrate: 播放振動
openServerInfo: 點擊貼文中的伺服器名稱以顯示伺服器資訊
languageForTranslation: 貼文翻譯語言
objectStorageS3ForcePathStyleDesc: 以 "s3.amazonaws.com/<bucket>/" 而非 "<bucket>.s3.amazonaws.com"
的格式建構端點EndpointURL。
indexable: 登錄至貼文搜尋引擎
origin: 來源
objectStorageS3ForcePathStyle: 使用基於路徑的端點EndpointURL
clickToShowPatterns: 點擊顯示模組模式Module Pattern

View file

@ -1,4 +1,14 @@
user-agent: *
allow: /
User-agent: *
Allow: /
# todo: sitemap
# Uncomment the following to block CommonCrawl
#
# User-agent: CCBot
# User-agent: CCBot/2.0
# User-agent: CCBot/3.1
# Disallow: /
# Uncomment the following to block ChatGPT
#
# User-agent: GPTBot
# Disallow: /

View file

@ -7,6 +7,7 @@ mod m20230627_185451_index_note_url;
mod m20230709_000510_move_antenna_to_cache;
mod m20230806_170616_fix_antenna_stream_ids;
mod m20230904_013244_is_indexable;
mod m20231002_143323_remove_integrations;
pub struct Migrator;
@ -19,6 +20,7 @@ impl MigratorTrait for Migrator {
Box::new(m20230709_000510_move_antenna_to_cache::Migration),
Box::new(m20230806_170616_fix_antenna_stream_ids::Migration),
Box::new(m20230904_013244_is_indexable::Migration),
Box::new(m20231002_143323_remove_integrations::Migration),
]
}
}

View file

@ -0,0 +1,117 @@
use sea_orm_migration::prelude::*;
#[derive(DeriveMigrationName)]
pub struct Migration;
#[async_trait::async_trait]
impl MigrationTrait for Migration {
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.alter_table(
Table::alter()
.table(UserProfile::Table)
.drop_column(UserProfile::Integrations)
.to_owned(),
)
.await?;
manager
.alter_table(
Table::alter()
.table(Meta::Table)
.drop_column(Meta::EnableTwitterIntegration)
.drop_column(Meta::TwitterConsumerKey)
.drop_column(Meta::TwitterConsumerSecret)
.drop_column(Meta::EnableGithubIntegration)
.drop_column(Meta::GithubClientId)
.drop_column(Meta::GithubClientSecret)
.drop_column(Meta::EnableDiscordIntegration)
.drop_column(Meta::DiscordClientId)
.drop_column(Meta::DiscordClientSecret)
.to_owned(),
)
.await?;
Ok(())
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.alter_table(
Table::alter()
.table(Meta::Table)
.add_column(ColumnDef::new(Meta::DiscordClientSecret).string())
.add_column(ColumnDef::new(Meta::DiscordClientId).string())
.add_column(
ColumnDef::new(Meta::EnableDiscordIntegration)
.boolean()
.not_null()
.default(false),
)
.add_column(ColumnDef::new(Meta::GithubClientSecret).string())
.add_column(ColumnDef::new(Meta::GithubClientId).string())
.add_column(
ColumnDef::new(Meta::EnableGithubIntegration)
.boolean()
.not_null()
.default(false),
)
.add_column(ColumnDef::new(Meta::TwitterConsumerSecret).string())
.add_column(ColumnDef::new(Meta::TwitterConsumerKey).string())
.add_column(
ColumnDef::new(Meta::EnableTwitterIntegration)
.boolean()
.not_null()
.default(false),
)
.to_owned(),
)
.await?;
manager
.alter_table(
Table::alter()
.table(UserProfile::Table)
.add_column(
ColumnDef::new(UserProfile::Integrations)
.json()
.default("{}"),
)
.to_owned(),
)
.await?;
Ok(())
}
}
#[derive(Iden)]
enum UserProfile {
Table,
#[iden = "integrations"]
Integrations,
}
#[derive(Iden)]
enum Meta {
Table,
#[iden = "enableTwitterIntegration"]
EnableTwitterIntegration,
#[iden = "twitterConsumerKey"]
TwitterConsumerKey,
#[iden = "twitterConsumerSecret"]
TwitterConsumerSecret,
#[iden = "enableGithubIntegration"]
EnableGithubIntegration,
#[iden = "githubClientId"]
GithubClientId,
#[iden = "githubClientSecret"]
GithubClientSecret,
#[iden = "enableDiscordIntegration"]
EnableDiscordIntegration,
#[iden = "discordClientId"]
DiscordClientId,
#[iden = "discordClientSecret"]
DiscordClientSecret,
}

View file

@ -71,24 +71,6 @@ pub struct Model {
pub sw_public_key: Option<String>,
#[sea_orm(column_name = "swPrivateKey")]
pub sw_private_key: Option<String>,
#[sea_orm(column_name = "enableTwitterIntegration")]
pub enable_twitter_integration: bool,
#[sea_orm(column_name = "twitterConsumerKey")]
pub twitter_consumer_key: Option<String>,
#[sea_orm(column_name = "twitterConsumerSecret")]
pub twitter_consumer_secret: Option<String>,
#[sea_orm(column_name = "enableGithubIntegration")]
pub enable_github_integration: bool,
#[sea_orm(column_name = "githubClientId")]
pub github_client_id: Option<String>,
#[sea_orm(column_name = "githubClientSecret")]
pub github_client_secret: Option<String>,
#[sea_orm(column_name = "enableDiscordIntegration")]
pub enable_discord_integration: bool,
#[sea_orm(column_name = "discordClientId")]
pub discord_client_id: Option<String>,
#[sea_orm(column_name = "discordClientSecret")]
pub discord_client_secret: Option<String>,
#[sea_orm(column_name = "pinnedUsers")]
pub pinned_users: StringVec,
#[sea_orm(column_name = "ToSUrl")]

View file

@ -46,8 +46,6 @@ pub struct Model {
pub pinned_page_id: Option<String>,
#[sea_orm(column_type = "JsonBinary")]
pub room: Json,
#[sea_orm(column_type = "JsonBinary")]
pub integrations: Json,
#[sea_orm(column_name = "injectFeaturedNote")]
pub inject_featured_note: bool,
#[sea_orm(column_name = "enableWordMute")]

View file

@ -98,7 +98,6 @@
"node-fetch": "3.3.2",
"nodemailer": "6.9.4",
"nsfwjs": "2.4.2",
"oauth": "^0.10.0",
"opencc-js": "^1.0.5",
"os-utils": "0.0.14",
"otpauth": "^9.1.4",

View file

@ -1,76 +1,29 @@
import type { Antenna } from "@/models/entities/antenna.js";
import type { Note } from "@/models/entities/note.js";
import type { User } from "@/models/entities/user.js";
import {
UserListJoinings,
UserGroupJoinings,
Blockings,
} from "@/models/index.js";
import { Blockings, UserProfiles } from "@/models/index.js";
import { getFullApAccount } from "./convert-host.js";
import * as Acct from "@/misc/acct.js";
import type { Packed } from "./schema.js";
import { Cache } from "./cache.js";
import { getWordHardMute } from "./check-word-mute.js";
const blockingCache = new Cache<User["id"][]>("blocking", 60 * 5);
const mutedWordsCache = new Cache<string[][] | undefined>("mutedWords", 60 * 5);
// NOTE: フォローしているユーザーのノート、リストのユーザーのノート、グループのユーザーのノート指定はパフォーマンス上の理由で無効になっている
/**
* noteUserFollowers / antennaUserFollowing
*/
export async function checkHitAntenna(
antenna: Antenna,
note: Note | Packed<"Note">,
noteUser: { id: User["id"]; username: string; host: string | null },
noteUserFollowers?: User["id"][],
antennaUserFollowing?: User["id"][],
): Promise<boolean> {
if (note.visibility === "specified") return false;
if (note.visibility === "home") return false;
// アンテナ作成者がノート作成者にブロックされていたらスキップ
const blockings = await blockingCache.fetch(noteUser.id, () =>
Blockings.findBy({ blockerId: noteUser.id }).then((res) =>
res.map((x) => x.blockeeId),
),
);
if (blockings.some((blocking) => blocking === antenna.userId)) return false;
if (note.visibility === "followers") {
if (noteUserFollowers && !noteUserFollowers.includes(antenna.userId))
return false;
if (antennaUserFollowing && !antennaUserFollowing.includes(note.userId))
return false;
if (!antenna.withReplies && note.replyId != null) return false;
if (antenna.withFile) {
if (note.fileIds && note.fileIds.length === 0) return false;
}
if (!antenna.withReplies && note.replyId != null) return false;
if (antenna.src === "home") {
if (noteUserFollowers && !noteUserFollowers.includes(antenna.userId))
return false;
if (antennaUserFollowing && !antennaUserFollowing.includes(note.userId))
return false;
} else if (antenna.src === "list") {
const listUsers = (
await UserListJoinings.findBy({
userListId: antenna.userListId!,
})
).map((x) => x.userId);
if (!listUsers.includes(note.userId)) return false;
} else if (antenna.src === "group") {
const joining = await UserGroupJoinings.findOneByOrFail({
id: antenna.userGroupJoiningId!,
});
const groupUsers = (
await UserGroupJoinings.findBy({
userGroupId: joining.userGroupId,
})
).map((x) => x.userId);
if (!groupUsers.includes(note.userId)) return false;
} else if (antenna.src === "users") {
if (antenna.src === "users") {
const accts = antenna.users.map((x) => {
const { username, host } = Acct.parse(x);
return getFullApAccount(username, host).toLowerCase();
@ -128,9 +81,20 @@ export async function checkHitAntenna(
if (matched) return false;
}
if (antenna.withFile) {
if (note.fileIds && note.fileIds.length === 0) return false;
}
// アンテナ作成者がノート作成者にブロックされていたらスキップ
const blockings = await blockingCache.fetch(noteUser.id, () =>
Blockings.findBy({ blockerId: noteUser.id }).then((res) =>
res.map((x) => x.blockeeId),
),
);
if (blockings.includes(antenna.userId)) return false;
const mutedWords = await mutedWordsCache.fetch(antenna.userId, () =>
UserProfiles.findOneBy({ userId: antenna.userId }).then(
(profile) => profile?.mutedWords,
),
);
if (await getWordHardMute(note, antenna.userId, mutedWords)) return false;
// TODO: eval expression

View file

@ -1,6 +1,5 @@
import RE2 from "re2";
import type { Note } from "@/models/entities/note.js";
import type { User } from "@/models/entities/user.js";
type NoteLike = {
userId: Note["userId"];
@ -9,10 +8,6 @@ type NoteLike = {
cw?: Note["cw"];
};
type UserLike = {
id: User["id"];
};
function checkWordMute(
note: NoteLike,
mutedWords: Array<string | string[]>,
@ -61,13 +56,10 @@ function checkWordMute(
export async function getWordHardMute(
note: NoteLike,
me: UserLike | null | undefined,
mutedWords: Array<string | string[]>,
meId: string | null | undefined,
mutedWords?: Array<string | string[]>,
): Promise<boolean> {
// 自分自身
if (me && note.userId === me.id) {
return false;
}
if (note.userId === meId || mutedWords == null) return false;
if (mutedWords.length > 0) {
return (

View file

@ -354,57 +354,6 @@ export class Meta {
})
public swPrivateKey: string | null;
@Column("boolean", {
default: false,
})
public enableTwitterIntegration: boolean;
@Column("varchar", {
length: 128,
nullable: true,
})
public twitterConsumerKey: string | null;
@Column("varchar", {
length: 128,
nullable: true,
})
public twitterConsumerSecret: string | null;
@Column("boolean", {
default: false,
})
public enableGithubIntegration: boolean;
@Column("varchar", {
length: 128,
nullable: true,
})
public githubClientId: string | null;
@Column("varchar", {
length: 128,
nullable: true,
})
public githubClientSecret: string | null;
@Column("boolean", {
default: false,
})
public enableDiscordIntegration: boolean;
@Column("varchar", {
length: 128,
nullable: true,
})
public discordClientId: string | null;
@Column("varchar", {
length: 128,
nullable: true,
})
public discordClientSecret: string | null;
@Column("varchar", {
length: 128,
nullable: true,

View file

@ -215,11 +215,6 @@ export class UserProfile {
@JoinColumn()
public pinnedPage: Page | null;
@Column("jsonb", {
default: {},
})
public integrations: Record<string, any>;
@Index()
@Column("boolean", {
default: false,

View file

@ -565,7 +565,6 @@ export const UserRepository = db.getRepository(User).extend({
hasUnreadNotification: this.getHasUnreadNotification(user.id),
hasPendingReceivedFollowRequest:
this.getHasPendingReceivedFollowRequest(user.id),
integrations: profile!.integrations,
mutedWords: profile!.mutedWords,
mutedInstances: profile!.mutedInstances,
mutingNotificationTypes: profile!.mutingNotificationTypes,

View file

@ -459,11 +459,6 @@ export const packedMeDetailedOnlySchema = {
nullable: false,
optional: false,
},
integrations: {
type: "object",
nullable: true,
optional: false,
},
mutedWords: {
type: "array",
nullable: false,

View file

@ -95,11 +95,25 @@ export default async (job: Bull.Job<InboxJobData>): Promise<string> => {
}
// HTTP-Signatureの検証
const httpSignatureValidated = httpSignature.verifySignature(
let httpSignatureValidated = httpSignature.verifySignature(
signature,
authUser.key.keyPem,
);
// If signature validation failed, try refetching the actor
if (!httpSignatureValidated) {
authUser.key = await dbResolver.refetchPublicKeyForApId(authUser.user);
if (authUser.key == null) {
return "skip: failed to re-resolve user publicKey";
}
httpSignatureValidated = httpSignature.verifySignature(
signature,
authUser.key.keyPem,
);
}
// また、signatureのsignerは、activity.actorと一致する必要がある
if (!httpSignatureValidated || authUser.user.uri !== activity.actor) {
// 一致しなくても、でもLD-Signatureがありそうならそっちも見る

View file

@ -86,11 +86,25 @@ export async function checkFetch(req: IncomingMessage): Promise<number> {
}
// HTTP-Signatureの検証
const httpSignatureValidated = httpSignature.verifySignature(
let httpSignatureValidated = httpSignature.verifySignature(
signature,
authUser.key.keyPem,
);
// If signature validation failed, try refetching the actor
if (!httpSignatureValidated) {
authUser.key = await dbResolver.refetchPublicKeyForApId(authUser.user);
if (authUser.key == null) {
return 403;
}
httpSignatureValidated = httpSignature.verifySignature(
signature,
authUser.key.keyPem,
);
}
if (!httpSignatureValidated) {
return 403;
}

View file

@ -17,7 +17,7 @@ import { Cache } from "@/misc/cache.js";
import { uriPersonCache, userByIdCache } from "@/services/user-cache.js";
import type { IObject } from "./type.js";
import { getApId } from "./type.js";
import { resolvePerson } from "./models/person.js";
import { resolvePerson, updatePerson } from "./models/person.js";
const publicKeyCache = new Cache<UserPublickey | null>("publicKey", 60 * 30);
const publicKeyByUserIdCache = new Cache<UserPublickey | null>(
@ -151,7 +151,7 @@ export default class DbResolver {
*/
public async getAuthUserFromKeyId(keyId: string): Promise<{
user: CacheableRemoteUser;
key: UserPublickey;
key: UserPublickey | null;
} | null> {
const key = await publicKeyCache.fetch(
keyId,
@ -203,4 +203,15 @@ export default class DbResolver {
key,
};
}
public async refetchPublicKeyForApId(
user: CacheableRemoteUser,
): Promise<UserPublickey | null> {
await updatePerson(user.uri!, undefined, undefined, user);
const key = await UserPublickeys.findOneBy({ userId: user.id });
if (key != null) {
await publicKeyByUserIdCache.set(user.id, key);
}
return key;
}
}

View file

@ -185,7 +185,7 @@ export async function createPerson(
const host = toPuny(new URL(object.id).hostname);
const { fields } = analyzeAttachments(person.attachment || []);
const fields = analyzeAttachments(person.attachment || []);
const tags = extractApHashtags(person.tag)
.map((tag) => normalizeForSearch(tag))
@ -642,39 +642,6 @@ export async function resolvePerson(
return await createPerson(uri, resolver);
}
const services: {
[x: string]: (id: string, username: string) => any;
} = {
"misskey:authentication:twitter": (userId, screenName) => ({
userId,
screenName,
}),
"misskey:authentication:github": (id, login) => ({ id, login }),
"misskey:authentication:discord": (id, name) => $discord(id, name),
};
const $discord = (id: string, name: string) => {
if (typeof name !== "string") {
name = "unknown#0000";
}
const [username, discriminator] = name.split("#");
return { id, username, discriminator };
};
function addService(target: { [x: string]: any }, source: IApPropertyValue) {
const service = services[source.name];
if (typeof source.value !== "string") {
source.value = "unknown";
}
const [id, username] = source.value.split("@");
if (service) {
target[source.name.split(":")[2]] = service(id, username);
}
}
export function analyzeAttachments(
attachments: IObject | IObject[] | undefined,
) {
@ -682,22 +649,17 @@ export function analyzeAttachments(
name: string;
value: string;
}[] = [];
const services: { [x: string]: any } = {};
if (Array.isArray(attachments)) {
for (const attachment of attachments.filter(isPropertyValue)) {
if (isPropertyValue(attachment.identifier)) {
addService(services, attachment.identifier);
} else {
fields.push({
name: attachment.name,
value: fromHtml(attachment.value),
});
}
}
}
return { fields, services };
return fields;
}
export async function updateFeatured(userId: User["id"], resolver?: Resolver) {

View file

@ -170,21 +170,6 @@ export const meta = {
optional: false,
nullable: false,
},
enableTwitterIntegration: {
type: "boolean",
optional: false,
nullable: false,
},
enableGithubIntegration: {
type: "boolean",
optional: false,
nullable: false,
},
enableDiscordIntegration: {
type: "boolean",
optional: false,
nullable: false,
},
enableServiceWorker: {
type: "boolean",
optional: false,
@ -326,36 +311,6 @@ export const meta = {
nullable: true,
format: "id",
},
twitterConsumerKey: {
type: "string",
optional: true,
nullable: true,
},
twitterConsumerSecret: {
type: "string",
optional: true,
nullable: true,
},
githubClientId: {
type: "string",
optional: true,
nullable: true,
},
githubClientSecret: {
type: "string",
optional: true,
nullable: true,
},
discordClientId: {
type: "string",
optional: true,
nullable: true,
},
discordClientSecret: {
type: "string",
optional: true,
nullable: true,
},
summaryProxy: {
type: "string",
optional: true,
@ -544,9 +499,6 @@ export default define(meta, paramDef, async (ps, me) => {
defaultLightTheme: instance.defaultLightTheme,
defaultDarkTheme: instance.defaultDarkTheme,
enableEmail: instance.enableEmail,
enableTwitterIntegration: instance.enableTwitterIntegration,
enableGithubIntegration: instance.enableGithubIntegration,
enableDiscordIntegration: instance.enableDiscordIntegration,
enableServiceWorker: instance.enableServiceWorker,
translatorAvailable:
instance.deeplAuthKey != null || instance.libreTranslateApiUrl != null,
@ -573,12 +525,6 @@ export default define(meta, paramDef, async (ps, me) => {
enableSensitiveMediaDetectionForVideos:
instance.enableSensitiveMediaDetectionForVideos,
proxyAccountId: instance.proxyAccountId,
twitterConsumerKey: instance.twitterConsumerKey,
twitterConsumerSecret: instance.twitterConsumerSecret,
githubClientId: instance.githubClientId,
githubClientSecret: instance.githubClientSecret,
discordClientId: instance.discordClientId,
discordClientSecret: instance.discordClientSecret,
summalyProxy: instance.summalyProxy,
email: instance.email,
smtpSecure: instance.smtpSecure,

View file

@ -46,13 +46,6 @@ export default define(meta, paramDef, async (ps, me) => {
};
}
const maskedKeys = ["accessToken", "accessTokenSecret", "refreshToken"];
Object.keys(profile.integrations).forEach((integration) => {
maskedKeys.forEach(
(key) => (profile.integrations[integration][key] = "<MASKED>"),
);
});
const signins = await Signins.findBy({ userId: user.id });
return {
@ -67,7 +60,6 @@ export default define(meta, paramDef, async (ps, me) => {
carefulBot: profile.carefulBot,
injectFeaturedNote: profile.injectFeaturedNote,
receiveAnnouncementEmail: profile.receiveAnnouncementEmail,
integrations: profile.integrations,
mutedWords: profile.mutedWords,
mutedInstances: profile.mutedInstances,
mutingNotificationTypes: profile.mutingNotificationTypes,

View file

@ -132,15 +132,6 @@ export const paramDef = {
deeplIsPro: { type: "boolean" },
libreTranslateApiUrl: { type: "string", nullable: true },
libreTranslateApiKey: { type: "string", nullable: true },
enableTwitterIntegration: { type: "boolean" },
twitterConsumerKey: { type: "string", nullable: true },
twitterConsumerSecret: { type: "string", nullable: true },
enableGithubIntegration: { type: "boolean" },
githubClientId: { type: "string", nullable: true },
githubClientSecret: { type: "string", nullable: true },
enableDiscordIntegration: { type: "boolean" },
discordClientId: { type: "string", nullable: true },
discordClientSecret: { type: "string", nullable: true },
enableEmail: { type: "boolean" },
email: { type: "string", nullable: true },
smtpSecure: { type: "boolean" },
@ -395,42 +386,6 @@ export default define(meta, paramDef, async (ps, me) => {
set.summalyProxy = ps.summalyProxy;
}
if (ps.enableTwitterIntegration !== undefined) {
set.enableTwitterIntegration = ps.enableTwitterIntegration;
}
if (ps.twitterConsumerKey !== undefined) {
set.twitterConsumerKey = ps.twitterConsumerKey;
}
if (ps.twitterConsumerSecret !== undefined) {
set.twitterConsumerSecret = ps.twitterConsumerSecret;
}
if (ps.enableGithubIntegration !== undefined) {
set.enableGithubIntegration = ps.enableGithubIntegration;
}
if (ps.githubClientId !== undefined) {
set.githubClientId = ps.githubClientId;
}
if (ps.githubClientSecret !== undefined) {
set.githubClientSecret = ps.githubClientSecret;
}
if (ps.enableDiscordIntegration !== undefined) {
set.enableDiscordIntegration = ps.enableDiscordIntegration;
}
if (ps.discordClientId !== undefined) {
set.discordClientId = ps.discordClientId;
}
if (ps.discordClientSecret !== undefined) {
set.discordClientSecret = ps.discordClientSecret;
}
if (ps.enableEmail !== undefined) {
set.enableEmail = ps.enableEmail;
}

View file

@ -33,7 +33,7 @@ export const meta = {
id: "4362f8dc-731f-4ad8-a694-be2a88922a24",
},
uriNull: {
message: "User ActivityPup URI is null.",
message: "User ActivityPub URI is null.",
code: "URI_NULL",
id: "bf326f31-d430-4f97-9933-5d61e4d48a23",
},

View file

@ -53,12 +53,12 @@ export const meta = {
id: "fcd2eef9-a9b2-4c4f-8624-038099e90aa5",
},
uriNull: {
message: "User ActivityPup URI is null.",
message: "User ActivityPub URI is null.",
code: "URI_NULL",
id: "bf326f31-d430-4f97-9933-5d61e4d48a23",
},
localUriNull: {
message: "Local User ActivityPup URI is null.",
message: "Local User ActivityPub URI is null.",
code: "URI_NULL",
id: "95ba11b9-90e8-43a5-ba16-7acc1ab32e71",
},

View file

@ -268,21 +268,6 @@ export const meta = {
optional: false,
nullable: false,
},
enableTwitterIntegration: {
type: "boolean",
optional: false,
nullable: false,
},
enableGithubIntegration: {
type: "boolean",
optional: false,
nullable: false,
},
enableDiscordIntegration: {
type: "boolean",
optional: false,
nullable: false,
},
enableServiceWorker: {
type: "boolean",
optional: false,
@ -343,21 +328,6 @@ export const meta = {
optional: false,
nullable: false,
},
twitter: {
type: "boolean",
optional: false,
nullable: false,
},
github: {
type: "boolean",
optional: false,
nullable: false,
},
discord: {
type: "boolean",
optional: false,
nullable: false,
},
serviceWorker: {
type: "boolean",
optional: false,
@ -493,10 +463,6 @@ export default define(meta, paramDef, async (ps, me) => {
})),
enableEmail: instance.enableEmail,
enableTwitterIntegration: instance.enableTwitterIntegration,
enableGithubIntegration: instance.enableGithubIntegration,
enableDiscordIntegration: instance.enableDiscordIntegration,
enableServiceWorker: instance.enableServiceWorker,
translatorAvailable:
@ -539,9 +505,6 @@ export default define(meta, paramDef, async (ps, me) => {
hcaptcha: instance.enableHcaptcha,
recaptcha: instance.enableRecaptcha,
objectStorage: instance.useObjectStorage,
twitter: instance.enableTwitterIntegration,
github: instance.enableGithubIntegration,
discord: instance.enableDiscordIntegration,
serviceWorker: instance.enableServiceWorker,
postEditing: true,
postImports: instance.experimentalFeatures?.postImports || false,

View file

@ -21,9 +21,6 @@ import signup from "./private/signup.js";
import signin from "./private/signin.js";
import signupPending from "./private/signup-pending.js";
import verifyEmail from "./private/verify-email.js";
import discord from "./service/discord.js";
import github from "./service/github.js";
import twitter from "./service/twitter.js";
import { koaBody } from "koa-body";
import {
convertId,
@ -181,10 +178,6 @@ router.post("/signin", signin);
router.post("/signup-pending", signupPending);
router.post("/verify-email", verifyEmail);
router.use(discord.routes());
router.use(github.routes());
router.use(twitter.routes());
router.post("/miauth/:session/check", async (ctx) => {
const token = await AccessTokens.findOneBy({
session: ctx.params.session,

View file

@ -1,333 +0,0 @@
import type Koa from "koa";
import Router from "@koa/router";
import { OAuth2 } from "oauth";
import { v4 as uuid } from "uuid";
import { IsNull } from "typeorm";
import { getJson } from "@/misc/fetch.js";
import config from "@/config/index.js";
import { publishMainStream } from "@/services/stream.js";
import { fetchMeta } from "@/misc/fetch-meta.js";
import { Users, UserProfiles } from "@/models/index.js";
import type { ILocalUser } from "@/models/entities/user.js";
import { redisClient } from "../../../db/redis.js";
import signin from "../common/signin.js";
function getUserToken(ctx: Koa.BaseContext): string | null {
return ((ctx.headers["cookie"] || "").match(/igi=(\w+)/) || [null, null])[1];
}
function compareOrigin(ctx: Koa.BaseContext): boolean {
function normalizeUrl(url?: string): string {
return url ? (url.endsWith("/") ? url.slice(0, url.length - 1) : url) : "";
}
const referer = ctx.headers["referer"];
return normalizeUrl(referer) === normalizeUrl(config.url);
}
// Init router
const router = new Router();
router.get("/disconnect/discord", async (ctx) => {
if (!compareOrigin(ctx)) {
ctx.throw(400, "invalid origin");
return;
}
const userToken = getUserToken(ctx);
if (!userToken) {
ctx.throw(400, "signin required");
return;
}
const user = await Users.findOneByOrFail({
host: IsNull(),
token: userToken,
});
const profile = await UserProfiles.findOneByOrFail({ userId: user.id });
profile.integrations.discord = undefined;
await UserProfiles.update(user.id, {
integrations: profile.integrations,
});
ctx.body = "Discordの連携を解除しました :v:";
// Publish i updated event
publishMainStream(
user.id,
"meUpdated",
await Users.pack(user, user, {
detail: true,
includeSecrets: true,
}),
);
});
async function getOAuth2() {
const meta = await fetchMeta(true);
if (meta.enableDiscordIntegration) {
return new OAuth2(
meta.discordClientId!,
meta.discordClientSecret!,
"https://discord.com/",
"api/oauth2/authorize",
"api/oauth2/token",
);
} else {
return null;
}
}
router.get("/connect/discord", async (ctx) => {
if (!compareOrigin(ctx)) {
ctx.throw(400, "invalid origin");
return;
}
const userToken = getUserToken(ctx);
if (!userToken) {
ctx.throw(400, "signin required");
return;
}
const params = {
redirect_uri: `${config.url}/api/dc/cb`,
scope: ["identify"],
state: uuid(),
response_type: "code",
};
redisClient.set(userToken, JSON.stringify(params));
const oauth2 = await getOAuth2();
ctx.redirect(oauth2!.getAuthorizeUrl(params));
});
router.get("/signin/discord", async (ctx) => {
const sessid = uuid();
const params = {
redirect_uri: `${config.url}/api/dc/cb`,
scope: ["identify"],
state: uuid(),
response_type: "code",
};
ctx.cookies.set("signin_with_discord_sid", sessid, {
path: "/",
secure: config.url.startsWith("https"),
httpOnly: true,
});
redisClient.set(sessid, JSON.stringify(params));
const oauth2 = await getOAuth2();
ctx.redirect(oauth2!.getAuthorizeUrl(params));
});
router.get("/dc/cb", async (ctx) => {
const userToken = getUserToken(ctx);
const oauth2 = await getOAuth2();
if (!userToken) {
const sessid = ctx.cookies.get("signin_with_discord_sid");
if (!sessid) {
ctx.throw(400, "invalid session");
return;
}
const code = ctx.query.code;
if (!code || typeof code !== "string") {
ctx.throw(400, "invalid session");
return;
}
const { redirect_uri, state } = await new Promise<any>((res, rej) => {
redisClient.get(sessid, async (_, state) => {
res(JSON.parse(state));
});
});
if (ctx.query.state !== state) {
ctx.throw(400, "invalid session");
return;
}
const { accessToken, refreshToken, expiresDate } = await new Promise<any>(
(res, rej) =>
oauth2!.getOAuthAccessToken(
code,
{
grant_type: "authorization_code",
redirect_uri,
},
(err, accessToken, refreshToken, result) => {
if (err) {
rej(err);
} else if (result.error) {
rej(result.error);
} else {
res({
accessToken,
refreshToken,
expiresDate: Date.now() + Number(result.expires_in) * 1000,
});
}
},
),
);
const { id, username, discriminator } = (await getJson(
"https://discord.com/api/users/@me",
"*/*",
10 * 1000,
{
Authorization: `Bearer ${accessToken}`,
},
)) as Record<string, unknown>;
if (
typeof id !== "string" ||
typeof username !== "string" ||
typeof discriminator !== "string"
) {
ctx.throw(400, "invalid session");
return;
}
const profile = await UserProfiles.createQueryBuilder()
.where("\"integrations\"->'discord'->>'id' = :id", { id: id })
.andWhere('"userHost" IS NULL')
.getOne();
if (profile == null) {
ctx.throw(
404,
`@${username}#${discriminator}と連携しているMisskeyアカウントはありませんでした...`,
);
return;
}
await UserProfiles.update(profile.userId, {
integrations: {
...profile.integrations,
discord: {
id: id,
accessToken: accessToken,
refreshToken: refreshToken,
expiresDate: expiresDate,
username: username,
discriminator: discriminator,
},
},
});
signin(
ctx,
(await Users.findOneBy({ id: profile.userId })) as ILocalUser,
true,
);
} else {
const code = ctx.query.code;
if (!code || typeof code !== "string") {
ctx.throw(400, "invalid session");
return;
}
const { redirect_uri, state } = await new Promise<any>((res, rej) => {
redisClient.get(userToken, async (_, state) => {
res(JSON.parse(state));
});
});
if (ctx.query.state !== state) {
ctx.throw(400, "invalid session");
return;
}
const { accessToken, refreshToken, expiresDate } = await new Promise<any>(
(res, rej) =>
oauth2!.getOAuthAccessToken(
code,
{
grant_type: "authorization_code",
redirect_uri,
},
(err, accessToken, refreshToken, result) => {
if (err) {
rej(err);
} else if (result.error) {
rej(result.error);
} else {
res({
accessToken,
refreshToken,
expiresDate: Date.now() + Number(result.expires_in) * 1000,
});
}
},
),
);
const { id, username, discriminator } = (await getJson(
"https://discord.com/api/users/@me",
"*/*",
10 * 1000,
{
Authorization: `Bearer ${accessToken}`,
},
)) as Record<string, unknown>;
if (
typeof id !== "string" ||
typeof username !== "string" ||
typeof discriminator !== "string"
) {
ctx.throw(400, "invalid session");
return;
}
const user = await Users.findOneByOrFail({
host: IsNull(),
token: userToken,
});
const profile = await UserProfiles.findOneByOrFail({ userId: user.id });
await UserProfiles.update(user.id, {
integrations: {
...profile.integrations,
discord: {
accessToken: accessToken,
refreshToken: refreshToken,
expiresDate: expiresDate,
id: id,
username: username,
discriminator: discriminator,
},
},
});
ctx.body = `Discord: @${username}#${discriminator} を、Misskey: @${user.username} に接続しました!`;
// Publish i updated event
publishMainStream(
user.id,
"meUpdated",
await Users.pack(user, user, {
detail: true,
includeSecrets: true,
}),
);
}
});
export default router;

View file

@ -1,296 +0,0 @@
import type Koa from "koa";
import Router from "@koa/router";
import { OAuth2 } from "oauth";
import { v4 as uuid } from "uuid";
import { IsNull } from "typeorm";
import { getJson } from "@/misc/fetch.js";
import config from "@/config/index.js";
import { publishMainStream } from "@/services/stream.js";
import { fetchMeta } from "@/misc/fetch-meta.js";
import { Users, UserProfiles } from "@/models/index.js";
import type { ILocalUser } from "@/models/entities/user.js";
import { redisClient } from "../../../db/redis.js";
import signin from "../common/signin.js";
function getUserToken(ctx: Koa.BaseContext): string | null {
return ((ctx.headers["cookie"] || "").match(/igi=(\w+)/) || [null, null])[1];
}
function compareOrigin(ctx: Koa.BaseContext): boolean {
function normalizeUrl(url?: string): string {
return url ? (url.endsWith("/") ? url.slice(0, url.length - 1) : url) : "";
}
const referer = ctx.headers["referer"];
return normalizeUrl(referer) === normalizeUrl(config.url);
}
// Init router
const router = new Router();
router.get("/disconnect/github", async (ctx) => {
if (!compareOrigin(ctx)) {
ctx.throw(400, "invalid origin");
return;
}
const userToken = getUserToken(ctx);
if (!userToken) {
ctx.throw(400, "signin required");
return;
}
const user = await Users.findOneByOrFail({
host: IsNull(),
token: userToken,
});
const profile = await UserProfiles.findOneByOrFail({ userId: user.id });
profile.integrations.github = undefined;
await UserProfiles.update(user.id, {
integrations: profile.integrations,
});
ctx.body = "GitHubの連携を解除しました :v:";
// Publish i updated event
publishMainStream(
user.id,
"meUpdated",
await Users.pack(user, user, {
detail: true,
includeSecrets: true,
}),
);
});
async function getOath2() {
const meta = await fetchMeta(true);
if (
meta.enableGithubIntegration &&
meta.githubClientId &&
meta.githubClientSecret
) {
return new OAuth2(
meta.githubClientId,
meta.githubClientSecret,
"https://github.com/",
"login/oauth/authorize",
"login/oauth/access_token",
);
} else {
return null;
}
}
router.get("/connect/github", async (ctx) => {
if (!compareOrigin(ctx)) {
ctx.throw(400, "invalid origin");
return;
}
const userToken = getUserToken(ctx);
if (!userToken) {
ctx.throw(400, "signin required");
return;
}
const params = {
redirect_uri: `${config.url}/api/gh/cb`,
scope: ["read:user"],
state: uuid(),
};
redisClient.set(userToken, JSON.stringify(params));
const oauth2 = await getOath2();
ctx.redirect(oauth2!.getAuthorizeUrl(params));
});
router.get("/signin/github", async (ctx) => {
const sessid = uuid();
const params = {
redirect_uri: `${config.url}/api/gh/cb`,
scope: ["read:user"],
state: uuid(),
};
ctx.cookies.set("signin_with_github_sid", sessid, {
path: "/",
secure: config.url.startsWith("https"),
httpOnly: true,
});
redisClient.set(sessid, JSON.stringify(params));
const oauth2 = await getOath2();
ctx.redirect(oauth2!.getAuthorizeUrl(params));
});
router.get("/gh/cb", async (ctx) => {
const userToken = getUserToken(ctx);
const oauth2 = await getOath2();
if (!userToken) {
const sessid = ctx.cookies.get("signin_with_github_sid");
if (!sessid) {
ctx.throw(400, "invalid session");
return;
}
const code = ctx.query.code;
if (!code || typeof code !== "string") {
ctx.throw(400, "invalid session");
return;
}
const { redirect_uri, state } = await new Promise<any>((res, rej) => {
redisClient.get(sessid, async (_, state) => {
res(JSON.parse(state));
});
});
if (ctx.query.state !== state) {
ctx.throw(400, "invalid session");
return;
}
const { accessToken } = await new Promise<any>((res, rej) =>
oauth2!.getOAuthAccessToken(
code,
{
redirect_uri,
},
(err, accessToken, refresh, result) => {
if (err) {
rej(err);
} else if (result.error) {
rej(result.error);
} else {
res({ accessToken });
}
},
),
);
const { login, id } = (await getJson(
"https://api.github.com/user",
"application/vnd.github.v3+json",
10 * 1000,
{
Authorization: `bearer ${accessToken}`,
},
)) as Record<string, unknown>;
if (typeof login !== "string" || typeof id !== "string") {
ctx.throw(400, "invalid session");
return;
}
const link = await UserProfiles.createQueryBuilder()
.where("\"integrations\"->'github'->>'id' = :id", { id: id })
.andWhere('"userHost" IS NULL')
.getOne();
if (link == null) {
ctx.throw(
404,
`@${login}と連携しているMisskeyアカウントはありませんでした...`,
);
return;
}
signin(
ctx,
(await Users.findOneBy({ id: link.userId })) as ILocalUser,
true,
);
} else {
const code = ctx.query.code;
if (!code || typeof code !== "string") {
ctx.throw(400, "invalid session");
return;
}
const { redirect_uri, state } = await new Promise<any>((res, rej) => {
redisClient.get(userToken, async (_, state) => {
res(JSON.parse(state));
});
});
if (ctx.query.state !== state) {
ctx.throw(400, "invalid session");
return;
}
const { accessToken } = await new Promise<any>((res, rej) =>
oauth2!.getOAuthAccessToken(
code,
{ redirect_uri },
(err, accessToken, refresh, result) => {
if (err) {
rej(err);
} else if (result.error) {
rej(result.error);
} else {
res({ accessToken });
}
},
),
);
const { login, id } = (await getJson(
"https://api.github.com/user",
"application/vnd.github.v3+json",
10 * 1000,
{
Authorization: `bearer ${accessToken}`,
},
)) as Record<string, unknown>;
if (typeof login !== "string" || typeof id !== "string") {
ctx.throw(400, "invalid session");
return;
}
const user = await Users.findOneByOrFail({
host: IsNull(),
token: userToken,
});
const profile = await UserProfiles.findOneByOrFail({ userId: user.id });
await UserProfiles.update(user.id, {
integrations: {
...profile.integrations,
github: {
accessToken: accessToken,
id: id,
login: login,
},
},
});
ctx.body = `GitHub: @${login} を、Misskey: @${user.username} に接続しました!`;
// Publish i updated event
publishMainStream(
user.id,
"meUpdated",
await Users.pack(user, user, {
detail: true,
includeSecrets: true,
}),
);
}
});
export default router;

View file

@ -1,226 +0,0 @@
import type Koa from "koa";
import Router from "@koa/router";
import { v4 as uuid } from "uuid";
import autwh from "autwh";
import { IsNull } from "typeorm";
import { publishMainStream } from "@/services/stream.js";
import config from "@/config/index.js";
import { fetchMeta } from "@/misc/fetch-meta.js";
import { Users, UserProfiles } from "@/models/index.js";
import type { ILocalUser } from "@/models/entities/user.js";
import signin from "../common/signin.js";
import { redisClient } from "../../../db/redis.js";
function getUserToken(ctx: Koa.BaseContext): string | null {
return ((ctx.headers["cookie"] || "").match(/igi=(\w+)/) || [null, null])[1];
}
function compareOrigin(ctx: Koa.BaseContext): boolean {
function normalizeUrl(url?: string): string {
return url == null
? ""
: url.endsWith("/")
? url.substr(0, url.length - 1)
: url;
}
const referer = ctx.headers["referer"];
return normalizeUrl(referer) === normalizeUrl(config.url);
}
// Init router
const router = new Router();
router.get("/disconnect/twitter", async (ctx) => {
if (!compareOrigin(ctx)) {
ctx.throw(400, "invalid origin");
return;
}
const userToken = getUserToken(ctx);
if (userToken == null) {
ctx.throw(400, "signin required");
return;
}
const user = await Users.findOneByOrFail({
host: IsNull(),
token: userToken,
});
const profile = await UserProfiles.findOneByOrFail({ userId: user.id });
profile.integrations.twitter = undefined;
await UserProfiles.update(user.id, {
integrations: profile.integrations,
});
ctx.body = "Twitterの連携を解除しました :v:";
// Publish i updated event
publishMainStream(
user.id,
"meUpdated",
await Users.pack(user, user, {
detail: true,
includeSecrets: true,
}),
);
});
async function getTwAuth() {
const meta = await fetchMeta(true);
if (
meta.enableTwitterIntegration &&
meta.twitterConsumerKey &&
meta.twitterConsumerSecret
) {
return autwh({
consumerKey: meta.twitterConsumerKey,
consumerSecret: meta.twitterConsumerSecret,
callbackUrl: `${config.url}/api/tw/cb`,
});
} else {
return null;
}
}
router.get("/connect/twitter", async (ctx) => {
if (!compareOrigin(ctx)) {
ctx.throw(400, "invalid origin");
return;
}
const userToken = getUserToken(ctx);
if (userToken == null) {
ctx.throw(400, "signin required");
return;
}
const twAuth = await getTwAuth();
const twCtx = await twAuth!.begin();
redisClient.set(userToken, JSON.stringify(twCtx));
ctx.redirect(twCtx.url);
});
router.get("/signin/twitter", async (ctx) => {
const twAuth = await getTwAuth();
const twCtx = await twAuth!.begin();
const sessid = uuid();
redisClient.set(sessid, JSON.stringify(twCtx));
ctx.cookies.set("signin_with_twitter_sid", sessid, {
path: "/",
secure: config.url.startsWith("https"),
httpOnly: true,
});
ctx.redirect(twCtx.url);
});
router.get("/tw/cb", async (ctx) => {
const userToken = getUserToken(ctx);
const twAuth = await getTwAuth();
if (userToken == null) {
const sessid = ctx.cookies.get("signin_with_twitter_sid");
if (sessid == null) {
ctx.throw(400, "invalid session");
return;
}
const get = new Promise<any>((res, rej) => {
redisClient.get(sessid, async (_, twCtx) => {
res(twCtx);
});
});
const twCtx = await get;
const verifier = ctx.query.oauth_verifier;
if (!verifier || typeof verifier !== "string") {
ctx.throw(400, "invalid session");
return;
}
const result = await twAuth!.done(JSON.parse(twCtx), verifier);
const link = await UserProfiles.createQueryBuilder()
.where("\"integrations\"->'twitter'->>'userId' = :id", {
id: result.userId,
})
.andWhere('"userHost" IS NULL')
.getOne();
if (link == null) {
ctx.throw(
404,
`@${result.screenName}と連携しているMisskeyアカウントはありませんでした...`,
);
return;
}
signin(
ctx,
(await Users.findOneBy({ id: link.userId })) as ILocalUser,
true,
);
} else {
const verifier = ctx.query.oauth_verifier;
if (!verifier || typeof verifier !== "string") {
ctx.throw(400, "invalid session");
return;
}
const get = new Promise<any>((res, rej) => {
redisClient.get(userToken, async (_, twCtx) => {
res(twCtx);
});
});
const twCtx = await get;
const result = await twAuth!.done(JSON.parse(twCtx), verifier);
const user = await Users.findOneByOrFail({
host: IsNull(),
token: userToken,
});
const profile = await UserProfiles.findOneByOrFail({ userId: user.id });
await UserProfiles.update(user.id, {
integrations: {
...profile.integrations,
twitter: {
accessToken: result.accessToken,
accessTokenSecret: result.accessTokenSecret,
userId: result.userId,
screenName: result.screenName,
},
},
});
ctx.body = `Twitter: @${result.screenName} を、Misskey: @${user.username} に接続しました!`;
// Publish i updated event
publishMainStream(
user.id,
"meUpdated",
await Users.pack(user, user, {
detail: true,
includeSecrets: true,
}),
);
}
});
export default router;

View file

@ -68,7 +68,7 @@ export default class extends Channel {
// そのためレコードが存在するかのチェックでは不十分なので、改めてgetWordHardMuteを呼んでいる
if (
this.userProfile &&
(await getWordHardMute(note, this.user, this.userProfile.mutedWords))
(await getWordHardMute(note, this.user?.id, this.userProfile.mutedWords))
)
return;

View file

@ -67,7 +67,7 @@ export default class extends Channel {
// そのためレコードが存在するかのチェックでは不十分なので、改めてgetWordHardMuteを呼んでいる
if (
this.userProfile &&
(await getWordHardMute(note, this.user, this.userProfile.mutedWords))
(await getWordHardMute(note, this.user?.id, this.userProfile.mutedWords))
)
return;

View file

@ -84,7 +84,7 @@ export default class extends Channel {
// そのためレコードが存在するかのチェックでは不十分なので、改めてgetWordHardMuteを呼んでいる
if (
this.userProfile &&
(await getWordHardMute(note, this.user, this.userProfile.mutedWords))
(await getWordHardMute(note, this.user?.id, this.userProfile.mutedWords))
)
return;

View file

@ -60,7 +60,7 @@ export default class extends Channel {
// そのためレコードが存在するかのチェックでは不十分なので、改めてgetWordHardMuteを呼んでいる
if (
this.userProfile &&
(await getWordHardMute(note, this.user, this.userProfile.mutedWords))
(await getWordHardMute(note, this.user?.id, this.userProfile.mutedWords))
)
return;

View file

@ -82,7 +82,7 @@ export default class extends Channel {
// そのためレコードが存在するかのチェックでは不十分なので、改めてgetWordHardMuteを呼んでいる
if (
this.userProfile &&
(await getWordHardMute(note, this.user, this.userProfile.mutedWords))
(await getWordHardMute(note, this.user?.id, this.userProfile.mutedWords))
)
return;

View file

@ -1,6 +1,6 @@
import * as mfm from "mfm-js";
import es from "../../db/elasticsearch.js";
import sonic from "../../db/sonic.js";
import es from "@/db/elasticsearch.js";
import sonic from "@/db/sonic.js";
import {
publishMainStream,
publishNotesStream,
@ -380,8 +380,7 @@ export default async (
)
.then((us) => {
for (const u of us) {
getWordHardMute(data, { id: u.userId }, u.mutedWords).then(
(shouldMute) => {
getWordHardMute(data, u.userId, u.mutedWords).then((shouldMute) => {
if (shouldMute) {
MutedNotes.insert({
id: genId(),
@ -390,8 +389,7 @@ export default async (
reason: "word",
});
}
},
);
});
}
});

View file

@ -9,8 +9,6 @@ import {
} from "@/models/index.js";
import { Not, IsNull, In } from "typeorm";
import type { Channel } from "@/models/entities/channel.js";
import { checkHitAntenna } from "@/misc/check-hit-antenna.js";
import { getAntennas } from "@/misc/antenna-cache.js";
import { readNotificationByQuery } from "@/server/api/common/read-notification.js";
import type { Packed } from "@/misc/schema.js";
@ -66,23 +64,6 @@ export default async function (
if (note.channelId && followingChannels.has(note.channelId)) {
readChannelNotes.push(note);
}
// if (note.user != null) {
// // たぶんnullになることは無いはずだけど一応
// for (const antenna of myAntennas) {
// if (
// await checkHitAntenna(
// antenna,
// note,
// note.user,
// undefined,
// Array.from(following),
// )
// ) {
// readAntennaNotes.push(note);
// }
// }
// }
}
if (

View file

@ -31,6 +31,7 @@
"@types/uuid": "9.0.3",
"@vitejs/plugin-vue": "4.3.4",
"@vue/compiler-sfc": "3.3.4",
"@vue/runtime-core": "3.3.4",
"autobind-decorator": "2.4.0",
"autosize": "6.0.1",
"blurhash": "2.0.5",

View file

@ -3,8 +3,9 @@ import type * as firefish from "firefish-js";
import { i18n } from "./i18n";
import { del, get, set } from "@/scripts/idb-proxy";
import { apiUrl } from "@/config";
import { alert, api, popup, popupMenu, success, waiting } from "@/os";
import { alert, api, popup, popupMenu, waiting } from "@/os";
import { reloadChannel, unisonReload } from "@/scripts/unison-reload";
import icon from "@/scripts/icon";
// TODO: 他のタブと永続化されたstateを同期
@ -249,7 +250,7 @@ export async function openAccountMenu(
...accountItemPromises,
{
type: "parent",
icon: "ph-plus ph-bold ph-lg",
icon: `${icon("ph-plus")}`,
text: i18n.ts.addAccount,
children: [
{
@ -268,13 +269,13 @@ export async function openAccountMenu(
},
{
type: "link",
icon: "ph-users ph-bold ph-lg",
icon: `${icon("ph-users")}`,
text: i18n.ts.manageAccounts,
to: "/settings/accounts",
},
{
type: "button",
icon: "ph-sign-out ph-bold ph-lg",
icon: `${icon("ph-sign-out")}`,
text: i18n.ts.logout,
action: () => {
signout();

View file

@ -8,7 +8,7 @@
>
<template #header>
<i
class="ph-warning-circle ph-bold ph-lg"
:class="icon('ph-warning-circle')"
style="margin-right: 0.5em"
></i>
<I18n :src="i18n.ts.reportAbuseOf" tag="span">
@ -47,6 +47,7 @@ import MkTextarea from "@/components/form/textarea.vue";
import MkButton from "@/components/MkButton.vue";
import * as os from "@/os";
import { i18n } from "@/i18n";
import icon from "@/scripts/icon";
const props = defineProps<{
user: firefish.entities.User;

View file

@ -8,16 +8,16 @@
<template v-if="!wait">
<template v-if="isFollowing">
<span v-if="full">{{ i18n.ts.unfollow }}</span
><i class="ph-minus ph-bold ph-lg"></i>
><i :class="icon('ph-minus')"></i>
</template>
<template v-else>
<span v-if="full">{{ i18n.ts.follow }}</span
><i class="ph-plus ph-bold ph-lg"></i>
><i :class="icon('ph-plus')"></i>
</template>
</template>
<template v-else>
<span v-if="full">{{ i18n.ts.processing }}</span
><i class="ph-circle-notch ph-bold ph-lg fa-pulse ph-fw ph-lg"></i>
><i :class="icon('ph-circle-notch fa-pulse ph-fw')"></i>
</template>
</button>
</template>
@ -26,6 +26,7 @@
import { ref } from "vue";
import * as os from "@/os";
import { i18n } from "@/i18n";
import icon from "@/scripts/icon";
const props = withDefaults(
defineProps<{

View file

@ -3,11 +3,12 @@
<div class="banner" :style="bannerStyle">
<div class="fade"></div>
<div class="name">
<i class="ph-television ph-bold ph-lg"></i> {{ channel.name }}
<i :class="icon('ph-television')"></i>
{{ channel.name }}
</div>
<div class="status">
<div>
<i class="ph-users ph-bold ph-lg ph-fw ph-lg"></i>
<i :class="icon('ph-users ph-fw')"></i>
<I18n
:src="i18n.ts._channel.usersCount"
tag="span"
@ -19,7 +20,7 @@
</I18n>
</div>
<div>
<i class="ph-pencil ph-bold ph-lg ph-fw ph-lg"></i>
<i :class="icon('ph-pencil ph-fw')"></i>
<I18n
:src="i18n.ts._channel.notesCount"
tag="span"
@ -52,6 +53,7 @@
<script lang="ts" setup>
import { computed } from "vue";
import { i18n } from "@/i18n";
import icon from "@/scripts/icon";
const props = defineProps<{
channel: Record<string, any>;

View file

@ -20,16 +20,16 @@
@click="() => (showBody = !showBody)"
>
<template v-if="showBody"
><i class="ph-caret-up ph-bold ph-lg"></i
><i :class="icon('ph-caret-up')"></i
></template>
<template v-else
><i class="ph-caret-down ph-bold ph-lg"></i
><i :class="icon('ph-caret-down')"></i
></template>
</button>
</div>
</header>
<transition
:name="$store.state.animation ? 'container-toggle' : ''"
:name="defaultStore.state.animation ? 'container-toggle' : ''"
@enter="enter"
@after-enter="afterEnter"
@leave="leave"
@ -62,6 +62,8 @@
<script lang="ts">
import { defineComponent } from "vue";
import { i18n } from "@/i18n";
import { defaultStore } from "@/store";
import icon from "@/scripts/icon";
export default defineComponent({
props: {
@ -107,6 +109,8 @@ export default defineComponent({
omitted: null,
ignoreOmit: false,
i18n,
icon,
defaultStore,
};
},
mounted() {

View file

@ -1,5 +1,5 @@
<template>
<transition :name="$store.state.animation ? 'fade' : ''" appear>
<transition :name="defaultStore.state.animation ? 'fade' : ''" appear>
<div
ref="rootEl"
class="nvlagfpb"
@ -17,6 +17,7 @@ import MkMenu from "@/components/MkMenu.vue";
import type { MenuItem } from "@/types/menu";
import contains from "@/scripts/contains";
import * as os from "@/os";
import { defaultStore } from "@/store";
const props = defineProps<{
items: MenuItem[];

View file

@ -4,6 +4,7 @@ import { TransitionGroup, defineComponent, h } from "vue";
import MkAd from "@/components/global/MkAd.vue";
import { i18n } from "@/i18n";
import { defaultStore } from "@/store";
import icon from "@/scripts/icon";
export default defineComponent({
props: {
@ -75,14 +76,14 @@ export default defineComponent({
[
h("span", [
h("i", {
class: "ph-caret-up ph-bold ph-lg icon",
class: `${icon("ph-caret-up")} icon`,
}),
getDateText(item.createdAt),
]),
h("span", [
getDateText(props.items[i + 1].createdAt),
h("i", {
class: "ph-caret-down ph-bold ph-lg icon",
class: `${icon("ph-caret-down")} icon`,
}),
]),
],

View file

@ -16,27 +16,27 @@
<i
v-if="type === 'success'"
:class="$style.iconInner"
class="ph-check ph-bold ph-lg"
class="ph-check ph-lg"
></i>
<i
v-else-if="type === 'error'"
:class="$style.iconInner"
class="ph-circle-wavy-warning ph-bold ph-lg"
class="ph-circle-wavy-warning ph-lg"
></i>
<i
v-else-if="type === 'warning'"
:class="$style.iconInner"
class="ph-warning ph-bold ph-lg"
class="ph-warning ph-lg"
></i>
<i
v-else-if="type === 'info'"
:class="$style.iconInner"
class="ph-info ph-bold ph-lg"
class="ph-info ph-lg"
></i>
<i
v-else-if="type === 'question'"
:class="$style.iconInner"
class="ph-circle-question ph-bold ph-lg"
class="ph-circle-question ph-lg"
></i>
<MkLoading
v-else-if="type === 'waiting'"
@ -75,7 +75,7 @@
@keydown="onInputKeydown"
>
<template v-if="input.type === 'password'" #prefix
><i class="ph-password ph-bold ph-lg"></i
><i :class="iconClass('ph-password')"></i
></template>
<template #caption>
<span
@ -109,7 +109,7 @@
class="_buttonIcon"
@click.stop="openSearchFilters"
>
<i class="ph-funnel ph-bold"></i>
<i :class="iconClass('ph-funnel', false)"></i>
</button>
</template>
</MkInput>
@ -213,6 +213,7 @@ import MkTextarea from "@/components/form/textarea.vue";
import MkSelect from "@/components/form/select.vue";
import * as os from "@/os";
import { i18n } from "@/i18n";
import iconClass from "@/scripts/icon";
interface Input {
type: HTMLInputElement["type"];
@ -364,7 +365,7 @@ async function openSearchFilters(ev) {
await os.popupMenu(
[
{
icon: "ph-user ph-bold ph-lg",
icon: `${icon("ph-user")}`,
text: i18n.ts._filters.fromUser,
action: () => {
os.selectUser().then((user) => {
@ -375,32 +376,32 @@ async function openSearchFilters(ev) {
{
type: "parent",
text: i18n.ts._filters.withFile,
icon: "ph-paperclip ph-bold ph-lg",
icon: `${icon("ph-paperclip")}`,
children: [
{
text: i18n.ts.image,
icon: "ph-image-square ph-bold ph-lg",
icon: `${icon("ph-image-square")}`,
action: () => {
inputValue.value += " has:image";
},
},
{
text: i18n.ts.video,
icon: "ph-video-camera ph-bold ph-lg",
icon: `${icon("ph-video-camera")}`,
action: () => {
inputValue.value += " has:video";
},
},
{
text: i18n.ts.audio,
icon: "ph-music-note ph-bold ph-lg",
icon: `${icon("ph-music-note")}`,
action: () => {
inputValue.value += " has:audio";
},
},
{
text: i18n.ts.file,
icon: "ph-file ph-bold ph-lg",
icon: `${icon("ph-file")}`,
action: () => {
inputValue.value += " has:file";
},
@ -408,14 +409,14 @@ async function openSearchFilters(ev) {
],
},
{
icon: "ph-link ph-bold ph-lg",
icon: `${icon("ph-link")}`,
text: i18n.ts._filters.fromDomain,
action: () => {
inputValue.value += " domain:";
},
},
{
icon: "ph-calendar-blank ph-bold ph-lg",
icon: `${icon("ph-calendar-blank")}`,
text: i18n.ts._filters.notesBefore,
action: () => {
os.inputDate({
@ -428,7 +429,7 @@ async function openSearchFilters(ev) {
},
},
{
icon: "ph-calendar-blank ph-bold ph-lg",
icon: `${icon("ph-calendar-blank")}`,
text: i18n.ts._filters.notesAfter,
action: () => {
os.inputDate({
@ -441,14 +442,14 @@ async function openSearchFilters(ev) {
},
},
{
icon: "ph-eye ph-bold ph-lg",
icon: `${icon("ph-eye")}`,
text: i18n.ts._filters.followingOnly,
action: () => {
inputValue.value += " filter:following ";
},
},
{
icon: "ph-users-three ph-bold ph-lg",
icon: `${icon("ph-users-three")}`,
text: i18n.ts._filters.followersOnly,
action: () => {
inputValue.value += " filter:followers ";

View file

@ -2,7 +2,7 @@
<transition name="slide-fade">
<div v-if="show" class="_panel _shadow _acrylic" :class="$style.root">
<div :class="$style.icon">
<i class="ph-hand-heart ph-bold ph-5x" />
<i :class="icon('ph-hand-heart ph-5x', false)" />
</div>
<div :class="$style.main">
<div :class="$style.title">
@ -52,7 +52,7 @@
:aria-label="i18n.t('close')"
@click="close"
>
<i class="ph-x ph-bold ph-lg"></i>
<i :class="icon('ph-x')"></i>
</button>
</div>
</transition>
@ -65,6 +65,7 @@ import { host } from "@/config";
import { i18n } from "@/i18n";
import * as os from "@/os";
import { instance } from "@/instance";
import icon from "@/scripts/icon";
const show = ref(false);

View file

@ -46,6 +46,7 @@ import bytes from "@/filters/bytes";
import * as os from "@/os";
import { i18n } from "@/i18n";
import { $i } from "@/account";
import icon from "@/scripts/icon";
const props = withDefaults(
defineProps<{
@ -75,7 +76,7 @@ function getMenu() {
return [
{
text: i18n.ts.rename,
icon: "ph-cursor-text ph-bold ph-lg",
icon: `${icon("ph-cursor-text")}`,
action: rename,
},
{
@ -83,19 +84,19 @@ function getMenu() {
? i18n.ts.unmarkAsSensitive
: i18n.ts.markAsSensitive,
icon: props.file.isSensitive
? "ph-eye ph-bold ph-lg"
: "ph-eye-slash ph-bold ph-lg",
? "ph-eye ph-lg"
: "ph-eye-slash ph-lg",
action: toggleSensitive,
},
{
text: i18n.ts.describeFile,
icon: "ph-subtitles ph-bold ph-lg",
icon: `${icon("ph-subtitles")}`,
action: describe,
},
null,
{
text: i18n.ts.copyUrl,
icon: "ph-link-simple ph-bold ph-lg",
icon: `${icon("ph-link-simple")}`,
action: copyUrl,
},
{
@ -103,13 +104,13 @@ function getMenu() {
href: props.file.url,
target: "_blank",
text: i18n.ts.download,
icon: "ph-download-simple ph-bold ph-lg",
icon: `${icon("ph-download-simple")}`,
download: props.file.name,
},
null,
{
text: i18n.ts.delete,
icon: "ph-trash ph-bold ph-lg",
icon: `${icon("ph-trash")}`,
danger: true,
action: deleteFile,
},

View file

@ -17,10 +17,10 @@
>
<p class="name">
<template v-if="hover"
><i class="ph-folder-notch-open ph-bold ph-lg ph-fw ph-lg"></i
><i :class="icon('ph-folder-notch-open ph-fw')"></i
></template>
<template v-if="!hover"
><i class="ph-folder-notch ph-bold ph-lg ph-fw ph-lg"></i
><i :class="icon('ph-folder-notch ph-fw')"></i
></template>
{{ folder.name }}
</p>
@ -42,6 +42,7 @@ import type * as firefish from "firefish-js";
import * as os from "@/os";
import { i18n } from "@/i18n";
import { defaultStore } from "@/store";
import icon from "@/scripts/icon";
const props = withDefaults(
defineProps<{
@ -248,7 +249,7 @@ function onContextmenu(ev: MouseEvent) {
[
{
text: i18n.ts.openInWindow,
icon: "ph-copy ph-bold ph-lg",
icon: `${icon("ph-copy")}`,
action: () => {
os.popup(
defineAsyncComponent(
@ -265,13 +266,13 @@ function onContextmenu(ev: MouseEvent) {
null,
{
text: i18n.ts.rename,
icon: "ph-cursor-text ph-bold ph-lg",
icon: `${icon("ph-cursor-text")}`,
action: rename,
},
null,
{
text: i18n.ts.delete,
icon: "ph-trash ph-bold ph-lg",
icon: `${icon("ph-trash")}`,
danger: true,
action: deleteFolder,
},

View file

@ -8,7 +8,7 @@
@dragleave="onDragleave"
@drop.stop="onDrop"
>
<i v-if="folder == null" class="ph-cloud ph-bold ph-lg"></i>
<i v-if="folder == null" :class="icon('ph-cloud')"></i>
<span>{{ folder == null ? i18n.ts.drive : folder.name }}</span>
</div>
</template>
@ -18,6 +18,7 @@ import { ref } from "vue";
import type * as firefish from "firefish-js";
import * as os from "@/os";
import { i18n } from "@/i18n";
import icon from "@/scripts/icon";
const props = defineProps<{
folder?: firefish.entities.DriveFolder;

View file

@ -12,7 +12,7 @@
/>
<template v-for="f in hierarchyFolders">
<span class="separator"
><i class="ph-caret-right ph-bold ph-lg"></i
><i :class="icon('ph-caret-right')"></i
></span>
<XNavFolder
:folder="f"
@ -24,14 +24,14 @@
/>
</template>
<span v-if="folder != null" class="separator"
><i class="ph-caret-right ph-bold ph-lg"></i
><i :class="icon('ph-caret-right')"></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 ph-bold ph-lg"></i>
<i :class="icon('ph-dots-three-outline')"></i>
</button>
</nav>
<div
@ -149,6 +149,7 @@ import { stream } from "@/stream";
import { defaultStore } from "@/store";
import { i18n } from "@/i18n";
import { uploadFile, uploads } from "@/scripts/upload";
import icon from "@/scripts/icon";
const props = withDefaults(
defineProps<{
@ -677,14 +678,14 @@ function getMenu() {
},
{
text: i18n.ts.upload,
icon: "ph-upload-simple ph-bold ph-lg",
icon: `${icon("ph-upload-simple")}`,
action: () => {
selectLocalFile();
},
},
{
text: i18n.ts.fromUrl,
icon: "ph-link-simple ph-bold ph-lg",
icon: `${icon("ph-link-simple")}`,
action: () => {
urlUpload();
},
@ -697,7 +698,7 @@ function getMenu() {
folder.value
? {
text: i18n.ts.renameFolder,
icon: "ph-cursor-text ph-bold ph-lg",
icon: `${icon("ph-cursor-text")}`,
action: () => {
renameFolder(folder.value);
},
@ -706,7 +707,7 @@ function getMenu() {
folder.value
? {
text: i18n.ts.deleteFolder,
icon: "ph-trash ph-bold ph-lg",
icon: `${icon("ph-trash")}`,
action: () => {
deleteFolder(
folder.value as firefish.entities.DriveFolder,
@ -716,7 +717,7 @@ function getMenu() {
: undefined,
{
text: i18n.ts.createFolder,
icon: "ph-folder-notch-plus ph-bold ph-lg",
icon: `${icon("ph-folder-notch-plus")}`,
action: () => {
createFolder();
},

View file

@ -8,33 +8,21 @@
:title="file.name"
:cover="fit !== 'contain'"
/>
<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 === 'image'" :class="icon('ph-file-image icon')"></i>
<i v-else-if="is === 'video'" :class="icon('ph-file-video icon')"></i>
<i
v-else-if="is === 'audio' || is === 'midi'"
class="ph-file-audio ph-bold ph-lg icon"
:class="icon('ph-file-audio 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-else-if="is === 'csv'" :class="icon('ph-file-csv icon')"></i>
<i v-else-if="is === 'pdf'" :class="icon('ph-file-pdf icon')"></i>
<i v-else-if="is === 'textfile'" :class="icon('ph-file-text icon')"></i>
<i v-else-if="is === 'archive'" :class="icon('ph-file-zip icon')"></i>
<i v-else :class="icon('ph-file icon')"></i>
<i
v-if="isThumbnailAvailable && is === 'video'"
class="ph-file-video ph-bold ph-lg icon-sub"
:class="icon('ph-file-video icon-sub')"
></i>
</button>
</template>
@ -43,6 +31,7 @@
import { computed } from "vue";
import type * as firefish from "firefish-js";
import ImgWithBlurhash from "@/components/MkImgWithBlurhash.vue";
import icon from "@/scripts/icon";
const props = defineProps<{
file: firefish.entities.DriveFile;

View file

@ -4,9 +4,7 @@
<i
class="toggle ph-fw ph-lg"
:class="
shown
? 'ph-caret-down ph-bold ph-lg'
: 'ph-caret-up ph-bold ph-lg'
icon(shown ? 'ph-caret-down ph-lg' : 'ph-caret-up ph-lg')
"
></i>
<slot></slot> ({{ emojis.length }})
@ -21,7 +19,7 @@
"
>
<i
class="ph-circle ph-fill ph-fw ph-lg"
class="ph-circle ph-fill ph-fw"
:style="{ color: skinTone + ' !important' }"
:aria-label="
props.skinToneLabels
@ -50,6 +48,7 @@
<script lang="ts" setup>
import { onMounted, ref, watch } from "vue";
import { addSkinTone } from "@/scripts/emojilist";
import icon from "@/scripts/icon";
const props = defineProps<{
emojis: string[];

View file

@ -75,7 +75,7 @@
<section>
<header class="_acrylic">
<i class="ph-alarm ph-bold ph-fw ph-lg"></i>
<i :class="icon('ph-alarm ph-fw')"></i>
{{ i18n.ts.recentUsed }}
</header>
<div class="body">
@ -135,28 +135,28 @@
:class="{ active: tab === 'index' }"
@click="tab = 'index'"
>
<i class="ph-asterisk ph-bold ph-lg ph-fw ph-lg"></i>
<i :class="icon('ph-asterisk ph-fw')"></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>
<i :class="icon('ph-smiley ph-fw')"></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>
<i :class="icon('ph-leaf ph-fw')"></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>
<i :class="icon('ph-hash ph-fw')"></i>
</button>
</div>
</div>
@ -182,6 +182,7 @@ import { deviceKind } from "@/scripts/device-kind";
import { emojiCategories, instance } from "@/instance";
import { i18n } from "@/i18n";
import { defaultStore } from "@/store";
import icon from "@/scripts/icon";
const props = withDefaults(
defineProps<{

View file

@ -1,13 +1,14 @@
<template>
<span class="mk-file-type-icon">
<template v-if="kind == 'image'"
><i class="ph-file-image ph-bold ph-lg"></i
><i :class="icon('ph-file-image')"></i
></template>
</span>
</template>
<script lang="ts" setup>
import { computed } from "vue";
import icon from "@/scripts/icon";
const props = defineProps<{
type: string;

View file

@ -10,15 +10,15 @@
:aria-controls="bodyId"
>
<template v-if="showBody"
><i class="ph-caret-up ph-bold ph-lg"></i
><i :class="icon('ph-caret-up')"></i
></template>
<template v-else
><i class="ph-caret-down ph-bold ph-lg"></i
><i :class="icon('ph-caret-down')"></i
></template>
</button>
</header>
<transition
:name="$store.state.animation ? 'folder-toggle' : ''"
:name="defaultStore.state.animation ? 'folder-toggle' : ''"
@enter="enter"
@after-enter="afterEnter"
@leave="leave"
@ -34,6 +34,8 @@
<script lang="ts">
import { defineComponent } from "vue";
import { getUniqueId } from "@/os";
import { defaultStore } from "@/store";
import icon from "@/scripts/icon";
const localStoragePrefix = "ui:folder:";

View file

@ -5,7 +5,7 @@
class="menu _button"
@click.stop="menu"
>
<i class="ph-dots-three-outline ph-bold ph-lg"></i>
<i :class="icon('ph-dots-three-outline')"></i>
</button>
<button
v-if="$i != null && $i.id != user.id"
@ -25,37 +25,37 @@
<template v-if="!wait">
<template v-if="isBlocking">
<span>{{ (state = i18n.ts.blocked) }}</span
><i class="ph-prohibit ph-bold ph-lg"></i>
><i :class="icon('ph-prohibit')"></i>
</template>
<template
v-else-if="hasPendingFollowRequestFromYou && user.isLocked"
>
<span>{{ (state = i18n.ts.followRequestPending) }}</span
><i class="ph-hourglass-medium ph-bold ph-lg"></i>
><i :class="icon('ph-hourglass-medium')"></i>
</template>
<template
v-else-if="hasPendingFollowRequestFromYou && !user.isLocked"
>
<!-- つまりリモートフォローの場合 -->
<span>{{ (state = i18n.ts.processing) }}</span
><i class="ph-circle-notch ph-bold ph-lg fa-pulse"></i>
><i :class="icon('ph-circle-notch fa-pulse')"></i>
</template>
<template v-else-if="isFollowing">
<span>{{ (state = i18n.ts.unfollow) }}</span
><i class="ph-minus ph-bold ph-lg"></i>
><i :class="icon('ph-minus')"></i>
</template>
<template v-else-if="!isFollowing && user.isLocked">
<span>{{ (state = i18n.ts.followRequest) }}</span
><i class="ph-lock-open ph-bold ph-lg"></i>
><i :class="icon('ph-lock-open')"></i>
</template>
<template v-else-if="!isFollowing && !user.isLocked">
<span>{{ (state = i18n.ts.follow) }}</span
><i class="ph-plus ph-bold ph-lg"></i>
><i :class="icon('ph-plus')"></i>
</template>
</template>
<template v-else>
<span>{{ (state = i18n.ts.processing) }}</span
><i class="ph-circle-notch ph-bold ph-lg fa-pulse ph-fw ph-lg"></i>
><i :class="icon('ph-circle-notch fa-pulse ph-fw')"></i>
</template>
</button>
</template>
@ -70,6 +70,7 @@ import { $i } from "@/account";
import { getUserMenu } from "@/scripts/get-user-menu";
import { useRouter } from "@/router";
import { vibrate } from "@/scripts/vibrate";
import icon from "@/scripts/icon";
const router = useRouter();

View file

@ -2,7 +2,7 @@
<div class="mk-google" @click.stop>
<input v-model="query" type="search" :placeholder="q" />
<button @click="search">
<i class="ph-magnifying-glass ph-bold ph-lg"></i>
<i :class="icon('ph-magnifying-glass')"></i>
{{ i18n.ts.searchByGoogle }}
</button>
</div>
@ -12,6 +12,7 @@
import { ref } from "vue";
import { i18n } from "@/i18n";
import { useRouter } from "@/router";
import icon from "@/scripts/icon";
const router = useRouter();

View file

@ -1,11 +1,7 @@
<template>
<div v-if="visible" class="info" :class="{ warn, card }">
<i v-if="warn" class="ph-warning ph-bold ph-lg"></i>
<i
v-else
class="ph-bold ph-lg"
:class="icon ? `ph-${icon}` : 'ph-info'"
></i>
<i v-if="warn" :class="iconClass('ph-warning')"></i>
<i v-else :class="iconClass(icon ? `ph-${icon}` : 'ph-info')"></i>
<slot></slot>
<button
v-if="closeable"
@ -14,7 +10,7 @@
:aria-label="i18n.t('close')"
@click.stop="close"
>
<i class="ph-x ph-bold ph-lg"></i>
<i :class="iconClass('ph-x')"></i>
</button>
</div>
</template>
@ -22,6 +18,7 @@
<script lang="ts" setup>
import { ref } from "vue";
import { i18n } from "@/i18n";
import iconClass from "@/scripts/icon";
const visible = ref(true);

View file

@ -12,17 +12,17 @@
style="margin-left: 0.5em"
@click="copy_"
>
<i class="ph-clipboard-text ph-bold"></i>
<i :class="icon('ph-clipboard-text', false)"></i>
</button>
</div>
</div>
</template>
<script lang="ts" setup>
import {} from "vue";
import copyToClipboard from "@/scripts/copy-to-clipboard";
import * as os from "@/os";
import { i18n } from "@/i18n";
import icon from "@/scripts/icon";
const props = withDefaults(
defineProps<{

View file

@ -33,7 +33,7 @@
v-if="item.indicate"
class="indicator"
:class="{
animateIndicator: $store.state.animation,
animateIndicator: defaultStore.state.animation,
}"
><i class="ph-circle ph-fill"></i
></span>
@ -50,7 +50,7 @@
v-if="item.indicate"
class="indicator"
:class="{
animateIndicator: $store.state.animation,
animateIndicator: defaultStore.state.animation,
}"
><i class="ph-circle ph-fill"></i
></span>

View file

@ -3,7 +3,7 @@
:is="self ? 'MkA' : 'a'"
ref="el"
class="xlcxczvw _link"
:[attr]="self ? url.substr(local.length) : url"
:[attr]="self ? url.substring(local.length) : url"
:rel="rel"
:target="target"
:title="url"
@ -12,7 +12,7 @@
<slot></slot>
<i
v-if="target === '_blank'"
class="ph-arrow-square-out ph-bold ph-lg icon"
:class="icon('ph-arrow-square-out icon')"
></i>
</component>
</template>
@ -22,6 +22,7 @@ import { defineAsyncComponent, ref } from "vue";
import { url as local } from "@/config";
import { useTooltip } from "@/scripts/use-tooltip";
import * as os from "@/os";
import icon from "@/scripts/icon";
const props = withDefaults(
defineProps<{

View file

@ -9,7 +9,7 @@
<div class="text">
<div class="wrapper">
<b style="display: block"
><i class="ph-warning ph-bold ph-lg"></i>
><i :class="icon('ph-warning')"></i>
{{ i18n.ts.sensitive }}</b
>
<span style="display: block">{{
@ -74,7 +74,7 @@
class="_button"
@click.stop="captionPopup"
>
<i class="ph-subtitles ph-bold ph-lg"></i>
<i :class="icon('ph-subtitles')"></i>
</button>
<button
v-if="!hide"
@ -82,7 +82,7 @@
class="_button"
@click.stop="hide = true"
>
<i class="ph-eye-slash ph-bold ph-lg"></i>
<i :class="icon('ph-eye-slash')"></i>
</button>
</div>
</div>
@ -98,6 +98,7 @@ import ImgWithBlurhash from "@/components/MkImgWithBlurhash.vue";
import { defaultStore } from "@/store";
import { i18n } from "@/i18n";
import * as os from "@/os";
import icon from "@/scripts/icon";
const props = defineProps<{
media: firefish.entities.DriveFile;

View file

@ -5,7 +5,7 @@
class="sensitive"
@click="hide = false"
>
<span class="icon"><i class="ph-warning ph-bold ph-lg"></i></span>
<span class="icon"><i :class="icon('ph-warning')"></i></span>
<b>{{ i18n.ts.sensitive }}</b>
<span>{{ i18n.ts.clickToShow }}</span>
</div>
@ -48,7 +48,7 @@
:download="media.name"
>
<span class="icon"
><i class="ph-download-simple ph-bold ph-lg"></i
><i :class="icon('ph-download-simple')"></i
></span>
<b>{{ media.name }}</b>
</a>
@ -62,6 +62,7 @@ import type * as firefish from "firefish-js";
import { ColdDeviceStorage } from "@/store";
import "vue-plyr/dist/vue-plyr.css";
import { i18n } from "@/i18n";
import icon from "@/scripts/icon";
const props = withDefaults(
defineProps<{

View file

@ -11,7 +11,7 @@
<span class="main">
<span class="username">@{{ username }}</span>
<span
v-if="host != localHost || $store.state.showFullAcct"
v-if="host != localHost || defaultStore.state.showFullAcct"
class="host"
>@{{ toUnicode(host) }}</span
>
@ -38,6 +38,7 @@ import { toUnicode } from "punycode";
import {} from "vue";
import { host as localHost } from "@/config";
import { $i } from "@/account";
import { defaultStore } from "@/store";
const props = defineProps<{
username: string;

View file

@ -57,7 +57,7 @@
v-if="item.indicate"
class="indicator"
:class="{
animateIndicator: $store.state.animation,
animateIndicator: defaultStore.state.animation,
}"
><i class="ph-circle ph-fill"></i
></span>
@ -74,8 +74,7 @@
>
<i
v-if="item.icon"
class="ph-fw ph-lg"
:class="item.icon"
:class="icon(`${item.icon} ph-fw`)"
></i>
<span :style="item.textStyle || ''">{{
item.text
@ -84,7 +83,7 @@
v-if="item.indicate"
class="indicator"
:class="{
animateIndicator: $store.state.animation,
animateIndicator: defaultStore.state.animation,
}"
><i class="ph-circle ph-fill"></i
></span>
@ -107,7 +106,7 @@
v-if="item.indicate"
class="indicator"
:class="{
animateIndicator: $store.state.animation,
animateIndicator: defaultStore.state.animation,
}"
><i class="ph-circle ph-fill"></i
></span>
@ -135,16 +134,13 @@
>
<i
v-if="item.icon"
class="ph-fw ph-lg"
:class="item.icon"
:class="icon(`${item.icon} ph-fw`)"
></i>
<span :style="item.textStyle || ''">{{
item.text
}}</span>
<span class="caret"
><i
class="ph-caret-right ph-bold ph-lg ph-fw ph-lg"
></i
><i :class="icon('ph-caret-right ph-fw')"></i
></span>
</button>
<button
@ -162,8 +158,7 @@
>
<i
v-if="item.icon"
class="ph-fw ph-lg"
:class="item.icon"
:class="icon(`${item.icon} ph-fw`)"
></i>
<MkAvatar
v-if="item.avatar"
@ -178,7 +173,7 @@
v-if="item.indicate"
class="indicator"
:class="{
animateIndicator: $store.state.animation,
animateIndicator: defaultStore.state.animation,
}"
><i class="ph-circle ph-fill"></i
></span>
@ -190,6 +185,7 @@
</div>
<div v-if="childMenu" class="child">
<XChild
v-if="childTarget && itemsEl"
ref="child"
:items="childMenu"
:target-element="childTarget"
@ -221,6 +217,8 @@ import type {
} from "@/types/menu";
import * as os from "@/os";
import { i18n } from "@/i18n";
import { defaultStore } from "@/store";
import icon from "@/scripts/icon";
const XChild = defineAsyncComponent(() => import("./MkMenu.child.vue"));
const focusTrap = ref();

View file

@ -5,10 +5,7 @@
</div>
<div class="mod-player-disabled" v-else-if="hide" @click="toggleVisible()">
<div>
<b
><i class="ph-warning ph-bold ph-lg"></i>
{{ i18n.ts.sensitive }}</b
>
<b><i class="ph-warning"></i> {{ i18n.ts.sensitive }}</b>
<span>{{ i18n.ts.clickToShow }}</span>
</div>
</div>
@ -40,16 +37,16 @@
</div>
<div class="controls">
<button class="play" @click="playPause()" v-if="!loading">
<i class="ph-pause ph-fill ph-lg" v-if="playing"></i>
<i class="ph-play ph-fill ph-lg" v-else></i>
<i class="ph-pause ph-fill" v-if="playing"></i>
<i class="ph-play ph-fill" v-else></i>
</button>
<MkLoading v-else :em="true" />
<button class="stop" @click="stop()">
<i class="ph-stop ph-fill ph-lg"></i>
<i class="ph-stop ph-fill"></i>
</button>
<button class="loop" @click="toggleLoop()">
<i class="ph-repeat ph-fill ph-lg" v-if="loop === -1"></i>
<i class="ph-repeat-once ph-fill ph-lg" v-else></i>
<i class="ph-repeat ph-fill" v-if="loop === -1"></i>
<i class="ph-repeat-once ph-fill" v-else></i>
</button>
<FormRange
class="progress"
@ -64,8 +61,8 @@
@update:modelValue="performSeek()"
></FormRange>
<button class="mute" @click="toggleMute()">
<i class="ph-speaker-simple-x ph-fill ph-lg" v-if="muted"></i>
<i class="ph-speaker-simple-high ph-fill ph-lg" v-else></i>
<i class="ph-speaker-simple-x ph-fill" v-if="muted"></i>
<i class="ph-speaker-simple-high ph-fill" v-else></i>
</button>
<FormRange
class="volume"
@ -84,7 +81,7 @@
:href="module.url"
target="_blank"
>
<i class="ph-download-simple ph-fill ph-lg"></i>
<i class="ph-download-simple ph-fill"></i>
</a>
</div>
<div class="buttons">
@ -94,7 +91,7 @@
class="_button"
@click.stop="captionPopup"
>
<i class="ph-subtitles ph-bold ph-lg"></i>
<i class="ph-subtitles"></i>
</button>
<button
v-if="!hide"
@ -102,7 +99,7 @@
class="_button"
@click.stop="toggleVisible()"
>
<i class="ph-eye-slash ph-bold ph-lg"></i>
<i class="ph-eye-slash"></i>
</button>
</div>
</div>
@ -116,6 +113,7 @@ import { i18n } from "@/i18n";
import * as os from "@/os";
import { defaultStore } from "@/store";
import { ChiptuneJsPlayer, ChiptuneJsConfig } from "@/scripts/chiptune2";
import icon from "@/scripts/icon";
const props = defineProps<{
module: firefish.entities.DriveFile;

View file

@ -15,14 +15,14 @@
class="_button"
@click="back()"
>
<i class="ph-caret-left ph-bold ph-lg"></i>
<i :class="icon('ph-caret-left')"></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"
:class="icon(pageMetadata?.value.icon)"
></i>
<span>{{ pageMetadata?.value.title }}</span>
</span>
@ -31,7 +31,7 @@
:aria-label="i18n.t('close')"
@click="$refs.modal.close()"
>
<i class="ph-x ph-bold ph-lg"></i>
<i :class="icon('ph-x')"></i>
</button>
</div>
<div class="body">
@ -64,6 +64,8 @@ import { i18n } from "@/i18n";
import type { PageMetadata } from "@/scripts/page-metadata";
import { provideMetadataReceiver } from "@/scripts/page-metadata";
import { Router } from "@/nirax";
import { defaultStore } from "@/store";
import icon from "@/scripts/icon";
const props = defineProps<{
initialPath: string;
@ -101,18 +103,18 @@ const contextmenu = computed(() => {
text: path.value,
},
{
icon: "ph-arrows-out-simple ph-bold ph-lg",
icon: `${icon("ph-arrows-out-simple")}`,
text: i18n.ts.showInPage,
action: expand,
},
{
icon: "ph-arrow-square-out ph-bold ph-lg",
icon: `${icon("ph-arrow-square-out")}`,
text: i18n.ts.popout,
action: popout,
},
null,
{
icon: "ph-arrow-square-out ph-bold ph-lg",
icon: `${icon("ph-arrow-square-out")}`,
text: i18n.ts.openInNewTab,
action: () => {
window.open(pageUrl.value, "_blank");
@ -120,7 +122,7 @@ const contextmenu = computed(() => {
},
},
{
icon: "ph-link-simple ph-bold ph-lg",
icon: `${icon("ph-link-simple")}`,
text: i18n.ts.copyLink,
action: () => {
copyToClipboard(pageUrl.value);

View file

@ -30,7 +30,7 @@
class="_button"
@click="$emit('close')"
>
<i class="ph-x ph-bold ph-lg"></i>
<i :class="icon('ph-x')"></i>
</button>
<span class="title">
<slot name="header"></slot>
@ -41,7 +41,7 @@
class="_button"
@click="$emit('close')"
>
<i class="ph-x ph-bold ph-lg"></i>
<i :class="icon('ph-x')"></i>
</button>
<button
v-if="props.withOkButton"
@ -50,7 +50,7 @@
:disabled="props.okButtonDisabled"
@click="$emit('ok')"
>
<i class="ph-check ph-bold ph-lg"></i>
<i :class="icon('ph-check')"></i>
</button>
</div>
<div class="body">
@ -67,6 +67,7 @@ import { shallowRef } from "vue";
import { FocusTrap } from "focus-trap-vue";
import MkModal from "./MkModal.vue";
import { i18n } from "@/i18n";
import icon from "@/scripts/icon";
const props = withDefaults(
defineProps<{

View file

@ -1,9 +1,6 @@
<template>
<div class="msjugskd _block">
<i
class="ph-airplane-takeoff ph-bold ph-lg"
style="margin-right: 8px"
/>
<i :class="icon('ph-airplane-takeoff')" style="margin-right: 8px" />
{{ i18n.ts.accountMoved }}
<MkMention class="link" :username="acct" :host="host" />
</div>
@ -12,6 +9,7 @@
<script lang="ts" setup>
import MkMention from "./MkMention.vue";
import { i18n } from "@/i18n";
import icon from "@/scripts/icon";
defineProps<{
acct: string;

View file

@ -27,23 +27,22 @@
>
<div class="line"></div>
<div v-if="appearNote._prId_" class="info">
<i class="ph-megaphone-simple-bold ph-lg"></i>
<i :class="icon('ph-megaphone-simple-bold')"></i>
{{ i18n.ts.promotion
}}<button class="_textButton hide" @click.stop="readPromo()">
{{ i18n.ts.hideThisNote }}
<i class="ph-x ph-bold ph-lg"></i>
<i :class="icon('ph-x')"></i>
</button>
</div>
<div v-if="appearNote._featuredId_" class="info">
<i class="ph-lightning ph-bold ph-lg"></i>
<i :class="icon('ph-lightning')"></i>
{{ i18n.ts.featured }}
</div>
<div v-if="pinned" class="info">
<i class="ph-push-pin ph-bold ph-lg"></i
>{{ i18n.ts.pinnedNote }}
<i :class="icon('ph-push-pin')"></i>{{ i18n.ts.pinnedNote }}
</div>
<div v-if="isRenote" class="renote">
<i class="ph-rocket-launch ph-bold ph-lg"></i>
<i :class="icon('ph-rocket-launch')"></i>
<I18n :src="i18n.ts.renotedBy" tag="span">
<template #user>
<MkA
@ -64,7 +63,7 @@
>
<i
v-if="isMyRenote"
class="ph-dots-three-outline ph-bold ph-lg dropdownIcon"
:class="icon('ph-dots-three-outline dropdownIcon')"
></i>
<MkTime :time="note.createdAt" />
</button>
@ -149,7 +148,7 @@
class="channel"
:to="`/channels/${appearNote.channel.id}`"
@click.stop
><i class="ph-television ph-bold"></i>
><i :class="icon('ph-television', false)"></i>
{{ appearNote.channel.name }}</MkA
>
</div>
@ -164,7 +163,7 @@
class="button _button"
@click.stop="reply()"
>
<i class="ph-arrow-u-up-left ph-bold ph-lg"></i>
<i :class="icon('ph-arrow-u-up-left')"></i>
<template
v-if="appearNote.repliesCount > 0 && !detailedView"
>
@ -209,7 +208,7 @@
class="button _button"
@click.stop="react()"
>
<i class="ph-smiley ph-bold ph-lg"></i>
<i :class="icon('ph-smiley')"></i>
</button>
<button
v-if="
@ -221,7 +220,7 @@
class="button _button reacted"
@click.stop="undoReact(appearNote)"
>
<i class="ph-minus ph-bold ph-lg"></i>
<i :class="icon('ph-minus')"></i>
</button>
<XQuoteButton class="button" :note="appearNote" />
<button
@ -234,7 +233,7 @@
class="button _button"
@click.stop="translate"
>
<i class="ph-translate ph-bold ph-lg"></i>
<i :class="icon('ph-translate')"></i>
</button>
<button
ref="menuButton"
@ -242,7 +241,7 @@
class="button _button"
@click.stop="menu()"
>
<i class="ph-dots-three-outline ph-bold ph-lg"></i>
<i :class="icon('ph-dots-three-outline')"></i>
</button>
</footer>
</div>
@ -273,7 +272,6 @@
<script lang="ts" setup>
import { computed, inject, onMounted, ref } from "vue";
import * as mfm from "mfm-js";
import type { Ref } from "vue";
import type * as firefish from "firefish-js";
import MkSubNoteContent from "./MkSubNoteContent.vue";
@ -303,6 +301,7 @@ import { useNoteCapture } from "@/scripts/use-note-capture";
import { notePage } from "@/filters/note";
import { deepClone } from "@/scripts/clone";
import { getNoteSummary } from "@/scripts/get-note-summary";
import icon from "@/scripts/icon";
const router = useRouter();
@ -360,7 +359,7 @@ const isDeleted = ref(false);
const muted = ref(
getWordSoftMute(
note.value,
$i,
$i.id,
defaultStore.state.mutedWords,
defaultStore.state.mutedLangs,
),
@ -495,7 +494,7 @@ function onContextmenu(ev: MouseEvent): void {
text: notePage(appearNote.value),
},
{
icon: "ph-browser ph-bold ph-lg",
icon: `${icon("ph-browser")}`,
text: i18n.ts.openInWindow,
action: () => {
os.pageWindow(notePage(appearNote.value));
@ -503,7 +502,7 @@ function onContextmenu(ev: MouseEvent): void {
},
notePage(appearNote.value) != location.pathname
? {
icon: "ph-arrows-out-simple ph-bold ph-lg",
icon: `${icon("ph-arrows-out-simple")}`,
text: i18n.ts.showInPage,
action: () => {
router.push(
@ -516,13 +515,13 @@ function onContextmenu(ev: MouseEvent): void {
null,
{
type: "a",
icon: "ph-arrow-square-out ph-bold ph-lg",
icon: `${icon("ph-arrow-square-out")}`,
text: i18n.ts.openInNewTab,
href: notePage(appearNote.value),
target: "_blank",
},
{
icon: "ph-link-simple ph-bold ph-lg",
icon: `${icon("ph-link-simple")}`,
text: i18n.ts.copyLink,
action: () => {
copyToClipboard(`${url}${notePage(appearNote.value)}`);
@ -531,7 +530,7 @@ function onContextmenu(ev: MouseEvent): void {
appearNote.value.user.host != null
? {
type: "a",
icon: "ph-arrow-square-up-right ph-bold ph-lg",
icon: `${icon("ph-arrow-square-up-right")}`,
text: i18n.ts.showOnRemote,
href:
appearNote.value.url ??
@ -569,7 +568,7 @@ function showRenoteMenu(viaKeyboard = false): void {
[
{
text: i18n.ts.unrenote,
icon: "ph-trash ph-bold ph-lg",
icon: `${icon("ph-trash")}`,
danger: true,
action: () => {
os.api("notes/delete", {

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