1
0
Fork 1
mirror of https://example.com synced 2024-11-22 02:36:38 +09:00

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
#disableHsts: true
# Number of worker processes
#clusterLimit: 1
# Worker only mode
#onlyQueueProcessor: 1
# Number of worker processes by type.
# The sum must not exceed the number of available cores.
#clusterLimits:
# web: 1
# queue: 1
# Job concurrency per worker
# deliverJobConcurrency: 128

3
.gitignore vendored
View file

@ -32,6 +32,9 @@ coverage
# docker dev config
/dev/docker-compose.yml
# ESLint
.eslintcache
# misskey
built
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:
# restart: unless-stopped
# image: docker.io/valeriansaliou/sonic:v1.4.0
# logging:
# driver: none
# networks:
# - calcnet
# volumes:

View file

@ -762,8 +762,7 @@ no: "No"
driveFilesCount: "Number of Drive files"
driveUsage: "Drive space usage"
noCrawle: "Reject crawler indexing"
noCrawleDescription: "Ask search engines to not index your profile page, posts, Pages,
etc."
noCrawleDescription: "Ask external search engines to not index your content."
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."
alwaysMarkSensitive: "Mark as NSFW by default"
@ -1141,8 +1140,6 @@ deletePasskeys: "Delete passkeys"
delete2faConfirm: "This will irreversibly delete 2FA on this account. Proceed?"
deletePasskeysConfirm: "This will irreversibly delete all passkeys and security keys on this account. Proceed?"
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"
showBigPostButton: "Show a bigger post button in the posting form"
confirm: "Confirm"
@ -1150,6 +1147,10 @@ emphasizeFollowed: "Highlight the \"Follows you\" sign on your follower info"
importZip: "Import ZIP"
exportZip: "Export ZIP"
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:
description: "Reduces the effort of server moderation through automatically recognizing

View file

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

View file

