
Lingua 🇮🇹 Italiano
Documentazione
Primi passi
Autenticazione
Utilizzo
Add Comments to Your iOS App
Questa è la libreria iOS ufficiale di FastComments.
Incorpora widget di commenti in tempo reale, chat e recensioni nella tua app iOS.
Repository
Funzionalità 
- Alberi di commenti gerarchici con risposte annidate e paginazione
- Feed sociale con creazione di post, reazioni e allegati multimediali
- Modalità chat live con scorrimento automatico e separatori di data
- Aggiornamenti in tempo reale via WebSocket (nuovi commenti, voti, presenza)
- Single Sign-On (SSO semplice per i test, SSO sicuro per la produzione)
- Editor di testo ricco con grassetto, corsivo, codice e @menzioni
- Votazione con stili configurabili (frecce su/giù o cuori)
- Azioni di moderazione: segnala, fissa, chiudi, blocca
- Temi completamente personalizzabili con preset e personalizzazione completa
- Pulsanti personalizzati nella barra degli strumenti per i commenti e la creazione di post nel feed
- Caricamento immagini
- Supporto per la regione UE
- Presenza utenti (indicatori online/offline)
- Filtraggio del feed basato sui tag
- Supporto alla localizzazione
Installazione 
Aggiungi FastCommentsUI al tuo progetto utilizzando Swift Package Manager.
In Xcode: File > Aggiungi dipendenze del pacchetto, quindi inserisci l'URL del repository.
Oppure aggiungilo al tuo Package.swift:
dependencies: [
.package(url: "https://github.com/fastcomments/fastcomments-ios.git", from: "1.0.0")
]
Poi aggiungi il prodotto al tuo target:
.target(
name: "YourApp",
dependencies: [
.product(name: "FastCommentsUI", package: "fastcomments-ios")
]
)
Importa entrambi i moduli dove necessario:
import FastCommentsUI
import FastCommentsSwift
Avvio rapido 
La configurazione minima per visualizzare un widget di commenti:
import SwiftUI
import FastCommentsUI
struct ContentView: View {
@StateObject private var sdk = FastCommentsSDK(
config: FastCommentsWidgetConfig(
tenantId: "demo",
urlId: "my-page-1",
url: "https://example.com/page-1",
pageTitle: "My Page"
)
)
var body: some View {
FastCommentsView(sdk: sdk)
.task {
try? await sdk.load()
}
}
}
Sostituisci "demo" con il tuo ID tenant FastComments. Il urlId identifica la pagina o il thread in cui vengono memorizzati i commenti.
Autenticazione (SSO) 
FastComments supporta tre modalità di autenticazione:
- Anonimo -- nessun token SSO; gli utenti ottengono identità basate sulla sessione
- SSO semplice -- token lato client per demo e test (non sicuro)
- SSO sicuro -- token firmato dal server per produzione
SSO semplice
Utile per demo e test locali. Chiunque può impersonare qualsiasi utente con SSO semplice, quindi non usarlo in produzione.
import FastCommentsSwift
let userData = SimpleSSOUserData(
username: "Jane Doe",
email: "jane@example.com",
avatar: "https://example.com/avatar.jpg"
)
let sso = FastCommentsSSO.createSimple(simpleSSOUserData: userData)
let token = try? sso.prepareToSend()
let config = FastCommentsWidgetConfig(
tenantId: "YOUR_TENANT_ID",
urlId: "my-page-1",
sso: token
)
let sdk = FastCommentsSDK(config: config)
SimpleSSOUserData supporta anche campi opzionali:
id-- ID utente (predefinito all'email se non impostato)displayName-- nome visualizzato separatodisplayLabel-- etichetta personalizzata mostrata accanto al nome (es. "VIP")websiteUrl-- link sul nome dell'utentelocale-- codice localeisProfileActivityPrivate-- nascondi attività del profilo (predefinito: true)
SSO sicuro
In produzione, il tuo backend genera un token SSO firmato usando il tuo API secret. L'app iOS recupera questo token dal tuo server e lo passa alla config.
Sul tuo backend (usando il FastComments Swift SDK o qualsiasi linguaggio):
let userData = SecureSSOUserData(
id: "user-123",
email: "user@example.com",
username: "Display Name",
avatar: "https://example.com/avatar.jpg"
)
let sso = try FastCommentsSSO.createSecure(apiKey: "YOUR_API_KEY", secureSSOUserData: userData)
let token = try sso.prepareToSend()
// Restituisci questo token alla tua app iOS tramite la tua API
Nella tua app iOS:
struct MyView: View {
@StateObject private var sdk = FastCommentsSDK(
config: FastCommentsWidgetConfig(
tenantId: "YOUR_TENANT_ID",
urlId: "my-page-1"
)
)
@State private var isLoadingToken = true
var body: some View {
Group {
if isLoadingToken {
ProgressView("Loading...")
} else {
FastCommentsView(sdk: sdk)
}
}
.task {
// Recupera il token dal tuo backend
let token = try? await fetchSSOTokenFromYourBackend()
// Crea una nuova config con il token, o impostalo prima del caricamento
isLoadingToken = false
try? await sdk.load()
}
}
}
SecureSSOUserData supporta campi aggiuntivi:
optedInNotifications-- opt-in alle notifiche emaildisplayLabel-- etichetta personalizzatadisplayName-- nome visualizzatowebsiteUrl-- URL del sito webgroupIds-- appartenenze ai gruppiisAdmin-- privilegi di adminisModerator-- privilegi di moderatoreisProfileActivityPrivate-- privacy del profilo
Commenti annidati 
Uso di base
struct CommentsPage: View {
@StateObject private var sdk = FastCommentsSDK(
config: FastCommentsWidgetConfig(
tenantId: "YOUR_TENANT_ID",
urlId: "article-42",
url: "https://example.com/article/42",
pageTitle: "Article Title"
)
)
var body: some View {
FastCommentsView(sdk: sdk)
.task {
try? await sdk.load()
}
}
}
Stili di voto
Lo stile di voto predefinito mostra frecce su/giù. Passa ._1 per voti a forma di cuore:
FastCommentsView(sdk: sdk, voteStyle: ._1)
| Stile | Aspetto |
|---|---|
._0 |
Pulsanti freccia su/giu con conteggio netto |
._1 |
Singolo pulsante a forma di cuore con conteggio |
Callback per eventi
Usa callback in stile modifier per gestire le interazioni dell'utente:
FastCommentsView(sdk: sdk)
.onCommentPosted { comment in
print("New comment: \(comment.commentHTML)")
}
.onReplyClick { renderableComment in
print("Replying to: \(renderableComment.comment.id)")
}
.onUserClick { context, userInfo, source in
// source è .name o .avatar
print("Tapped \(userInfo.displayName)")
}
Applicare un tema
Passa un tema tramite l'ambiente SwiftUI:
FastCommentsView(sdk: sdk)
.fastCommentsTheme(myTheme)
.task { try? await sdk.load() }
Oppure impostalo direttamente sull'SDK:
sdk.theme = FastCommentsTheme.modern
Direzione di ordinamento
sdk.defaultSortDirection = .nf // I più recenti prima (predefinito)
sdk.defaultSortDirection = .of // I più vecchi prima
sdk.defaultSortDirection = .mr // Più rilevanti
Chat in tempo reale 
LiveChatView fornisce un'esperienza di chat in tempo reale con scorrimento automatico, separatori di data e un layout compatto. Configura automaticamente l'SDK per l'ordinamento dal più vecchio al più recente e la visualizzazione live immediata.
struct ChatView: View {
@StateObject private var sdk: FastCommentsSDK = {
let config = FastCommentsWidgetConfig(
tenantId: "YOUR_TENANT_ID",
urlId: "chat-room-1",
sso: ssoToken // SSO consigliato in modo che gli utenti abbiano nomi
)
return FastCommentsSDK(config: config)
}()
var body: some View {
LiveChatView(sdk: sdk)
.onCommentPosted { comment in
print("Sent: \(comment.commentHTML)")
}
.task {
try? await sdk.load()
}
}
}
LiveChatView supporta i seguenti callback:
.onCommentPosted-- viene attivato quando l'utente invia un messaggio.onCommentDeleted-- viene attivato quando un messaggio viene eliminato.onUserClick-- viene attivato quando si tocca il nome o l'avatar di un utente
Feed sociale 
Il sistema del feed è un SDK separato (FastCommentsFeedSDK) con la propria view.
Caricamento e visualizzazione del feed
struct FeedPage: View {
@StateObject private var sdk: FastCommentsFeedSDK = {
let config = FastCommentsWidgetConfig(
tenantId: "YOUR_TENANT_ID",
urlId: "my-feed",
sso: ssoToken
)
return FastCommentsFeedSDK(config: config)
}()
@State private var commentsPost: FeedPost?
var body: some View {
FastCommentsFeedView(sdk: sdk)
.onPostSelected { post in
commentsPost = post
}
.onCommentsRequested { post in
commentsPost = post
}
.onSharePost { post in
// Mostra il foglio di condivisione
}
.onUserClick { context, userInfo, source in
// Vai al profilo utente
}
.onMediaClick { mediaItem, index in
// Mostra visualizzatore immagini a schermo intero
}
.task {
try? await sdk.load()
}
}
}
La view del feed include automaticamente pull-to-refresh e infinite scroll.
Creazione di post
Usa FeedPostCreateView per presentare un modulo di creazione post:
@State private var showCreatePost = false
// Nel corpo della tua view:
.sheet(isPresented: $showCreatePost) {
FeedPostCreateView(
sdk: sdk,
onPostCreated: { post in
showCreatePost = false
Task { try? await sdk.refresh() }
},
onCancelled: {
showCreatePost = false
}
)
}
Reazioni ai post
L'SDK gestisce le reazioni con aggiornamenti ottimistici:
try await sdk.reactPost(postId: post.id, reactionType: "l")
// Verifica lo stato della reazione
let hasLiked = sdk.hasUserReacted(postId: post.id, reactType: "l")
let likeCount = sdk.getLikeCount(postId: post.id)
Apertura dei commenti su un post
Usa CommentsSheet per visualizzare i commenti di un post del feed. Crea internamente un'istanza di FastCommentsSDK usando la config del feed SDK:
.sheet(item: $commentsPost) { post in
CommentsSheet(post: post, feedSDK: sdk, onUserClick: { context, userInfo, source in
// Gestisci il click dell'utente
})
}
Nota: FeedPost deve conformarsi a Identifiable per .sheet(item:). Aggiungi questa extension:
extension FeedPost: @retroactive Identifiable {}
Filtraggio del feed basato sui tag
Implementa il protocollo TagSupplier per filtrare i post del feed in base ai tag:
struct TeamTagSupplier: TagSupplier {
func getTags(currentUser: UserSessionInfo?) -> [String]? {
guard let user = currentUser else { return nil }
return ["team:\(user.id ?? "")", "public"]
}
}
sdk.tagSupplier = TeamTagSupplier()
Ritorna nil per un feed globale non filtrato.
Salvataggio e ripristino dello stato del feed
Conserva lo stato di paginazione tra gli eventi del ciclo di vita della view:
let state = sdk.savePaginationState()
// Successivamente...
sdk.restorePaginationState(state)
Eliminazione dei post
sdk.onPostDeleted = { postId in
print("Post \(postId) was deleted")
}
Temi 
Preimpostazioni del tema
Quattro preimpostazioni integrate sono disponibili:
// Predefiniti di sistema
sdk.theme = FastCommentsTheme.default
// Schede con ombre e angoli arrotondati grandi
sdk.theme = FastCommentsTheme.modern
// Stile piatto, senza ombre, piccolo raggio degli angoli, senza linee del thread
sdk.theme = FastCommentsTheme.minimal
// Imposta tutti i colori delle azioni su un singolo colore del brand
sdk.theme = FastCommentsTheme.allPrimary(.indigo)
Stili di visualizzazione dei commenti
var theme = FastCommentsTheme()
theme.commentStyle = .flat // Elenco piatto con divisori (predefinito)
theme.commentStyle = .card // Schede arrotondate con ombre
theme.commentStyle = .bubble // Stile a bolle di chat
Colori
Tutte le proprietà colore sono opzionali. I valori non impostati ricadono sui valori predefiniti di sistema sensati.
var theme = FastCommentsTheme()
// Colori del brand
theme.primaryColor = .indigo
theme.primaryLightColor = .indigo.opacity(0.6)
theme.primaryDarkColor = Color(red: 0.2, green: 0.1, blue: 0.5)
// Sfondi
theme.commentBackgroundColor = Color(.secondarySystemGroupedBackground)
theme.containerBackgroundColor = Color(.systemGroupedBackground)
// Pulsanti di azione
theme.actionButtonColor = .indigo
theme.replyButtonColor = .indigo
theme.toggleRepliesButtonColor = .indigo.opacity(0.8)
theme.loadMoreButtonTextColor = .indigo
// Voti
theme.voteActiveColor = .red
theme.voteCountColor = .primary
theme.voteCountZeroColor = .secondary
theme.voteDividerColor = Color(.separator)
// Link
theme.linkColor = .indigo
theme.linkColorPressed = .indigo.opacity(0.5)
// Dialog
theme.dialogHeaderBackgroundColor = .indigo
theme.dialogHeaderTextColor = .white
// Barra di input
theme.inputBarBackgroundColor = Color(.systemBackground)
theme.inputBarBorderColor = Color(.separator)
// Altro
theme.onlineIndicatorColor = .green
theme.separatorColor = Color(.separator)
theme.badgeBackgroundColor = .gray.opacity(0.2)
theme.threadLineColor = .indigo.opacity(0.15)
Tipografia
theme.commenterNameFont = .subheadline.weight(.bold)
theme.bodyFont = .body
theme.captionFont = .caption
theme.actionFont = .caption.weight(.medium)
Layout e spaziatura
theme.cornerRadius = .large // .none, .small, .medium, .large
theme.commentSpacing = 4 // Punti tra le righe dei commenti
theme.nestingIndent = 20 // Punti di indentazione per ogni livello di annidamento
theme.avatarSize = 36 // Diametro dell'avatar per i commenti di primo livello
theme.replyAvatarSize = 28 // Diametro dell'avatar per le risposte annidate
Effetti visivi
theme.showShadows = true // Ombre sottili sulle schede
theme.showThreadLine = true // Linea verticale che connette le risposte annidate
theme.animateVotes = true // Animazione a molla sulle modifiche dei voti
Applicazione dei temi
Due approcci:
// Tramite ambiente SwiftUI (consigliato per la gerarchia delle viste)
FastCommentsView(sdk: sdk)
.fastCommentsTheme(theme)
// Direttamente sul SDK
sdk.theme = theme
Pulsanti personalizzati della barra degli strumenti 
Pulsanti della barra degli strumenti dei commenti
Implementa il protocollo CustomToolbarButton per aggiungere pulsanti alla barra degli strumenti per l'inserimento dei commenti:
struct EmojiButton: CustomToolbarButton {
let id = "emoji"
let iconSystemName = "face.smiling" // Nome SF Symbol
let contentDescription = "Add Emoji"
let badgeText: String? = nil // Conteggio badge opzionale
func onClick(text: Binding<String>) {
text.wrappedValue += "\u{1F44D}"
}
// Override opzionali (di default true)
func isEnabled() -> Bool { true }
func isVisible() -> Bool { true }
}
Passa pulsanti personalizzati quando crei la view:
FastCommentsView(
sdk: sdk,
customToolbarButtons: [EmojiButton(), CodeBlockButton()]
)
Oppure aggiungili globalmente allo SDK (si applica a tutte le istanze):
sdk.addGlobalCustomToolbarButton(EmojiButton())
sdk.removeGlobalCustomToolbarButton(id: "emoji")
sdk.clearGlobalCustomToolbarButtons()
Pulsanti della barra degli strumenti del feed
Implementa FeedCustomToolbarButton per il form di creazione del post:
struct HashtagButton: FeedCustomToolbarButton {
let id = "hashtag"
let iconSystemName = "number"
let contentDescription = "Add Hashtag"
func onClick(content: Binding<String>) {
content.wrappedValue += "#"
}
}
Passali alla view di creazione:
FeedPostCreateView(
sdk: sdk,
customToolbarButtons: [HashtagButton()],
onPostCreated: { _ in },
onCancelled: { }
)
Oppure impostali globalmente sull'SDK del feed:
sdk.globalFeedToolbarButtons = [HashtagButton()]
Moderazione 
Azioni disponibili per tutti gli utenti
- Segnala/Rimuovi segnalazione -- segnala un commento per la revisione
try await sdk.flagComment(commentId: commentId)
try await sdk.unflagComment(commentId: commentId)
- Blocca/Sblocca -- nascondi tutti i commenti di un utente (per visualizzatore)
try await sdk.blockUser(commentId: commentId)
try await sdk.unblockUser(commentId: commentId)
Azioni riservate agli amministratori
- Fissa/Rimuovi fissaggio -- fissa un commento in cima alla discussione
try await sdk.pinComment(commentId: commentId)
try await sdk.unpinComment(commentId: commentId)
- Blocca/Sblocca -- impedisci nuove risposte a un commento
try await sdk.lockComment(commentId: commentId)
try await sdk.unlockComment(commentId: commentId)
Tutte le azioni di moderazione sono disponibili anche tramite il menu contestuale del commento nell'interfaccia utente. Le azioni da amministratore compaiono solo quando l'utente corrente è un amministratore del sito (impostato tramite il flag SSO isAdmin o la configurazione della dashboard).
Aggiornamenti in tempo reale 
Dopo aver chiamato sdk.load(), l'SDK si sottoscrive automaticamente agli eventi WebSocket per il urlId configurato. Vengono gestiti i seguenti eventi:
- Nuovi commenti, modifiche e eliminazioni
- Voti (nuovi e rimossi)
- Modifiche allo stato di pin, lock, flag e block
- Presenza utente (ingresso/uscita)
- Apertura/chiusura thread
- Assegnazioni di badge
- Aggiornamenti della configurazione del server
Controllo della visualizzazione in tempo reale
Per impostazione predefinita, i nuovi commenti di altri utenti compaiono immediatamente:
sdk.showLiveRightAway = true // Predefinito: mostra immediatamente
Imposta questo valore su false per mettere in coda i nuovi commenti dietro un pulsante "N nuovi commenti", consentendo all'utente di scegliere quando mostrarli:
sdk.showLiveRightAway = false
Presenza utente
Gli indicatori online/offline compaiono automaticamente sugli avatar degli utenti quando il server abilita il tracciamento della presenza. Non è necessaria alcuna configurazione aggiuntiva sul client.
Paginazione 
Dimensione della pagina
// Commenti: predefinito 30
sdk.pageSize = 50
// Feed: predefinito 10
feedSDK.pageSize = 20
Caricamento di altri commenti
L'interfaccia utente mostra automaticamente i controlli di impaginazione. Puoi anche avviare l'impaginazione programmaticamente:
// Carica la pagina successiva
try await sdk.loadMore()
// Carica tutti i rimanenti (disabilitato se >2000 commenti per motivi di prestazioni)
try await sdk.loadAll()
// Controlla lo stato
sdk.hasMore // Se esistono altre pagine
sdk.shouldShowLoadAll()
sdk.getCountRemainingToShow()
Impaginazione dei commenti figli
Le risposte annidate vengono caricate su richiesta (lazy). Quando un utente espande un thread, vengono caricate le prime 5 risposte figlie. Compare un controllo "carica altre risposte" se ne esistono altre. Questo viene gestito automaticamente dall'interfaccia utente.
Stato e osservabilità 
Sia FastCommentsSDK che FastCommentsFeedSDK sono classi ObservableObject con proprietà @Published. Puoi osservare queste proprietà nelle tue view SwiftUI per aggiornamenti reattivi dell'interfaccia utente.
Proprietà @Published di FastCommentsSDK
| Property | Type | Description |
|---|---|---|
commentCountOnServer |
Int |
Conteggio totale dei commenti sul server |
newRootCommentCount |
Int |
Commenti nuovi in buffer (quando showLiveRightAway è false) |
currentUser |
UserSessionInfo? |
Utente autenticato corrente |
isSiteAdmin |
Bool |
Se l'utente corrente è amministratore del sito |
isClosed |
Bool |
Se il thread dei commenti è chiuso |
hasBillingIssue |
Bool |
Se c'è un problema di fatturazione |
isLoading |
Bool |
Se è in corso una richiesta di rete |
hasMore |
Bool |
Se esistono altre pagine di commenti |
blockingErrorMessage |
String? |
Errore che impedisce il funzionamento dell'interfaccia |
warningMessage |
String? |
Messaggio di avviso non bloccante |
isDemo |
Bool |
Se in esecuzione in modalità demo |
commentsVisible |
Bool |
Interruttore per la visibilità dei commenti |
toolbarEnabled |
Bool |
Se la barra degli strumenti di formattazione è mostrata |
Proprietà @Published di FastCommentsFeedSDK
| Property | Type | Description |
|---|---|---|
feedPosts |
[FeedPost] |
Post del feed attualmente caricati |
hasMore |
Bool |
Se esistono altre pagine |
currentUser |
UserSessionInfo? |
Utente autenticato corrente |
blockingErrorMessage |
String? |
Messaggio di errore bloccante |
isLoading |
Bool |
Se è in corso una richiesta di rete |
newPostsCount |
Int |
Numero di nuovi post dall'ultimo caricamento |
Albero dei commenti
L'albero dei commenti è accessibile tramite sdk.commentsTree:
// Elenco piatto di nodi visibili per il rendering
sdk.commentsTree.visibleNodes
// Ricerca di un commento per ID
sdk.commentsTree.commentsById["comment-id"]
Regione UE 
Per utilizzare il data center UE, imposta il campo region nella tua configurazione:
let config = FastCommentsWidgetConfig(
tenantId: "YOUR_TENANT_ID",
urlId: "my-page",
region: "eu"
)
Questo indirizza tutte le richieste API e le connessioni WebSocket a eu.fastcomments.com.
Pulizia 
Quando hai finito di usare un'istanza dell'SDK (ad es., la vista viene chiusa), chiama cleanup() per chiudere la connessione WebSocket e annullare le attività in background:
sdk.cleanup()
Per le viste gestite da @StateObject di SwiftUI, questo viene tipicamente chiamato in .onDisappear o quando la vista viene deallocata.
Caricamento immagini 
Commenti
let imageUrl = try await sdk.uploadImage(imageData: jpegData, filename: "photo.jpg")
Restituisce la stringa URL dell'immagine caricata.
Post del Feed
let mediaItem = try await feedSDK.uploadImage(imageData: jpegData, filename: "photo.jpg")
// Carica più immagini in parallelo
let mediaItems = try await feedSDK.uploadImages(images: [
(jpegData1, "photo1.jpg"),
(jpegData2, "photo2.jpg")
])
Menzioni utenti 
Cerca utenti per supportare l'autocompletamento delle @mention:
let results = try await sdk.searchUsers(query: "jan")
// Restituisce [UserSearchResult] con userId, username, avatar, ecc.
La CommentInputBar integrata gestisce automaticamente l'autocompletamento delle @mention.
Modifica e eliminazione dei commenti 
Modifica
try await sdk.editComment(commentId: commentId, newText: "Updated text")
Il server rigenera l'HTML. Il commento locale viene aggiornato automaticamente.
Elimina
try await sdk.deleteComment(commentId: commentId)
L'eliminazione di un commento rimuove anche i suoi discendenti dall'albero locale.
Entrambe le azioni sono disponibili tramite il menu contestuale del commento nell'interfaccia utente quando l'utente corrente è l'autore del commento (o un amministratore del sito).
Gestione degli errori 
I metodi dell'SDK lanciano FastCommentsError, che aderisce al protocollo LocalizedError:
do {
try await sdk.load()
} catch let error as FastCommentsError {
print(error.translatedError ?? error.reason ?? "Unknown error")
} catch {
print(error.localizedDescription)
}
FastCommentsError properties:
code-- codice di errore restituito dall'APIreason-- descrizione dell'errore in inglesetranslatedError-- messaggio di errore localizzato fornito dal server
Anche gli errori bloccanti vengono esposti automaticamente tramite sdk.blockingErrorMessage, che le viste integrate mostrano all'utente.
Localizzazione 
Passa un codice locale nella configurazione per localizzare le stringhe fornite dal server:
let config = FastCommentsWidgetConfig(
tenantId: "YOUR_TENANT_ID",
urlId: "my-page",
locale: "fr_fr"
)
Le stringhe dell'interfaccia utente lato client usano la localizzazione basata sui bundle iOS.
App di esempio 
Il repository include un'app completa di esempio in ExampleApp/ con dimostrazioni di:
- Commenti annidati con SSO e temi personalizzati
- Feed social con creazione di post e filtraggio per tag
- Chat in tempo reale
- Flussi SSO semplici e sicuri
- Pulsanti della barra degli strumenti personalizzati (commenti e feed)
Hai bisogno di aiuto?
Se riscontri problemi o hai domande sulla Libreria iOS, per favore:
Contribuire
I contributi sono benvenuti! Visita il repository GitHub per le linee guida sui contributi.