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 - [ ] 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` 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 --> <!-- Uncomment if your merge request has multiple authors -->
<!-- Co-authored-by: Name <email@email.com> --> <!-- Co-authored-by: Name <email@email.com> -->

View file

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

View file

@ -4,6 +4,24 @@ Breaking changes are indicated by the :warning: icon.
## v1.0.5 (unreleased) ## 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 ### dev17
- Added `lang` parameter to `notes/create` and `notes/edit`. - Added `lang` parameter to `notes/create` and `notes/edit`.

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -341,9 +341,6 @@ dayX: "{day}"
monthX: "{month}" monthX: "{month}"
yearX: "{year}" yearX: "{year}"
pages: "Pages" pages: "Pages"
integration: "Intégrations"
connectService: "Connexion"
disconnectService: "Déconnexion"
enableLocalTimeline: "Activer le fil local" enableLocalTimeline: "Activer le fil local"
enableGlobalTimeline: "Activer le fil global" enableGlobalTimeline: "Activer le fil global"
disablingTimelinesInfo: "Même si vous désactivez ces fils, les administrateur·rice·s 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" smtpSecure: "Utiliser SSL/TLS implicitement dans les connexions SMTP"
smtpSecureInfo: "Désactiver cette option lorsque STARTTLS est utilisé" smtpSecureInfo: "Désactiver cette option lorsque STARTTLS est utilisé"
testEmail: "Tester la distribution de courriel" testEmail: "Tester la distribution de courriel"
wordMute: "Filtre de mots" wordMute: "Filtre de mots et langages"
regexpError: "Erreur dexpression régulière" regexpError: "Erreur dexpression régulière"
instanceMute: "Serveur masqué" instanceMute: "Serveur masqué"
userSaysSomething: "{name} a dit quelque chose" userSaysSomething: "{name} a dit quelque chose"
@ -960,7 +957,8 @@ _accountDelete:
inProgress: "Suppression en cours" inProgress: "Suppression en cours"
_ad: _ad:
back: "Retour" back: "Retour"
reduceFrequencyOfThisAd: "Voir cette publicité moins souvent" reduceFrequencyOfThisAd: "Voir cette bannière moins souvent"
adsBy: Bannière communautaire par {by}
_forgotPassword: _forgotPassword:
enterEmail: "Entrez ici l'adresse e-mail que vous avez enregistrée pour votre compte. 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 Un lien vous permettant de réinitialiser votre mot de passe sera envoyé à cette
@ -1145,6 +1143,13 @@ _wordMute:
soft: "Doux" soft: "Doux"
hard: "Strict" hard: "Strict"
mutedNotes: "Publications masquées" 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: _instanceMute:
instanceMuteDescription2: "Séparer avec des sauts de lignes" instanceMuteDescription2: "Séparer avec des sauts de lignes"
title: "Masque les publications provenant des serveurs listés." 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 serveur dune publication
indexable: Indexable indexable: Indexable
languageForTranslation: Langage post-traduction 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}" monthX: "{month}"
yearX: "{year}" yearX: "{year}"
pages: "Halaman" pages: "Halaman"
integration: "Integrasi"
connectService: "Sambungkan"
disconnectService: "Putuskan"
enableLocalTimeline: "Nyalakan linimasa lokal" enableLocalTimeline: "Nyalakan linimasa lokal"
enableGlobalTimeline: "Nyalakan linimasa global" enableGlobalTimeline: "Nyalakan linimasa global"
disablingTimelinesInfo: "Admin dan Moderator akan selalu memiliki akses ke semua linimasa disablingTimelinesInfo: "Admin dan Moderator akan selalu memiliki akses ke semua linimasa
@ -1269,8 +1266,8 @@ _tutorial:
{introduction} atau \"Halo dunia!\" yang sederhana." {introduction} atau \"Halo dunia!\" yang sederhana."
step5_1: "Linimasa, linimasa di mana-mana!" step5_1: "Linimasa, linimasa di mana-mana!"
step5_2: "Servermu memiliki {timelines} lini masa berbeda yang diaktifkan." step5_2: "Servermu memiliki {timelines} lini masa berbeda yang diaktifkan."
step5_3: "Lini masa Beranda {icon} adalah tempat di mana kamu bisa melihat postingan step5_3: "Lini masa Beranda {icon} adalah tempat kamu bisa melihat postingan dari
dari akun yang kamu ikuti." akun yang kamu ikuti."
step5_4: "Linimasa Lokal {icon} adalah tempat kamu dapat melihat postingan dari step5_4: "Linimasa Lokal {icon} adalah tempat kamu dapat melihat postingan dari
siapa pun di server ini." siapa pun di server ini."
step6_1: "Jadi, tempat apa ini?" step6_1: "Jadi, tempat apa ini?"

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -331,9 +331,6 @@ dayX: "{day}"
monthX: "{month}" monthX: "{month}"
yearX: "{year}" yearX: "{year}"
pages: "Strony" pages: "Strony"
integration: "Integracje"
connectService: "Połącz"
disconnectService: "Rozłącz"
enableLocalTimeline: "Włącz lokalną oś czasu" enableLocalTimeline: "Włącz lokalną oś czasu"
enableGlobalTimeline: "Włącz globalną oś czasu" enableGlobalTimeline: "Włącz globalną oś czasu"
disablingTimelinesInfo: "Administratorzy i moderatorzy będą zawsze mieć dostęp do 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}" monthX: "mês de {month}"
yearX: "Ano {year}" yearX: "Ano {year}"
pages: "Páginas" pages: "Páginas"
integration: "Integração"
connectService: "Conectar"
disconnectService: "Desconectar"
enableLocalTimeline: "Ativar linha do tempo local" enableLocalTimeline: "Ativar linha do tempo local"
enableGlobalTimeline: "Ativar linha do tempo global" enableGlobalTimeline: "Ativar linha do tempo global"
disablingTimelinesInfo: "Se você desabilitar essas linhas do tempo, administradores disablingTimelinesInfo: "Se você desabilitar essas linhas do tempo, administradores

View file

@ -316,9 +316,6 @@ dayX: "{day}"
monthX: "{month}" monthX: "{month}"
yearX: "{year}" yearX: "{year}"
pages: "Pagini" pages: "Pagini"
integration: "Integrare"
connectService: "Conectează"
disconnectService: "Deconectează"
enableLocalTimeline: "Activează cronologia locală" enableLocalTimeline: "Activează cronologia locală"
enableGlobalTimeline: "Activeaza cronologia globală" enableGlobalTimeline: "Activeaza cronologia globală"
disablingTimelinesInfo: "Administratorii și Moderatorii vor avea mereu access la toate cronologiile, chiar dacă nu sunt activate." 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} месяц" monthX: "{month} месяц"
yearX: "{year} год" yearX: "{year} год"
pages: "Страницы" pages: "Страницы"
integration: "Интеграции"
connectService: "Подключиться"
disconnectService: "Отключиться"
enableLocalTimeline: "Включить локальную ленту" enableLocalTimeline: "Включить локальную ленту"
enableGlobalTimeline: "Включить глобальную ленту" enableGlobalTimeline: "Включить глобальную ленту"
disablingTimelinesInfo: "У администраторов и модераторов есть доступ ко всем лентам, disablingTimelinesInfo: "У администраторов и модераторов есть доступ ко всем лентам,

View file

@ -317,9 +317,6 @@ dayX: "{day}"
monthX: "{month}" monthX: "{month}"
yearX: "{year}" yearX: "{year}"
pages: "Stránky" pages: "Stránky"
integration: "Integrácia"
connectService: "Pripojiť"
disconnectService: "Odpojiť"
enableLocalTimeline: "Povoliť lokálnu časovú os" enableLocalTimeline: "Povoliť lokálnu časovú os"
enableGlobalTimeline: "Povoliť globá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é." 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}' dayX: '{day}'
monthX: '{month}' monthX: '{month}'
yearX: '{year}' yearX: '{year}'
connectService: Anslut
disconnectService: Bortkoppla
enableLocalTimeline: Anslut till lokal tidslinje enableLocalTimeline: Anslut till lokal tidslinje
invite: Bjud in invite: Bjud in
driveCapacityPerLocalAccount: Enhetens kapacitet per lokal användare driveCapacityPerLocalAccount: Enhetens kapacitet per lokal användare
@ -779,7 +777,6 @@ serverLogs: Serverloggar
deleteAll: Radera alla deleteAll: Radera alla
removeAllFollowing: Sluta följa alla följda användare removeAllFollowing: Sluta följa alla följda användare
medium: Mellan medium: Mellan
integration: Integreringar
xl: XL xl: XL
desktop: Skrivbord desktop: Skrivbord
createNew: Skapa nya createNew: Skapa nya

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,76 +1,29 @@
import type { Antenna } from "@/models/entities/antenna.js"; import type { Antenna } from "@/models/entities/antenna.js";
import type { Note } from "@/models/entities/note.js"; import type { Note } from "@/models/entities/note.js";
import type { User } from "@/models/entities/user.js"; import type { User } from "@/models/entities/user.js";
import { import { Blockings, UserProfiles } from "@/models/index.js";
UserListJoinings,
UserGroupJoinings,
Blockings,
} from "@/models/index.js";
import { getFullApAccount } from "./convert-host.js"; import { getFullApAccount } from "./convert-host.js";
import * as Acct from "@/misc/acct.js"; import * as Acct from "@/misc/acct.js";
import type { Packed } from "./schema.js"; import type { Packed } from "./schema.js";
import { Cache } from "./cache.js"; import { Cache } from "./cache.js";
import { getWordHardMute } from "./check-word-mute.js";
const blockingCache = new Cache<User["id"][]>("blocking", 60 * 5); 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( export async function checkHitAntenna(
antenna: Antenna, antenna: Antenna,
note: Note | Packed<"Note">, note: Note | Packed<"Note">,
noteUser: { id: User["id"]; username: string; host: string | null }, noteUser: { id: User["id"]; username: string; host: string | null },
noteUserFollowers?: User["id"][],
antennaUserFollowing?: User["id"][],
): Promise<boolean> { ): Promise<boolean> {
if (note.visibility === "specified") return false; if (note.visibility === "specified") return false;
if (note.visibility === "home") return false; if (note.visibility === "home") return false;
if (!antenna.withReplies && note.replyId != null) return false;
// アンテナ作成者がノート作成者にブロックされていたらスキップ if (antenna.withFile) {
const blockings = await blockingCache.fetch(noteUser.id, () => if (note.fileIds && note.fileIds.length === 0) return false;
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.src === "users") {
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") {
const accts = antenna.users.map((x) => { const accts = antenna.users.map((x) => {
const { username, host } = Acct.parse(x); const { username, host } = Acct.parse(x);
return getFullApAccount(username, host).toLowerCase(); return getFullApAccount(username, host).toLowerCase();
@ -128,9 +81,20 @@ export async function checkHitAntenna(
if (matched) return false; 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 // TODO: eval expression

View file

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

View file

@ -354,57 +354,6 @@ export class Meta {
}) })
public swPrivateKey: string | null; 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", { @Column("varchar", {
length: 128, length: 128,
nullable: true, nullable: true,

View file

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

View file

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

View file

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

View file

@ -95,11 +95,25 @@ export default async (job: Bull.Job<InboxJobData>): Promise<string> => {
} }
// HTTP-Signatureの検証 // HTTP-Signatureの検証
const httpSignatureValidated = httpSignature.verifySignature( let httpSignatureValidated = httpSignature.verifySignature(
signature, signature,
authUser.key.keyPem, 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と一致する必要がある // また、signatureのsignerは、activity.actorと一致する必要がある
if (!httpSignatureValidated || authUser.user.uri !== activity.actor) { if (!httpSignatureValidated || authUser.user.uri !== activity.actor) {
// 一致しなくても、でもLD-Signatureがありそうならそっちも見る // 一致しなくても、でもLD-Signatureがありそうならそっちも見る

View file

@ -86,11 +86,25 @@ export async function checkFetch(req: IncomingMessage): Promise<number> {
} }
// HTTP-Signatureの検証 // HTTP-Signatureの検証
const httpSignatureValidated = httpSignature.verifySignature( let httpSignatureValidated = httpSignature.verifySignature(
signature, signature,
authUser.key.keyPem, 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) { if (!httpSignatureValidated) {
return 403; return 403;
} }

View file

@ -17,7 +17,7 @@ import { Cache } from "@/misc/cache.js";
import { uriPersonCache, userByIdCache } from "@/services/user-cache.js"; import { uriPersonCache, userByIdCache } from "@/services/user-cache.js";
import type { IObject } from "./type.js"; import type { IObject } from "./type.js";
import { getApId } 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 publicKeyCache = new Cache<UserPublickey | null>("publicKey", 60 * 30);
const publicKeyByUserIdCache = new Cache<UserPublickey | null>( const publicKeyByUserIdCache = new Cache<UserPublickey | null>(
@ -151,7 +151,7 @@ export default class DbResolver {
*/ */
public async getAuthUserFromKeyId(keyId: string): Promise<{ public async getAuthUserFromKeyId(keyId: string): Promise<{
user: CacheableRemoteUser; user: CacheableRemoteUser;
key: UserPublickey; key: UserPublickey | null;
} | null> { } | null> {
const key = await publicKeyCache.fetch( const key = await publicKeyCache.fetch(
keyId, keyId,
@ -203,4 +203,15 @@ export default class DbResolver {
key, 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 host = toPuny(new URL(object.id).hostname);
const { fields } = analyzeAttachments(person.attachment || []); const fields = analyzeAttachments(person.attachment || []);
const tags = extractApHashtags(person.tag) const tags = extractApHashtags(person.tag)
.map((tag) => normalizeForSearch(tag)) .map((tag) => normalizeForSearch(tag))
@ -642,39 +642,6 @@ export async function resolvePerson(
return await createPerson(uri, resolver); 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( export function analyzeAttachments(
attachments: IObject | IObject[] | undefined, attachments: IObject | IObject[] | undefined,
) { ) {
@ -682,22 +649,17 @@ export function analyzeAttachments(
name: string; name: string;
value: string; value: string;
}[] = []; }[] = [];
const services: { [x: string]: any } = {};
if (Array.isArray(attachments)) { if (Array.isArray(attachments)) {
for (const attachment of attachments.filter(isPropertyValue)) { for (const attachment of attachments.filter(isPropertyValue)) {
if (isPropertyValue(attachment.identifier)) {
addService(services, attachment.identifier);
} else {
fields.push({ fields.push({
name: attachment.name, name: attachment.name,
value: fromHtml(attachment.value), value: fromHtml(attachment.value),
}); });
} }
} }
}
return { fields, services }; return fields;
} }
export async function updateFeatured(userId: User["id"], resolver?: Resolver) { export async function updateFeatured(userId: User["id"], resolver?: Resolver) {

View file

@ -170,21 +170,6 @@ export const meta = {
optional: false, optional: false,
nullable: 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: { enableServiceWorker: {
type: "boolean", type: "boolean",
optional: false, optional: false,
@ -326,36 +311,6 @@ export const meta = {
nullable: true, nullable: true,
format: "id", 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: { summaryProxy: {
type: "string", type: "string",
optional: true, optional: true,
@ -544,9 +499,6 @@ export default define(meta, paramDef, async (ps, me) => {
defaultLightTheme: instance.defaultLightTheme, defaultLightTheme: instance.defaultLightTheme,
defaultDarkTheme: instance.defaultDarkTheme, defaultDarkTheme: instance.defaultDarkTheme,
enableEmail: instance.enableEmail, enableEmail: instance.enableEmail,
enableTwitterIntegration: instance.enableTwitterIntegration,
enableGithubIntegration: instance.enableGithubIntegration,
enableDiscordIntegration: instance.enableDiscordIntegration,
enableServiceWorker: instance.enableServiceWorker, enableServiceWorker: instance.enableServiceWorker,
translatorAvailable: translatorAvailable:
instance.deeplAuthKey != null || instance.libreTranslateApiUrl != null, instance.deeplAuthKey != null || instance.libreTranslateApiUrl != null,
@ -573,12 +525,6 @@ export default define(meta, paramDef, async (ps, me) => {
enableSensitiveMediaDetectionForVideos: enableSensitiveMediaDetectionForVideos:
instance.enableSensitiveMediaDetectionForVideos, instance.enableSensitiveMediaDetectionForVideos,
proxyAccountId: instance.proxyAccountId, proxyAccountId: instance.proxyAccountId,
twitterConsumerKey: instance.twitterConsumerKey,
twitterConsumerSecret: instance.twitterConsumerSecret,
githubClientId: instance.githubClientId,
githubClientSecret: instance.githubClientSecret,
discordClientId: instance.discordClientId,
discordClientSecret: instance.discordClientSecret,
summalyProxy: instance.summalyProxy, summalyProxy: instance.summalyProxy,
email: instance.email, email: instance.email,
smtpSecure: instance.smtpSecure, 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 }); const signins = await Signins.findBy({ userId: user.id });
return { return {
@ -67,7 +60,6 @@ export default define(meta, paramDef, async (ps, me) => {
carefulBot: profile.carefulBot, carefulBot: profile.carefulBot,
injectFeaturedNote: profile.injectFeaturedNote, injectFeaturedNote: profile.injectFeaturedNote,
receiveAnnouncementEmail: profile.receiveAnnouncementEmail, receiveAnnouncementEmail: profile.receiveAnnouncementEmail,
integrations: profile.integrations,
mutedWords: profile.mutedWords, mutedWords: profile.mutedWords,
mutedInstances: profile.mutedInstances, mutedInstances: profile.mutedInstances,
mutingNotificationTypes: profile.mutingNotificationTypes, mutingNotificationTypes: profile.mutingNotificationTypes,

View file

@ -132,15 +132,6 @@ export const paramDef = {
deeplIsPro: { type: "boolean" }, deeplIsPro: { type: "boolean" },
libreTranslateApiUrl: { type: "string", nullable: true }, libreTranslateApiUrl: { type: "string", nullable: true },
libreTranslateApiKey: { 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" }, enableEmail: { type: "boolean" },
email: { type: "string", nullable: true }, email: { type: "string", nullable: true },
smtpSecure: { type: "boolean" }, smtpSecure: { type: "boolean" },
@ -395,42 +386,6 @@ export default define(meta, paramDef, async (ps, me) => {
set.summalyProxy = ps.summalyProxy; 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) { if (ps.enableEmail !== undefined) {
set.enableEmail = ps.enableEmail; set.enableEmail = ps.enableEmail;
} }

View file

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

View file

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

View file

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

View file

@ -21,9 +21,6 @@ import signup from "./private/signup.js";
import signin from "./private/signin.js"; import signin from "./private/signin.js";
import signupPending from "./private/signup-pending.js"; import signupPending from "./private/signup-pending.js";
import verifyEmail from "./private/verify-email.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 { koaBody } from "koa-body";
import { import {
convertId, convertId,
@ -181,10 +178,6 @@ router.post("/signin", signin);
router.post("/signup-pending", signupPending); router.post("/signup-pending", signupPending);
router.post("/verify-email", verifyEmail); router.post("/verify-email", verifyEmail);
router.use(discord.routes());
router.use(github.routes());
router.use(twitter.routes());
router.post("/miauth/:session/check", async (ctx) => { router.post("/miauth/:session/check", async (ctx) => {
const token = await AccessTokens.findOneBy({ const token = await AccessTokens.findOneBy({
session: ctx.params.session, 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を呼んでいる // そのためレコードが存在するかのチェックでは不十分なので、改めてgetWordHardMuteを呼んでいる
if ( if (
this.userProfile && this.userProfile &&
(await getWordHardMute(note, this.user, this.userProfile.mutedWords)) (await getWordHardMute(note, this.user?.id, this.userProfile.mutedWords))
) )
return; return;

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -9,8 +9,6 @@ import {
} from "@/models/index.js"; } from "@/models/index.js";
import { Not, IsNull, In } from "typeorm"; import { Not, IsNull, In } from "typeorm";
import type { Channel } from "@/models/entities/channel.js"; 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 { readNotificationByQuery } from "@/server/api/common/read-notification.js";
import type { Packed } from "@/misc/schema.js"; import type { Packed } from "@/misc/schema.js";
@ -66,23 +64,6 @@ export default async function (
if (note.channelId && followingChannels.has(note.channelId)) { if (note.channelId && followingChannels.has(note.channelId)) {
readChannelNotes.push(note); 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 ( if (

View file

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

View file

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

View file

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

View file

@ -8,16 +8,16 @@
<template v-if="!wait"> <template v-if="!wait">
<template v-if="isFollowing"> <template v-if="isFollowing">
<span v-if="full">{{ i18n.ts.unfollow }}</span <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>
<template v-else> <template v-else>
<span v-if="full">{{ i18n.ts.follow }}</span <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> </template>
<template v-else> <template v-else>
<span v-if="full">{{ i18n.ts.processing }}</span <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> </template>
</button> </button>
</template> </template>
@ -26,6 +26,7 @@
import { ref } from "vue"; import { ref } from "vue";
import * as os from "@/os"; import * as os from "@/os";
import { i18n } from "@/i18n"; import { i18n } from "@/i18n";
import icon from "@/scripts/icon";
const props = withDefaults( const props = withDefaults(
defineProps<{ defineProps<{

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -8,7 +8,7 @@
@dragleave="onDragleave" @dragleave="onDragleave"
@drop.stop="onDrop" @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> <span>{{ folder == null ? i18n.ts.drive : folder.name }}</span>
</div> </div>
</template> </template>
@ -18,6 +18,7 @@ import { ref } from "vue";
import type * as firefish from "firefish-js"; import type * as firefish from "firefish-js";
import * as os from "@/os"; import * as os from "@/os";
import { i18n } from "@/i18n"; import { i18n } from "@/i18n";
import icon from "@/scripts/icon";
const props = defineProps<{ const props = defineProps<{
folder?: firefish.entities.DriveFolder; folder?: firefish.entities.DriveFolder;

View file

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

View file

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

View file

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

View file

@ -75,7 +75,7 @@
<section> <section>
<header class="_acrylic"> <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 }} {{ i18n.ts.recentUsed }}
</header> </header>
<div class="body"> <div class="body">
@ -135,28 +135,28 @@
:class="{ active: tab === 'index' }" :class="{ active: tab === 'index' }"
@click="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>
<button <button
class="_button tab" class="_button tab"
:class="{ active: tab === 'custom' }" :class="{ active: tab === 'custom' }"
@click="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>
<button <button
class="_button tab" class="_button tab"
:class="{ active: tab === 'unicode' }" :class="{ active: tab === 'unicode' }"
@click="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>
<button <button
class="_button tab" class="_button tab"
:class="{ active: tab === 'tags' }" :class="{ active: tab === 'tags' }"
@click="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> </button>
</div> </div>
</div> </div>
@ -182,6 +182,7 @@ import { deviceKind } from "@/scripts/device-kind";
import { emojiCategories, instance } from "@/instance"; import { emojiCategories, instance } from "@/instance";
import { i18n } from "@/i18n"; import { i18n } from "@/i18n";
import { defaultStore } from "@/store"; import { defaultStore } from "@/store";
import icon from "@/scripts/icon";
const props = withDefaults( const props = withDefaults(
defineProps<{ defineProps<{

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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