@ -1,7 +1,7 @@
---
_lang_: "Português"
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}"
search: "Buscar"
notifications: "Notificações"
@ -44,7 +44,8 @@ copyContent: "Copiar conteúdos"
copyLink: "Copiar hiperligação"
delete: "Eliminar"
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"
sendMessage: "Enviar uma mensagem"
copyUsername: "Copiar nome de utilizador"
@ -64,9 +65,11 @@ import: "Importar"
export: "Exportar"
files: "Ficheiros"
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}?"
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."
lists: "Listas"
noLists: "Não tens nenhuma lista"
@ -81,9 +84,12 @@ error: "Erro"
somethingHappened: "Ocorreu um erro"
retry: "Tentar novamente"
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."
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."
pageLoadErrorDescription: "Isto é normalmente causado por erros de rede ou pela cache
do browser. Experimenta limpar a cache e tenta novamente após algum tempo."
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"
privacy: "Privacidade"
makeFollowManuallyApprove: "Pedidos de seguimento precisam ser aprovados"
@ -108,7 +114,8 @@ sensitive: "Conteúdo sensível"
add: "Adicionar"
reaction: "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"
attachCancel: "Remover anexo"
markAsSensitive: "Marcar como sensível"
@ -137,13 +144,18 @@ emojiUrl: "URL do Emoji"
addEmoji: "Adicionar um Emoji"
settingGuide: "Guia de configuração"
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ô"
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"
flagAsCatDescription: "Ative essa opção para marcar essa conta como gato."
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"
addAccount: "Adicionar Conta"
loginFailed: "Não consegui logar"
@ -156,7 +168,10 @@ searchWith: "Buscar: {q}"
youHaveNoLists: "Não tem nenhuma lista"
followConfirm: "Tem certeza que quer deixar de seguir {name}?"
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"
selectUser: "Selecionar utilizador"
recipient: "Morada"
@ -186,11 +201,15 @@ instanceInfo: "Informações da instância"
statistics: "Estatisticas"
clearQueue: "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"
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"
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"
mutedUsers: "Silenciar utilizador"
blockedUsers: "Utilizadores bloqueados"
@ -238,7 +257,9 @@ saved: "Salvo"
messaging: "Chat"
upload: "Enviando"
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"
fromUrl: "Da URL"
uploadFromUrl: "Carregamento de URL"
@ -262,8 +283,8 @@ yearsOld: "{age} anos"
registeredDate: "Data de registro"
location: "Lugar, colocar"
theme: "tema"
themeForLightMode: "Temas usados no modo de luz"
themeForDarkMode: "Temas usados no modo escuro"
themeForLightMode: "Tema a usar no Modo Diurno"
themeForDarkMode: "Temas usados no Modo Noturno"
light: "Claro"
dark: "Escuro"
lightThemes: "Tema claro"
@ -271,7 +292,7 @@ darkThemes: "Tema escuro"
syncDeviceDarkMode: "Sincronize com o modo escuro do dispositivo"
drive: "Unidades"
fileName: "Nome do Ficheiro"
selectFile: "Selecione os arquivos"
selectFile: "Selecione o arquivo"
selectFiles: "Selecione os arquivos"
selectFolder: "Selecionar uma pasta"
selectFolders: "Selecionar uma pasta"
@ -286,8 +307,9 @@ emptyFolder: "A pasta está vazia"
unableToDelete: "Não é possível eliminar"
inputNewFileName: "Por favor, digite um novo nome para a pasta!"
inputNewDescription: "Insira uma nova legenda"
inputNewFolderName: "Por favor, digite um novo nome para a pasta!"
circularReferenceFolder: "A pasta de destino é uma subpasta da pasta que você deseja mover."
inputNewFolderName: "Por favor, digite um novo nome para a pasta"
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."
copyUrl: "Copiar URL"
rename: "Renomear"
@ -321,7 +343,8 @@ connectService: "Conectar"
disconnectService: "Desconectar"
enableLocalTimeline: "Ativar linha do tempo local"
enableGlobalTimeline: "Ativar linha do tempo global"
disablingTimelinesInfo: "Se você desabilitar essas linhas do tempo, administradores 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"
enableRegistration: "Permitir que qualquer pessoa se registre"
invite: "Convidar"
@ -333,9 +356,11 @@ bannerUrl: "URL da imagem do banner"
backgroundImageUrl: "URL da imagem de fundo"
basicInfo: "Informações básicas"
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"
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"
pinnedNotes: "Post fixado"
hcaptcha: "hCaptcha"
@ -346,18 +371,21 @@ recaptcha: "reCAPTCHA"
enableRecaptcha: "Habilitar reCAPTCHA"
recaptchaSiteKey: "Chave do sítio web"
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"
manageAntennas: "Gestão de antena"
name: "Nome"
antennaSource: "Origem de entrada"
antennaKeywords: "Palavras-chave recebidas"
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"
withFileAntenna: "Apenas notas com arquivos anexados"
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"
withReplies: "Incluindo resposta"
connectedTo: "Você está conectado à seguinte conta"
@ -433,15 +461,19 @@ showFeaturedNotesInTimeline: "Mostrar notas recomendadas na linha do tempo"
objectStorage: "Armazenamento de objetos"
useObjectStorage: "Usar armazenamento de objetos"
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"
objectStorageBucketDesc: "Especifique o nome do bucket do serviço a ser usado."
objectStoragePrefix: "Prefixo"
objectStoragePrefixDesc: "Ele é armazenado neste diretório de prefixo."
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"
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"
objectStorageUseSSLDesc: "Desative-o se não quiser usar https para conexões de API"
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"
serverLogs: "Registro do servidor"
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"
sounds: "Sons"
listen: "Ouvir"
@ -618,7 +651,8 @@ _pages:
_dailyRannum:
arg1: "Valor mínimo"
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:
arg1: "Listas"
seedRandom: "Aleatório (com semente)"
@ -634,7 +668,8 @@ _pages:
_seedRandomPick:
arg1: "Semente"
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:
arg1: "Lista de texto"
pick: "Escolhe a partir da lista"
@ -665,7 +700,8 @@ _pages:
_for:
arg1: "Número de repetições"
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!"
types:
string: "Texto"
@ -730,3 +766,5 @@ _deck:
list: "Listas"
mentions: "Menções"
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",
"version": "1.0.5-dev7",
"version": "1.0.5-dev11",
"codename": "aqua",
"repository": {
"type": "git",
"url": "https://code.naskya.net/naskya/firefish"
},
"packageManager": "pnpm@8.7.0",
"packageManager": "pnpm@8.7.1",
"private": true,
"scripts": {
"rebuild": "pnpm run clean && pnpm node ./scripts/build-greet.js && pnpm -r --parallel run build && pnpm run gulp",
"build": "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": "./scripts/build-greet.sh && pnpm -r --parallel run build && pnpm run gulp",
"start": "pnpm --filter backend run start",
"start:test": "pnpm --filter backend run start:test",
"init": "pnpm run migrate",
@ -33,18 +33,19 @@
"chokidar": "^3.3.1"
},
"dependencies": {
"@bull-board/api": "5.7.2",
"@bull-board/ui": "5.7.2",
"@bull-board/api": "5.8.0",
"@bull-board/ui": "5.8.0",
"@napi-rs/cli": "^2.16.2",
"@tensorflow/tfjs": "^3.21.0",
"@tensorflow/tfjs": "^4.10.0",
"js-yaml": "4.1.0",
"seedrandom": "^3.0.5"
},
"devDependencies": {
"@biomejs/biome": "1.0.0",
"@types/gulp": "4.0.13",
"@types/gulp-rename": "2.0.2",
"@types/node": "20.4.9",
"chalk": "4.1.2",
"@types/node": "20.5.8",
"add": "2.0.6",
"cross-env": "7.0.3",
"execa": "5.1.1",
"gulp": "4.0.2",
@ -53,7 +54,7 @@
"gulp-replace": "1.1.4",
"gulp-terser": "2.1.0",
"install-peers": "^1.0.4",
"rome": "^12.1.3",
"typescript": "5.1.6"
"pnpm": "8.7.1",
"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 m20230709_000510_move_antenna_to_cache;
mod m20230806_170616_fix_antenna_stream_ids;
mod m20230904_013244_is_indexable;
pub struct Migrator;
@ -17,6 +18,7 @@ impl MigratorTrait for Migrator {
Box::new(m20230627_185451_index_note_url::Migration),
Box::new(m20230709_000510_move_antenna_to_cache::Migration),
Box::new(m20230806_170616_fix_antenna_stream_ids::Migration),
Box::new(m20230904_013244_is_indexable::Migration),
]
}
}

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",
"universal": "napi universal",
"version": "napi version",
"format": "cargo fmt --all",
"lint": "cargo clippy --fix"
"format": "cargo fmt --all -- --check",
"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>,
#[sea_orm(column_name = "speakAsCat")]
pub speak_as_cat: bool,
#[sea_orm(column_name = "isIndexable")]
pub is_indexable: bool,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]

