Firefish v1.0.5-dev11

This commit is contained in:
naskya 2023-09-04 17:47:24 +09:00
parent 5f4a8fd80b
commit 3d81d35b7e
Signed by: naskya
GPG key ID: 164DFF24E2D40139
474 changed files with 14265 additions and 3522 deletions

View file

@ -143,11 +143,11 @@ reservedUsernames: [
# Whether disable HSTS # Whether disable HSTS
#disableHsts: true #disableHsts: true
# Number of worker processes # Number of worker processes by type.
#clusterLimit: 1 # The sum must not exceed the number of available cores.
#clusterLimits:
# Worker only mode # web: 1
#onlyQueueProcessor: 1 # queue: 1
# Job concurrency per worker # Job concurrency per worker
# deliverJobConcurrency: 128 # deliverJobConcurrency: 128

3
.gitignore vendored
View file

@ -32,6 +32,9 @@ coverage
# docker dev config # docker dev config
/dev/docker-compose.yml /dev/docker-compose.yml
# ESLint
.eslintcache
# misskey # misskey
built built
db db

12
biome.json Normal file
View file

@ -0,0 +1,12 @@
{
"$schema": "https://biomejs.dev/schemas/1.0.0/schema.json",
"organizeImports": {
"enabled": true
},
"linter": {
"enabled": true,
"rules": {
"recommended": true
}
}
}

View file

@ -61,6 +61,8 @@ services:
# sonic: # sonic:
# restart: unless-stopped # restart: unless-stopped
# image: docker.io/valeriansaliou/sonic:v1.4.0 # image: docker.io/valeriansaliou/sonic:v1.4.0
# logging:
# driver: none
# networks: # networks:
# - calcnet # - calcnet
# volumes: # volumes:

View file

@ -762,8 +762,7 @@ no: "No"
driveFilesCount: "Number of Drive files" driveFilesCount: "Number of Drive files"
driveUsage: "Drive space usage" driveUsage: "Drive space usage"
noCrawle: "Reject crawler indexing" noCrawle: "Reject crawler indexing"
noCrawleDescription: "Ask search engines to not index your profile page, posts, Pages, noCrawleDescription: "Ask external search engines to not index your content."
etc."
lockedAccountInfo: "Unless you set your post visiblity to \"Followers only\", your lockedAccountInfo: "Unless you set your post visiblity to \"Followers only\", your
posts will be visible to anyone, even if you require followers to be manually approved." posts will be visible to anyone, even if you require followers to be manually approved."
alwaysMarkSensitive: "Mark as NSFW by default" alwaysMarkSensitive: "Mark as NSFW by default"
@ -1141,8 +1140,6 @@ deletePasskeys: "Delete passkeys"
delete2faConfirm: "This will irreversibly delete 2FA on this account. Proceed?" delete2faConfirm: "This will irreversibly delete 2FA on this account. Proceed?"
deletePasskeysConfirm: "This will irreversibly delete all passkeys and security keys on this account. Proceed?" deletePasskeysConfirm: "This will irreversibly delete all passkeys and security keys on this account. Proceed?"
inputNotMatch: "Input does not match" inputNotMatch: "Input does not match"
detectPostLanguage: "Automatically detect the language and show a translate button for posts in foreign languages"
languageForTranslation: "Language used for post translation"
addRe: "Add \"re:\" at the beginning of comment in reply to a post with a content warning" addRe: "Add \"re:\" at the beginning of comment in reply to a post with a content warning"
showBigPostButton: "Show a bigger post button in the posting form" showBigPostButton: "Show a bigger post button in the posting form"
confirm: "Confirm" confirm: "Confirm"
@ -1150,6 +1147,10 @@ emphasizeFollowed: "Highlight the \"Follows you\" sign on your follower info"
importZip: "Import ZIP" importZip: "Import ZIP"
exportZip: "Export ZIP" exportZip: "Export ZIP"
emojiPackCreator: "Emoji pack creator" emojiPackCreator: "Emoji pack creator"
indexable: "Indexable"
indexableDescription: "Allow built-in search to show your public posts"
languageForTranslation: "Post translation language"
detectPostLanguage: "Automatically detect the language and show a translate button for posts in foreign languages"
_sensitiveMediaDetection: _sensitiveMediaDetection:
description: "Reduces the effort of server moderation through automatically recognizing description: "Reduces the effort of server moderation through automatically recognizing

View file

@ -874,7 +874,7 @@ pubSub: "Cuentas Pub/Sub"
lastCommunication: "Última comunicación" lastCommunication: "Última comunicación"
resolved: "Resuelto" resolved: "Resuelto"
unresolved: "Sin resolver" unresolved: "Sin resolver"
breakFollow: "Dejar de seguir" breakFollow: "Quitar seguidor"
itsOn: "¡Está encendido!" itsOn: "¡Está encendido!"
itsOff: "¡Está apagado!" itsOff: "¡Está apagado!"
emailRequiredForSignup: "Se requere una dirección de correo electrónico para el registro emailRequiredForSignup: "Se requere una dirección de correo electrónico para el registro

View file

@ -309,11 +309,11 @@ emptyDrive: "Le Drive est vide"
emptyFolder: "Le dossier est vide" emptyFolder: "Le dossier est vide"
unableToDelete: "Suppression impossible" unableToDelete: "Suppression impossible"
inputNewFileName: "Entrez un nouveau nom de fichier" inputNewFileName: "Entrez un nouveau nom de fichier"
inputNewDescription: "Veuillez entrer une nouvelle description" inputNewDescription: "Veuillez entrer une nouvelle description au fichier"
inputNewFolderName: "Entrez un nouveau nom de dossier" inputNewFolderName: "Entrez un nouveau nom de dossier"
circularReferenceFolder: "Le dossier de destination est un sous-dossier du dossier circularReferenceFolder: "Le dossier de destination est un sous-dossier du dossier
que vous souhaitez déplacer." que vous souhaitez déplacer."
hasChildFilesOrFolders: "Impossible de supprimer ce dossier car il n'est pas vide." hasChildFilesOrFolders: "Impossible de supprimer ce dossier, car il n'est pas vide."
copyUrl: "Copier lURL" copyUrl: "Copier lURL"
rename: "Renommer" rename: "Renommer"
avatar: "Avatar" avatar: "Avatar"
@ -605,7 +605,7 @@ disablePlayer: "Fermer le lecteur vidéo"
expandTweet: "Étendre le tweet" expandTweet: "Étendre le tweet"
themeEditor: "Éditeur de thèmes" themeEditor: "Éditeur de thèmes"
description: "Description" description: "Description"
describeFile: "Ajouter une description d'image" describeFile: "Ajouter une description"
enterFileDescription: "Saisissez une description" enterFileDescription: "Saisissez une description"
author: "Auteur·rice" author: "Auteur·rice"
leaveConfirm: "Vous avez des modifications non-sauvegardées. Voulez-vous les ignorer leaveConfirm: "Vous avez des modifications non-sauvegardées. Voulez-vous les ignorer
@ -2085,7 +2085,7 @@ silenceThisInstance: Masquer ce serveur
silencedInstances: Serveurs masqués silencedInstances: Serveurs masqués
silenced: Masqué silenced: Masqué
deleted: Effacé deleted: Effacé
editNote: Modifier publication editNote: Modifier la publication
edited: 'Modifié à {date} {time}' edited: 'Modifié à {date} {time}'
flagShowTimelineRepliesDescription: Si activé, affiche dans le fil les réponses des flagShowTimelineRepliesDescription: Si activé, affiche dans le fil les réponses des
utilisatieur·rice·s aux publications des autres. utilisatieur·rice·s aux publications des autres.
@ -2209,4 +2209,4 @@ addRe: Ajouter "re:" au début dun avertissement de contenu (CW) en réponse
confirm: Confirmer confirm: Confirmer
importZip: Importer ZIP importZip: Importer ZIP
exportZip: Exporter ZIP exportZip: Exporter ZIP
emojiPackCreator: Créateur de pack demoji emojiPackCreator: Créateur de pack démoji

View file

@ -15,7 +15,7 @@ gotIt: "Ho capito!"
cancel: "Annulla" cancel: "Annulla"
enterUsername: "Inserisci un nome utente" enterUsername: "Inserisci un nome utente"
renotedBy: "Boost da {user}" renotedBy: "Boost da {user}"
noNotes: "Nessuna nota!" noNotes: "Nessun post"
noNotifications: "Nessuna notifica" noNotifications: "Nessuna notifica"
instance: "Server" instance: "Server"
settings: "Impostazioni" settings: "Impostazioni"
@ -35,10 +35,10 @@ users: "Utenti"
addUser: "Aggiungi utente" addUser: "Aggiungi utente"
favorite: "Aggiungi ai preferiti" favorite: "Aggiungi ai preferiti"
favorites: "Preferiti" favorites: "Preferiti"
unfavorite: "Rimuovi nota dai preferiti" unfavorite: "Rimuovi post dai preferiti"
favorited: "Aggiunta ai tuoi preferiti." favorited: "Aggiunto ai tuoi preferiti."
alreadyFavorited: "Già tra i tuoi preferiti." alreadyFavorited: "Già tra i tuoi preferiti."
cantFavorite: "Impossibile aggiungere la nota ai preferiti." cantFavorite: "Impossibile aggiungere il post ai preferiti."
pin: "Fissa sul profilo" pin: "Fissa sul profilo"
unpin: "Non fissare sul profilo" unpin: "Non fissare sul profilo"
copyContent: "Copia il contenuto" copyContent: "Copia il contenuto"
@ -71,7 +71,7 @@ driveFileDeleteConfirm: "Vuoi davvero eliminare il file \"{name}\"? Sarà rimoss
unfollowConfirm: "Vuoi davvero smettere di seguire {name}?" unfollowConfirm: "Vuoi davvero smettere di seguire {name}?"
exportRequested: "Hai richiesto un'esportazione, e potrebbe volerci tempo. Quando exportRequested: "Hai richiesto un'esportazione, e potrebbe volerci tempo. Quando
sarà compiuta, il file verrà aggiunto direttamente al Drive." sarà compiuta, il file verrà aggiunto direttamente al Drive."
importRequested: "Hai richiesto un'importazione. Può volerci tempo. " importRequested: "Hai richiesto un'importazione. Potrebbe impiegare del tempo."
lists: "Liste" lists: "Liste"
noLists: "Nessuna lista" noLists: "Nessuna lista"
note: "Post" note: "Post"
@ -84,10 +84,10 @@ manageLists: "Gestisci liste"
error: "Errore" error: "Errore"
somethingHappened: "Si è verificato un problema" somethingHappened: "Si è verificato un problema"
retry: "Riprova" retry: "Riprova"
pageLoadError: "Caricamento pagina non riuscito. " pageLoadError: "Errore nel caricamento della pagina."
pageLoadErrorDescription: "Questo viene normalmente causato dalla rete o dalla cache pageLoadErrorDescription: "Di solito succede per errori di rete o a causa della cache
del browser. Si prega di pulire la cache, o di attendere e riprovare più tardi." del browser. Prova a pulire la cache o a riprovare più tardi."
serverIsDead: "Il server non risponde. Si prega di attendere e riprovare più tardi." serverIsDead: "Il server non risponde. Attendi e riprova più tardi."
youShouldUpgradeClient: "Per visualizzare la pagina è necessario aggiornare il client youShouldUpgradeClient: "Per visualizzare la pagina è necessario aggiornare il client
alla nuova versione e ricaricare." alla nuova versione e ricaricare."
enterListName: "Nome della lista" enterListName: "Nome della lista"
@ -122,7 +122,7 @@ markAsSensitive: "Segna come sensibile"
unmarkAsSensitive: "Segna come non sensibile" unmarkAsSensitive: "Segna come non sensibile"
enterFileName: "Nome del file" enterFileName: "Nome del file"
mute: "Silenzia" mute: "Silenzia"
unmute: "Riattiva" unmute: "Non silenziare"
block: "Blocca" block: "Blocca"
unblock: "Sblocca" unblock: "Sblocca"
suspend: "Sospendi" suspend: "Sospendi"
@ -135,25 +135,25 @@ selectList: "Seleziona una lista"
selectAntenna: "Scegli un'antenna" selectAntenna: "Scegli un'antenna"
selectWidget: "Seleziona widget" selectWidget: "Seleziona widget"
editWidgets: "Modifica i widget" editWidgets: "Modifica i widget"
editWidgetsExit: "Modifica fine" editWidgetsExit: "Fine modifica"
customEmojis: "Emoji personalizzati" customEmojis: "Emoji personalizzati"
emoji: "Emoji" emoji: "Emoji"
emojis: "Emoji" emojis: "Emoji"
emojiName: "Nome dell'emoji" emojiName: "Nome dell'emoji"
emojiUrl: "URL dell'emoji" emojiUrl: "URL dell'emoji"
addEmoji: "Aggiungi un emoji" addEmoji: "Aggiungi un emoji"
settingGuide: "Configurazione suggerita" settingGuide: "Impostazioni suggerite"
cacheRemoteFiles: "Memorizzazione nella cache dei file remoti" cacheRemoteFiles: "Memorizzazione nella cache dei file remoti"
cacheRemoteFilesDescription: "Disabilitando questa opzione, i file remoti verranno cacheRemoteFilesDescription: "Disabilitando questa opzione, i file remoti verranno
scaricati direttamente dal loro server. L'opzione permette di risparmiare spazio scaricati direttamente dal loro server. L'opzione permette di risparmiare spazio
ma aumenta il traffico di rete e non verranno generate anteprime." ma aumenta il traffico di rete e non verranno generate anteprime."
flagAsBot: "Io sono un robot" flagAsBot: "Questo account è un bot"
flagAsBotDescription: "Se l'account esegue principalmente operazioni automatiche, flagAsBotDescription: "Se l'account esegue principalmente operazioni automatiche,
attiva quest'opzione. Quando attivata, opera come un segnalatore per gli altri sviluppatori attiva quest'opzione. Quando attivata, opera come un segnalatore per gli altri sviluppatori
allo scopo di prevenire catene dinterazione senza fine con altri bot, e di adeguare allo scopo di prevenire catene dinterazione senza fine con altri bot, e di adeguare
i sistemi interni di Firefish perché trattino questo account come un bot." i sistemi interni di Firefish perché trattino questo account come un bot."
flagAsCat: "Io sono un gatto" flagAsCat: "Sei un gatto? 😺"
flagAsCatDescription: "Abilita l'opzione \"Io sono un gatto\" per l'account." flagAsCatDescription: "Ti compariranno le orecchie e parlerai come un gatto!"
autoAcceptFollowed: "Accetta in automatico i follow dagli account che segui" autoAcceptFollowed: "Accetta in automatico i follow dagli account che segui"
addAccount: "Aggiungi account" addAccount: "Aggiungi account"
loginFailed: "Accesso non riuscito" loginFailed: "Accesso non riuscito"
@ -171,17 +171,17 @@ proxyAccountDescription: "Un account proxy è un account che funziona da followe
una lista, le attività di quell'utente potrebbero comunque non essere visualizzate una lista, le attività di quell'utente potrebbero comunque non essere visualizzate
in locale se nessun altro utente lo segue su questo server, l'account proxy si occuperà in locale se nessun altro utente lo segue su questo server, l'account proxy si occuperà
di seguire e acquisire i post." di seguire e acquisire i post."
host: "Server remoto" host: "Host"
selectUser: "Seleziona utente" selectUser: "Seleziona utente"
recipient: "Destinatario" recipient: "Destinatario(i)"
annotation: "Descrizione" annotation: "Annotazioni"
federation: "Federazione" federation: "Federazione"
instances: "Server" instances: "Server"
registeredAt: "Registrato presso" registeredAt: "Registrato presso"
latestRequestSentAt: "Ultima richiesta inviata" latestRequestSentAt: "Ultima richiesta inviata"
latestRequestReceivedAt: "Ultima richiesta ricevuta" latestRequestReceivedAt: "Ultima richiesta ricevuta"
latestStatus: "Ultimo stato" latestStatus: "Ultimo stato"
storageUsage: "Volume di dischi" storageUsage: "Spazio occupato"
charts: "Grafici" charts: "Grafici"
perHour: "All'ora" perHour: "All'ora"
perDay: "al giorno" perDay: "al giorno"
@ -190,9 +190,9 @@ blockThisInstance: "Blocca questo server"
operations: "Operazioni" operations: "Operazioni"
software: "Software" software: "Software"
version: "Versione" version: "Versione"
metadata: "Metadato" metadata: "Metadati"
monitor: "Monitorare" monitor: "Monitor"
jobQueue: "Coda di lavoro" jobQueue: "Coda dei job"
cpuAndMemory: "CPU e Memoria" cpuAndMemory: "CPU e Memoria"
network: "Rete" network: "Rete"
disk: "Disco" disk: "Disco"
@ -203,7 +203,7 @@ clearQueueConfirmTitle: "Vuoi davvero svuotare la coda?"
clearQueueConfirmText: "I post ancora in coda non verranno più federati. Solitamente, clearQueueConfirmText: "I post ancora in coda non verranno più federati. Solitamente,
non è necessario eseguire questa operazione." non è necessario eseguire questa operazione."
clearCachedFiles: "Svuota cache" clearCachedFiles: "Svuota cache"
clearCachedFilesConfirm: "Vuoi davvero svuotare la cache da tutti i file remoti?" clearCachedFilesConfirm: "Vuoi davvero svuotare la cache di tutti i file remoti?"
blockedInstances: "Server bloccati" blockedInstances: "Server bloccati"
blockedInstancesDescription: "Elenca gli hostname dei server che vuoi bloccare. Non blockedInstancesDescription: "Elenca gli hostname dei server che vuoi bloccare. Non
potranno più comunicare con il tuo server." potranno più comunicare con il tuo server."
@ -213,7 +213,7 @@ blockedUsers: "Account bloccati"
noUsers: "Nessun utente trovato" noUsers: "Nessun utente trovato"
editProfile: "Modifica profilo" editProfile: "Modifica profilo"
noteDeleteConfirm: "Vuoi eliminare questo post?" noteDeleteConfirm: "Vuoi eliminare questo post?"
pinLimitExceeded: "Non puoi fissare più post di così" pinLimitExceeded: "Hai già fissato il massimo possibile di post"
intro: "L'installazione di Firefish è finita! Si prega di creare un account amministratore." intro: "L'installazione di Firefish è finita! Si prega di creare un account amministratore."
done: "Fine" done: "Fine"
processing: "Elaborazione in corso" processing: "Elaborazione in corso"
@ -221,12 +221,12 @@ preview: "Anteprima"
default: "Predefinito" default: "Predefinito"
noCustomEmojis: "Nessun emoji" noCustomEmojis: "Nessun emoji"
noJobs: "Nessun lavoro" noJobs: "Nessun lavoro"
federating: "Federando" federating: "Federazione in corso"
blocked: "Bloccato" blocked: "Bloccato"
suspended: "Sospes@" suspended: "Sospeso"
all: "Tutti" all: "Tutti"
subscribing: "Iscrivendo" subscribing: "Sottoscrizione in corso"
publishing: "Pubblicando" publishing: "Pubblicazione in corso"
notResponding: "Nessuna risposta" notResponding: "Nessuna risposta"
instanceFollowing: "Seguiti da te su questo server" instanceFollowing: "Seguiti da te su questo server"
instanceFollowers: "Chi ti segue su questo server" instanceFollowers: "Chi ti segue su questo server"
@ -238,8 +238,8 @@ currentPassword: "Password attuale"
newPassword: "Nuova Password" newPassword: "Nuova Password"
newPasswordRetype: "Conferma password" newPasswordRetype: "Conferma password"
attachFile: "Allega file" attachFile: "Allega file"
more: "Altri!" more: "Altro!"
featured: "Tendenze" featured: "In primo piano"
usernameOrUserId: "Nome utente o ID utente" usernameOrUserId: "Nome utente o ID utente"
noSuchUser: "Nessun utente trovato" noSuchUser: "Nessun utente trovato"
lookup: "Cercare" lookup: "Cercare"
@ -249,7 +249,7 @@ remove: "Elimina"
removed: "Il tuo Tweet è stato eliminato" removed: "Il tuo Tweet è stato eliminato"
removeAreYouSure: "Eliminare \"{x}\"?" removeAreYouSure: "Eliminare \"{x}\"?"
deleteAreYouSure: "Eliminare \"{x}\"?" deleteAreYouSure: "Eliminare \"{x}\"?"
resetAreYouSure: "Reimposta" resetAreYouSure: "Vuoi reimpostare?"
saved: "Salvato" saved: "Salvato"
messaging: "Messaggi" messaging: "Messaggi"
upload: "Carica" upload: "Carica"
@ -265,8 +265,8 @@ noMoreHistory: "Non c'è più cronologia da visualizzare"
startMessaging: "Nuovo messaggio" startMessaging: "Nuovo messaggio"
nUsersRead: "Letto da {n} persone" nUsersRead: "Letto da {n} persone"
agreeTo: "Sono d'accordo con {0}" agreeTo: "Sono d'accordo con {0}"
tos: "Termini di servizio" tos: "Termini d'uso"
start: "Inizia!" start: "Inizia"
home: "Home" home: "Home"
remoteUserCaution: "Può darsi che le informazioni siano incomplete perché questo è remoteUserCaution: "Può darsi che le informazioni siano incomplete perché questo è
un utente remoto." un utente remoto."
@ -274,7 +274,7 @@ activity: "Attività"
images: "Immagini" images: "Immagini"
birthday: "Compleanno" birthday: "Compleanno"
yearsOld: "{age}Anni" yearsOld: "{age}Anni"
registeredDate: "Iscrizione a.." registeredDate: "Iscrizione il"
location: "Posizione" location: "Posizione"
theme: "Tema" theme: "Tema"
themeForLightMode: "Tema da utilizzare per il modo chiaro" themeForLightMode: "Tema da utilizzare per il modo chiaro"
@ -285,7 +285,7 @@ lightThemes: "Tema Chiaro"
darkThemes: "Tema Scuro" darkThemes: "Tema Scuro"
syncDeviceDarkMode: "Sincronizza il tema scuro con le impostazioni del dispositivo" syncDeviceDarkMode: "Sincronizza il tema scuro con le impostazioni del dispositivo"
drive: "Drive" drive: "Drive"
fileName: "Nome dell'allegato" fileName: "Nome file"
selectFile: "Scelta allegato" selectFile: "Scelta allegato"
selectFiles: "Scelta allegato" selectFiles: "Scelta allegato"
selectFolder: "Seleziona cartella" selectFolder: "Seleziona cartella"
@ -298,25 +298,25 @@ deleteFolder: "Elimina cartella"
addFile: "Allega" addFile: "Allega"
emptyDrive: "Il Drive è vuoto" emptyDrive: "Il Drive è vuoto"
emptyFolder: "La cartella è vuota" emptyFolder: "La cartella è vuota"
unableToDelete: "Eliminazione impossibile" unableToDelete: "Impossibile rimuovere"
inputNewFileName: "Inserisci nome del nuovo file" inputNewFileName: "Inserisci nome del nuovo file"
inputNewDescription: "Inserisci una nuova descrizione" inputNewDescription: "Inserisci una nuova descrizione"
inputNewFolderName: "Inserisci nome della nuova cartella" inputNewFolderName: "Inserisci nome della nuova cartella"
circularReferenceFolder: "La cartella di destinazione è una sottocartella della cartella circularReferenceFolder: "La cartella di destinazione è una sottocartella della cartella
che vuoi spostare." che vuoi spostare."
hasChildFilesOrFolders: "La cartella non può essere rimossa perché non è vuota" hasChildFilesOrFolders: "La cartella non può essere rimossa perché non è vuota."
copyUrl: "Copia URL" copyUrl: "Copia URL"
rename: "Modifica nome" rename: "Modifica nome"
avatar: "Foto del profilo" avatar: "Foto del profilo"
banner: "Intestazione" banner: "Intestazione"
nsfw: "Contenuti sensibili" nsfw: "Contenuti sensibili"
whenServerDisconnected: "Quando la connessione col server è persa" whenServerDisconnected: "Quando la connessione col server è persa"
disconnectedFromServer: "Disconness@ dal server" disconnectedFromServer: "Server disconnesso"
reload: "Ricarica" reload: "Ricarica"
doNothing: "Nessun'azione" doNothing: "Nessun'azione"
reloadConfirm: "Vuoi ricaricare?" reloadConfirm: "Vuoi ricaricare?"
watch: "Osserva" watch: "Osserva"
unwatch: "Smetti di Osserva" unwatch: "Smetti di osservare"
accept: "Accetta" accept: "Accetta"
reject: "Rifiuta" reject: "Rifiuta"
normal: "Normale" normal: "Normale"
@ -324,7 +324,7 @@ instanceName: "Nome del server"
instanceDescription: "Descrizione del server" instanceDescription: "Descrizione del server"
maintainerName: "Nome dell'Amministratore" maintainerName: "Nome dell'Amministratore"
maintainerEmail: "Indirizzo e-mail dell'Amministratore" maintainerEmail: "Indirizzo e-mail dell'Amministratore"
tosUrl: "Termini di servizio URL" tosUrl: "URL Termini d'uso"
thisYear: "Anno" thisYear: "Anno"
thisMonth: "Mese" thisMonth: "Mese"
today: "Oggi" today: "Oggi"
@ -333,8 +333,8 @@ monthX: "{month}"
yearX: "{year}" yearX: "{year}"
pages: "Pagine" pages: "Pagine"
integration: "Integrazioni" integration: "Integrazioni"
connectService: "Connessione" connectService: "Connetti"
disconnectService: "Disconnessione " 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
@ -342,16 +342,16 @@ disablingTimelinesInfo: "Anche se disabiliti queste timeline, gli amministratori
registration: "Iscriviti" registration: "Iscriviti"
enableRegistration: "Permettere nuove registrazioni" enableRegistration: "Permettere nuove registrazioni"
invite: "Invita" invite: "Invita"
driveCapacityPerLocalAccount: "Volume del Drive per utente locale" driveCapacityPerLocalAccount: "Dimensione Drive per utenti locali"
driveCapacityPerRemoteAccount: "Volume del Drive per utente remoto" driveCapacityPerRemoteAccount: "Dimensione Drive per utenti remoti"
inMb: "in Megabytes" inMb: "In megabytes"
iconUrl: "URL di icona (favicon, ecc.)" iconUrl: "URL icona"
bannerUrl: "URL dell'immagine d'intestazione" bannerUrl: "URL dell'immagine banner"
backgroundImageUrl: "URL dello sfondo" backgroundImageUrl: "URL dello sfondo"
basicInfo: "Informazioni fondamentali" basicInfo: "Informazioni di base"
pinnedUsers: "Utenti in evidenza" pinnedUsers: "Utenti in fissati"
pinnedUsersDescription: "Elenca gli/le utenti che vuoi fissare in cima alla pagina pinnedUsersDescription: "Elenca gli utenti che vuoi fissare in cima alla pagina \"\
\"Esplora\", un@ per riga." Esplora\", uno per riga."
pinnedPages: "Pagine in evidenza" pinnedPages: "Pagine in evidenza"
pinnedPagesDescription: "Specifica il percorso delle pagine che vuoi fissare in cima pinnedPagesDescription: "Specifica il percorso delle pagine che vuoi fissare in cima
alla home page del server. Una pagina per riga." alla home page del server. Una pagina per riga."
@ -371,20 +371,20 @@ avoidMultiCaptchaConfirm: "Utilizzare diversi Captcha può causare interferenze.
antennas: "Antenne" antennas: "Antenne"
manageAntennas: "Gestore delle antenne" manageAntennas: "Gestore delle antenne"
name: "Nome" name: "Nome"
antennaSource: "Fonte dell'antenna" antennaSource: "Origine dell'antenna"
antennaKeywords: "Parole chiavi da ricevere" antennaKeywords: "Parole chiavi da ricevere"
antennaExcludeKeywords: "Parole chiavi da escludere" antennaExcludeKeywords: "Parole chiavi da escludere"
antennaKeywordsDescription: "Separare con uno spazio indica la condizione \"E\". Separare antennaKeywordsDescription: "Separare con uno spazio indica la condizione \"AND\"
con un'interruzzione riga indica la condizione \"O\"." . Separare con un'interruzione riga indica la condizione \"OR\"."
notifyAntenna: "Notifica i nuovi post" notifyAntenna: "Notifica i nuovi post"
withFileAntenna: "Solo post con file allegati" withFileAntenna: "Solo post con allegati"
enableServiceworker: "Abilita ServiceWorker" enableServiceworker: "Abilita ServiceWorker"
antennaUsersDescription: "Inserisci solo un nome utente per riga" antennaUsersDescription: "Inserisci solo un nome utente per riga"
caseSensitive: "Sensibile alla distinzione tra maiuscole e minuscole" caseSensitive: "Sensibile alla distinzione tra maiuscole e minuscole"
withReplies: "Includere le risposte" withReplies: "Includere le risposte"
connectedTo: "Stai seguendo questi account" connectedTo: "Stai seguendo questi account"
notesAndReplies: "Post e risposte" notesAndReplies: "Post e risposte"
withFiles: "Con file in allegato" withFiles: "Con file allegati"
silence: "Silenzia" silence: "Silenzia"
silenceConfirm: "Vuoi davvero silenziare l'utente?" silenceConfirm: "Vuoi davvero silenziare l'utente?"
unsilence: "Riattiva" unsilence: "Riattiva"
@ -433,7 +433,7 @@ invites: "Inviti"
groupName: "Nome del gruppo" groupName: "Nome del gruppo"
members: "Membri" members: "Membri"
transfer: "Trasferisci" transfer: "Trasferisci"
messagingWithUser: "Iniziare una chat con un altr@ utente" messagingWithUser: "Chat privata"
messagingWithGroup: "Chattare in gruppo" messagingWithGroup: "Chattare in gruppo"
title: "Titolo" title: "Titolo"
text: "Testo" text: "Testo"
@ -452,8 +452,8 @@ invitations: "Invita"
invitationCode: "Codice di invito" invitationCode: "Codice di invito"
checking: "Confermando" checking: "Confermando"
available: "Consigliati" available: "Consigliati"
unavailable: "Il nome utente è già in uso" unavailable: "Nome già in uso"
usernameInvalidFormat: "Il nome utente può contenere solo lettere, numeri e '_'" usernameInvalidFormat: "Puoi usare solo lettere maiuscole, minuscole, numeri e '_'"
tooShort: "Troppo breve" tooShort: "Troppo breve"
tooLong: "Troppo lungo" tooLong: "Troppo lungo"
weakPassword: "Password debole" weakPassword: "Password debole"
@ -556,14 +556,14 @@ scratchpadDescription: "Lo Scratchpad offre un ambiente per esperimenti di AiScr
output: "Uscita" output: "Uscita"
script: "Script" script: "Script"
disablePagesScript: "Disabilita AiScript nelle pagine" disablePagesScript: "Disabilita AiScript nelle pagine"
updateRemoteUser: "Aggiornare le informazioni di utente remot@" updateRemoteUser: "Aggiorna le informazioni dell'utente remoto"
deleteAllFiles: "Elimina tutti i file" deleteAllFiles: "Elimina tutti i file"
deleteAllFilesConfirm: "Vuoi davvero eliminare tutti i file?" deleteAllFilesConfirm: "Vuoi davvero eliminare tutti i file?"
removeAllFollowing: "Smetti di seguire tutti" removeAllFollowing: "Smetti di seguire tutti"
removeAllFollowingDescription: "Smetti di seguire tutti gli account del server {host}. removeAllFollowingDescription: "Smetti di seguire tutti gli account del server {host}.
È utile specialmente se il server non esiste più." È utile specialmente se il server non esiste più."
userSuspended: "L'utente è sospes@." userSuspended: "L'utente è sospeso."
userSilenced: "L'utente è silenziat@." userSilenced: "L'utente è silenziato."
yourAccountSuspendedTitle: "Questo account è sospeso." yourAccountSuspendedTitle: "Questo account è sospeso."
yourAccountSuspendedDescription: "Questo account è stato sospeso a causa di una violazione yourAccountSuspendedDescription: "Questo account è stato sospeso a causa di una violazione
dei termini di servizio del server. Contattare l'amministrazione per i dettagli. dei termini di servizio del server. Contattare l'amministrazione per i dettagli.
@ -906,8 +906,7 @@ _ad:
reduceFrequencyOfThisAd: "Visualizza questa pubblicità meno spesso" reduceFrequencyOfThisAd: "Visualizza questa pubblicità meno spesso"
_forgotPassword: _forgotPassword:
enterEmail: "Inserisci l'indirizzo di posta elettronica che hai registrato nel tuo enterEmail: "Inserisci l'indirizzo di posta elettronica che hai registrato nel tuo
profilo. Il collegamento necessario per ripristinare la password verrà inviato profilo. Il di link ripristino della password verrà inviato a questo indirizzo."
a questo indirizzo."
ifNoEmail: "Se non hai registrato alcun indirizzo e-mail, contatta l'admin del server." ifNoEmail: "Se non hai registrato alcun indirizzo e-mail, contatta l'admin del server."
contactAdmin: "Poiché questo server non permette l'uso di indirizzi mail, contatta contactAdmin: "Poiché questo server non permette l'uso di indirizzi mail, contatta
l'admin per poter ripristinare la password." l'admin per poter ripristinare la password."
@ -970,7 +969,7 @@ _mfm:
blockCode: "Codice (blocco)" blockCode: "Codice (blocco)"
inlineMath: "Espressione matematica(Immersione)" inlineMath: "Espressione matematica(Immersione)"
blockMath: "Formula matematica (blocco)" blockMath: "Formula matematica (blocco)"
quote: "Cita il nota" quote: "Citazione"
emoji: "Emoji personalizzati" emoji: "Emoji personalizzati"
search: "Cerca" search: "Cerca"
flip: "Inverti" flip: "Inverti"
@ -989,8 +988,8 @@ _mfm:
x4: "Estremamente più grande" x4: "Estremamente più grande"
x4Description: "Mostra il contenuto estremamente più ingrandito." x4Description: "Mostra il contenuto estremamente più ingrandito."
blur: "Sfocatura" blur: "Sfocatura"
blurDescription: "È possibile rendere sfocato il contenuto. Spostando il cursore blurDescription: "È possibile rendere sfocato il contenuto. Diventerà visibile al
su di esso tornerà visibile chiaramente." passaggio del puntatore."
font: "Tipo di carattere" font: "Tipo di carattere"
fontDescription: "Puoi scegliere il tipo di carattere per il contenuto." fontDescription: "Puoi scegliere il tipo di carattere per il contenuto."
rainbow: "Arcobaleno" rainbow: "Arcobaleno"
@ -1123,7 +1122,7 @@ _theme:
header: "Intestazione" header: "Intestazione"
navBg: "Sfondo della barra laterale" navBg: "Sfondo della barra laterale"
navFg: "Testo della barra laterale" navFg: "Testo della barra laterale"
navHoverFg: "Testo della barra laterale (al passaggio del mouse)" navHoverFg: "Testo della barra laterale (hover)"
navActive: "Testo della barra laterale (attivo)" navActive: "Testo della barra laterale (attivo)"
navIndicator: "Indicatore di barra laterale" navIndicator: "Indicatore di barra laterale"
link: "Link" link: "Link"
@ -1138,19 +1137,19 @@ _theme:
infoWarnFg: "Testo di avviso" infoWarnFg: "Testo di avviso"
cwBg: "Sfondo del CW" cwBg: "Sfondo del CW"
cwFg: "Testo del pulsante CW" cwFg: "Testo del pulsante CW"
cwHoverBg: "Sfondo del pulsante CW (sorvolato)" cwHoverBg: "Sfondo del pulsante CW (hover)"
toastBg: "Sfondo di notifica a comparsa" toastBg: "Sfondo di notifica a comparsa"
toastFg: "Testo di notifica a comparsa" toastFg: "Testo di notifica a comparsa"
buttonBg: "Sfondo del pulsante" buttonBg: "Sfondo del pulsante"
buttonHoverBg: "Sfondo del pulsante (sorvolato)" buttonHoverBg: "Sfondo del pulsante (hover)"
inputBorder: "Inquadra casella di testo" inputBorder: "Inquadra casella di testo"
listItemHoverBg: "Sfondo della voce di elenco (sorvolato)" listItemHoverBg: "Sfondo della voce di elenco (hover)"
driveFolderBg: "Sfondo della cartella di disco" driveFolderBg: "Sfondo della cartella di disco"
badge: "Distintivo" badge: "Distintivo"
messageBg: "Sfondo della chat" messageBg: "Sfondo della chat"
modalBg: Sfondo modale modalBg: Sfondo modale
scrollbarHandle: Barra di scorrimento scrollbarHandle: Barra di scorrimento
scrollbarHandleHover: Barra di scorrimento (Hover) scrollbarHandleHover: Barra di scorrimento (hover)
accent: Accento accent: Accento
fgHighlighted: Testo evidenziato fgHighlighted: Testo evidenziato
accentLighten: Accento (chiaro) accentLighten: Accento (chiaro)
@ -1512,7 +1511,7 @@ _pages:
width: "Larghezza" width: "Larghezza"
height: "Altezza" height: "Altezza"
id: Canvas ID id: Canvas ID
note: "Post embedded" note: "Post integrato"
_note: _note:
id: "Post ID" id: "Post ID"
idDescription: "In alternativa puoi incollare qui l'URL del post." idDescription: "In alternativa puoi incollare qui l'URL del post."
@ -1670,7 +1669,7 @@ _pages:
arg2: Valore massimo arg2: Valore massimo
strLen: Lunghezza del testo strLen: Lunghezza del testo
join: Concatenazione testo join: Concatenazione testo
splitStrByLine: Suddividi il testo al fine riga splitStrByLine: Suddividi su più righe
subtract: Sottrazione subtract: Sottrazione
lt: < A è minore di B lt: < A è minore di B
gt: '> A è maggiore di B' gt: '> A è maggiore di B'
@ -1741,7 +1740,7 @@ _notification:
fileUploaded: "File caricato correttamente" fileUploaded: "File caricato correttamente"
youGotMention: "{name} ti ha menzionato" youGotMention: "{name} ti ha menzionato"
youGotReply: "{name} ti ha risposto" youGotReply: "{name} ti ha risposto"
youGotQuote: "{name} ha citato il tuo Nota e ha detto" youGotQuote: "{name} ti ha citato"
youRenoted: "Boost da {name}" youRenoted: "Boost da {name}"
youGotPoll: "{name} ha votato" youGotPoll: "{name} ha votato"
youGotMessagingMessageFromUser: "{name} ti ha mandato un messaggio" youGotMessagingMessageFromUser: "{name} ti ha mandato un messaggio"
@ -1789,7 +1788,7 @@ _deck:
widgets: "Widget" widgets: "Widget"
notifications: "Notifiche" notifications: "Notifiche"
tl: "Timeline" tl: "Timeline"
antenna: "Antenne" antenna: "Antenna"
list: "Liste" list: "Liste"
mentions: "Menzioni" mentions: "Menzioni"
direct: "Messaggi diretti" direct: "Messaggi diretti"
@ -2060,7 +2059,7 @@ shuffle: Casuale
subscribePushNotification: Abilita le notifiche push subscribePushNotification: Abilita le notifiche push
unsubscribePushNotification: Disabilita le notifiche push unsubscribePushNotification: Disabilita le notifiche push
pushNotificationAlreadySubscribed: Le notifiche push sono già abilitate pushNotificationAlreadySubscribed: Le notifiche push sono già abilitate
driveCapOverrideCaption: Imposta la capacità predefinita inserendo il valore 0. driveCapOverrideCaption: Reimposta la capacità predefinita inserendo il valore 0.
numberOfPageCacheDescription: Aumentare questo numero migliorerà l'esperienza degli numberOfPageCacheDescription: Aumentare questo numero migliorerà l'esperienza degli
utenti ma aumenterà il carico sul server e l'uso di memoria. utenti ma aumenterà il carico sul server e l'uso di memoria.
type: Tipo type: Tipo

View file

@ -1,7 +1,7 @@
---
_lang_: "Português" _lang_: "Português"
headlineFirefish: "Uma rede ligada por notas" headlineFirefish: "Uma rede ligada por notas"
introFirefish: "Bem-vindo! Firefish é um serviço de microblogue descentralizado de código aberto.\nCria \"notas\" e partilha o que te ocorre com todos à tua volta. 📡\nCom \"reações\" podes também expressar logo o que sentes às notas de todos. 👍\nExploremos um novo mundo! 🚀" introFirefish: "Bem-vindo! Firefish é um serviço de microblogue descentralizado de
código aberto, gratuito para sempre! 🚀"
monthAndDay: "{day}/{month}" monthAndDay: "{day}/{month}"
search: "Buscar" search: "Buscar"
notifications: "Notificações" notifications: "Notificações"
@ -44,7 +44,8 @@ copyContent: "Copiar conteúdos"
copyLink: "Copiar hiperligação" copyLink: "Copiar hiperligação"
delete: "Eliminar" delete: "Eliminar"
deleteAndEdit: "Eliminar e editar" deleteAndEdit: "Eliminar e editar"
deleteAndEditConfirm: "Tens a certeza que pretendes eliminar esta nota e editá-la? Irás perder todas as suas reações, renotas e respostas." deleteAndEditConfirm: "Tens a certeza que pretendes eliminar esta nota e editá-la?
Irás perder todas as suas reações, renotas e respostas."
addToList: "Adicionar a lista" addToList: "Adicionar a lista"
sendMessage: "Enviar uma mensagem" sendMessage: "Enviar uma mensagem"
copyUsername: "Copiar nome de utilizador" copyUsername: "Copiar nome de utilizador"
@ -64,9 +65,11 @@ import: "Importar"
export: "Exportar" export: "Exportar"
files: "Ficheiros" files: "Ficheiros"
download: "Descarregar" download: "Descarregar"
driveFileDeleteConfirm: "Tens a certeza que pretendes apagar o ficheiro \"{name}\"? As notas que tenham este ficheiro anexado serão também apagadas." driveFileDeleteConfirm: "Tens a certeza que pretendes apagar o ficheiro \"{name}\"\
? As notas que tenham este ficheiro anexado serão também apagadas."
unfollowConfirm: "Tens a certeza que queres deixar de seguir {name}?" unfollowConfirm: "Tens a certeza que queres deixar de seguir {name}?"
exportRequested: "Pediste uma exportação. Este processo pode demorar algum tempo. Será adicionado à tua Drive após a conclusão do processo." exportRequested: "Pediste uma exportação. Este processo pode demorar algum tempo.
Será adicionado à tua Drive após a conclusão do processo."
importRequested: "Pediste uma importação. Este processo pode demorar algum tempo." importRequested: "Pediste uma importação. Este processo pode demorar algum tempo."
lists: "Listas" lists: "Listas"
noLists: "Não tens nenhuma lista" noLists: "Não tens nenhuma lista"
@ -81,9 +84,12 @@ error: "Erro"
somethingHappened: "Ocorreu um erro" somethingHappened: "Ocorreu um erro"
retry: "Tentar novamente" retry: "Tentar novamente"
pageLoadError: "Ocorreu um erro ao carregar a página." pageLoadError: "Ocorreu um erro ao carregar a página."
pageLoadErrorDescription: "Isto é normalmente causado por erros de rede ou pela cache do browser. Experimenta limpar a cache e tenta novamente após algum tempo." pageLoadErrorDescription: "Isto é normalmente causado por erros de rede ou pela cache
serverIsDead: "O servidor não está respondendo. Por favor espere um pouco e tente novamente." do browser. Experimenta limpar a cache e tenta novamente após algum tempo."
youShouldUpgradeClient: "Para visualizar essa página, por favor recarregue-a para atualizar seu cliente." serverIsDead: "O servidor não está respondendo. Por favor espere um pouco e tente
novamente."
youShouldUpgradeClient: "Para visualizar essa página, por favor recarregue-a para
atualizar seu cliente."
enterListName: "Insira um nome para a lista" enterListName: "Insira um nome para a lista"
privacy: "Privacidade" privacy: "Privacidade"
makeFollowManuallyApprove: "Pedidos de seguimento precisam ser aprovados" makeFollowManuallyApprove: "Pedidos de seguimento precisam ser aprovados"
@ -108,7 +114,8 @@ sensitive: "Conteúdo sensível"
add: "Adicionar" add: "Adicionar"
reaction: "Reações" reaction: "Reações"
reactionSetting: "Quais reações a mostrar no selecionador de reações" reactionSetting: "Quais reações a mostrar no selecionador de reações"
reactionSettingDescription2: "Arraste para reordenar, clique para excluir, pressione + para adicionar." reactionSettingDescription2: "Arraste para reordenar, clique para excluir, pressione
+ para adicionar."
rememberNoteVisibility: "Lembrar das configurações de visibilidade de notas" rememberNoteVisibility: "Lembrar das configurações de visibilidade de notas"
attachCancel: "Remover anexo" attachCancel: "Remover anexo"
markAsSensitive: "Marcar como sensível" markAsSensitive: "Marcar como sensível"
@ -137,13 +144,18 @@ emojiUrl: "URL do Emoji"
addEmoji: "Adicionar um Emoji" addEmoji: "Adicionar um Emoji"
settingGuide: "Guia de configuração" settingGuide: "Guia de configuração"
cacheRemoteFiles: "Memória transitória de arquivos remotos" cacheRemoteFiles: "Memória transitória de arquivos remotos"
cacheRemoteFilesDescription: "Se você desabilitar essa configuração, os arquivos remotos não serão armazenados em memória transitória e serão vinculados diretamente. Economiza o armazenamento do servidor, mas não gera miniaturas, o que aumenta o tráfego." cacheRemoteFilesDescription: "Se você desabilitar essa configuração, os arquivos remotos
não serão armazenados em memória transitória e serão vinculados diretamente. Economiza
o armazenamento do servidor, mas não gera miniaturas, o que aumenta o tráfego."
flagAsBot: "Marcar conta como robô" flagAsBot: "Marcar conta como robô"
flagAsBotDescription: "Se esta conta for operada por um programa, ative este sinalizador. Quando ativado, serve como um sinalizador para evitar o encadeamento de reações para outros programadores, e o manuseio do sistema do Firefish é adequado para bots." flagAsBotDescription: "Se esta conta for operada por um programa, ative este sinalizador.
Quando ativado, serve como um sinalizador para evitar o encadeamento de reações
para outros programadores, e o manuseio do sistema do Firefish é adequado para bots."
flagAsCat: "Marcar conta como gato" flagAsCat: "Marcar conta como gato"
flagAsCatDescription: "Ative essa opção para marcar essa conta como gato." flagAsCatDescription: "Ative essa opção para marcar essa conta como gato."
flagShowTimelineReplies: "Mostrar respostas na linha de tempo" flagShowTimelineReplies: "Mostrar respostas na linha de tempo"
flagShowTimelineRepliesDescription: "Quando ativado, a linha do tempo mostra as respostas às outras notas do utilizador, além da nota do utilizador." flagShowTimelineRepliesDescription: "Quando ativado, a linha do tempo mostra as respostas
às outras notas do utilizador, além da nota do utilizador."
autoAcceptFollowed: "Aprove automaticamente os seguidores dos seguintes utilizadores" autoAcceptFollowed: "Aprove automaticamente os seguidores dos seguintes utilizadores"
addAccount: "Adicionar Conta" addAccount: "Adicionar Conta"
loginFailed: "Não consegui logar" loginFailed: "Não consegui logar"
@ -156,7 +168,10 @@ searchWith: "Buscar: {q}"
youHaveNoLists: "Não tem nenhuma lista" youHaveNoLists: "Não tem nenhuma lista"
followConfirm: "Tem certeza que quer deixar de seguir {name}?" followConfirm: "Tem certeza que quer deixar de seguir {name}?"
proxyAccount: "Conta proxy" proxyAccount: "Conta proxy"
proxyAccountDescription: "Uma conta proxy é uma conta que atua como seguidora remota para utilizadores sob determinadas condições. Por exemplo, quando um utilizador lista um utilizador remoto, a atividade não será entregue à instância, a menos que alguém esteja seguindo o utilizador listado, portanto, a conta proxy deve seguir." proxyAccountDescription: "Uma conta proxy é uma conta que atua como seguidora remota
para utilizadores sob determinadas condições. Por exemplo, quando um utilizador
lista um utilizador remoto, a atividade não será entregue à instância, a menos que
alguém esteja seguindo o utilizador listado, portanto, a conta proxy deve seguir."
host: "hospedeiro" host: "hospedeiro"
selectUser: "Selecionar utilizador" selectUser: "Selecionar utilizador"
recipient: "Morada" recipient: "Morada"
@ -186,11 +201,15 @@ instanceInfo: "Informações da instância"
statistics: "Estatisticas" statistics: "Estatisticas"
clearQueue: "Limpar a fila" clearQueue: "Limpar a fila"
clearQueueConfirmTitle: "Quer limpar a fila?" clearQueueConfirmTitle: "Quer limpar a fila?"
clearQueueConfirmText: "Postagens não entregues não serão mais entregues. Normalmente você não precisa fazer isso." clearQueueConfirmText: "Postagens não entregues não serão mais entregues. Normalmente
você não precisa fazer isso."
clearCachedFiles: "Limpar memória transitória" clearCachedFiles: "Limpar memória transitória"
clearCachedFilesConfirm: "Tem certeza de que deseja excluir todos os arquivos remotos armazenados em memória transitória?" clearCachedFilesConfirm: "Tem certeza de que deseja excluir todos os arquivos remotos
armazenados em memória transitória?"
blockedInstances: "Instância bloqueada" blockedInstances: "Instância bloqueada"
blockedInstancesDescription: "Defina os anfitriões das instâncias que deseja bloquear, separados por quebras de linha. Uma instância bloqueada não poderá interagir com esta instância." blockedInstancesDescription: "Defina os anfitriões das instâncias que deseja bloquear,
separados por quebras de linha. Uma instância bloqueada não poderá interagir com
esta instância."
muteAndBlock: "Silenciar e bloquear" muteAndBlock: "Silenciar e bloquear"
mutedUsers: "Silenciar utilizador" mutedUsers: "Silenciar utilizador"
blockedUsers: "Utilizadores bloqueados" blockedUsers: "Utilizadores bloqueados"
@ -238,7 +257,9 @@ saved: "Salvo"
messaging: "Chat" messaging: "Chat"
upload: "Enviando" upload: "Enviando"
keepOriginalUploading: "Manter a imagem original" keepOriginalUploading: "Manter a imagem original"
keepOriginalUploadingDescription: "Mantenha a versão original ao carregar a imagem. Quando desligado, a imagem para publicação na web será gerada no navegador no momento do upload." keepOriginalUploadingDescription: "Mantenha a versão original ao carregar a imagem.
Quando desligado, a imagem para publicação na web será gerada no navegador no momento
do upload."
fromDrive: "\nDa unidade" fromDrive: "\nDa unidade"
fromUrl: "Da URL" fromUrl: "Da URL"
uploadFromUrl: "Carregamento de URL" uploadFromUrl: "Carregamento de URL"
@ -262,8 +283,8 @@ yearsOld: "{age} anos"
registeredDate: "Data de registro" registeredDate: "Data de registro"
location: "Lugar, colocar" location: "Lugar, colocar"
theme: "tema" theme: "tema"
themeForLightMode: "Temas usados no modo de luz" themeForLightMode: "Tema a usar no Modo Diurno"
themeForDarkMode: "Temas usados no modo escuro" themeForDarkMode: "Temas usados no Modo Noturno"
light: "Claro" light: "Claro"
dark: "Escuro" dark: "Escuro"
lightThemes: "Tema claro" lightThemes: "Tema claro"
@ -271,7 +292,7 @@ darkThemes: "Tema escuro"
syncDeviceDarkMode: "Sincronize com o modo escuro do dispositivo" syncDeviceDarkMode: "Sincronize com o modo escuro do dispositivo"
drive: "Unidades" drive: "Unidades"
fileName: "Nome do Ficheiro" fileName: "Nome do Ficheiro"
selectFile: "Selecione os arquivos" selectFile: "Selecione o arquivo"
selectFiles: "Selecione os arquivos" selectFiles: "Selecione os arquivos"
selectFolder: "Selecionar uma pasta" selectFolder: "Selecionar uma pasta"
selectFolders: "Selecionar uma pasta" selectFolders: "Selecionar uma pasta"
@ -286,8 +307,9 @@ emptyFolder: "A pasta está vazia"
unableToDelete: "Não é possível eliminar" unableToDelete: "Não é possível eliminar"
inputNewFileName: "Por favor, digite um novo nome para a pasta!" inputNewFileName: "Por favor, digite um novo nome para a pasta!"
inputNewDescription: "Insira uma nova legenda" inputNewDescription: "Insira uma nova legenda"
inputNewFolderName: "Por favor, digite um novo nome para a pasta!" inputNewFolderName: "Por favor, digite um novo nome para a pasta"
circularReferenceFolder: "A pasta de destino é uma subpasta da pasta que você deseja mover." circularReferenceFolder: "A pasta de destino é uma subpasta da pasta que você deseja
mover."
hasChildFilesOrFolders: "Esta pasta não está vazia e não pode ser excluída." hasChildFilesOrFolders: "Esta pasta não está vazia e não pode ser excluída."
copyUrl: "Copiar URL" copyUrl: "Copiar URL"
rename: "Renomear" rename: "Renomear"
@ -321,7 +343,8 @@ connectService: "Conectar"
disconnectService: "Desconectar" 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 e moderadores ainda poderão usá-las por conveniência." disablingTimelinesInfo: "Se você desabilitar essas linhas do tempo, administradores
e moderadores ainda poderão usá-las por conveniência."
registration: "Registar" registration: "Registar"
enableRegistration: "Permitir que qualquer pessoa se registre" enableRegistration: "Permitir que qualquer pessoa se registre"
invite: "Convidar" invite: "Convidar"
@ -333,9 +356,11 @@ bannerUrl: "URL da imagem do banner"
backgroundImageUrl: "URL da imagem de fundo" backgroundImageUrl: "URL da imagem de fundo"
basicInfo: "Informações básicas" basicInfo: "Informações básicas"
pinnedUsers: "Utilizador fixado" pinnedUsers: "Utilizador fixado"
pinnedUsersDescription: "Descreva os utilizadores que você deseja fixar na página \"Localizar\", etc., separados por quebras de linha." pinnedUsersDescription: "Descreva os utilizadores que você deseja fixar na página
\"Localizar\", etc., separados por quebras de linha."
pinnedPages: "Página fixada" pinnedPages: "Página fixada"
pinnedPagesDescription: "Descreva o caminho da página que você deseja fixar na página superior da instância, separada por quebras de linha." pinnedPagesDescription: "Descreva o caminho da página que você deseja fixar na página
superior da instância, separada por quebras de linha."
pinnedClipId: "ID do clipe a ser fixado" pinnedClipId: "ID do clipe a ser fixado"
pinnedNotes: "Post fixado" pinnedNotes: "Post fixado"
hcaptcha: "hCaptcha" hcaptcha: "hCaptcha"
@ -346,18 +371,21 @@ recaptcha: "reCAPTCHA"
enableRecaptcha: "Habilitar reCAPTCHA" enableRecaptcha: "Habilitar reCAPTCHA"
recaptchaSiteKey: "Chave do sítio web" recaptchaSiteKey: "Chave do sítio web"
recaptchaSecretKey: "Chave secreta" recaptchaSecretKey: "Chave secreta"
avoidMultiCaptchaConfirm: "O uso de vários captchas pode causar interferência. Deseja desativar outros captchas? Você também pode cancelar e deixar vários captchas ativados." avoidMultiCaptchaConfirm: "O uso de vários captchas pode causar interferência. Deseja
desativar outros captchas? Você também pode cancelar e deixar vários captchas ativados."
antennas: "Antenas" antennas: "Antenas"
manageAntennas: "Gestão de antena" manageAntennas: "Gestão de antena"
name: "Nome" name: "Nome"
antennaSource: "Origem de entrada" antennaSource: "Origem de entrada"
antennaKeywords: "Palavras-chave recebidas" antennaKeywords: "Palavras-chave recebidas"
antennaExcludeKeywords: "Palavras-chave negativas" antennaExcludeKeywords: "Palavras-chave negativas"
antennaKeywordsDescription: "Se você separá-lo com um espaço, será uma especificação AND, e se você separá-lo com uma quebra de linha, será uma especificação OR." antennaKeywordsDescription: "Se você separá-lo com um espaço, será uma especificação
AND, e se você separá-lo com uma quebra de linha, será uma especificação OR."
notifyAntenna: "Notificar novas notas" notifyAntenna: "Notificar novas notas"
withFileAntenna: "Apenas notas com arquivos anexados" withFileAntenna: "Apenas notas com arquivos anexados"
enableServiceworker: "Ative as notificações push para o seu navegador" enableServiceworker: "Ative as notificações push para o seu navegador"
antennaUsersDescription: "Especificar nomes de utilizador separados por quebras de linha" antennaUsersDescription: "Especificar nomes de utilizador separados por quebras de
linha"
caseSensitive: "Maiúsculas e minúsculas" caseSensitive: "Maiúsculas e minúsculas"
withReplies: "Incluindo resposta" withReplies: "Incluindo resposta"
connectedTo: "Você está conectado à seguinte conta" connectedTo: "Você está conectado à seguinte conta"
@ -433,15 +461,19 @@ showFeaturedNotesInTimeline: "Mostrar notas recomendadas na linha do tempo"
objectStorage: "Armazenamento de objetos" objectStorage: "Armazenamento de objetos"
useObjectStorage: "Usar armazenamento de objetos" useObjectStorage: "Usar armazenamento de objetos"
objectStorageBaseUrl: "URL base" objectStorageBaseUrl: "URL base"
objectStorageBaseUrlDesc: "O URL usado para referência. Se você estiver usando um CDN ou Proxy, seu URL, S3:'https: // <bucket> .s3.amazonaws.com', GCS, etc .:'https://storage.googleapis.com/ <bucket>' ." objectStorageBaseUrlDesc: "O URL usado para referência. Se você estiver usando um
CDN ou Proxy, seu URL, S3:'https: // <bucket> .s3.amazonaws.com', GCS, etc .:'https://storage.googleapis.com/
<bucket>' ."
objectStorageBucket: "Bucket" objectStorageBucket: "Bucket"
objectStorageBucketDesc: "Especifique o nome do bucket do serviço a ser usado." objectStorageBucketDesc: "Especifique o nome do bucket do serviço a ser usado."
objectStoragePrefix: "Prefixo" objectStoragePrefix: "Prefixo"
objectStoragePrefixDesc: "Ele é armazenado neste diretório de prefixo." objectStoragePrefixDesc: "Ele é armazenado neste diretório de prefixo."
objectStorageEndpoint: "Ponto final" objectStorageEndpoint: "Ponto final"
objectStorageEndpointDesc: "Especifique vazio para S3, caso contrário, especifique o ponto final para cada serviço. Especifique como'<host>'ou'<host>: <port>'." objectStorageEndpointDesc: "Especifique vazio para S3, caso contrário, especifique
o ponto final para cada serviço. Especifique como'<host>'ou'<host>: <port>'."
objectStorageRegion: "Região" objectStorageRegion: "Região"
objectStorageRegionDesc: "Especifique uma região como 'xx-east-1'. Caso seu serviço não tenha o conceito de região, ele deve estar vazio ou 'us-east-1'." objectStorageRegionDesc: "Especifique uma região como 'xx-east-1'. Caso seu serviço
não tenha o conceito de região, ele deve estar vazio ou 'us-east-1'."
objectStorageUseSSL: "Usar SSL" objectStorageUseSSL: "Usar SSL"
objectStorageUseSSLDesc: "Desative-o se não quiser usar https para conexões de API" objectStorageUseSSLDesc: "Desative-o se não quiser usar https para conexões de API"
objectStorageUseProxy: "Usar proxy" objectStorageUseProxy: "Usar proxy"
@ -449,7 +481,8 @@ objectStorageUseProxyDesc: "Se você não usa proxy para conexão de API, desati
objectStorageSetPublicRead: "Definir 'public-read' ao fazer o upload" objectStorageSetPublicRead: "Definir 'public-read' ao fazer o upload"
serverLogs: "Registro do servidor" serverLogs: "Registro do servidor"
deleteAll: "Apagar Tudo" deleteAll: "Apagar Tudo"
showFixedPostForm: "Exibir o formulário de postagem na parte superior da linha do tempo" showFixedPostForm: "Exibir o formulário de postagem na parte superior da linha do
tempo"
newNoteRecived: "Nova nota recebida" newNoteRecived: "Nova nota recebida"
sounds: "Sons" sounds: "Sons"
listen: "Ouvir" listen: "Ouvir"
@ -618,7 +651,8 @@ _pages:
_dailyRannum: _dailyRannum:
arg1: "Valor mínimo" arg1: "Valor mínimo"
arg2: "Valor máximo" arg2: "Valor máximo"
dailyRandomPick: "Escolher aleatoriamente de uma lista (Muda uma vez por dia para cada usuário)" dailyRandomPick: "Escolher aleatoriamente de uma lista (Muda uma vez por dia
para cada usuário)"
_dailyRandomPick: _dailyRandomPick:
arg1: "Listas" arg1: "Listas"
seedRandom: "Aleatório (com semente)" seedRandom: "Aleatório (com semente)"
@ -634,7 +668,8 @@ _pages:
_seedRandomPick: _seedRandomPick:
arg1: "Semente" arg1: "Semente"
arg2: "Listas" arg2: "Listas"
DRPWPM: "Escolher aleatoriamente de uma lista ponderada (Muda uma vez por dia para cada usuário)" DRPWPM: "Escolher aleatoriamente de uma lista ponderada (Muda uma vez por dia
para cada usuário)"
_DRPWPM: _DRPWPM:
arg1: "Lista de texto" arg1: "Lista de texto"
pick: "Escolhe a partir da lista" pick: "Escolhe a partir da lista"
@ -665,7 +700,8 @@ _pages:
_for: _for:
arg1: "Número de repetições" arg1: "Número de repetições"
arg2: "Ação" arg2: "Ação"
typeError: "Espaço {slot} aceita valores de tipo \"{expect}\", mas o valor dado é do tipo \"{actual}\"!" typeError: "Espaço {slot} aceita valores de tipo \"{expect}\", mas o valor dado
é do tipo \"{actual}\"!"
thereIsEmptySlot: "O espaço {slot} está vazio!" thereIsEmptySlot: "O espaço {slot} está vazio!"
types: types:
string: "Texto" string: "Texto"
@ -730,3 +766,5 @@ _deck:
list: "Listas" list: "Listas"
mentions: "Menções" mentions: "Menções"
direct: "Notas diretas" direct: "Notas diretas"
editNote: Editar post
edited: Editado a {date} às {time}

View file

@ -1 +1 @@
9ea14ceed6ec996fcfe139e5835e033895ca84f5 d9f7e2bede4f0715810b72374d181ba283cae0d5

View file

@ -1,16 +1,16 @@
{ {
"name": "firefish", "name": "firefish",
"version": "1.0.5-dev7", "version": "1.0.5-dev11",
"codename": "aqua", "codename": "aqua",
"repository": { "repository": {
"type": "git", "type": "git",
"url": "https://code.naskya.net/naskya/firefish" "url": "https://code.naskya.net/naskya/firefish"
}, },
"packageManager": "pnpm@8.7.0", "packageManager": "pnpm@8.7.1",
"private": true, "private": true,
"scripts": { "scripts": {
"rebuild": "pnpm run clean && pnpm node ./scripts/build-greet.js && pnpm -r --parallel run build && pnpm run gulp", "rebuild": "pnpm run clean && ./scripts/build-greet.sh && pnpm -r --parallel run build && pnpm run gulp",
"build": "pnpm node ./scripts/build-greet.js && pnpm -r --parallel run build && pnpm run gulp", "build": "./scripts/build-greet.sh && pnpm -r --parallel run build && pnpm run gulp",
"start": "pnpm --filter backend run start", "start": "pnpm --filter backend run start",
"start:test": "pnpm --filter backend run start:test", "start:test": "pnpm --filter backend run start:test",
"init": "pnpm run migrate", "init": "pnpm run migrate",
@ -33,18 +33,19 @@
"chokidar": "^3.3.1" "chokidar": "^3.3.1"
}, },
"dependencies": { "dependencies": {
"@bull-board/api": "5.7.2", "@bull-board/api": "5.8.0",
"@bull-board/ui": "5.7.2", "@bull-board/ui": "5.8.0",
"@napi-rs/cli": "^2.16.2", "@napi-rs/cli": "^2.16.2",
"@tensorflow/tfjs": "^3.21.0", "@tensorflow/tfjs": "^4.10.0",
"js-yaml": "4.1.0", "js-yaml": "4.1.0",
"seedrandom": "^3.0.5" "seedrandom": "^3.0.5"
}, },
"devDependencies": { "devDependencies": {
"@biomejs/biome": "1.0.0",
"@types/gulp": "4.0.13", "@types/gulp": "4.0.13",
"@types/gulp-rename": "2.0.2", "@types/gulp-rename": "2.0.2",
"@types/node": "20.4.9", "@types/node": "20.5.8",
"chalk": "4.1.2", "add": "2.0.6",
"cross-env": "7.0.3", "cross-env": "7.0.3",
"execa": "5.1.1", "execa": "5.1.1",
"gulp": "4.0.2", "gulp": "4.0.2",
@ -53,7 +54,7 @@
"gulp-replace": "1.1.4", "gulp-replace": "1.1.4",
"gulp-terser": "2.1.0", "gulp-terser": "2.1.0",
"install-peers": "^1.0.4", "install-peers": "^1.0.4",
"rome": "^12.1.3", "pnpm": "8.7.1",
"typescript": "5.1.6" "typescript": "5.2.2"
} }
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 124 KiB

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.8 KiB

After

Width:  |  Height:  |  Size: 8 KiB

View file

@ -6,6 +6,7 @@ mod m20230531_180824_drop_reversi;
mod m20230627_185451_index_note_url; 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;
pub struct Migrator; pub struct Migrator;
@ -17,6 +18,7 @@ impl MigratorTrait for Migrator {
Box::new(m20230627_185451_index_note_url::Migration), Box::new(m20230627_185451_index_note_url::Migration),
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),
] ]
} }
} }