View file

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

View file

@ -19,7 +19,12 @@ const ev = new Xev();
* Init process
*/
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) {
await masterMain();

View file

@ -30,40 +30,34 @@ const themeColor = chalk.hex("#31748f");
function greet() {
if (!envOption.quiet) {
//#region Firefish logo
const v = `v${meta.version}`;
console.log(
themeColor(
" ▄▄▄▄▄▄▄ ▄▄▄ ▄▄▄▄▄▄ ▄▄▄▄▄▄▄ ▄▄▄▄▄▄▄ ▄▄▄ ▄▄▄▄▄▄▄ ▄▄ ▄▄ ◯ ",
"██████╗ ██╗██████╗ ███████╗███████╗██╗███████╗██╗ ██╗ ○ ▄ ▄ ",
),
);
console.log(
themeColor(
"█ █ █ ▄ █ █ █ █ █ █ █ █ █ ○ ▄ ▄",
"██╔════╝██║██╔══██╗██╔════╝██╔════╝██║██╔════╝██║ ██║ ⚬ █▄▄ █▄▄ ",
),
);
console.log(
themeColor(
"█ ▄▄▄█ █ █ █ █ █ ▄▄▄█ ▄▄▄█ █ ▄▄▄▄▄█ █▄█ █ ⚬ █▄▄ █▄▄ ",
"█████╗ ██║██████╔╝█████╗ █████╗ ██║███████╗███████║ ▄▄▄▄▄▄ ▄ ",
),
);
console.log(
themeColor(
"█ █▄▄▄█ █ █▄▄█▄█ █▄▄▄█ █▄▄▄█ █ █▄▄▄▄▄█ █ ▄▄▄▄▄▄ ▄",
"██╔══╝ ██║██╔══██╗██╔══╝ ██╔══╝ ██║╚════██║██╔══██║ █ █ █▄▄ ",
),
);
console.log(
themeColor(
"█ ▄▄▄█ █ ▄▄ █ ▄▄▄█ ▄▄▄█ █▄▄▄▄▄ █ ▄ █ █ █ █▄▄",
"██║ ██║██║ ██║███████╗██║ ██║███████║██║ ██║ █ ● ● █ ",
),
);
console.log(
themeColor(
"█ █ █ █ █ █ █ █▄▄▄█ █ █ █▄▄▄▄▄█ █ █ █ █ █ ● ● █",
),
);
console.log(
themeColor(
"█▄▄▄█ █▄▄▄█▄▄▄█ █▄█▄▄▄▄▄▄▄█▄▄▄█ █▄▄▄█▄▄▄▄▄▄▄█▄▄█ █▄▄█ ▀▄▄▄▄▄▄▀",
"╚═╝ ╚═╝╚═╝ ╚═╝╚══════╝╚═╝ ╚═╝╚══════╝╚═╝ ╚═╝ ▀▄▄▄▄▄▄▀ ",
),
);
//#endregion
@ -117,7 +111,7 @@ export async function masterMain() {
bootLogger.succ("Firefish initialized");
if (!envOption.disableClustering) {
await spawnWorkers(config.clusterLimit);
await spawnWorkers(config.clusterLimits);
}
bootLogger.succ(
@ -126,7 +120,11 @@ export async function masterMain() {
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/queue-stats.js").then((x) => x.default());
import("../daemons/janitor.js").then((x) => x.default());
@ -142,7 +140,7 @@ function showEnvironment(): void {
if (env !== "production") {
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) {
const workers = Math.min(limit, os.cpus().length);
bootLogger.info(`Starting ${workers} worker${workers === 1 ? "" : "s"}...`);
await Promise.all([...Array(workers)].map(spawnWorker));
async function spawnWorkers(
clusterLimits: Required<Config["clusterLimits"]>,
): Promise<void> {
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");
}
function spawnWorker(): Promise<void> {
function spawnWorker(mode: "web" | "queue"): Promise<void> {
return new Promise((res) => {
const worker = cluster.fork();
const worker = cluster.fork({ mode });
worker.on("message", (message) => {
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);
}
if (message !== "ready") return;

View file

@ -1,6 +1,7 @@
import cluster from "node:cluster";
import { initDb } from "../db/postgre.js";
import config from "@/config/index.js";
import os from "node:os";
/**
* Init worker process
@ -8,13 +9,20 @@ import config from "@/config/index.js";
export async function workerMain() {
await initDb();
if (!config.onlyQueueProcessor) {
if (!process.env.mode || process.env.mode === "web") {
// start server
await import("../server/index.js").then((x) => x.default());
}
// start job queue
import("../queue/index.js").then((x) => x.default());
if (!process.env.mode || process.env.mode === "queue") {
// 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) {
// Send a 'ready' message to parent process

View file

@ -59,6 +59,23 @@ export default function load() {
if (config.cacheServer && !config.cacheServer.prefix)
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);
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -415,7 +415,7 @@ export async function resolveNote(
`host ${extractDbHost(uri)} is blocked`,
);
const unlock = await getApLock(uri);
const lock = await getApLock(uri);
try {
//#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.
return await createNote(uri, resolver, true);
} finally {
unlock();
await lock.release();
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -608,7 +608,7 @@ export default define(meta, paramDef, async (ps, user) => {
throw new ApiError(meta.errors.noSuchNote);
}
if (publishing) {
if (publishing && user.isIndexable) {
index(note, true);
// 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 { getAgentByUrl } from "@/misc/fetch.js";
import { fetchMeta } from "@/misc/fetch-meta.js";
import { Notes } from "@/models/index.js";
import { ApiError } from "../../error.js";
import { getNote } from "../../common/getters.js";
import define from "../../define.js";
@ -12,7 +11,7 @@ import define from "../../define.js";
export const meta = {
tags: ["notes"],
requireCredential: false,
requireCredential: true,
requireCredentialPrivateMode: true,
res: {

View file

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

View file

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

View file

@ -7,16 +7,16 @@ doctype html
//
-
▄▄▄▄▄▄▄ ▄▄▄ ▄▄▄▄▄▄ ▄▄▄▄▄▄▄ ▄▄▄▄▄▄▄ ▄▄▄ ▄▄▄▄▄▄▄ ▄▄ ▄▄ ◯
█ █ █ ▄ █ █ █ █ █ █ █ █ █ ○ ▄ ▄
█ ▄▄▄█ █ █ █ █ █ ▄▄▄█ ▄▄▄█ █ ▄▄▄▄▄█ █▄█ █ ⚬ █▄▄ █▄▄
█ █▄▄▄█ █ █▄▄█▄█ █▄▄▄█ █▄▄▄█ █ █▄▄▄▄▄█ █ ▄▄▄▄▄▄ ▄
█ ▄▄▄█ █ ▄▄ █ ▄▄▄█ ▄▄▄█ █▄▄▄▄▄ █ ▄ █ █ █ █▄▄
█ █ █ █ █ █ █ █▄▄▄█ █ █ █▄▄▄▄▄█ █ █ █ █ █ ● ● █
█▄▄▄█ █▄▄▄█▄▄▄█ █▄█▄▄▄▄▄▄▄█▄▄▄█ █▄▄▄█▄▄▄▄▄▄▄█▄▄█ █▄▄█ ▀▄▄▄▄▄▄▀
██████╗ ██╗██████╗ ███████╗███████╗██╗███████╗██╗ ██╗ ○ ▄ ▄
██╔════╝██║██╔══██╗██╔════╝██╔════╝██║██╔════╝██║ ██║ ⚬ █▄▄ █▄▄
█████╗ ██║██████╔╝█████╗ █████╗ ██║███████╗███████║ ▄▄▄▄▄▄ ▄
██╔══╝ ██║██╔══██╗██╔══╝ ██╔══╝ ██║╚════██║██╔══██║ █ █ █▄▄
██║ ██║██║ ██║███████╗██║ ██║███████║██║ ██║ █ ● ● █
╚═╝ ╚═╝╚═╝ ╚═╝╚══════╝╚═╝ ╚═╝╚══════╝╚═╝ ╚═╝ ▀▄▄▄▄▄▄▀
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
html

View file

@ -24,9 +24,11 @@ block meta
unless privateMode
if profile.noCrawle
meta(name='robots' content='noindex,nofollow,noarchive,nocache,noimageindex')
if profile.preventAiLearning
meta(name='robots' content='noai')
meta(name='robots' content='noimageai')
meta(name='GPTBot' content='noindex')
meta(name='misskey:user-username' content=user.username)
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}`;
const unlock = await getChartInsertLock(lockKey);
const lock = await getChartInsertLock(lockKey);
try {
// ロック内でもう1回チェックする
const currentLog = (await repository.findOneBy({
@ -466,7 +466,7 @@ export default abstract class Chart<T extends Schema> {
return log;
} finally {
unlock();
await lock.release();
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -199,7 +199,7 @@
</template>
<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 MkModal from "@/components/MkModal.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 selectedValue = ref(props.select?.default ?? null);
let disabledReason = ref<null | "charactersExceeded" | "charactersBelow">(null);
const disabledReason = ref<null | "charactersExceeded" | "charactersBelow">(
null,
);
const okButtonDisabled = computed<boolean>(() => {
if (props.input) {
if (props.input.minLength) {

View file

@ -39,7 +39,7 @@
<script lang="ts" setup>
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 MkDriveFileThumbnail from "@/components/MkDriveFileThumbnail.vue";
import bytes from "@/filters/bytes";
@ -160,7 +160,7 @@ function rename() {
if (canceled) return;
os.api("drive/files/update", {
fileId: props.file.id,
name: name,
name,
});
});
}
@ -179,7 +179,7 @@ function describe() {
{
done: (result) => {
if (!result || result.canceled) return;
let comment = result.result;
const comment = result.result;
os.api("drive/files/update", {
fileId: props.file.id,
comment: comment.length === 0 ? null : comment,

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,10 +1,10 @@
<template>
<div
class="hpaizdrt"
ref="ticker"
v-tooltip="
`${capitalize(instance.softwareName)} ${instance.softwareVersion}`
"
ref="ticker"
class="hpaizdrt"
:style="bg"
>
<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
const instance = props.instance ?? {

View file

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

View file

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

View file

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

View file

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

View file

@ -14,8 +14,8 @@
width: width && !asDrawer ? width + 'px' : '',
maxHeight: maxHeight ? maxHeight + 'px' : '',
}"
@contextmenu.self="(e) => e.preventDefault()"
tabindex="-1"
@contextmenu.self="(e) => e.preventDefault()"
>
<template v-for="item in items2">
<div v-if="item === null" class="divider"></div>
@ -47,7 +47,7 @@
v-if="item.avatar"
:user="item.avatar"
class="avatar"
disableLink
disable-link
/>
<span :style="item.textStyle || ''">{{
item.text
@ -100,7 +100,7 @@
<MkAvatar
:user="item.user"
class="avatar"
disableLink
disable-link
/><MkUserName :user="item.user" />
<span
v-if="item.indicate"
@ -168,7 +168,7 @@
v-if="item.avatar"
:user="item.avatar"
class="avatar"
disableLink
disable-link
/>
<span :style="item.textStyle || ''">{{
item.text
@ -210,11 +210,16 @@ import {
ref,
watch,
} from "vue";
import { FocusTrap } from "focus-trap-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 { i18n } from "@/i18n";
import { FocusTrap } from "focus-trap-vue";
const XChild = defineAsyncComponent(() => import("./MkMenu.child.vue"));
const focusTrap = ref();
@ -233,13 +238,13 @@ const emit = defineEmits<{
(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(
() => props.items,
@ -267,8 +272,8 @@ watch(
},
);
let childMenu = ref<MenuItem[] | null>();
let childTarget = ref<HTMLElement | null>();
const childMenu = ref<MenuItem[] | null>();
const childTarget = ref<HTMLElement | null>();
function closeChild() {
childMenu.value = null;

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,6 +1,6 @@
<template>
<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="header">
<MkUserName :user="$i" />
@ -11,7 +11,7 @@
:text="preprocess(text).trim()"
:author="$i"
:i="$i"
advancedMfm
advanced-mfm
/>
</div>
</div>

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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