View file

@ -0,0 +1,74 @@
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(User::Table)
.add_column(
ColumnDef::new(User::IsIndexable)
.boolean()
.not_null()
.default(true),
)
.to_owned(),
)
.await?;
manager
.alter_table(
Table::alter()
.table(UserProfile::Table)
.add_column(
ColumnDef::new(UserProfile::IsIndexable)
.boolean()
.not_null()
.default(true),
)
.to_owned(),
)
.await?;
Ok(())
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.alter_table(
Table::alter()
.table(User::Table)
.drop_column(User::IsIndexable)
.to_owned(),
)
.await?;
manager
.alter_table(
Table::alter()
.table(UserProfile::Table)
.drop_column(UserProfile::IsIndexable)
.to_owned(),
)
.await?;
Ok(())
}
}
/// Learn more at https://docs.rs/sea-query#iden
#[derive(Iden)]
enum User {
Table,
#[iden = "isIndexable"]
IsIndexable,
}
#[derive(Iden)]
enum UserProfile {
Table,
#[iden = "isIndexable"]
IsIndexable,
}

View file

@ -41,7 +41,7 @@
"prepublishOnly": "napi prepublish -t npm", "prepublishOnly": "napi prepublish -t npm",
"universal": "napi universal", "universal": "napi universal",
"version": "napi version", "version": "napi version",
"format": "cargo fmt --all", "format": "cargo fmt --all -- --check",
"lint": "cargo clippy --fix" "lint": "cargo clippy --fix --allow-dirty --allow-staged && cargo fmt --all -- --check"
} }
} }

View file

@ -74,6 +74,8 @@ pub struct Model {
pub also_known_as: Option<String>, pub also_known_as: Option<String>,
#[sea_orm(column_name = "speakAsCat")] #[sea_orm(column_name = "speakAsCat")]
pub speak_as_cat: bool, pub speak_as_cat: bool,
#[sea_orm(column_name = "isIndexable")]
pub is_indexable: bool,
} }
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]

View file

@ -75,6 +75,8 @@ pub struct Model {
pub moderation_note: String, pub moderation_note: String,
#[sea_orm(column_name = "preventAiLearning")] #[sea_orm(column_name = "preventAiLearning")]
pub prevent_ai_learning: bool, pub prevent_ai_learning: bool,
#[sea_orm(column_name = "isIndexable")]
pub is_indexable: bool,
} }
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]

File diff suppressed because one or more lines are too long

View file

@ -16,17 +16,17 @@
"build": "pnpm swc src -d built -D", "build": "pnpm swc src -d built -D",
"build:debug": "pnpm swc src -d built -s -D", "build:debug": "pnpm swc src -d built -s -D",
"watch": "pnpm swc src -d built -D -w", "watch": "pnpm swc src -d built -D -w",
"lint": "pnpm rome check --apply *", "lint": "pnpm biome check --apply **/*.ts ; pnpm run format",
"format": "pnpm rome format * --write" "format": "pnpm biome format * --write"
}, },
"optionalDependencies": { "optionalDependencies": {
"@swc/core-android-arm64": "1.3.11", "@swc/core-android-arm64": "1.3.11",
"@tensorflow/tfjs-node": "3.21.1" "@tensorflow/tfjs-node": "3.21.1"
}, },
"dependencies": { "dependencies": {
"@bull-board/api": "5.7.2", "@bull-board/api": "5.8.0",
"@bull-board/koa": "5.7.2", "@bull-board/koa": "5.8.0",
"@bull-board/ui": "5.7.2", "@bull-board/ui": "5.8.0",
"@discordapp/twemoji": "14.1.2", "@discordapp/twemoji": "14.1.2",
"@elastic/elasticsearch": "7.17.0", "@elastic/elasticsearch": "7.17.0",
"@koa/cors": "3.4.3", "@koa/cors": "3.4.3",
@ -39,17 +39,16 @@
"@tensorflow/tfjs": "^4.2.0", "@tensorflow/tfjs": "^4.2.0",
"adm-zip": "^0.5.10", "adm-zip": "^0.5.10",
"ajv": "8.12.0", "ajv": "8.12.0",
"archiver": "5.3.1", "archiver": "6.0.0",
"argon2": "^0.30.3", "argon2": "^0.31.1",
"autolinker": "4.0.0", "autolinker": "4.0.0",
"autwh": "0.1.0", "autwh": "0.1.0",
"aws-sdk": "2.1413.0", "aws-sdk": "2.1413.0",
"axios": "^1.4.0", "axios": "^1.4.0",
"bcryptjs": "2.4.3", "bcryptjs": "2.4.3",
"blurhash": "2.0.5", "blurhash": "2.0.5",
"bull": "4.11.2", "bull": "4.11.3",
"cacheable-lookup": "TheEssem/cacheable-lookup", "cacheable-lookup": "TheEssem/cacheable-lookup",
"cbor": "8.1.0",
"chalk": "5.3.0", "chalk": "5.3.0",
"chalk-template": "0.4.0", "chalk-template": "0.4.0",
"chokidar": "^3.5.3", "chokidar": "^3.5.3",
@ -61,19 +60,19 @@
"deep-email-validator": "0.1.21", "deep-email-validator": "0.1.21",
"escape-regexp": "0.0.1", "escape-regexp": "0.0.1",
"feed": "4.2.2", "feed": "4.2.2",
"file-type": "17.1.6", "file-type": "18.5.0",
"firefish-js": "workspace:*", "firefish-js": "workspace:*",
"fluent-ffmpeg": "2.1.2", "fluent-ffmpeg": "2.1.2",
"got": "12.5.3", "got": "13.0.0",
"gunzip-maybe": "^1.4.2", "gunzip-maybe": "^1.4.2",
"hpagent": "0.1.2", "hpagent": "1.2.0",
"ioredis": "5.3.2", "ioredis": "5.3.2",
"ip-cidr": "3.1.0", "ip-cidr": "3.1.0",
"is-svg": "4.3.2", "is-svg": "5.0.0",
"js-yaml": "4.1.0", "js-yaml": "4.1.0",
"jsdom": "20.0.3", "jsdom": "22.1.0",
"json5": "2.2.3", "json5": "2.2.3",
"jsonld": "8.2.0", "jsonld": "8.2.1",
"jsrsasign": "10.8.6", "jsrsasign": "10.8.6",
"koa": "2.14.2", "koa": "2.14.2",
"koa-body": "^6.0.1", "koa-body": "^6.0.1",
@ -87,10 +86,10 @@
"koa-slow": "2.1.0", "koa-slow": "2.1.0",
"koa-views": "7.0.2", "koa-views": "7.0.2",
"megalodon": "workspace:*", "megalodon": "workspace:*",
"meilisearch": "0.33.0", "meilisearch": "0.34.1",
"mfm-js": "0.23.3", "mfm-js": "0.23.3",
"mime-types": "2.1.35", "mime-types": "2.1.35",
"msgpackr": "1.9.6", "msgpackr": "1.9.7",
"multer": "1.4.4-lts.1", "multer": "1.4.4-lts.1",
"native-utils": "link:native-utils", "native-utils": "link:native-utils",
"nested-property": "4.0.0", "nested-property": "4.0.0",
@ -102,19 +101,18 @@
"os-utils": "0.0.14", "os-utils": "0.0.14",
"otpauth": "^9.1.4", "otpauth": "^9.1.4",
"parse5": "7.1.2", "parse5": "7.1.2",
"pg": "8.11.2", "pg": "8.11.3",
"private-ip": "2.3.4", "private-ip": "3.0.1",
"probe-image-size": "7.2.3", "probe-image-size": "7.2.3",
"promise-limit": "2.7.0", "promise-limit": "2.7.0",
"punycode": "2.3.0", "punycode": "2.3.0",
"pureimage": "0.3.15", "pureimage": "0.4.8",
"qrcode": "1.5.3", "qrcode": "1.5.3",
"qs": "6.11.2", "qs": "6.11.2",
"random-seed": "0.3.0", "random-seed": "0.3.0",
"ratelimiter": "3.4.1", "ratelimiter": "3.4.1",
"re2": "1.20.1", "re2": "1.20.3",
"redis-lock": "0.1.4", "redis-semaphore": "5.5.0",
"redis-semaphore": "5.4.0",
"reflect-metadata": "0.1.13", "reflect-metadata": "0.1.13",
"rename": "1.0.4", "rename": "1.0.4",
"rndstr": "1.0.0", "rndstr": "1.0.0",
@ -122,12 +120,12 @@
"sanitize-html": "2.11.0", "sanitize-html": "2.11.0",
"seedrandom": "^3.0.5", "seedrandom": "^3.0.5",
"semver": "7.5.4", "semver": "7.5.4",
"sharp": "0.32.4", "sharp": "0.32.5",
"sonic-channel": "^1.3.1", "sonic-channel": "^1.3.1",
"stringz": "2.1.0", "stringz": "2.1.0",
"summaly": "2.7.0", "summaly": "2.7.0",
"syslog-pro": "1.0.0", "syslog-pro": "1.0.0",
"systeminformation": "5.18.13", "systeminformation": "5.21.3",
"tar-stream": "^3.1.6", "tar-stream": "^3.1.6",
"tesseract.js": "^4.1.1", "tesseract.js": "^4.1.1",
"tinycolor2": "1.6.0", "tinycolor2": "1.6.0",
@ -136,7 +134,7 @@
"typeorm": "0.3.17", "typeorm": "0.3.17",
"ulid": "2.3.0", "ulid": "2.3.0",
"uuid": "9.0.0", "uuid": "9.0.0",
"web-push": "3.6.4", "web-push": "3.6.5",
"websocket": "1.0.34", "websocket": "1.0.34",
"xev": "3.0.2" "xev": "3.0.2"
}, },
@ -145,7 +143,6 @@
"@swc/core": "^1.3.75", "@swc/core": "^1.3.75",
"@types/adm-zip": "^0.5.0", "@types/adm-zip": "^0.5.0",
"@types/bcryptjs": "2.4.2", "@types/bcryptjs": "2.4.2",
"@types/cbor": "6.0.0",
"@types/escape-regexp": "0.0.1", "@types/escape-regexp": "0.0.1",
"@types/fluent-ffmpeg": "2.1.21", "@types/fluent-ffmpeg": "2.1.21",
"@types/js-yaml": "4.0.5", "@types/js-yaml": "4.0.5",

View file

@ -19,7 +19,12 @@ const ev = new Xev();
* Init process * Init process
*/ */
export default async function () { export default async function () {
process.title = `Firefish (${cluster.isPrimary ? "master" : "worker"})`; const mode =
process.env.mode && ["web", "queue"].includes(process.env.mode)
? `(${process.env.mode})`
: "";
const type = cluster.isPrimary ? "(master)" : "(worker)";
process.title = `Firefish ${mode} ${type}`;
if (cluster.isPrimary || envOption.disableClustering) { if (cluster.isPrimary || envOption.disableClustering) {
await masterMain(); await masterMain();

View file

@ -30,40 +30,34 @@ const themeColor = chalk.hex("#31748f");
function greet() { function greet() {
if (!envOption.quiet) { if (!envOption.quiet) {
//#region Firefish logo //#region Firefish logo
const v = `v${meta.version}`;
console.log( console.log(
themeColor( themeColor(
" ▄▄▄▄▄▄▄ ▄▄▄ ▄▄▄▄▄▄ ▄▄▄▄▄▄▄ ▄▄▄▄▄▄▄ ▄▄▄ ▄▄▄▄▄▄▄ ▄▄ ▄▄ ◯ ", "██████╗ ██╗██████╗ ███████╗███████╗██╗███████╗██╗ ██╗ ○ ▄ ▄ ",
), ),
); );
console.log( console.log(
themeColor( themeColor(
"█ █ █ ▄ █ █ █ █ █ █ █ █ █ ○ ▄ ▄", "██╔════╝██║██╔══██╗██╔════╝██╔════╝██║██╔════╝██║ ██║ ⚬ █▄▄ █▄▄ ",
), ),
); );
console.log( console.log(
themeColor( themeColor(
"█ ▄▄▄█ █ █ █ █ █ ▄▄▄█ ▄▄▄█ █ ▄▄▄▄▄█ █▄█ █ ⚬ █▄▄ █▄▄ ", "█████╗ ██║██████╔╝█████╗ █████╗ ██║███████╗███████║ ▄▄▄▄▄▄ ▄ ",
), ),
); );
console.log( console.log(
themeColor( themeColor(
"█ █▄▄▄█ █ █▄▄█▄█ █▄▄▄█ █▄▄▄█ █ █▄▄▄▄▄█ █ ▄▄▄▄▄▄ ▄", "██╔══╝ ██║██╔══██╗██╔══╝ ██╔══╝ ██║╚════██║██╔══██║ █ █ █▄▄ ",
), ),
); );
console.log( console.log(
themeColor( themeColor(
"█ ▄▄▄█ █ ▄▄ █ ▄▄▄█ ▄▄▄█ █▄▄▄▄▄ █ ▄ █ █ █ █▄▄", "██║ ██║██║ ██║███████╗██║ ██║███████║██║ ██║ █ ● ● █ ",
), ),
); );
console.log( console.log(
themeColor( themeColor(
"█ █ █ █ █ █ █ █▄▄▄█ █ █ █▄▄▄▄▄█ █ █ █ █ █ ● ● █", "╚═╝ ╚═╝╚═╝ ╚═╝╚══════╝╚═╝ ╚═╝╚══════╝╚═╝ ╚═╝ ▀▄▄▄▄▄▄▀ ",
),
);
console.log(
themeColor(
"█▄▄▄█ █▄▄▄█▄▄▄█ █▄█▄▄▄▄▄▄▄█▄▄▄█ █▄▄▄█▄▄▄▄▄▄▄█▄▄█ █▄▄█ ▀▄▄▄▄▄▄▀",
), ),
); );
//#endregion //#endregion
@ -117,7 +111,7 @@ export async function masterMain() {
bootLogger.succ("Firefish initialized"); bootLogger.succ("Firefish initialized");
if (!envOption.disableClustering) { if (!envOption.disableClustering) {
await spawnWorkers(config.clusterLimit); await spawnWorkers(config.clusterLimits);
} }
bootLogger.succ( bootLogger.succ(
@ -126,7 +120,11 @@ export async function masterMain() {
true, true,
); );
if (!envOption.noDaemons && !config.onlyQueueProcessor) { if (
!envOption.noDaemons &&
config.clusterLimits?.web &&
config.clusterLimits?.web >= 1
) {
import("../daemons/server-stats.js").then((x) => x.default()); import("../daemons/server-stats.js").then((x) => x.default());
import("../daemons/queue-stats.js").then((x) => x.default()); import("../daemons/queue-stats.js").then((x) => x.default());
import("../daemons/janitor.js").then((x) => x.default()); import("../daemons/janitor.js").then((x) => x.default());
@ -142,7 +140,7 @@ function showEnvironment(): void {
if (env !== "production") { if (env !== "production") {
logger.warn("The environment is not in production mode."); logger.warn("The environment is not in production mode.");
logger.warn("DO NOT USE FOR PRODUCTION PURPOSE!", null, true); logger.warn("DO NOT USE THIS IN PRODUCTION!", null, true);
} }
} }
@ -200,19 +198,35 @@ async function connectDb(): Promise<void> {
} }
} }
async function spawnWorkers(limit = 1) { async function spawnWorkers(
const workers = Math.min(limit, os.cpus().length); clusterLimits: Required<Config["clusterLimits"]>,
bootLogger.info(`Starting ${workers} worker${workers === 1 ? "" : "s"}...`); ): Promise<void> {
await Promise.all([...Array(workers)].map(spawnWorker)); const modes = ["web", "queue"];
const cpus = os.cpus().length;
for (const mode of modes.filter((mode) => clusterLimits[mode] > cpus)) {
bootLogger.warn(
`configuration warning: cluster limit for ${mode} exceeds number of cores (${cpus})`,
);
}
const total = modes.reduce((acc, mode) => acc + clusterLimits[mode], 0);
const workers = new Array(total);
workers.fill("web", 0, clusterLimits?.web);
workers.fill("queue", clusterLimits?.web);
bootLogger.info(
`Starting ${clusterLimits?.web} web workers and ${clusterLimits?.queue} queue workers (total ${total})...`,
);
await Promise.all(workers.map((mode) => spawnWorker(mode)));
bootLogger.succ("All workers started"); bootLogger.succ("All workers started");
} }
function spawnWorker(): Promise<void> { function spawnWorker(mode: "web" | "queue"): Promise<void> {
return new Promise((res) => { return new Promise((res) => {
const worker = cluster.fork(); const worker = cluster.fork({ mode });
worker.on("message", (message) => { worker.on("message", (message) => {
if (message === "listenFailed") { if (message === "listenFailed") {
bootLogger.error("The server Listen failed due to the previous error."); bootLogger.error("The server listen failed due to the previous error.");
process.exit(1); process.exit(1);
} }
if (message !== "ready") return; if (message !== "ready") return;

View file

@ -1,6 +1,7 @@
import cluster from "node:cluster"; import cluster from "node:cluster";
import { initDb } from "../db/postgre.js"; import { initDb } from "../db/postgre.js";
import config from "@/config/index.js"; import config from "@/config/index.js";
import os from "node:os";
/** /**
* Init worker process * Init worker process
@ -8,13 +9,20 @@ import config from "@/config/index.js";
export async function workerMain() { export async function workerMain() {
await initDb(); await initDb();
if (!config.onlyQueueProcessor) { if (!process.env.mode || process.env.mode === "web") {
// start server // start server
await import("../server/index.js").then((x) => x.default()); await import("../server/index.js").then((x) => x.default());
} }
// start job queue if (!process.env.mode || process.env.mode === "queue") {
import("../queue/index.js").then((x) => x.default()); // start job queue
import("../queue/index.js").then((x) => x.default());
if (process.env.mode === "queue") {
// if this is an exclusive queue worker, renice to have higher priority
os.setPriority(os.constants.priority.PRIORITY_BELOW_NORMAL);
}
}
if (cluster.isWorker) { if (cluster.isWorker) {
// Send a 'ready' message to parent process // Send a 'ready' message to parent process

View file

@ -59,6 +59,23 @@ export default function load() {
if (config.cacheServer && !config.cacheServer.prefix) if (config.cacheServer && !config.cacheServer.prefix)
config.cacheServer.prefix = mixin.hostname; config.cacheServer.prefix = mixin.hostname;
if (!config.clusterLimits) {
config.clusterLimits = {
web: 1,
queue: 1,
};
} else {
config.clusterLimits = {
web: 1,
queue: 1,
...config.clusterLimits,
};
if (config.clusterLimits.web! < 1 || config.clusterLimits.queue! < 1) {
throw new Error("Invalid cluster limits");
}
}
return Object.assign(config, mixin); return Object.assign(config, mixin);
} }

View file

@ -69,9 +69,10 @@ export type Source = {
accesslog?: string; accesslog?: string;
clusterLimit?: number; clusterLimits?: {
web?: number;
onlyQueueProcessor?: boolean; queue?: number;
};
cuid?: { cuid?: {
length?: number; length?: number;

View file

@ -1,33 +1,49 @@
import { redisClient } from "../db/redis.js"; import { redisClient } from "../db/redis.js";
import { promisify } from "node:util"; import { Mutex } from "redis-semaphore";
import redisLock from "redis-lock";
/** /**
* Retry delay (ms) for lock acquisition * Retry delay (ms) for lock acquisition
*/ */
const retryDelay = 100; const retryDelay = 100;
const lock: (key: string, timeout?: number) => Promise<() => void> = redisClient
? promisify(redisLock(redisClient, retryDelay))
: async () => () => {};
/** /**
* Get AP Object lock * Get AP Object lock
* @param uri AP object ID * @param uri AP object ID
* @param timeout Lock timeout (ms), The timeout releases previous lock. * @param timeout Lock timeout (ms), The timeout releases previous lock.
* @returns Unlock function * @returns Unlock function
*/ */
export function getApLock(uri: string, timeout = 30 * 1000) { export async function getApLock(
return lock(`ap-object:${uri}`, timeout); uri: string,
timeout = 30 * 1000,
): Promise<Mutex> {
const lock = new Mutex(redisClient, `ap-object:${uri}`, {
lockTimeout: timeout,
retryInterval: retryDelay,
});
await lock.acquire();
return lock;
} }
export function getFetchInstanceMetadataLock( export async function getFetchInstanceMetadataLock(
host: string, host: string,
timeout = 30 * 1000, timeout = 30 * 1000,
) { ): Promise<Mutex> {
return lock(`instance:${host}`, timeout); const lock = new Mutex(redisClient, `instance:${host}`, {
lockTimeout: timeout,
retryInterval: retryDelay,
});
await lock.acquire();
return lock;
} }
export function getChartInsertLock(lockKey: string, timeout = 30 * 1000) { export async function getChartInsertLock(
return lock(`chart-insert:${lockKey}`, timeout); lockKey: string,
timeout = 30 * 1000,
): Promise<Mutex> {
const lock = new Mutex(redisClient, `chart-insert:${lockKey}`, {
lockTimeout: timeout,
retryInterval: retryDelay,
});
await lock.acquire();
return lock;
} }

View file

@ -167,6 +167,12 @@ export class UserProfile {
}) })
public noCrawle: boolean; public noCrawle: boolean;
@Column("boolean", {
default: true,
comment: "Whether User is indexable.",
})
public isIndexable: boolean;
@Column("boolean", { @Column("boolean", {
default: true, default: true,
}) })

View file

@ -278,6 +278,13 @@ export class User {
}) })
public driveCapacityOverrideMb: number | null; public driveCapacityOverrideMb: number | null;
@Index()
@Column("boolean", {
default: true,
comment: "Whether the User is indexable.",
})
public isIndexable: boolean;
constructor(data: Partial<User>) { constructor(data: Partial<User>) {
if (data == null) return; if (data == null) return;

View file

@ -455,6 +455,7 @@ export const UserRepository = db.getRepository(User).extend({
isModerator: user.isModerator || falsy, isModerator: user.isModerator || falsy,
isBot: user.isBot || falsy, isBot: user.isBot || falsy,
isLocked: user.isLocked, isLocked: user.isLocked,
isIndexable: user.isIndexable,
isCat: user.isCat || falsy, isCat: user.isCat || falsy,
speakAsCat: user.speakAsCat || falsy, speakAsCat: user.speakAsCat || falsy,
instance: user.host instance: user.host

View file

@ -66,6 +66,11 @@ export const packedUserLiteSchema = {
nullable: false, nullable: false,
optional: true, optional: true,
}, },
isIndexable: {
type: "boolean",
nullable: false,
optional: true,
},
speakAsCat: { speakAsCat: {
type: "boolean", type: "boolean",
nullable: false, nullable: false,

View file

@ -32,7 +32,7 @@ export default async function (
// Interrupt if you block the announcement destination // Interrupt if you block the announcement destination
if (await shouldBlockInstance(extractDbHost(uri))) return; if (await shouldBlockInstance(extractDbHost(uri))) return;
const unlock = await getApLock(uri); const lock = await getApLock(uri);
try { try {
// Check if something with the same URI is already registered // Check if something with the same URI is already registered
@ -60,9 +60,10 @@ export default async function (
throw e; throw e;
} }
if (!(await Notes.isVisibleForMe(renote, actor.id))) if (renote != null && !(await Notes.isVisibleForMe(renote, actor.id))) {
return "skip: invalid actor for this activity"; console.log("skip: invalid actor for this activity");
return;
}
logger.info(`Creating the (Re)Note: ${uri}`); logger.info(`Creating the (Re)Note: ${uri}`);
const activityAudience = await parseAudience( const activityAudience = await parseAudience(
@ -79,6 +80,6 @@ export default async function (
uri, uri,
}); });
} finally { } finally {
unlock(); await lock.release();
} }
} }

View file

@ -31,7 +31,7 @@ export default async function (
} }
} }
const unlock = await getApLock(uri); const lock = await getApLock(uri);
try { try {
const exist = await fetchNote(note); const exist = await fetchNote(note);
@ -46,6 +46,6 @@ export default async function (
throw e; throw e;
} }
} finally { } finally {
unlock(); await lock.release();
} }
} }

View file

@ -13,7 +13,7 @@ export default async function (
): Promise<string> { ): Promise<string> {
logger.info(`Deleting the Note: ${uri}`); logger.info(`Deleting the Note: ${uri}`);
const unlock = await getApLock(uri); const lock = await getApLock(uri);
try { try {
const dbResolver = new DbResolver(); const dbResolver = new DbResolver();
@ -39,6 +39,6 @@ export default async function (
await deleteNode(actor, note); await deleteNode(actor, note);
return "ok: note deleted"; return "ok: note deleted";
} finally { } finally {
unlock(); await lock.release();
} }
} }

View file

@ -68,13 +68,13 @@ export class LdSignature {
...options, ...options,
"@context": "https://w3id.org/identity/v1", "@context": "https://w3id.org/identity/v1",
}; };
transformedOptions.type = undefined; delete transformedOptions["type"];
transformedOptions.id = undefined; delete transformedOptions["id"];
transformedOptions.signatureValue = undefined; delete transformedOptions["signatureValue"];
const canonizedOptions = await this.normalize(transformedOptions); const canonizedOptions = await this.normalize(transformedOptions);
const optionsHash = this.sha256(canonizedOptions); const optionsHash = this.sha256(canonizedOptions);
const transformedData = { ...data }; const transformedData = { ...data };
transformedData.signature = undefined; delete transformedData["signature"];
const cannonidedData = await this.normalize(transformedData); const cannonidedData = await this.normalize(transformedData);
if (this.debug) console.debug(`cannonidedData: ${cannonidedData}`); if (this.debug) console.debug(`cannonidedData: ${cannonidedData}`);
const documentHash = this.sha256(cannonidedData); const documentHash = this.sha256(cannonidedData);

View file

@ -415,7 +415,7 @@ export async function resolveNote(
`host ${extractDbHost(uri)} is blocked`, `host ${extractDbHost(uri)} is blocked`,
); );
const unlock = await getApLock(uri); const lock = await getApLock(uri);
try { try {
//#region Returns if already registered with this server //#region Returns if already registered with this server
@ -439,7 +439,7 @@ export async function resolveNote(
// Since the attached Note Object may be disguised, always specify the uri and fetch it from the server. // Since the attached Note Object may be disguised, always specify the uri and fetch it from the server.
return await createNote(uri, resolver, true); return await createNote(uri, resolver, true);
} finally { } finally {
unlock(); await lock.release();
} }
} }

View file

@ -205,10 +205,10 @@ export async function createPerson(
if (typeof person.followers === "string") { if (typeof person.followers === "string") {
try { try {
let data = await fetch(person.followers, { const data = await fetch(person.followers, {
headers: { Accept: "application/json" }, headers: { Accept: "application/json" },
}); });
let json_data = JSON.parse(await data.text()); const json_data = JSON.parse(await data.text());
followersCount = json_data.totalItems; followersCount = json_data.totalItems;
} catch { } catch {
@ -220,10 +220,10 @@ export async function createPerson(
if (typeof person.following === "string") { if (typeof person.following === "string") {
try { try {
let data = await fetch(person.following, { const data = await fetch(person.following, {
headers: { Accept: "application/json" }, headers: { Accept: "application/json" },
}); });
let json_data = JSON.parse(await data.text()); const json_data = JSON.parse(await data.text());
followingCount = json_data.totalItems; followingCount = json_data.totalItems;
} catch (e) { } catch (e) {
@ -235,10 +235,10 @@ export async function createPerson(
if (typeof person.outbox === "string") { if (typeof person.outbox === "string") {
try { try {
let data = await fetch(person.outbox, { const data = await fetch(person.outbox, {
headers: { Accept: "application/json" }, headers: { Accept: "application/json" },
}); });
let json_data = JSON.parse(await data.text()); const json_data = JSON.parse(await data.text());
notesCount = json_data.totalItems; notesCount = json_data.totalItems;
} catch (e) { } catch (e) {
@ -302,6 +302,7 @@ export async function createPerson(
tags, tags,
isBot, isBot,
isCat: (person as any).isCat === true, isCat: (person as any).isCat === true,
isIndexable: person.indexable,
}), }),
)) as IRemoteUser; )) as IRemoteUser;
@ -547,6 +548,7 @@ export async function updatePerson(
tags, tags,
isBot: getApType(object) !== "Person", isBot: getApType(object) !== "Person",
isCat: (person as any).isCat === true, isCat: (person as any).isCat === true,
isIndexable: person.indexable,
isLocked: !!person.manuallyApprovesFollowers, isLocked: !!person.manuallyApprovesFollowers,
movedToUri: person.movedTo || null, movedToUri: person.movedTo || null,
alsoKnownAs: person.alsoKnownAs || null, alsoKnownAs: person.alsoKnownAs || null,

View file

@ -30,6 +30,7 @@ export const renderActivity = (x: any): IActivity | null => {
Emoji: "toot:Emoji", Emoji: "toot:Emoji",
featured: "toot:featured", featured: "toot:featured",
discoverable: "toot:discoverable", discoverable: "toot:discoverable",
indexable: "toot:indexable",
// schema // schema
schema: "http://schema.org#", schema: "http://schema.org#",
PropertyValue: "schema:PropertyValue", PropertyValue: "schema:PropertyValue",

View file

@ -81,6 +81,7 @@ export async function renderPerson(user: ILocalUser) {
discoverable: !!user.isExplorable, discoverable: !!user.isExplorable,
publicKey: renderKey(user, keypair, "#main-key"), publicKey: renderKey(user, keypair, "#main-key"),
isCat: user.isCat, isCat: user.isCat,
indexable: user.isIndexable,
attachment: attachment.length ? attachment : undefined, attachment: attachment.length ? attachment : undefined,
} as any; } as any;

View file

@ -190,8 +190,9 @@ export interface IActor extends IObject {
movedTo?: string; movedTo?: string;
alsoKnownAs?: string[]; alsoKnownAs?: string[];
discoverable?: boolean; discoverable?: boolean;
indexable?: boolean;
inbox: string; inbox: string;
sharedInbox?: string; // backward compatibility.. ig sharedInbox?: string; // Backwards compatibility
publicKey?: { publicKey?: {
id: string; id: string;
publicKeyPem: string; publicKeyPem: string;

View file

@ -60,6 +60,7 @@ export default define(meta, paramDef, async (ps, me) => {
emailVerified: profile.emailVerified, emailVerified: profile.emailVerified,
autoAcceptFollowed: profile.autoAcceptFollowed, autoAcceptFollowed: profile.autoAcceptFollowed,
noCrawle: profile.noCrawle, noCrawle: profile.noCrawle,
isIndexable: profile.isIndexable,
preventAiLearning: profile.preventAiLearning, preventAiLearning: profile.preventAiLearning,
alwaysMarkNsfw: profile.alwaysMarkNsfw, alwaysMarkNsfw: profile.alwaysMarkNsfw,
autoSensitive: profile.autoSensitive, autoSensitive: profile.autoSensitive,

View file

@ -1,5 +1,4 @@
import { promisify } from "node:util"; import { decode } from "msgpackr";
import * as cbor from "cbor";
import define from "../../../define.js"; import define from "../../../define.js";
import { import {
UserProfiles, UserProfiles,
@ -12,7 +11,6 @@ import { procedures, hash } from "../../../2fa.js";
import { publishMainStream } from "@/services/stream.js"; import { publishMainStream } from "@/services/stream.js";
import { comparePassword } from "@/misc/password.js"; import { comparePassword } from "@/misc/password.js";
const cborDecodeFirst = promisify(cbor.decodeFirst) as any;
const rpIdHashReal = hash(Buffer.from(config.hostname, "utf-8")); const rpIdHashReal = hash(Buffer.from(config.hostname, "utf-8"));
export const meta = { export const meta = {
@ -64,7 +62,7 @@ export default define(meta, paramDef, async (ps, user) => {
const clientDataJSONHash = hash(Buffer.from(ps.clientDataJSON, "utf-8")); const clientDataJSONHash = hash(Buffer.from(ps.clientDataJSON, "utf-8"));
const attestation = await cborDecodeFirst(ps.attestationObject); const attestation = decode(Buffer.from(ps.attestationObject, "utf-8"));
const rpIdHash = attestation.authData.slice(0, 32); const rpIdHash = attestation.authData.slice(0, 32);
if (!rpIdHashReal.equals(rpIdHash)) { if (!rpIdHashReal.equals(rpIdHash)) {
@ -81,7 +79,7 @@ export default define(meta, paramDef, async (ps, user) => {
const credentialIdLength = authData.readUInt16BE(53); const credentialIdLength = authData.readUInt16BE(53);
const credentialId = authData.slice(55, 55 + credentialIdLength); const credentialId = authData.slice(55, 55 + credentialIdLength);
const publicKeyData = authData.slice(55 + credentialIdLength); const publicKeyData = authData.slice(55 + credentialIdLength);
const publicKey: Map<number, any> = await cborDecodeFirst(publicKeyData); const publicKey: Map<number, any> = decode(publicKeyData);
if (publicKey.get(3) !== -7) { if (publicKey.get(3) !== -7) {
throw new Error("alg mismatch"); throw new Error("alg mismatch");
} }

View file

@ -120,6 +120,7 @@ export const paramDef = {
isBot: { type: "boolean" }, isBot: { type: "boolean" },
isCat: { type: "boolean" }, isCat: { type: "boolean" },
speakAsCat: { type: "boolean" }, speakAsCat: { type: "boolean" },
isIndexable: { type: "boolean" },
injectFeaturedNote: { type: "boolean" }, injectFeaturedNote: { type: "boolean" },
receiveAnnouncementEmail: { type: "boolean" }, receiveAnnouncementEmail: { type: "boolean" },
alwaysMarkNsfw: { type: "boolean" }, alwaysMarkNsfw: { type: "boolean" },
@ -206,6 +207,10 @@ export default define(meta, paramDef, async (ps, _user, token) => {
if (typeof ps.preventAiLearning === "boolean") if (typeof ps.preventAiLearning === "boolean")
profileUpdates.preventAiLearning = ps.preventAiLearning; profileUpdates.preventAiLearning = ps.preventAiLearning;
if (typeof ps.isCat === "boolean") updates.isCat = ps.isCat; if (typeof ps.isCat === "boolean") updates.isCat = ps.isCat;
if (typeof ps.isIndexable === "boolean") {
updates.isIndexable = ps.isIndexable;
profileUpdates.isIndexable = ps.isIndexable;
}
if (typeof ps.speakAsCat === "boolean") updates.speakAsCat = ps.speakAsCat; if (typeof ps.speakAsCat === "boolean") updates.speakAsCat = ps.speakAsCat;
if (typeof ps.injectFeaturedNote === "boolean") if (typeof ps.injectFeaturedNote === "boolean")
profileUpdates.injectFeaturedNote = ps.injectFeaturedNote; profileUpdates.injectFeaturedNote = ps.injectFeaturedNote;

View file

@ -608,7 +608,7 @@ export default define(meta, paramDef, async (ps, user) => {
throw new ApiError(meta.errors.noSuchNote); throw new ApiError(meta.errors.noSuchNote);
} }
if (publishing) { if (publishing && user.isIndexable) {
index(note, true); index(note, true);
// Publish update event for the updated note details // Publish update event for the updated note details

View file

@ -4,7 +4,6 @@ import config from "@/config/index.js";
import { Converter } from "opencc-js"; import { Converter } from "opencc-js";
import { getAgentByUrl } from "@/misc/fetch.js"; import { getAgentByUrl } from "@/misc/fetch.js";
import { fetchMeta } from "@/misc/fetch-meta.js"; import { fetchMeta } from "@/misc/fetch-meta.js";
import { Notes } from "@/models/index.js";
import { ApiError } from "../../error.js"; import { ApiError } from "../../error.js";
import { getNote } from "../../common/getters.js"; import { getNote } from "../../common/getters.js";
import define from "../../define.js"; import define from "../../define.js";
@ -12,7 +11,7 @@ import define from "../../define.js";
export const meta = { export const meta = {
tags: ["notes"], tags: ["notes"],
requireCredential: false, requireCredential: true,
requireCredentialPrivateMode: true, requireCredentialPrivateMode: true,
res: { res: {

View file

@ -3,10 +3,11 @@
"name": "Firefish", "name": "Firefish",
"description": "An open source, decentralized social media platform that's free forever!", "description": "An open source, decentralized social media platform that's free forever!",
"start_url": "/", "start_url": "/",
"scope": "/",
"display": "standalone", "display": "standalone",
"background_color": "#1f1d2e", "background_color": "#1f1d2e",
"theme_color": "#31748f", "theme_color": "#31748f",
"orientation": "portrait-primary", "orientation": "natural",
"icons": [ "icons": [
{ {
"src": "/static-assets/icons/192.png", "src": "/static-assets/icons/192.png",

View file

@ -11,7 +11,6 @@ export const manifestHandler = async (ctx: Koa.Context) => {
const instance = await fetchMeta(true); const instance = await fetchMeta(true);
res.short_name = instance.name || "Firefish"; res.short_name = instance.name || "Firefish";
res.name = instance.name || "Firefish";
if (instance.themeColor) res.theme_color = instance.themeColor; if (instance.themeColor) res.theme_color = instance.themeColor;
for (const icon of res.icons) { for (const icon of res.icons) {
icon.src = `${icon.src}?v=${config.version.replace(/[^0-9]/g, "")}`; icon.src = `${icon.src}?v=${config.version.replace(/[^0-9]/g, "")}`;

View file

@ -7,16 +7,16 @@ doctype html
// //
- -
▄▄▄▄▄▄▄ ▄▄▄ ▄▄▄▄▄▄ ▄▄▄▄▄▄▄ ▄▄▄▄▄▄▄ ▄▄▄ ▄▄▄▄▄▄▄ ▄▄ ▄▄ ◯
█ █ █ ▄ █ █ █ █ █ █ █ █ █ ○ ▄ ▄ ██████╗ ██╗██████╗ ███████╗███████╗██╗███████╗██╗ ██╗ ○ ▄ ▄
█ ▄▄▄█ █ █ █ █ █ ▄▄▄█ ▄▄▄█ █ ▄▄▄▄▄█ █▄█ █ ⚬ █▄▄ █▄▄ ██╔════╝██║██╔══██╗██╔════╝██╔════╝██║██╔════╝██║ ██║ ⚬ █▄▄ █▄▄
█ █▄▄▄█ █ █▄▄█▄█ █▄▄▄█ █▄▄▄█ █ █▄▄▄▄▄█ █ ▄▄▄▄▄▄ ▄ █████╗ ██║██████╔╝█████╗ █████╗ ██║███████╗███████║ ▄▄▄▄▄▄ ▄
█ ▄▄▄█ █ ▄▄ █ ▄▄▄█ ▄▄▄█ █▄▄▄▄▄ █ ▄ █ █ █ █▄▄ ██╔══╝ ██║██╔══██╗██╔══╝ ██╔══╝ ██║╚════██║██╔══██║ █ █ █▄▄
█ █ █ █ █ █ █ █▄▄▄█ █ █ █▄▄▄▄▄█ █ █ █ █ █ ● ● █ ██║ ██║██║ ██║███████╗██║ ██║███████║██║ ██║ █ ● ● █
█▄▄▄█ █▄▄▄█▄▄▄█ █▄█▄▄▄▄▄▄▄█▄▄▄█ █▄▄▄█▄▄▄▄▄▄▄█▄▄█ █▄▄█ ▀▄▄▄▄▄▄▀ ╚═╝ ╚═╝╚═╝ ╚═╝╚══════╝╚═╝ ╚═╝╚══════╝╚═╝ ╚═╝ ▀▄▄▄▄▄▄▀
Thank you for using Firefish! Thank you for using Firefish!
If you are reading this message... how about joining the development? If you're reading this message... how about helping out with development?
https://git.joinfirefish.org/firefish/firefish https://git.joinfirefish.org/firefish/firefish
html html

View file

@ -24,9 +24,11 @@ block meta
unless privateMode unless privateMode
if profile.noCrawle if profile.noCrawle
meta(name='robots' content='noindex,nofollow,noarchive,nocache,noimageindex') meta(name='robots' content='noindex,nofollow,noarchive,nocache,noimageindex')
if profile.preventAiLearning if profile.preventAiLearning
meta(name='robots' content='noai') meta(name='robots' content='noai')
meta(name='robots' content='noimageai') meta(name='robots' content='noimageai')
meta(name='GPTBot' content='noindex')
meta(name='misskey:user-username' content=user.username) meta(name='misskey:user-username' content=user.username)
meta(name='misskey:user-id' content=user.id) meta(name='misskey:user-id' content=user.id)

View file

@ -430,7 +430,7 @@ export default abstract class Chart<T extends Schema> {
? `${this.name}:${date}:${span}:${group}` ? `${this.name}:${date}:${span}:${group}`
: `${this.name}:${date}:${span}`; : `${this.name}:${date}:${span}`;
const unlock = await getChartInsertLock(lockKey); const lock = await getChartInsertLock(lockKey);
try { try {
// ロック内でもう1回チェックする // ロック内でもう1回チェックする
const currentLog = (await repository.findOneBy({ const currentLog = (await repository.findOneBy({
@ -466,7 +466,7 @@ export default abstract class Chart<T extends Schema> {
return log; return log;
} finally { } finally {
unlock(); await lock.release();
} }
} }

View file

@ -15,7 +15,7 @@ export async function fetchInstanceMetadata(
instance: Instance, instance: Instance,
force = false, force = false,
): Promise<void> { ): Promise<void> {
const unlock = await getFetchInstanceMetadataLock(instance.host); const lock = await getFetchInstanceMetadataLock(instance.host);
if (!force) { if (!force) {
const _instance = await Instances.findOneBy({ host: instance.host }); const _instance = await Instances.findOneBy({ host: instance.host });
@ -24,7 +24,7 @@ export async function fetchInstanceMetadata(
_instance?.infoUpdatedAt && _instance?.infoUpdatedAt &&
now - _instance.infoUpdatedAt.getTime() < 1000 * 60 * 60 * 24 now - _instance.infoUpdatedAt.getTime() < 1000 * 60 * 60 * 24
) { ) {
unlock(); await lock.release();
return; return;
} }
} }
@ -53,7 +53,7 @@ export async function fetchInstanceMetadata(
} as Record<string, any>; } as Record<string, any>;
if (info) { if (info) {
updates.softwareName = info.software?.name.toLowerCase(); updates.softwareName = info.software?.name?.toLowerCase() || null;
updates.softwareVersion = info.software?.version; updates.softwareVersion = info.software?.version;
updates.openRegistrations = info.openRegistrations; updates.openRegistrations = info.openRegistrations;
updates.maintainerName = info.metadata updates.maintainerName = info.metadata
@ -80,24 +80,24 @@ export async function fetchInstanceMetadata(
} catch (e) { } catch (e) {
logger.error(`Failed to update metadata of ${instance.host}: ${e}`); logger.error(`Failed to update metadata of ${instance.host}: ${e}`);
} finally { } finally {
unlock(); await lock.release();
} }
} }
type NodeInfo = { type NodeInfo = {
openRegistrations?: any; openRegistrations?: boolean;
software?: { software?: {
name?: any; name?: string;
version?: any; version?: string;
}; };
metadata?: { metadata?: {
name?: any; name?: string;
nodeName?: any; nodeName?: string;
nodeDescription?: any; nodeDescription?: string;
description?: any; description?: string;
maintainer?: { maintainer?: {
name?: any; name?: string;
email?: any; email?: string;
}; };
}; };
}; };

View file

@ -165,6 +165,7 @@ export default async (
createdAt: User["createdAt"]; createdAt: User["createdAt"];
isBot: User["isBot"]; isBot: User["isBot"];
inbox?: User["inbox"]; inbox?: User["inbox"];
isIndexable?: User["isIndexable"];
}, },
data: Option, data: Option,
silent = false, silent = false,
@ -652,7 +653,9 @@ export default async (
} }
// Register to search database // Register to search database
await index(note, false); if (user.isIndexable) {
await index(note, false);
}
}); });
async function renderNoteOrRenoteActivity(data: Option, note: Note) { async function renderNoteOrRenoteActivity(data: Option, note: Note) {

View file

@ -1,6 +1,7 @@
{ {
"extends": ["@eslint-sets/vue3", "@eslint-sets/vue3-ts"], "extends": ["@eslint-sets/vue3", "@eslint-sets/vue3-ts"],
"plugins": ["file-progress", "prettier"], "plugins": ["file-progress", "prettier"],
"ignorePatterns": ["**/*.json5"],
"rules": { "rules": {
"file-progress/activate": 1 "file-progress/activate": 1
} }

View file

@ -5,9 +5,9 @@
"watch": "pnpm vite build --watch --mode development", "watch": "pnpm vite build --watch --mode development",
"build": "pnpm vite build", "build": "pnpm vite build",
"build:debug": "pnpm run build", "build:debug": "pnpm run build",
"lint": "pnpm rome check **/*.ts --apply && pnpm run lint:vue", "lint": "pnpm biome check **/*.ts --apply ; pnpm run lint:vue",
"lint:vue": "pnpm paralint --ext .vue --fix '**/*.vue' --cache", "lint:vue": "pnpm eslint src --fix '**/*.vue' --cache ; pnpm run format",
"format": "pnpm rome format * --write && pnpm prettier --write '**/*.{scss,vue}' --cache --cache-strategy metadata" "format": "pnpm biome format * --write && pnpm prettier --write '**/*.{scss,vue}' --cache --cache-strategy metadata"
}, },
"devDependencies": { "devDependencies": {
"@discordapp/twemoji": "14.1.2", "@discordapp/twemoji": "14.1.2",
@ -16,7 +16,7 @@
"@phosphor-icons/web": "^2.0.3", "@phosphor-icons/web": "^2.0.3",
"@rollup/plugin-alias": "5.0.0", "@rollup/plugin-alias": "5.0.0",
"@rollup/plugin-json": "6.0.0", "@rollup/plugin-json": "6.0.0",
"@rollup/pluginutils": "^5.0.3", "@rollup/pluginutils": "^5.0.4",
"@syuilo/aiscript": "0.11.1", "@syuilo/aiscript": "0.11.1",
"@types/escape-regexp": "0.0.1", "@types/escape-regexp": "0.0.1",
"@types/glob": "8.1.0", "@types/glob": "8.1.0",
@ -28,15 +28,15 @@
"@types/seedrandom": "3.0.5", "@types/seedrandom": "3.0.5",
"@types/throttle-debounce": "5.0.0", "@types/throttle-debounce": "5.0.0",
"@types/tinycolor2": "1.4.3", "@types/tinycolor2": "1.4.3",
"@types/uuid": "9.0.2", "@types/uuid": "9.0.3",
"@vitejs/plugin-vue": "4.3.1", "@vitejs/plugin-vue": "4.3.4",
"@vue/compiler-sfc": "3.3.4", "@vue/compiler-sfc": "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",
"broadcast-channel": "5.2.0", "broadcast-channel": "5.3.0",
"browser-image-resizer": "github:misskey-dev/browser-image-resizer", "browser-image-resizer": "github:misskey-dev/browser-image-resizer",
"chart.js": "4.3.3", "chart.js": "4.4.0",
"chartjs-adapter-date-fns": "3.0.0", "chartjs-adapter-date-fns": "3.0.0",
"chartjs-chart-matrix": "^2.0.1", "chartjs-chart-matrix": "^2.0.1",
"chartjs-plugin-gradient": "0.6.1", "chartjs-plugin-gradient": "0.6.1",
@ -62,37 +62,36 @@
"katex": "0.16.8", "katex": "0.16.8",
"matter-js": "0.19.0", "matter-js": "0.19.0",
"mfm-js": "0.23.3", "mfm-js": "0.23.3",
"paralint": "^1.2.1", "photoswipe": "5.3.9",
"photoswipe": "5.3.8", "prettier": "3.0.3",
"prettier": "3.0.2",
"prettier-plugin-vue": "1.1.6", "prettier-plugin-vue": "1.1.6",
"prismjs": "1.29.0", "prismjs": "1.29.0",
"punycode": "2.3.0", "punycode": "2.3.0",
"rndstr": "1.0.0", "rndstr": "1.0.0",
"rollup": "3.28.0", "rollup": "3.28.1",
"s-age": "1.1.2", "s-age": "1.1.2",
"sass": "1.66.0", "sass": "1.66.1",
"seedrandom": "3.0.5", "seedrandom": "3.0.5",
"strict-event-emitter-types": "2.0.0", "strict-event-emitter-types": "2.0.0",
"stringz": "2.1.0", "stringz": "2.1.0",
"swiper": "10.2.0", "swiper": "10.2.0",
"syuilo-password-strength": "0.0.1", "syuilo-password-strength": "0.0.1",
"textarea-caret": "3.1.0", "textarea-caret": "3.1.0",
"three": "0.155.0", "three": "0.156.0",
"throttle-debounce": "5.0.0", "throttle-debounce": "5.0.0",
"tinycolor2": "1.6.0", "tinycolor2": "1.6.0",
"tinyld": "1.3.4", "tinyld": "^1.3.4",
"tsc-alias": "1.8.7", "tsc-alias": "1.8.7",
"tsconfig-paths": "4.2.0", "tsconfig-paths": "4.2.0",
"twemoji-parser": "14.0.0", "twemoji-parser": "14.0.0",
"typescript": "5.1.6", "typescript": "5.2.2",
"unicode-emoji-json": "^0.4.0", "unicode-emoji-json": "^0.4.0",
"uuid": "9.0.0", "uuid": "9.0.0",
"vanilla-tilt": "1.8.1", "vanilla-tilt": "1.8.1",
"vite": "4.4.9", "vite": "4.4.9",
"vite-plugin-compression": "^0.5.1", "vite-plugin-compression": "^0.5.1",
"vue": "3.3.4", "vue": "3.3.4",
"vue-draggable-plus": "^0.2.5", "vue-draggable-plus": "^0.2.6",
"vue-isyourpasswordsafe": "^2.0.0", "vue-isyourpasswordsafe": "^2.0.0",
"vue-plyr": "^7.0.0", "vue-plyr": "^7.0.0",
"vue-prism-editor": "2.0.0-alpha.2" "vue-prism-editor": "2.0.0-alpha.2"

View file

@ -1,10 +1,10 @@
import { defineAsyncComponent, reactive } from "vue"; import { defineAsyncComponent, reactive } from "vue";
import * as misskey from "firefish-js"; import type * as misskey 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 { waiting, api, popup, popupMenu, success, alert } from "@/os"; import { alert, api, popup, popupMenu, success, waiting } from "@/os";
import { unisonReload, reloadChannel } from "@/scripts/unison-reload"; import { reloadChannel, unisonReload } from "@/scripts/unison-reload";
// TODO: 他のタブと永続化されたstateを同期 // TODO: 他のタブと永続化されたstateを同期
@ -29,7 +29,7 @@ export async function signout() {
const accounts = await getAccounts(); const accounts = await getAccounts();
//#region Remove service worker registration // #region Remove service worker registration
try { try {
if (navigator.serviceWorker.controller) { if (navigator.serviceWorker.controller) {
const registration = await navigator.serviceWorker.ready; const registration = await navigator.serviceWorker.ready;
@ -53,7 +53,7 @@ export async function signout() {
}); });
} }
} catch (err) {} } catch (err) {}
//#endregion // #endregion
document.cookie = "igi=; path=/"; document.cookie = "igi=; path=/";

View file

@ -8,8 +8,10 @@
<div :class="$style.time"> <div :class="$style.time">
<MkTime :time="announcement.createdAt" /> <MkTime :time="announcement.createdAt" />
<div v-if="announcement.updatedAt"> <div v-if="announcement.updatedAt">
{{ i18n.ts.updatedAt }}: <small>
<MkTime :time="announcement.createdAt" /> {{ i18n.ts.updatedAt }}:
<MkTime :time="announcement.createdAt" />
</small>
</div> </div>
</div> </div>
<Mfm :text="text" /> <Mfm :text="text" />
@ -80,6 +82,6 @@ const gotIt = () => {
} }
.gotIt { .gotIt {
margin: 8px 0 0 0; margin: 1rem 0 1rem 2rem;
} }
</style> </style>

View file

@ -199,7 +199,7 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { onBeforeUnmount, onMounted, ref, shallowRef, computed } from "vue"; import { computed, onBeforeUnmount, onMounted, ref, shallowRef } from "vue";
import * as Acct from "firefish-js/built/acct"; import * as Acct from "firefish-js/built/acct";
import MkModal from "@/components/MkModal.vue"; import MkModal from "@/components/MkModal.vue";
import MkButton from "@/components/MkButton.vue"; import MkButton from "@/components/MkButton.vue";
@ -281,7 +281,9 @@ const modal = shallowRef<InstanceType<typeof MkModal>>();
const inputValue = ref<string | number | null>(props.input?.default ?? null); const inputValue = ref<string | number | null>(props.input?.default ?? null);
const selectedValue = ref(props.select?.default ?? null); const selectedValue = ref(props.select?.default ?? null);
let disabledReason = ref<null | "charactersExceeded" | "charactersBelow">(null); const disabledReason = ref<null | "charactersExceeded" | "charactersBelow">(
null,
);
const okButtonDisabled = computed<boolean>(() => { const okButtonDisabled = computed<boolean>(() => {
if (props.input) { if (props.input) {
if (props.input.minLength) { if (props.input.minLength) {

View file

@ -39,7 +39,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import { computed, defineAsyncComponent, ref } from "vue"; import { computed, defineAsyncComponent, ref } from "vue";
import * as Misskey from "firefish-js"; import type * as Misskey from "firefish-js";
import copyToClipboard from "@/scripts/copy-to-clipboard"; import copyToClipboard from "@/scripts/copy-to-clipboard";
import MkDriveFileThumbnail from "@/components/MkDriveFileThumbnail.vue"; import MkDriveFileThumbnail from "@/components/MkDriveFileThumbnail.vue";
import bytes from "@/filters/bytes"; import bytes from "@/filters/bytes";
@ -160,7 +160,7 @@ function rename() {
if (canceled) return; if (canceled) return;
os.api("drive/files/update", { os.api("drive/files/update", {
fileId: props.file.id, fileId: props.file.id,
name: name, name,
}); });
}); });
} }
@ -179,7 +179,7 @@ function describe() {
{ {
done: (result) => { done: (result) => {
if (!result || result.canceled) return; if (!result || result.canceled) return;
let comment = result.result; const comment = result.result;
os.api("drive/files/update", { os.api("drive/files/update", {
fileId: props.file.id, fileId: props.file.id,
comment: comment.length === 0 ? null : comment, comment: comment.length === 0 ? null : comment,

View file

@ -38,7 +38,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import { computed, defineAsyncComponent, ref } from "vue"; import { computed, defineAsyncComponent, ref } from "vue";
import * as Misskey from "firefish-js"; import type * as Misskey 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";
@ -131,7 +131,7 @@ function onDrop(ev: DragEvent) {
return; return;
} }
//#region // #region
const driveFile = ev.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FILE_); const driveFile = ev.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FILE_);
if (driveFile != null && driveFile !== "") { if (driveFile != null && driveFile !== "") {
const file = JSON.parse(driveFile); const file = JSON.parse(driveFile);
@ -141,9 +141,9 @@ function onDrop(ev: DragEvent) {
folderId: props.folder.id, folderId: props.folder.id,
}); });
} }
//#endregion // #endregion
//#region // #region
const driveFolder = ev.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FOLDER_); const driveFolder = ev.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FOLDER_);
if (driveFolder != null && driveFolder !== "") { if (driveFolder != null && driveFolder !== "") {
const folder = JSON.parse(driveFolder); const folder = JSON.parse(driveFolder);
@ -175,7 +175,7 @@ function onDrop(ev: DragEvent) {
} }
}); });
} }
//#endregion // #endregion
} }
function onDragstart(ev: DragEvent) { function onDragstart(ev: DragEvent) {
@ -207,7 +207,7 @@ function rename() {
if (canceled) return; if (canceled) return;
os.api("drive/folders/update", { os.api("drive/folders/update", {
folderId: props.folder.id, folderId: props.folder.id,
name: name, name,
}); });
}); });
} }

View file

@ -15,7 +15,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import { ref } from "vue"; import { ref } from "vue";
import * as Misskey from "firefish-js"; import type * as Misskey from "firefish-js";
import * as os from "@/os"; import * as os from "@/os";
import { i18n } from "@/i18n"; import { i18n } from "@/i18n";
@ -86,7 +86,7 @@ function onDrop(ev: DragEvent) {
return; return;
} }
//#region // #region
const driveFile = ev.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FILE_); const driveFile = ev.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FILE_);
if (driveFile != null && driveFile !== "") { if (driveFile != null && driveFile !== "") {
const file = JSON.parse(driveFile); const file = JSON.parse(driveFile);
@ -96,9 +96,9 @@ function onDrop(ev: DragEvent) {
folderId: props.folder ? props.folder.id : null, folderId: props.folder ? props.folder.id : null,
}); });
} }
//#endregion // #endregion
//#region // #region
const driveFolder = ev.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FOLDER_); const driveFolder = ev.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FOLDER_);
if (driveFolder != null && driveFolder !== "") { if (driveFolder != null && driveFolder !== "") {
const folder = JSON.parse(driveFolder); const folder = JSON.parse(driveFolder);
@ -110,7 +110,7 @@ function onDrop(ev: DragEvent) {
parentId: props.folder ? props.folder.id : null, parentId: props.folder ? props.folder.id : null,
}); });
} }
//#endregion // #endregion
} }
</script> </script>

View file

@ -139,7 +139,7 @@ import {
ref, ref,
watch, watch,
} from "vue"; } from "vue";
import * as Misskey from "firefish-js"; import type * as Misskey from "firefish-js";
import MkButton from "./MkButton.vue"; import MkButton from "./MkButton.vue";
import XNavFolder from "@/components/MkDrive.navFolder.vue"; import XNavFolder from "@/components/MkDrive.navFolder.vue";
import XFolder from "@/components/MkDrive.folder.vue"; import XFolder from "@/components/MkDrive.folder.vue";
@ -294,7 +294,7 @@ function onDrop(ev: DragEvent): any {
return; return;
} }
//#region // #region
const driveFile = ev.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FILE_); const driveFile = ev.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FILE_);
if (driveFile != null && driveFile !== "") { if (driveFile != null && driveFile !== "") {
const file = JSON.parse(driveFile); const file = JSON.parse(driveFile);
@ -305,9 +305,9 @@ function onDrop(ev: DragEvent): any {
folderId: folder.value ? folder.value.id : null, folderId: folder.value ? folder.value.id : null,
}); });
} }
//#endregion // #endregion
//#region // #region
const driveFolder = ev.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FOLDER_); const driveFolder = ev.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FOLDER_);
if (driveFolder != null && driveFolder !== "") { if (driveFolder != null && driveFolder !== "") {
const droppedFolder = JSON.parse(driveFolder); const droppedFolder = JSON.parse(driveFolder);
@ -339,7 +339,7 @@ function onDrop(ev: DragEvent): any {
} }
}); });
} }
//#endregion // #endregion
} }
function selectLocalFile() { function selectLocalFile() {
@ -354,7 +354,7 @@ function urlUpload() {
}).then(({ canceled, result: url }) => { }).then(({ canceled, result: url }) => {
if (canceled || !url) return; if (canceled || !url) return;
os.api("drive/files/upload-from-url", { os.api("drive/files/upload-from-url", {
url: url, url,
folderId: folder.value ? folder.value.id : undefined, folderId: folder.value ? folder.value.id : undefined,
}); });
@ -372,7 +372,7 @@ function createFolder() {
}).then(({ canceled, result: name }) => { }).then(({ canceled, result: name }) => {
if (canceled) return; if (canceled) return;
os.api("drive/folders/create", { os.api("drive/folders/create", {
name: name, name,
parentId: folder.value ? folder.value.id : undefined, parentId: folder.value ? folder.value.id : undefined,
}).then((createdFolder) => { }).then((createdFolder) => {
addFolder(createdFolder, true); addFolder(createdFolder, true);
@ -389,7 +389,7 @@ function renameFolder(folderToRename: Misskey.entities.DriveFolder) {
if (canceled) return; if (canceled) return;
os.api("drive/folders/update", { os.api("drive/folders/update", {
folderId: folderToRename.id, folderId: folderToRename.id,
name: name, name,
}).then((updatedFolder) => { }).then((updatedFolder) => {
// FIXME: // FIXME:
move(updatedFolder); move(updatedFolder);

View file

@ -68,7 +68,7 @@ const is = computed(() => {
"application/x-tar", "application/x-tar",
"application/gzip", "application/gzip",
"application/x-7z-compressed", "application/x-7z-compressed",
].some((archiveType) => archiveType === props.file.type) ].includes(props.file.type)
) )
return "archive"; return "archive";
return "unknown"; return "unknown";

View file

@ -37,7 +37,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import { ref } from "vue"; import { ref } from "vue";
import * as Misskey from "firefish-js"; import type * as Misskey from "firefish-js";
import XDrive from "@/components/MkDrive.vue"; import XDrive from "@/components/MkDrive.vue";
import XModalWindow from "@/components/MkModalWindow.vue"; import XModalWindow from "@/components/MkModalWindow.vue";
import number from "@/filters/number"; import number from "@/filters/number";

View file

@ -15,7 +15,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import {} from "vue"; import {} from "vue";
import * as Misskey from "firefish-js"; import type * as Misskey from "firefish-js";
import XDrive from "@/components/MkDrive.vue"; import XDrive from "@/components/MkDrive.vue";
import XWindow from "@/components/MkWindow.vue"; import XWindow from "@/components/MkWindow.vue";
import { i18n } from "@/i18n"; import { i18n } from "@/i18n";

View file

@ -48,7 +48,7 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { ref, watch, onMounted } from "vue"; import { onMounted, ref, watch } from "vue";
import { addSkinTone } from "@/scripts/emojilist"; import { addSkinTone } from "@/scripts/emojilist";
const props = defineProps<{ const props = defineProps<{

View file

@ -1,5 +1,5 @@
<template> <template>
<FocusTrap v-bind:active="isActive"> <FocusTrap :active="isActive">
<div <div
class="omfetrab" class="omfetrab"
:class="['s' + size, 'w' + width, 'h' + height, { asDrawer }]" :class="['s' + size, 'w' + width, 'h' + height, { asDrawer }]"
@ -163,14 +163,15 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { ref, computed, watch, onMounted } from "vue"; import { computed, onMounted, ref, watch } from "vue";
import * as Misskey from "firefish-js"; import type * as Misskey from "firefish-js";
import { FocusTrap } from "focus-trap-vue";
import XSection from "@/components/MkEmojiPicker.section.vue"; import XSection from "@/components/MkEmojiPicker.section.vue";
import type { UnicodeEmojiDef } from "@/scripts/emojilist";
import { import {
emojilist, emojilist,
unicodeEmojiCategories,
UnicodeEmojiDef,
getNicelyLabeledCategory, getNicelyLabeledCategory,
unicodeEmojiCategories,
} from "@/scripts/emojilist"; } from "@/scripts/emojilist";
import { getStaticImageUrl } from "@/scripts/get-static-image-url"; import { getStaticImageUrl } from "@/scripts/get-static-image-url";
import Ripple from "@/components/MkRipple.vue"; import Ripple from "@/components/MkRipple.vue";
@ -180,7 +181,6 @@ 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 { FocusTrap } from "focus-trap-vue";
const props = withDefaults( const props = withDefaults(
defineProps<{ defineProps<{

View file

@ -8,7 +8,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import { ref } from "vue"; import { ref } from "vue";
import * as Misskey from "firefish-js"; import type * as Misskey from "firefish-js";
import * as os from "@/os"; import * as os from "@/os";
const meta = ref<Misskey.entities.DetailedInstanceMetadata>(); const meta = ref<Misskey.entities.DetailedInstanceMetadata>();

View file

@ -1,14 +1,15 @@
<template> <template>
<button <button
v-if="!hideMenu" v-if="!hideMenu"
v-tooltip="i18n.ts.menu"
class="menu _button" class="menu _button"
@click.stop="menu" @click.stop="menu"
v-tooltip="i18n.ts.menu"
> >
<i class="ph-dots-three-outline ph-bold ph-lg"></i> <i class="ph-dots-three-outline ph-bold ph-lg"></i>
</button> </button>
<button <button
v-if="!hideFollowButton && $i != null && $i.id != user.id" v-if="!hideFollowButton && $i != null && $i.id != user.id"
v-tooltip="full ? null : `${state} ${user.name || user.username}`"
class="kpoogebi _button follow-button" class="kpoogebi _button follow-button"
:class="{ :class="{
wait, wait,
@ -18,9 +19,8 @@
blocking: isBlocking, blocking: isBlocking,
}" }"
:disabled="wait" :disabled="wait"
@click.stop="onClick"
:aria-label="`${state} ${user.name || user.username}`" :aria-label="`${state} ${user.name || user.username}`"
v-tooltip="full ? null : `${state} ${user.name || user.username}`" @click.stop="onClick"
> >
<template v-if="!wait"> <template v-if="!wait">
<template v-if="isBlocking"> <template v-if="isBlocking">
@ -89,13 +89,13 @@ const props = withDefaults(
const isBlocking = computed(() => props.user.isBlocking); const isBlocking = computed(() => props.user.isBlocking);
let state = ref(i18n.ts.processing); const state = ref(i18n.ts.processing);
let isFollowing = ref(props.user.isFollowing); const isFollowing = ref(props.user.isFollowing);
let hasPendingFollowRequestFromYou = ref( const hasPendingFollowRequestFromYou = ref(
props.user.hasPendingFollowRequestFromYou, props.user.hasPendingFollowRequestFromYou,
); );
let wait = ref(false); const wait = ref(false);
const connection = stream.useChannel("main"); const connection = stream.useChannel("main");
const hideFollowButton = props.hideFollowButton ?? false; const hideFollowButton = props.hideFollowButton ?? false;

View file

@ -64,7 +64,6 @@
<script lang="ts" setup> <script lang="ts" setup>
import { ref } from "vue"; import { ref } from "vue";
import {} from "vue";
import XModalWindow from "@/components/MkModalWindow.vue"; import XModalWindow from "@/components/MkModalWindow.vue";
import MkButton from "@/components/MkButton.vue"; import MkButton from "@/components/MkButton.vue";
import MkInput from "@/components/form/input.vue"; import MkInput from "@/components/form/input.vue";
@ -77,11 +76,11 @@ const emit = defineEmits<{
(ev: "closed"): void; (ev: "closed"): void;
}>(); }>();
let dialog: InstanceType<typeof XModalWindow> = ref(); const dialog: InstanceType<typeof XModalWindow> = ref();
let username = ref(""); const username = ref("");
let email = ref(""); const email = ref("");
let processing = ref(false); const processing = ref(false);
async function onSubmit() { async function onSubmit() {
processing.value = true; processing.value = true;

View file

@ -3,7 +3,7 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent, defineAsyncComponent } from "vue"; import { defineAsyncComponent, defineComponent } from "vue";
export default defineComponent({ export default defineComponent({
components: { components: {

View file

@ -8,7 +8,7 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { onMounted, nextTick, watch, shallowRef, ref } from "vue"; import { nextTick, onMounted, ref, shallowRef, watch } from "vue";
import { Chart } from "chart.js"; import { Chart } from "chart.js";
import * as os from "@/os"; import * as os from "@/os";
import { defaultStore } from "@/store"; import { defaultStore } from "@/store";
@ -26,8 +26,8 @@ const props = defineProps<{
const rootEl = shallowRef<HTMLDivElement>(null); const rootEl = shallowRef<HTMLDivElement>(null);
const chartEl = shallowRef<HTMLCanvasElement>(null); const chartEl = shallowRef<HTMLCanvasElement>(null);
const now = new Date(); const now = new Date();
let chartInstance: Chart = null; let chartInstance: Chart = null,
let fetching = ref(true); fetching = ref(true);
const { handler: externalTooltipHandler } = useChartTooltip({ const { handler: externalTooltipHandler } = useChartTooltip({
position: "middle", position: "middle",
@ -233,7 +233,7 @@ async function renderChart() {
return ["Active: " + v.v]; return ["Active: " + v.v];
}, },
}, },
//mode: 'index', // mode: 'index',
animation: { animation: {
duration: 0, duration: 0,
}, },

View file

@ -28,7 +28,6 @@
<script lang="ts" setup> <script lang="ts" setup>
import { ref } from "vue"; import { ref } from "vue";
import {} from "vue";
import type * as misskey from "firefish-js"; import type * as misskey from "firefish-js";
import bytes from "@/filters/bytes"; import bytes from "@/filters/bytes";
import number from "@/filters/number"; import number from "@/filters/number";

View file

@ -49,7 +49,7 @@ const props = withDefaults(
); );
const canvas = ref<HTMLCanvasElement>(); const canvas = ref<HTMLCanvasElement>();
let loaded = ref(false); const loaded = ref(false);
function draw() { function draw() {
if (props.hash == null || canvas.value == null) return; if (props.hash == null || canvas.value == null) return;

View file

@ -11,8 +11,8 @@
v-if="closeable" v-if="closeable"
v-tooltip="i18n.ts.close" v-tooltip="i18n.ts.close"
class="_buttonIcon close" class="_buttonIcon close"
@click.stop="close"
:aria-label="i18n.t('close')" :aria-label="i18n.t('close')"
@click.stop="close"
> >
<i class="ph-x ph-bold ph-lg"></i> <i class="ph-x ph-bold ph-lg"></i>
</button> </button>

View file

@ -26,7 +26,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import { ref } from "vue"; import { ref } from "vue";
import * as firefish from "firefish-js"; import type * as firefish from "firefish-js";
import MkMiniChart from "@/components/MkMiniChart.vue"; import MkMiniChart from "@/components/MkMiniChart.vue";
import * as os from "@/os"; import * as os from "@/os";
import { getProxiedImageUrlNullable } from "@/scripts/media-proxy"; import { getProxiedImageUrlNullable } from "@/scripts/media-proxy";
@ -35,7 +35,7 @@ const props = defineProps<{
instance: firefish.entities.Instance; instance: firefish.entities.Instance;
}>(); }>();
let chartValues = ref<number[] | null>(null); const chartValues = ref<number[] | null>(null);
os.apiGet("charts/instance", { os.apiGet("charts/instance", {
host: props.instance.host, host: props.instance.host,

View file

@ -58,11 +58,11 @@
<script lang="ts" setup> <script lang="ts" setup>
import { ref } from "vue"; import { ref } from "vue";
import type { Instance } from "firefish-js/built/entities";
import MkInput from "@/components/form/input.vue"; import MkInput from "@/components/form/input.vue";
import XModalWindow from "@/components/MkModalWindow.vue"; import XModalWindow from "@/components/MkModalWindow.vue";
import * as os from "@/os"; import * as os from "@/os";
import { i18n } from "@/i18n"; import { i18n } from "@/i18n";
import { Instance } from "firefish-js/built/entities";
const emit = defineEmits<{ const emit = defineEmits<{
(ev: "ok", selected: Instance): void; (ev: "ok", selected: Instance): void;
@ -70,10 +70,10 @@ const emit = defineEmits<{
(ev: "closed"): void; (ev: "closed"): void;
}>(); }>();
let hostname = ref(""); const hostname = ref("");
let instances: Instance[] = ref([]); const instances: Instance[] = ref([]);
let selected: Instance | null = ref(null); const selected: Instance | null = ref(null);
let dialogEl = ref<InstanceType<typeof XModalWindow>>(); const dialogEl = ref<InstanceType<typeof XModalWindow>>();
let searchOrderLatch = 0; let searchOrderLatch = 0;
const search = () => { const search = () => {

View file

@ -116,11 +116,11 @@ import { initChart } from "@/scripts/init-chart";
initChart(); initChart();
const chartLimit = 500; const chartLimit = 500;
let chartSpan = ref<"hour" | "day">("hour"); const chartSpan = ref<"hour" | "day">("hour");
let chartSrc = ref("active-users"); const chartSrc = ref("active-users");
let heatmapSrc = ref("active-users"); const heatmapSrc = ref("active-users");
let subDoughnutEl = shallowRef<HTMLCanvasElement>(); const subDoughnutEl = shallowRef<HTMLCanvasElement>();
let pubDoughnutEl = shallowRef<HTMLCanvasElement>(); const pubDoughnutEl = shallowRef<HTMLCanvasElement>();
const { handler: externalTooltipHandler1 } = useChartTooltip({ const { handler: externalTooltipHandler1 } = useChartTooltip({
position: "middle", position: "middle",

View file

@ -1,10 +1,10 @@
<template> <template>
<div <div
class="hpaizdrt" ref="ticker"
v-tooltip=" v-tooltip="
`${capitalize(instance.softwareName)} ${instance.softwareVersion}` `${capitalize(instance.softwareName)} ${instance.softwareVersion}`
" "
ref="ticker" class="hpaizdrt"
:style="bg" :style="bg"
> >
<img class="icon" :src="getInstanceIcon(instance)" aria-hidden="true" /> <img class="icon" :src="getInstanceIcon(instance)" aria-hidden="true" />
@ -29,7 +29,7 @@ const props = defineProps<{
}; };
}>(); }>();
let ticker = ref<HTMLElement | null>(null); const ticker = ref<HTMLElement | null>(null);
// if no instance data is given, this is for the local instance // if no instance data is given, this is for the local instance
const instance = props.instance ?? { const instance = props.instance ?? {

View file

@ -1,5 +1,5 @@
<template> <template>
<div class="media" v-size="{ max: [350] }"> <div v-size="{ max: [350] }" class="media">
<button v-if="hide" class="hidden" @click="hide = false"> <button v-if="hide" class="hidden" @click="hide = false">
<ImgWithBlurhash <ImgWithBlurhash
:hash="media.blurhash" :hash="media.blurhash"
@ -89,7 +89,7 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { watch, ref, computed } from "vue"; import { computed, ref, watch } from "vue";
import VuePlyr from "vue-plyr"; import VuePlyr from "vue-plyr";
import "vue-plyr/dist/vue-plyr.css"; import "vue-plyr/dist/vue-plyr.css";
import type * as misskey from "firefish-js"; import type * as misskey from "firefish-js";
@ -104,7 +104,7 @@ const props = defineProps<{
raw?: boolean; raw?: boolean;
}>(); }>();
let hide = ref(true); const hide = ref(true);
const plyr = ref(); const plyr = ref();

View file

@ -71,7 +71,7 @@ const props = withDefaults(
); );
const audioEl = ref<HTMLAudioElement | null>(); const audioEl = ref<HTMLAudioElement | null>();
let hide = ref(true); const hide = ref(true);
function volumechange() { function volumechange() {
if (audioEl.value) if (audioEl.value)

View file

@ -29,7 +29,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import { onMounted, ref } from "vue"; import { onMounted, ref } from "vue";
import * as misskey from "firefish-js"; import type * as misskey from "firefish-js";
import PhotoSwipeLightbox from "photoswipe/lightbox"; import PhotoSwipeLightbox from "photoswipe/lightbox";
import PhotoSwipe from "photoswipe"; import PhotoSwipe from "photoswipe";
import "photoswipe/style.css"; import "photoswipe/style.css";
@ -125,11 +125,11 @@ onMounted(() => {
className: "pwsp__alt-text-container", className: "pwsp__alt-text-container",
appendTo: "wrapper", appendTo: "wrapper",
onInit: (el, pwsp) => { onInit: (el, pwsp) => {
let textBox = document.createElement("p"); const textBox = document.createElement("p");
textBox.className = "pwsp__alt-text"; textBox.className = "pwsp__alt-text";
el.appendChild(textBox); el.appendChild(textBox);
let preventProp = function (ev: Event): void { const preventProp = function (ev: Event): void {
ev.stopPropagation(); ev.stopPropagation();
}; };

View file

@ -14,7 +14,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import { nextTick, onMounted, ref } from "vue"; import { nextTick, onMounted, ref } from "vue";
import MkMenu from "./MkMenu.vue"; import MkMenu from "./MkMenu.vue";
import { MenuItem } from "@/types/menu"; import type { MenuItem } from "@/types/menu";
const props = defineProps<{ const props = defineProps<{
items: MenuItem[]; items: MenuItem[];

View file

@ -14,8 +14,8 @@
width: width && !asDrawer ? width + 'px' : '', width: width && !asDrawer ? width + 'px' : '',
maxHeight: maxHeight ? maxHeight + 'px' : '', maxHeight: maxHeight ? maxHeight + 'px' : '',
}" }"
@contextmenu.self="(e) => e.preventDefault()"
tabindex="-1" tabindex="-1"
@contextmenu.self="(e) => e.preventDefault()"
> >
<template v-for="item in items2"> <template v-for="item in items2">
<div v-if="item === null" class="divider"></div> <div v-if="item === null" class="divider"></div>
@ -47,7 +47,7 @@
v-if="item.avatar" v-if="item.avatar"
:user="item.avatar" :user="item.avatar"
class="avatar" class="avatar"
disableLink disable-link
/> />
<span :style="item.textStyle || ''">{{ <span :style="item.textStyle || ''">{{
item.text item.text
@ -100,7 +100,7 @@
<MkAvatar <MkAvatar
:user="item.user" :user="item.user"
class="avatar" class="avatar"
disableLink disable-link
/><MkUserName :user="item.user" /> /><MkUserName :user="item.user" />
<span <span
v-if="item.indicate" v-if="item.indicate"
@ -168,7 +168,7 @@
v-if="item.avatar" v-if="item.avatar"
:user="item.avatar" :user="item.avatar"
class="avatar" class="avatar"
disableLink disable-link
/> />
<span :style="item.textStyle || ''">{{ <span :style="item.textStyle || ''">{{
item.text item.text
@ -210,11 +210,16 @@ import {
ref, ref,
watch, watch,
} from "vue"; } from "vue";
import { FocusTrap } from "focus-trap-vue";
import FormSwitch from "@/components/form/switch.vue"; import FormSwitch from "@/components/form/switch.vue";
import { MenuItem, InnerMenuItem, MenuPending, MenuAction } from "@/types/menu"; import type {
InnerMenuItem,
MenuAction,
MenuItem,
MenuPending,
} from "@/types/menu";
import * as os from "@/os"; import * as os from "@/os";
import { i18n } from "@/i18n"; import { i18n } from "@/i18n";
import { FocusTrap } from "focus-trap-vue";
const XChild = defineAsyncComponent(() => import("./MkMenu.child.vue")); const XChild = defineAsyncComponent(() => import("./MkMenu.child.vue"));
const focusTrap = ref(); const focusTrap = ref();
@ -233,13 +238,13 @@ const emit = defineEmits<{
(ev: "close", actioned?: boolean): void; (ev: "close", actioned?: boolean): void;
}>(); }>();
let itemsEl = ref<HTMLDivElement>(); const itemsEl = ref<HTMLDivElement>();
let items2: InnerMenuItem[] = ref([]); const items2: InnerMenuItem[] = ref([]);
let child = ref<InstanceType<typeof XChild>>(); const child = ref<InstanceType<typeof XChild>>();
let childShowingItem = ref<MenuItem | null>(); const childShowingItem = ref<MenuItem | null>();
watch( watch(
() => props.items, () => props.items,
@ -267,8 +272,8 @@ watch(
}, },
); );
let childMenu = ref<MenuItem[] | null>(); const childMenu = ref<MenuItem[] | null>();
let childTarget = ref<HTMLElement | null>(); const childTarget = ref<HTMLElement | null>();
function closeChild() { function closeChild() {
childMenu.value = null; childMenu.value = null;

View file

@ -25,7 +25,7 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { watch, ref } from "vue"; import { ref, watch } from "vue";
import { v4 as uuid } from "uuid"; import { v4 as uuid } from "uuid";
import tinycolor from "tinycolor2"; import tinycolor from "tinycolor2";
import { useInterval } from "@/scripts/use-interval"; import { useInterval } from "@/scripts/use-interval";
@ -37,10 +37,10 @@ const props = defineProps<{
const viewBoxX = 50; const viewBoxX = 50;
const viewBoxY = 50; const viewBoxY = 50;
const gradientId = uuid(); const gradientId = uuid();
let polylinePoints = ref(""); const polylinePoints = ref("");
let polygonPoints = ref(""); const polygonPoints = ref("");
let headX = ref<number | null>(null); const headX = ref<number | null>(null);
let headY = ref<number | null>(null); const headY = ref<number | null>(null);
const accent = tinycolor( const accent = tinycolor(
getComputedStyle(document.documentElement).getPropertyValue("--accent"), getComputedStyle(document.documentElement).getPropertyValue("--accent"),
); );

View file

@ -25,6 +25,7 @@
<div <div
v-show="manualShowing != null ? manualShowing : showing" v-show="manualShowing != null ? manualShowing : showing"
v-hotkey.global="keymap" v-hotkey.global="keymap"
v-focus
:class="[ :class="[
$style.root, $style.root,
{ {
@ -44,7 +45,6 @@
'--transformOrigin': transformOrigin, '--transformOrigin': transformOrigin,
}" }"
tabindex="-1" tabindex="-1"
v-focus
> >
<div <div
class="_modalBg data-cy-bg" class="_modalBg data-cy-bg"
@ -78,20 +78,20 @@
<script lang="ts" setup> <script lang="ts" setup>
import { import {
computed,
nextTick, nextTick,
onMounted, onMounted,
watch,
provide,
onUnmounted, onUnmounted,
provide,
ref, ref,
shallowRef, shallowRef,
computed, watch,
} from "vue"; } from "vue";
import { FocusTrap } from "focus-trap-vue";
import * as os from "@/os"; import * as os from "@/os";
import { isTouchUsing } from "@/scripts/touch"; import { isTouchUsing } from "@/scripts/touch";
import { defaultStore } from "@/store"; import { defaultStore } from "@/store";
import { deviceKind } from "@/scripts/device-kind"; import { deviceKind } from "@/scripts/device-kind";
import { FocusTrap } from "focus-trap-vue";
function getFixedContainer(el: Element | null): Element | null { function getFixedContainer(el: Element | null): Element | null {
if (el == null || el.tagName === "BODY") return null; if (el == null || el.tagName === "BODY") return null;
@ -139,13 +139,13 @@ const emit = defineEmits<{
provide("modal", true); provide("modal", true);
let maxHeight = ref<number>(); const maxHeight = ref<number>();
let fixed = ref(false); const fixed = ref(false);
let transformOrigin = ref("center"); const transformOrigin = ref("center");
let showing = ref(true); const showing = ref(true);
let content = shallowRef<HTMLElement>(); const content = shallowRef<HTMLElement>();
const zIndex = os.claimZIndex(props.zPriority); const zIndex = os.claimZIndex(props.zPriority);
let useSendAnime = ref(false); const useSendAnime = ref(false);
const type = computed<ModalTypes>(() => { const type = computed<ModalTypes>(() => {
if (props.preferType === "auto") { if (props.preferType === "auto") {
if ( if (
@ -164,7 +164,7 @@ const type = computed<ModalTypes>(() => {
const isEnableBgTransparent = computed( const isEnableBgTransparent = computed(
() => props.transparentBg && type.value === "popup", () => props.transparentBg && type.value === "popup",
); );
let transitionName = computed(() => const transitionName = computed(() =>
defaultStore.state.animation defaultStore.state.animation
? useSendAnime.value ? useSendAnime.value
? "send" ? "send"
@ -175,7 +175,7 @@ let transitionName = computed(() =>
: "modal" : "modal"
: "", : "",
); );
let transitionDuration = computed(() => const transitionDuration = computed(() =>
transitionName.value === "send" transitionName.value === "send"
? 400 ? 400
: transitionName.value === "modal-popup" : transitionName.value === "modal-popup"
@ -235,8 +235,7 @@ const align = () => {
const width = content.value!.offsetWidth; const width = content.value!.offsetWidth;
const height = content.value!.offsetHeight; const height = content.value!.offsetHeight;
let left; let left, top;
let top;
const x = srcRect.left + (fixed.value ? 0 : window.pageXOffset); const x = srcRect.left + (fixed.value ? 0 : window.pageXOffset);
const y = srcRect.top + (fixed.value ? 0 : window.pageYOffset); const y = srcRect.top + (fixed.value ? 0 : window.pageYOffset);
@ -321,8 +320,8 @@ const align = () => {
left = 0; left = 0;
} }
let transformOriginX = "center"; let transformOriginX = "center",
let transformOriginY = "center"; transformOriginY = "center";
if ( if (
top >= top >=

View file

@ -28,8 +28,8 @@
</span> </span>
<button <button
class="_button" class="_button"
@click="$refs.modal.close()"
:aria-label="i18n.t('close')" :aria-label="i18n.t('close')"
@click="$refs.modal.close()"
> >
<i class="ph-x ph-bold ph-lg"></i> <i class="ph-x ph-bold ph-lg"></i>
</button> </button>
@ -52,7 +52,8 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { ComputedRef, provide, ref, computed } from "vue"; import type { ComputedRef } from "vue";
import { computed, provide, ref } from "vue";
import MkModal from "@/components/MkModal.vue"; import MkModal from "@/components/MkModal.vue";
import { popout as _popout } from "@/scripts/popout"; import { popout as _popout } from "@/scripts/popout";
import copyToClipboard from "@/scripts/copy-to-clipboard"; import copyToClipboard from "@/scripts/copy-to-clipboard";
@ -60,7 +61,8 @@ import { url } from "@/config";
import * as os from "@/os"; import * as os from "@/os";
import { mainRouter, routes } from "@/router"; import { mainRouter, routes } from "@/router";
import { i18n } from "@/i18n"; import { i18n } from "@/i18n";
import { PageMetadata, provideMetadataReceiver } from "@/scripts/page-metadata"; import type { PageMetadata } from "@/scripts/page-metadata";
import { provideMetadataReceiver } from "@/scripts/page-metadata";
import { Router } from "@/nirax"; import { Router } from "@/nirax";
const props = defineProps<{ const props = defineProps<{
@ -76,12 +78,12 @@ const router = new Router(routes, props.initialPath);
router.addListener("push", (ctx) => {}); router.addListener("push", (ctx) => {});
let pageMetadata = ref<null | ComputedRef<PageMetadata>>(); const pageMetadata = ref<null | ComputedRef<PageMetadata>>();
let rootEl = ref(); const rootEl = ref();
let modal = ref<InstanceType<typeof MkModal>>(); const modal = ref<InstanceType<typeof MkModal>>();
let path = ref(props.initialPath); const path = ref(props.initialPath);
let width = ref(860); const width = ref(860);
let height = ref(660); const height = ref(660);
const history = []; const history = [];
provide("router", router); provide("router", router);

View file

@ -25,10 +25,10 @@
<div ref="headerEl" class="header"> <div ref="headerEl" class="header">
<button <button
v-if="props.withOkButton" v-if="props.withOkButton"
v-tooltip="i18n.ts.close"
:aria-label="i18n.t('close')" :aria-label="i18n.t('close')"
class="_button" class="_button"
@click="$emit('close')" @click="$emit('close')"
v-tooltip="i18n.ts.close"
> >
<i class="ph-x ph-bold ph-lg"></i> <i class="ph-x ph-bold ph-lg"></i>
</button> </button>
@ -92,9 +92,9 @@ const emit = defineEmits<{
(event: "ok"): void; (event: "ok"): void;
}>(); }>();
let modal = shallowRef<InstanceType<typeof MkModal>>(); const modal = shallowRef<InstanceType<typeof MkModal>>();
let rootEl = shallowRef<HTMLElement>(); const rootEl = shallowRef<HTMLElement>();
let headerEl = shallowRef<HTMLElement>(); const headerEl = shallowRef<HTMLElement>();
const close = (ev) => { const close = (ev) => {
modal.value?.close(ev); modal.value?.close(ev);

View file

@ -1,15 +1,15 @@
<template> <template>
<div <div
:aria-label="accessibleLabel"
v-if="!muted.muted" v-if="!muted.muted"
v-show="!isDeleted" v-show="!isDeleted"
:id="appearNote.id"
ref="el" ref="el"
v-hotkey="keymap" v-hotkey="keymap"
v-size="{ max: [500, 350] }" v-size="{ max: [500, 350] }"
:aria-label="accessibleLabel"
class="tkcbzcuz note-container" class="tkcbzcuz note-container"
:tabindex="!isDeleted ? '-1' : null" :tabindex="!isDeleted ? '-1' : null"
:class="{ renote: isRenote }" :class="{ renote: isRenote }"
:id="appearNote.id"
> >
<MkNoteSub <MkNoteSub
v-if="appearNote.reply && !detailedView && !collapsedReply" v-if="appearNote.reply && !detailedView && !collapsedReply"
@ -19,10 +19,10 @@
<div <div
v-if="!detailedView" v-if="!detailedView"
class="note-context" class="note-context"
@click="noteClick"
:class="{ :class="{
collapsedReply: collapsedReply && appearNote.reply, collapsedReply: collapsedReply && appearNote.reply,
}" }"
@click="noteClick"
> >
<div v-if="!collapsedReply" class="line"></div> <div v-if="!collapsedReply" class="line"></div>
<div v-if="appearNote._prId_" class="info"> <div v-if="appearNote._prId_" class="info">
@ -87,11 +87,11 @@
</div> </div>
<article <article
class="article" class="article"
@contextmenu.stop="onContextmenu"
@click="noteClick"
:style="{ :style="{
cursor: expandOnNoteClick && !detailedView ? 'pointer' : '', cursor: expandOnNoteClick && !detailedView ? 'pointer' : '',
}" }"
@contextmenu.stop="onContextmenu"
@click="noteClick"
> >
<div class="main"> <div class="main">
<div class="header-container"> <div class="header-container">
@ -103,15 +103,15 @@
class="text" class="text"
:note="appearNote" :note="appearNote"
:detailed="true" :detailed="true"
:detailedView="detailedView" :detailed-view="detailedView"
:parentId="appearNote.parentId" :parent-id="appearNote.parentId"
@push="(e) => router.push(notePage(e))" @push="(e) => router.push(notePage(e))"
@focusfooter="footerEl.focus()" @focusfooter="footerEl.focus()"
@expanded="(e) => setPostExpanded(e)" @expanded="(e) => setPostExpanded(e)"
></MkSubNoteContent> ></MkSubNoteContent>
<div v-if="translating || translation" class="translation"> <div v-if="translating || translation" class="translation">
<MkLoading v-if="translating" mini /> <MkLoading v-if="translating" mini />
<div v-else class="translated"> <div v-else-if="translation != null" class="translated">
<b <b
>{{ >{{
i18n.t("translatedFrom", { i18n.t("translatedFrom", {
@ -171,7 +171,7 @@
class="button" class="button"
:note="appearNote" :note="appearNote"
:count="appearNote.renoteCount" :count="appearNote.renoteCount"
:detailedView="detailedView" :detailed-view="detailedView"
/> />
<XStarButtonNoEmoji <XStarButtonNoEmoji
v-if="!enableEmojiReactions" v-if="!enableEmojiReactions"
@ -212,9 +212,9 @@
appearNote.myReaction != null appearNote.myReaction != null
" "
ref="reactButton" ref="reactButton"
v-tooltip.noDelay.bottom="i18n.ts.removeReaction"
class="button _button reacted" class="button _button reacted"
@click.stop="undoReact(appearNote)" @click.stop="undoReact(appearNote)"
v-tooltip.noDelay.bottom="i18n.ts.removeReaction"
> >
<i class="ph-minus ph-bold ph-lg"></i> <i class="ph-minus ph-bold ph-lg"></i>
</button> </button>
@ -225,7 +225,7 @@
isForeignLanguage && isForeignLanguage &&
translation == null translation == null
" "
class="button _button accent" class="button _button"
@click.stop="translate" @click.stop="translate"
v-tooltip.noDelay.bottom="i18n.ts.translate" v-tooltip.noDelay.bottom="i18n.ts.translate"
> >
@ -272,8 +272,8 @@ import * as mfm from "mfm-js";
import type { Ref } from "vue"; import type { Ref } from "vue";
import type * as misskey from "firefish-js"; import type * as misskey from "firefish-js";
import { detect as detectLanguage_ } from "tinyld"; import { detect as detectLanguage_ } from "tinyld";
import MkNoteSub from "@/components/MkNoteSub.vue";
import MkSubNoteContent from "./MkSubNoteContent.vue"; import MkSubNoteContent from "./MkSubNoteContent.vue";
import MkNoteSub from "@/components/MkNoteSub.vue";
import XNoteHeader from "@/components/MkNoteHeader.vue"; import XNoteHeader from "@/components/MkNoteHeader.vue";
import XRenoteButton from "@/components/MkRenoteButton.vue"; import XRenoteButton from "@/components/MkRenoteButton.vue";
import XReactionsViewer from "@/components/MkReactionsViewer.vue"; import XReactionsViewer from "@/components/MkReactionsViewer.vue";
@ -284,7 +284,7 @@ import MkVisibility from "@/components/MkVisibility.vue";
import copyToClipboard from "@/scripts/copy-to-clipboard"; import copyToClipboard from "@/scripts/copy-to-clipboard";
import { url } from "@/config"; import { url } from "@/config";
import { pleaseLogin } from "@/scripts/please-login"; import { pleaseLogin } from "@/scripts/please-login";
import { focusPrev, focusNext } from "@/scripts/focus"; import { focusNext, focusPrev } from "@/scripts/focus";
import { getWordSoftMute } from "@/scripts/check-word-mute"; import { getWordSoftMute } from "@/scripts/check-word-mute";
import { useRouter } from "@/router"; import { useRouter } from "@/router";
import { userPage } from "@/filters/user"; import { userPage } from "@/filters/user";
@ -310,7 +310,7 @@ const props = defineProps<{
const inChannel = inject("inChannel", null); const inChannel = inject("inChannel", null);
let note = ref(deepClone(props.note)); const note = ref(deepClone(props.note));
const softMuteReasonI18nSrc = (what?: string) => { const softMuteReasonI18nSrc = (what?: string) => {
if (what === "note") return i18n.ts.userSaysSomethingReason; if (what === "note") return i18n.ts.userSaysSomethingReason;
@ -346,7 +346,7 @@ const starButton = ref<InstanceType<typeof XStarButton>>();
const renoteButton = ref<InstanceType<typeof XRenoteButton>>(); const renoteButton = ref<InstanceType<typeof XRenoteButton>>();
const renoteTime = ref<HTMLElement>(); const renoteTime = ref<HTMLElement>();
const reactButton = ref<HTMLElement>(); const reactButton = ref<HTMLElement>();
let appearNote = computed(() => const appearNote = computed(() =>
isRenote ? (note.value.renote as misskey.entities.Note) : note.value, isRenote ? (note.value.renote as misskey.entities.Note) : note.value,
); );
const isMyRenote = $i && $i.id === note.value.userId; const isMyRenote = $i && $i.id === note.value.userId;
@ -359,59 +359,57 @@ const translation = ref(null);
const translating = ref(false); const translating = ref(false);
const enableEmojiReactions = defaultStore.state.enableEmojiReactions; const enableEmojiReactions = defaultStore.state.enableEmojiReactions;
const expandOnNoteClick = defaultStore.state.expandOnNoteClick; const expandOnNoteClick = defaultStore.state.expandOnNoteClick;
const lang = localStorage.getItem("lang");
const translateLang = localStorage.getItem("translateLang");
const detectLanguage = (src: string): string => { function detectLanguage(text: string) {
const nodes = mfm.parse(src); const nodes = mfm.parse(text);
const filtered = mfm.extract(nodes, (node) => { const filtered = mfm.extract(nodes, (node) => {
return node.type === "text" || node.type === "quote"; return node.type === "text" || node.type === "quote";
}); });
const purified = mfm.toString(filtered).trim(); const purified = mfm.toString(filtered);
return detectLanguage_(purified); return detectLanguage_(purified);
}; }
const localTranslateLang = localStorage.getItem("translateLang");
const localLang = localStorage.getItem("lang");
const isForeignLanguage: boolean = const isForeignLanguage: boolean =
defaultStore.state.detectPostLanguage && defaultStore.state.detectPostLanguage &&
appearNote.value.text != null && appearNote.value.text != null &&
(() => { (() => {
const targetLanguage = ( const targetLang = (translateLang || lang || navigator.language)?.slice(
localTranslateLang || 0,
localLang || 2,
navigator.language );
)?.slice(0, 2); const postLang = detectLanguage(appearNote.value.text);
const postLanguage = detectLanguage(appearNote.value.text); return postLang !== "" && postLang !== targetLang;
return postLanguage !== "" && postLanguage !== targetLanguage;
})(); })();
const translate_ = async (noteId: number, targetLang: string) => { async function translate_(noteId: number, targetLang: string) {
return await os.api("notes/translate", { return await os.api("notes/translate", {
noteId: noteId, noteId: noteId,
targetLang: targetLang, targetLang: targetLang,
}); });
}; }
const translate = async () => { async function translate() {
if (translation.value != null) return; if (translation.value != null) return;
translating.value = true; translating.value = true;
translation.value = await translate_( translation.value = await translate_(
appearNote.value.id, appearNote.value.id,
localTranslateLang || localLang || navigator.language, translateLang || lang || navigator.language,
); );
// use UI language as the second translation target // use UI language as the second translation language
if ( if (
localTranslateLang != null && translateLang != null &&
localLang != null && lang != null &&
localTranslateLang !== localLang && translateLang !== lang &&
(!translation.value || (!translation.value ||
translation.value.sourceLang.toLowerCase() === translation.value.sourceLang.toLowerCase() ===
localTranslateLang.slice(0, 2)) translateLang.slice(0, 2))
) )
translation.value = await translate_(appearNote.value.id, localLang); translation.value = await translate_(appearNote.value.id, lang);
translating.value = false; translating.value = false;
}; }
const keymap = { const keymap = {
r: () => reply(true), r: () => reply(true),
@ -451,7 +449,7 @@ function react(viaKeyboard = false): void {
(reaction) => { (reaction) => {
os.api("notes/reactions/create", { os.api("notes/reactions/create", {
noteId: appearNote.value.id, noteId: appearNote.value.id,
reaction: reaction, reaction,
}); });
}, },
() => { () => {
@ -582,7 +580,7 @@ function showRenoteMenu(viaKeyboard = false): void {
], ],
renoteTime.value, renoteTime.value,
{ {
viaKeyboard: viaKeyboard, viaKeyboard,
}, },
); );
} }
@ -626,7 +624,7 @@ function readPromo() {
isDeleted.value = true; isDeleted.value = true;
} }
let postIsExpanded = ref(false); const postIsExpanded = ref(false);
function setPostExpanded(val: boolean) { function setPostExpanded(val: boolean) {
postIsExpanded.value = val; postIsExpanded.value = val;
@ -985,10 +983,6 @@ defineExpose({
&.reacted { &.reacted {
color: var(--accent); color: var(--accent);
} }
&.accent {
color: var(--accent);
}
} }
} }
} }

View file

@ -10,27 +10,27 @@
:class="{ renote: isRenote }" :class="{ renote: isRenote }"
> >
<MkNoteSub <MkNoteSub
v-if="conversation"
v-for="note in conversation" v-for="note in conversation"
v-if="conversation"
:key="note.id" :key="note.id"
class="reply-to" class="reply-to"
:note="note" :note="note"
:detailedView="true" :detailed-view="true"
/> />
<MkLoading v-else-if="note.reply" mini /> <MkLoading v-else-if="note.reply" mini />
<MkNoteSub <MkNoteSub
v-if="note.reply" v-if="note.reply"
:note="note.reply" :note="note.reply"
class="reply-to" class="reply-to"
:detailedView="true" :detailed-view="true"
/> />
<MkNote <MkNote
ref="noteEl" ref="noteEl"
@contextmenu.stop="onContextmenu"
tabindex="-1" tabindex="-1"
:note="note" :note="note"
detailedView detailed-view
@contextmenu.stop="onContextmenu"
></MkNote> ></MkNote>
<MkTab v-model="tab" :style="'underline'" @update:modelValue="loadTab"> <MkTab v-model="tab" :style="'underline'" @update:modelValue="loadTab">
@ -41,22 +41,22 @@
}}</span> }}</span>
{{ i18n.ts._notification._types.reply }} {{ i18n.ts._notification._types.reply }}
</option> </option>
<option value="renotes" v-if="note.renoteCount > 0"> <option v-if="note.renoteCount > 0" value="renotes">
<!-- <i class="ph-repeat ph-bold ph-lg"></i> --> <!-- <i class="ph-repeat ph-bold ph-lg"></i> -->
<span class="count">{{ note.renoteCount }}</span> <span class="count">{{ note.renoteCount }}</span>
{{ i18n.ts._notification._types.renote }} {{ i18n.ts._notification._types.renote }}
</option> </option>
<option value="reactions" v-if="reactionsCount > 0"> <option v-if="reactionsCount > 0" value="reactions">
<!-- <i class="ph-smiley ph-bold ph-lg"></i> --> <!-- <i class="ph-smiley ph-bold ph-lg"></i> -->
<span class="count">{{ reactionsCount }}</span> <span class="count">{{ reactionsCount }}</span>
{{ i18n.ts.reaction }} {{ i18n.ts.reaction }}
</option> </option>
<option value="quotes" v-if="directQuotes?.length > 0"> <option v-if="directQuotes?.length > 0" value="quotes">
<!-- <i class="ph-quotes ph-bold ph-lg"></i> --> <!-- <i class="ph-quotes ph-bold ph-lg"></i> -->
<span class="count">{{ directQuotes.length }}</span> <span class="count">{{ directQuotes.length }}</span>
{{ i18n.ts._notification._types.quote }} {{ i18n.ts._notification._types.quote }}
</option> </option>
<option value="clips" v-if="clips?.length > 0"> <option v-if="clips?.length > 0" value="clips">
<!-- <i class="ph-paperclip ph-bold ph-lg"></i> --> <!-- <i class="ph-paperclip ph-bold ph-lg"></i> -->
<span class="count">{{ clips.length }}</span> <span class="count">{{ clips.length }}</span>
{{ i18n.ts.clips }} {{ i18n.ts.clips }}
@ -64,26 +64,26 @@
</MkTab> </MkTab>
<MkNoteSub <MkNoteSub
v-if="directReplies && tab === 'replies'"
v-for="note in directReplies" v-for="note in directReplies"
v-if="directReplies && tab === 'replies'"
:key="note.id" :key="note.id"
:note="note" :note="note"
class="reply" class="reply"
:conversation="replies" :conversation="replies"
:detailedView="true" :detailed-view="true"
:parentId="note.id" :parent-id="note.id"
/> />
<MkLoading v-else-if="tab === 'replies' && note.repliesCount > 0" /> <MkLoading v-else-if="tab === 'replies' && note.repliesCount > 0" />
<MkNoteSub <MkNoteSub
v-if="directQuotes && tab === 'quotes'"
v-for="note in directQuotes" v-for="note in directQuotes"
v-if="directQuotes && tab === 'quotes'"
:key="note.id" :key="note.id"
:note="note" :note="note"
class="reply" class="reply"
:conversation="replies" :conversation="replies"
:detailedView="true" :detailed-view="true"
:parentId="note.id" :parent-id="note.id"
/> />
<MkLoading v-else-if="tab === 'quotes' && directQuotes.length > 0" /> <MkLoading v-else-if="tab === 'quotes' && directQuotes.length > 0" />
@ -94,8 +94,8 @@
:pagination="pagination" :pagination="pagination"
> --> > -->
<MkUserCardMini <MkUserCardMini
v-if="tab === 'renotes' && renotes"
v-for="item in renotes" v-for="item in renotes"
v-if="tab === 'renotes' && renotes"
:key="item.user.id" :key="item.user.id"
:user="item.user" :user="item.user"
:with-chart="false" :with-chart="false"
@ -151,11 +151,12 @@
<script lang="ts" setup> <script lang="ts" setup>
import { onMounted, onUnmounted, onUpdated, ref } from "vue"; import { onMounted, onUnmounted, onUpdated, ref } from "vue";
import * as misskey from "firefish-js"; import type * as misskey from "firefish-js";
import type { NoteUpdatedEvent } from "firefish-js/built/streaming.types";
import MkTab from "@/components/MkTab.vue"; import MkTab from "@/components/MkTab.vue";
import MkNote from "@/components/MkNote.vue"; import MkNote from "@/components/MkNote.vue";
import MkNoteSub from "@/components/MkNoteSub.vue"; import MkNoteSub from "@/components/MkNoteSub.vue";
import XRenoteButton from "@/components/MkRenoteButton.vue"; import type XRenoteButton from "@/components/MkRenoteButton.vue";
import MkUserCardMini from "@/components/MkUserCardMini.vue"; import MkUserCardMini from "@/components/MkUserCardMini.vue";
import MkReactedUsers from "@/components/MkReactedUsers.vue"; import MkReactedUsers from "@/components/MkReactedUsers.vue";
import { pleaseLogin } from "@/scripts/please-login"; import { pleaseLogin } from "@/scripts/please-login";
@ -170,16 +171,15 @@ import { getNoteMenu } from "@/scripts/get-note-menu";
import { useNoteCapture } from "@/scripts/use-note-capture"; import { useNoteCapture } from "@/scripts/use-note-capture";
import { deepClone } from "@/scripts/clone"; import { deepClone } from "@/scripts/clone";
import { stream } from "@/stream"; import { stream } from "@/stream";
import { NoteUpdatedEvent } from "firefish-js/built/streaming.types";
const props = defineProps<{ const props = defineProps<{
note: misskey.entities.Note; note: misskey.entities.Note;
pinned?: boolean; pinned?: boolean;
}>(); }>();
let tab = ref("replies"); const tab = ref("replies");
let note = ref(deepClone(props.note)); const note = ref(deepClone(props.note));
const softMuteReasonI18nSrc = (what?: string) => { const softMuteReasonI18nSrc = (what?: string) => {
if (what === "note") return i18n.ts.userSaysSomethingReason; if (what === "note") return i18n.ts.userSaysSomethingReason;
@ -214,12 +214,12 @@ const muted = ref(
); );
const translation = ref(null); const translation = ref(null);
const translating = ref(false); const translating = ref(false);
let conversation = ref<null | misskey.entities.Note[]>([]); const conversation = ref<null | misskey.entities.Note[]>([]);
const replies = ref<misskey.entities.Note[]>([]); const replies = ref<misskey.entities.Note[]>([]);
let directReplies = ref<null | misskey.entities.Note[]>([]); const directReplies = ref<null | misskey.entities.Note[]>([]);
let directQuotes = ref<null | misskey.entities.Note[]>([]); const directQuotes = ref<null | misskey.entities.Note[]>([]);
let clips = ref(); const clips = ref();
let renotes = ref(); const renotes = ref();
let isScrolling; let isScrolling;
const reactionsCount = Object.values(props.note.reactions).reduce( const reactionsCount = Object.values(props.note.reactions).reduce(
@ -238,7 +238,7 @@ const keymap = {
useNoteCapture({ useNoteCapture({
rootEl: el, rootEl: el,
note: note, note,
isDeletedRef: isDeleted, isDeletedRef: isDeleted,
}); });
@ -260,7 +260,7 @@ function react(viaKeyboard = false): void {
(reaction) => { (reaction) => {
os.api("notes/reactions/create", { os.api("notes/reactions/create", {
noteId: note.value.id, noteId: note.value.id,
reaction: reaction, reaction,
}); });
}, },
() => { () => {

View file

@ -49,7 +49,6 @@
<script lang="ts" setup> <script lang="ts" setup>
import { ref } from "vue"; import { ref } from "vue";
import {} from "vue";
import type * as misskey from "firefish-js"; import type * as misskey from "firefish-js";
import { defaultStore } from "@/store"; import { defaultStore } from "@/store";
import MkVisibility from "@/components/MkVisibility.vue"; import MkVisibility from "@/components/MkVisibility.vue";
@ -63,7 +62,7 @@ const props = defineProps<{
pinned?: boolean; pinned?: boolean;
}>(); }>();
let note = ref(props.note); const note = ref(props.note);
const showTicker = const showTicker =
defaultStore.state.instanceTicker === "always" || defaultStore.state.instanceTicker === "always" ||

View file

@ -1,6 +1,6 @@
<template> <template>
<div v-size="{ min: [350, 500] }" class="fefdfafb"> <div v-size="{ min: [350, 500] }" class="fefdfafb">
<MkAvatar class="avatar" :user="$i" disableLink /> <MkAvatar class="avatar" :user="$i" disable-link />
<div class="main"> <div class="main">
<div class="header"> <div class="header">
<MkUserName :user="$i" /> <MkUserName :user="$i" />
@ -11,7 +11,7 @@
:text="preprocess(text).trim()" :text="preprocess(text).trim()"
:author="$i" :author="$i"
:i="$i" :i="$i"
advancedMfm advanced-mfm
/> />
</div> </div>
</div> </div>

View file

@ -11,7 +11,7 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import * as misskey from "firefish-js"; import type * as misskey from "firefish-js";
import XNoteHeader from "@/components/MkNoteHeader.vue"; import XNoteHeader from "@/components/MkNoteHeader.vue";
import MkSubNoteContent from "@/components/MkSubNoteContent.vue"; import MkSubNoteContent from "@/components/MkSubNoteContent.vue";

View file

@ -1,10 +1,10 @@
<template> <template>
<article <article
v-if="!muted.muted || muted.what === 'reply'" v-if="!muted.muted || muted.what === 'reply'"
:id="detailedView ? appearNote.id : null"
ref="el" ref="el"
v-size="{ max: [450, 500] }" v-size="{ max: [450, 500] }"
class="wrpstxzv" class="wrpstxzv"
:id="detailedView ? appearNote.id : null"
tabindex="-1" tabindex="-1"
:class="{ :class="{
children: depth > 1, children: depth > 1,
@ -16,8 +16,8 @@
<div v-if="conversation && depth > 1" class="line"></div> <div v-if="conversation && depth > 1" class="line"></div>
<div <div
class="main" class="main"
@click="noteClick"
:style="{ cursor: expandOnNoteClick ? 'pointer' : '' }" :style="{ cursor: expandOnNoteClick ? 'pointer' : '' }"
@click="noteClick"
> >
<div class="avatar-container"> <div class="avatar-container">
<MkAvatar class="avatar" :user="appearNote.user" /> <MkAvatar class="avatar" :user="appearNote.user" />
@ -32,9 +32,9 @@
<MkSubNoteContent <MkSubNoteContent
class="text" class="text"
:note="note" :note="note"
:parentId="parentId" :parent-id="parentId"
:conversation="conversation" :conversation="conversation"
:detailedView="detailedView" :detailed-view="detailedView"
@focusfooter="footerEl.focus()" @focusfooter="footerEl.focus()"
/> />
<div v-if="translating || translation" class="translation"> <div v-if="translating || translation" class="translation">
@ -117,9 +117,9 @@
appearNote.myReaction != null appearNote.myReaction != null
" "
ref="reactButton" ref="reactButton"
v-tooltip.noDelay.bottom="i18n.ts.removeReaction"
class="button _button reacted" class="button _button reacted"
@click.stop="undoReact(appearNote)" @click.stop="undoReact(appearNote)"
v-tooltip.noDelay.bottom="i18n.ts.removeReaction"
> >
<i class="ph-minus ph-bold ph-lg"></i> <i class="ph-minus ph-bold ph-lg"></i>
</button> </button>
@ -130,7 +130,7 @@
isForeignLanguage && isForeignLanguage &&
translation == null translation == null
" "
class="button _button accent" class="button _button"
@click.stop="translate" @click.stop="translate"
v-tooltip.noDelay.bottom="i18n.ts.translate" v-tooltip.noDelay.bottom="i18n.ts.translate"
> >
@ -149,17 +149,17 @@
</div> </div>
<template v-if="conversation"> <template v-if="conversation">
<MkNoteSub <MkNoteSub
v-if="replyLevel < 11 && depth < 5"
v-for="reply in replies" v-for="reply in replies"
v-if="replyLevel < 11 && depth < 5"
:key="reply.id" :key="reply.id"
:note="reply" :note="reply"
class="reply" class="reply"
:class="{ single: replies.length == 1 }" :class="{ single: replies.length == 1 }"
:conversation="conversation" :conversation="conversation"
:depth="replies.length == 1 ? depth : depth + 1" :depth="replies.length == 1 ? depth : depth + 1"
:replyLevel="replyLevel + 1" :reply-level="replyLevel + 1"
:parentId="appearNote.id" :parent-id="appearNote.id"
:detailedView="detailedView" :detailed-view="detailedView"
/> />
<div v-else-if="replies.length > 0" class="more"> <div v-else-if="replies.length > 0" class="more">
<div class="line"></div> <div class="line"></div>
@ -189,9 +189,9 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { inject, ref, computed } from "vue"; import { computed, inject, ref } from "vue";
import type { Ref } from "vue"; import type { Ref } from "vue";
import * as misskey from "firefish-js"; import type * as misskey from "firefish-js";
import * as mfm from "mfm-js"; import * as mfm from "mfm-js";
import { detect as detectLanguage_ } from "tinyld"; import { detect as detectLanguage_ } from "tinyld";
import XNoteHeader from "@/components/MkNoteHeader.vue"; import XNoteHeader from "@/components/MkNoteHeader.vue";
@ -237,7 +237,7 @@ const props = withDefaults(
}, },
); );
let note = ref(deepClone(props.note)); const note = ref(deepClone(props.note));
const softMuteReasonI18nSrc = (what?: string) => { const softMuteReasonI18nSrc = (what?: string) => {
if (what === "note") return i18n.ts.userSaysSomethingReason; if (what === "note") return i18n.ts.userSaysSomethingReason;
@ -261,7 +261,7 @@ const menuButton = ref<HTMLElement>();
const starButton = ref<InstanceType<typeof XStarButton>>(); const starButton = ref<InstanceType<typeof XStarButton>>();
const renoteButton = ref<InstanceType<typeof XRenoteButton>>(); const renoteButton = ref<InstanceType<typeof XRenoteButton>>();
const reactButton = ref<HTMLElement>(); const reactButton = ref<HTMLElement>();
let appearNote = computed(() => const appearNote = computed(() =>
isRenote ? (note.value.renote as misskey.entities.Note) : note.value, isRenote ? (note.value.renote as misskey.entities.Note) : note.value,
); );
const isDeleted = ref(false); const isDeleted = ref(false);
@ -280,59 +280,57 @@ const replies: misskey.entities.Note[] =
.reverse() ?? []; .reverse() ?? [];
const enableEmojiReactions = defaultStore.state.enableEmojiReactions; const enableEmojiReactions = defaultStore.state.enableEmojiReactions;
const expandOnNoteClick = defaultStore.state.expandOnNoteClick; const expandOnNoteClick = defaultStore.state.expandOnNoteClick;
const lang = localStorage.getItem("lang");
const translateLang = localStorage.getItem("translateLang");
const detectLanguage = (src: string): string => { function detectLanguage(text: string) {
const nodes = mfm.parse(src); const nodes = mfm.parse(text);
const filtered = mfm.extract(nodes, (node) => { const filtered = mfm.extract(nodes, (node) => {
return node.type === "text" || node.type === "quote"; return node.type === "text" || node.type === "quote";
}); });
const purified = mfm.toString(filtered).trim(); const purified = mfm.toString(filtered);
return detectLanguage_(purified); return detectLanguage_(purified);
}; }
const localTranslateLang = localStorage.getItem("translateLang");
const localLang = localStorage.getItem("lang");
const isForeignLanguage: boolean = const isForeignLanguage: boolean =
defaultStore.state.detectPostLanguage && defaultStore.state.detectPostLanguage &&
appearNote.value.text != null && appearNote.value.text != null &&
(() => { (() => {
const targetLanguage = ( const targetLang = (translateLang || lang || navigator.language)?.slice(
localTranslateLang || 0,
localLang || 2,
navigator.language );
)?.slice(0, 2); const postLang = detectLanguage(appearNote.value.text);
const postLanguage = detectLanguage(appearNote.value.text); return postLang !== "" && postLang !== targetLang;
return postLanguage !== "" && postLanguage !== targetLanguage;
})(); })();
const translate_ = async (noteId: number, targetLang: string) => { async function translate_(noteId: number, targetLang: string) {
return await os.api("notes/translate", { return await os.api("notes/translate", {
noteId: noteId, noteId: noteId,
targetLang: targetLang, targetLang: targetLang,
}); });
}; }
const translate = async () => { async function translate() {
if (translation.value != null) return; if (translation.value != null) return;
translating.value = true; translating.value = true;
translation.value = await translate_( translation.value = await translate_(
appearNote.value.id, appearNote.value.id,
localTranslateLang || localLang || navigator.language, translateLang || lang || navigator.language,
); );
// use UI language as the second translation target // use UI language as the second translation language
if ( if (
localTranslateLang != null && translateLang != null &&
localLang != null && lang != null &&
localTranslateLang !== localLang && translateLang !== lang &&
(!translation.value || (!translation.value ||
translation.value.sourceLang.toLowerCase() === translation.value.sourceLang.toLowerCase() ===
localTranslateLang.slice(0, 2)) translateLang.slice(0, 2))
) )
translation.value = await translate_(appearNote.value.id, localLang); translation.value = await translate_(appearNote.value.id, lang);
translating.value = false; translating.value = false;
}; }
useNoteCapture({ useNoteCapture({
rootEl: el, rootEl: el,
@ -358,7 +356,7 @@ function react(viaKeyboard = false): void {
(reaction) => { (reaction) => {
os.api("notes/reactions/create", { os.api("notes/reactions/create", {
noteId: appearNote.value.id, noteId: appearNote.value.id,
reaction: reaction, reaction,
}); });
}, },
() => { () => {

View file

@ -12,7 +12,7 @@
</template> </template>
<template #default="{ items: notes }"> <template #default="{ items: notes }">
<div class="giivymft" :class="{ noGap }" ref="tlEl"> <div ref="tlEl" class="giivymft" :class="{ noGap }">
<XList <XList
ref="notes" ref="notes"
v-slot="{ item: note }" v-slot="{ item: note }"

View file

@ -208,7 +208,7 @@
<MkFollowButton <MkFollowButton
:user="notification.user" :user="notification.user"
:full="true" :full="true"
:hideMenu="true" :hide-menu="true"
/></div /></div
></span> ></span>
<span <span
@ -262,8 +262,8 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { ref, onMounted, onUnmounted, watch } from "vue"; import { onMounted, onUnmounted, ref, watch } from "vue";
import * as misskey from "firefish-js"; import type * as misskey from "firefish-js";
import XReactionIcon from "@/components/MkReactionIcon.vue"; import XReactionIcon from "@/components/MkReactionIcon.vue";
import MkFollowButton from "@/components/MkFollowButton.vue"; import MkFollowButton from "@/components/MkFollowButton.vue";
import XReactionTooltip from "@/components/MkReactionTooltip.vue"; import XReactionTooltip from "@/components/MkReactionTooltip.vue";
@ -300,8 +300,7 @@ const defaultReaction = ["⭐", "👍", "❤️"].includes(instance.defaultReact
? instance.defaultReaction ? instance.defaultReaction
: "⭐"; : "⭐";
let readObserver: IntersectionObserver | undefined; let readObserver: IntersectionObserver | undefined, connection;
let connection;
onMounted(() => { onMounted(() => {
if (!props.notification.isRead) { if (!props.notification.isRead) {

View file

@ -41,7 +41,6 @@
<script lang="ts" setup> <script lang="ts" setup>
import { computed, ref } from "vue"; import { computed, ref } from "vue";
import {} from "vue";
import { notificationTypes } from "firefish-js"; import { notificationTypes } from "firefish-js";
import MkSwitch from "./form/switch.vue"; import MkSwitch from "./form/switch.vue";
import MkInfo from "./MkInfo.vue"; import MkInfo from "./MkInfo.vue";
@ -65,12 +64,12 @@ const props = withDefaults(
}, },
); );
let includingTypes = computed(() => props.includingTypes || []); const includingTypes = computed(() => props.includingTypes || []);
const dialog = ref<InstanceType<typeof XModalWindow>>(); const dialog = ref<InstanceType<typeof XModalWindow>>();
let typesMap = ref<Record<(typeof notificationTypes)[number], boolean>>({}); const typesMap = ref<Record<(typeof notificationTypes)[number], boolean>>({});
let useGlobalSetting = ref( const useGlobalSetting = ref(
(includingTypes.value === null || includingTypes.value.length === 0) && (includingTypes.value === null || includingTypes.value.length === 0) &&
props.showGlobalToggle, props.showGlobalToggle,
); );

View file

@ -28,7 +28,7 @@ const emit = defineEmits<{
}>(); }>();
const zIndex = os.claimZIndex("high"); const zIndex = os.claimZIndex("high");
let showing = ref(true); const showing = ref(true);
onMounted(() => { onMounted(() => {
window.setTimeout(() => { window.setTimeout(() => {

View file

@ -26,7 +26,7 @@
" "
:key="notification.id" :key="notification.id"
:note="notification.note" :note="notification.note"
:collapsedReply=" :collapsed-reply="
notification.type === 'reply' || notification.type === 'reply' ||
(notification.type === 'mention' && (notification.type === 'mention' &&
notification.note.replyId != null) notification.note.replyId != null)
@ -46,9 +46,10 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { onUnmounted, onMounted, computed, ref } from "vue"; import { computed, onMounted, onUnmounted, ref } from "vue";
import { notificationTypes } from "firefish-js"; import type { notificationTypes } from "firefish-js";
import MkPagination, { Paging } from "@/components/MkPagination.vue"; import type { Paging } from "@/components/MkPagination.vue";
import MkPagination from "@/components/MkPagination.vue";
import XNotification from "@/components/MkNotification.vue"; import XNotification from "@/components/MkNotification.vue";
import XList from "@/components/MkDateSeparatedList.vue"; import XList from "@/components/MkDateSeparatedList.vue";
import XNote from "@/components/MkNote.vue"; import XNote from "@/components/MkNote.vue";

View file

@ -8,8 +8,8 @@
:buttons-left="buttonsLeft" :buttons-left="buttonsLeft"
:buttons-right="buttonsRight" :buttons-right="buttonsRight"
:contextmenu="contextmenu" :contextmenu="contextmenu"
@closed="$emit('closed')"
class="page-window" class="page-window"
@closed="$emit('closed')"
> >
<template #header> <template #header>
<template v-if="pageMetadata?.value"> <template v-if="pageMetadata?.value">
@ -30,7 +30,8 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { ComputedRef, provide, ref, computed } from "vue"; import type { ComputedRef } from "vue";
import { computed, provide, ref } from "vue";
import RouterView from "@/components/global/RouterView.vue"; import RouterView from "@/components/global/RouterView.vue";
import XWindow from "@/components/MkWindow.vue"; import XWindow from "@/components/MkWindow.vue";
import { popout as _popout } from "@/scripts/popout"; import { popout as _popout } from "@/scripts/popout";
@ -39,7 +40,8 @@ import { url } from "@/config";
import { mainRouter, routes } from "@/router"; import { mainRouter, routes } from "@/router";
import { Router } from "@/nirax"; import { Router } from "@/nirax";
import { i18n } from "@/i18n"; import { i18n } from "@/i18n";
import { PageMetadata, provideMetadataReceiver } from "@/scripts/page-metadata"; import type { PageMetadata } from "@/scripts/page-metadata";
import { provideMetadataReceiver } from "@/scripts/page-metadata";
const props = defineProps<{ const props = defineProps<{
initialPath: string; initialPath: string;
@ -51,8 +53,8 @@ defineEmits<{
const router = new Router(routes, props.initialPath); const router = new Router(routes, props.initialPath);
let pageMetadata = ref<null | ComputedRef<PageMetadata>>(); const pageMetadata = ref<null | ComputedRef<PageMetadata>>();
let windowEl = ref<InstanceType<typeof XWindow>>(); const windowEl = ref<InstanceType<typeof XWindow>>();
const history = ref<{ path: string; key: any }[]>([ const history = ref<{ path: string; key: any }[]>([
{ {
path: router.getCurrentPath(), path: router.getCurrentPath(),

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