feat: add participant scheduling with iTIP invitations and inline calendar invitation banner

Add organizer/attendee UI with RSVP, contact autocomplete for participants,
scheduling messages, and inline calendar invitation banner in email viewer
with auto-detect .ics attachments, RSVP/import to calendar, and cancellation display.
This commit is contained in:
Matthieu MALVACHE
2026-02-17 01:09:57 +01:00
committed by Matthieu MALVACHE
parent 74763b384d
commit b65b2f26df
30 changed files with 2352 additions and 137 deletions
+31 -2
View File
@@ -225,6 +225,23 @@
"success_mailto": "Abmeldeanfrage an Ihr E-Mail-Programm gesendet",
"error": "Abmeldung nicht möglich",
"dismiss": "Schließen"
},
"calendar_invitation": {
"loading": "Veranstaltungsdetails werden geladen…",
"title": "Kalendereinladung",
"cancelled_title": "Veranstaltung abgesagt",
"organizer": "Organisiert von {name}",
"attendees": "{count, plural, one {# Teilnehmer} other {# Teilnehmer}}",
"accept": "Annehmen",
"maybe": "Vielleicht",
"decline": "Ablehnen",
"add_to_calendar": "Zum Kalender hinzufügen",
"added": "Zum Kalender hinzugefügt",
"rsvp_sent": "Antwort gesendet",
"parse_error": "Einladung konnte nicht gelesen werden",
"no_calendar": "Kalender nicht verfügbar",
"select_calendar": "Kalender auswählen",
"already_in_calendar": "Bereits in deinem Kalender"
}
},
"email_composer": {
@@ -1066,7 +1083,16 @@
"accepted": "Zugesagt",
"declined": "Abgelehnt",
"tentative": "Vorläufig",
"needs_action": "Antwort ausstehend"
"needs_action": "Antwort ausstehend",
"remove": "Entfernen",
"email_placeholder": "E-Mail-Adresse hinzufügen oder Kontakte durchsuchen",
"send_invitations": "Einladungen an Teilnehmer senden",
"status_summary": "{accepted} zugesagt, {pending} ausstehend",
"cancel_notification": "Die Teilnehmer werden über die Absage benachrichtigt",
"you_organizer": "Sie sind der Organisator",
"you_attendee": "Sie sind ein Teilnehmer",
"no_participants": "Keine Teilnehmer",
"count": "{count, plural, one {# Teilnehmer} other {# Teilnehmer}}"
},
"recurrence": {
"title": "Wiederholung",
@@ -1128,7 +1154,10 @@
"event_move_error": "Termin konnte nicht verschoben werden",
"alert_title": "Bevorstehender Termin",
"alert_now": "Beginnt jetzt",
"alert_in_minutes": "In {count} Min."
"alert_in_minutes": "In {count} Min.",
"invitation_sent": "Einladungen gesendet",
"rsvp_updated": "Antwort aktualisiert",
"rsvp_error": "Antwort konnte nicht aktualisiert werden"
},
"status": {
"loading_calendars": "Kalender werden geladen...",
+31 -2
View File
@@ -225,6 +225,23 @@
"success_mailto": "Unsubscribe request sent to your email client",
"error": "Unable to unsubscribe",
"dismiss": "Dismiss"
},
"calendar_invitation": {
"loading": "Loading event details…",
"title": "Calendar Invitation",
"cancelled_title": "Event Cancelled",
"organizer": "Organized by {name}",
"attendees": "{count, plural, one {# attendee} other {# attendees}}",
"accept": "Accept",
"maybe": "Maybe",
"decline": "Decline",
"add_to_calendar": "Add to calendar",
"added": "Added to calendar",
"rsvp_sent": "Response sent",
"parse_error": "Could not read invitation",
"no_calendar": "Calendar not available",
"select_calendar": "Select calendar",
"already_in_calendar": "Already in your calendar"
}
},
"email_composer": {
@@ -1066,7 +1083,16 @@
"accepted": "Accepted",
"declined": "Declined",
"tentative": "Tentative",
"needs_action": "Needs action"
"needs_action": "Needs action",
"remove": "Remove",
"email_placeholder": "Add email address or search contacts",
"send_invitations": "Send invitations to participants",
"status_summary": "{accepted} accepted, {pending} pending",
"cancel_notification": "Participants will be notified of the cancellation",
"you_organizer": "You are the organizer",
"you_attendee": "You are an attendee",
"no_participants": "No participants",
"count": "{count, plural, one {# participant} other {# participants}}"
},
"recurrence": {
"title": "Recurrence",
@@ -1128,7 +1154,10 @@
"event_move_error": "Failed to move event",
"alert_title": "Upcoming event",
"alert_now": "Starting now",
"alert_in_minutes": "In {count} min"
"alert_in_minutes": "In {count} min",
"invitation_sent": "Invitations sent",
"rsvp_updated": "Response updated",
"rsvp_error": "Failed to update response"
},
"status": {
"loading_calendars": "Loading calendars...",
+31 -2
View File
@@ -225,6 +225,23 @@
"success_mailto": "Solicitud de cancelación enviada a su cliente de correo",
"error": "No se pudo cancelar la suscripción",
"dismiss": "Descartar"
},
"calendar_invitation": {
"loading": "Cargando detalles del evento…",
"title": "Invitación de calendario",
"cancelled_title": "Evento cancelado",
"organizer": "Organizado por {name}",
"attendees": "{count, plural, one {# asistente} other {# asistentes}}",
"accept": "Aceptar",
"maybe": "Quizás",
"decline": "Rechazar",
"add_to_calendar": "Añadir al calendario",
"added": "Añadido al calendario",
"rsvp_sent": "Respuesta enviada",
"parse_error": "No se pudo leer la invitación",
"no_calendar": "Calendario no disponible",
"select_calendar": "Seleccionar calendario",
"already_in_calendar": "Ya está en tu calendario"
}
},
"email_composer": {
@@ -1066,7 +1083,16 @@
"accepted": "Aceptado",
"declined": "Rechazado",
"tentative": "Provisional",
"needs_action": "Pendiente de respuesta"
"needs_action": "Pendiente de respuesta",
"remove": "Eliminar",
"email_placeholder": "Añadir dirección de correo o buscar contactos",
"send_invitations": "Enviar invitaciones a los participantes",
"status_summary": "{accepted} aceptado(s), {pending} pendiente(s)",
"cancel_notification": "Se notificará a los participantes de la cancelación",
"you_organizer": "Eres el organizador",
"you_attendee": "Eres un participante",
"no_participants": "Sin participantes",
"count": "{count, plural, one {# participante} other {# participantes}}"
},
"recurrence": {
"title": "Recurrencia",
@@ -1128,7 +1154,10 @@
"event_move_error": "Error al mover el evento",
"alert_title": "Evento próximo",
"alert_now": "Comienza ahora",
"alert_in_minutes": "En {count} min"
"alert_in_minutes": "En {count} min",
"invitation_sent": "Invitaciones enviadas",
"rsvp_updated": "Respuesta actualizada",
"rsvp_error": "Error al actualizar la respuesta"
},
"status": {
"loading_calendars": "Cargando calendarios...",
+31 -2
View File
@@ -225,6 +225,23 @@
"success_mailto": "Demande de désabonnement envoyée à votre client mail",
"error": "Impossible de se désabonner",
"dismiss": "Ignorer"
},
"calendar_invitation": {
"loading": "Chargement des détails…",
"title": "Invitation calendrier",
"cancelled_title": "Événement annulé",
"organizer": "Organisé par {name}",
"attendees": "{count, plural, one {# participant} other {# participants}}",
"accept": "Accepter",
"maybe": "Peut-être",
"decline": "Refuser",
"add_to_calendar": "Ajouter au calendrier",
"added": "Ajouté au calendrier",
"rsvp_sent": "Réponse envoyée",
"parse_error": "Impossible de lire l'invitation",
"no_calendar": "Calendrier non disponible",
"select_calendar": "Choisir un calendrier",
"already_in_calendar": "Déjà dans votre calendrier"
}
},
"email_composer": {
@@ -1066,7 +1083,16 @@
"accepted": "Accepté",
"declined": "Refusé",
"tentative": "Provisoire",
"needs_action": "En attente de réponse"
"needs_action": "En attente de réponse",
"remove": "Retirer",
"email_placeholder": "Ajouter une adresse e-mail ou chercher des contacts",
"send_invitations": "Envoyer les invitations aux participants",
"status_summary": "{accepted} accepté(s), {pending} en attente",
"cancel_notification": "Les participants seront informés de l'annulation",
"you_organizer": "Vous êtes l'organisateur",
"you_attendee": "Vous êtes un participant",
"no_participants": "Aucun participant",
"count": "{count, plural, one {# participant} other {# participants}}"
},
"recurrence": {
"title": "Récurrence",
@@ -1128,7 +1154,10 @@
"event_move_error": "Échec du déplacement de l'événement",
"alert_title": "Événement à venir",
"alert_now": "Commence maintenant",
"alert_in_minutes": "Dans {count} min"
"alert_in_minutes": "Dans {count} min",
"invitation_sent": "Invitations envoyées",
"rsvp_updated": "Réponse mise à jour",
"rsvp_error": "Échec de la mise à jour de la réponse"
},
"status": {
"loading_calendars": "Chargement des calendriers...",
+31 -2
View File
@@ -225,6 +225,23 @@
"success_mailto": "Richiesta di annullamento iscrizione inviata al tuo client email",
"error": "Impossibile annullare l'iscrizione",
"dismiss": "Ignora"
},
"calendar_invitation": {
"loading": "Caricamento dettagli evento…",
"title": "Invito calendario",
"cancelled_title": "Evento annullato",
"organizer": "Organizzato da {name}",
"attendees": "{count, plural, one {# partecipante} other {# partecipanti}}",
"accept": "Accetta",
"maybe": "Forse",
"decline": "Rifiuta",
"add_to_calendar": "Aggiungi al calendario",
"added": "Aggiunto al calendario",
"rsvp_sent": "Risposta inviata",
"parse_error": "Impossibile leggere l'invito",
"no_calendar": "Calendario non disponibile",
"select_calendar": "Seleziona calendario",
"already_in_calendar": "Già nel tuo calendario"
}
},
"email_composer": {
@@ -1066,7 +1083,16 @@
"accepted": "Accettato",
"declined": "Rifiutato",
"tentative": "Provvisorio",
"needs_action": "In attesa di risposta"
"needs_action": "In attesa di risposta",
"remove": "Rimuovi",
"email_placeholder": "Aggiungi indirizzo email o cerca contatti",
"send_invitations": "Invia inviti ai partecipanti",
"status_summary": "{accepted} accettato/i, {pending} in attesa",
"cancel_notification": "I partecipanti saranno avvisati della cancellazione",
"you_organizer": "Sei l'organizzatore",
"you_attendee": "Sei un partecipante",
"no_participants": "Nessun partecipante",
"count": "{count, plural, one {# partecipante} other {# partecipanti}}"
},
"recurrence": {
"title": "Ricorrenza",
@@ -1128,7 +1154,10 @@
"event_move_error": "Spostamento dell'evento non riuscito",
"alert_title": "Evento in arrivo",
"alert_now": "Inizia ora",
"alert_in_minutes": "Tra {count} min"
"alert_in_minutes": "Tra {count} min",
"invitation_sent": "Inviti inviati",
"rsvp_updated": "Risposta aggiornata",
"rsvp_error": "Impossibile aggiornare la risposta"
},
"status": {
"loading_calendars": "Caricamento calendari...",
+31 -2
View File
@@ -225,6 +225,23 @@
"success_mailto": "購読解除リクエストをメールクライアントに送信しました",
"error": "購読解除できませんでした",
"dismiss": "閉じる"
},
"calendar_invitation": {
"loading": "イベント詳細を読み込み中…",
"title": "カレンダー招待",
"cancelled_title": "イベントがキャンセルされました",
"organizer": "{name} が主催",
"attendees": "{count}名の参加者",
"accept": "承諾",
"maybe": "未定",
"decline": "辞退",
"add_to_calendar": "カレンダーに追加",
"added": "カレンダーに追加しました",
"rsvp_sent": "回答を送信しました",
"parse_error": "招待を読み込めませんでした",
"no_calendar": "カレンダーが利用できません",
"select_calendar": "カレンダーを選択",
"already_in_calendar": "カレンダーに登録済み"
}
},
"email_composer": {
@@ -1066,7 +1083,16 @@
"accepted": "承諾",
"declined": "辞退",
"tentative": "仮承諾",
"needs_action": "未回答"
"needs_action": "未回答",
"remove": "削除",
"email_placeholder": "メールアドレスを追加または連絡先を検索",
"send_invitations": "参加者に招待を送信",
"status_summary": "{accepted}人承諾、{pending}人保留",
"cancel_notification": "キャンセルの通知が参加者に送信されます",
"you_organizer": "あなたは主催者です",
"you_attendee": "あなたは参加者です",
"no_participants": "参加者なし",
"count": "{count}人の参加者"
},
"recurrence": {
"title": "繰り返し",
@@ -1128,7 +1154,10 @@
"event_move_error": "イベントの移動に失敗しました",
"alert_title": "予定のイベント",
"alert_now": "まもなく開始",
"alert_in_minutes": "{count}分後"
"alert_in_minutes": "{count}分後",
"invitation_sent": "招待を送信しました",
"rsvp_updated": "回答を更新しました",
"rsvp_error": "回答の更新に失敗しました"
},
"status": {
"loading_calendars": "カレンダーを読み込み中...",
+31 -2
View File
@@ -225,6 +225,23 @@
"success_mailto": "Uitschrijfverzoek verzonden naar je e-mailclient",
"error": "Kan niet uitschrijven",
"dismiss": "Sluiten"
},
"calendar_invitation": {
"loading": "Evenementdetails laden…",
"title": "Agenda-uitnodiging",
"cancelled_title": "Evenement geannuleerd",
"organizer": "Georganiseerd door {name}",
"attendees": "{count, plural, one {# deelnemer} other {# deelnemers}}",
"accept": "Accepteren",
"maybe": "Misschien",
"decline": "Weigeren",
"add_to_calendar": "Toevoegen aan agenda",
"added": "Toegevoegd aan agenda",
"rsvp_sent": "Reactie verzonden",
"parse_error": "Kan uitnodiging niet lezen",
"no_calendar": "Agenda niet beschikbaar",
"select_calendar": "Agenda selecteren",
"already_in_calendar": "Staat al in je agenda"
}
},
"email_composer": {
@@ -1066,7 +1083,16 @@
"accepted": "Geaccepteerd",
"declined": "Geweigerd",
"tentative": "Voorlopig",
"needs_action": "Reactie vereist"
"needs_action": "Reactie vereist",
"remove": "Verwijderen",
"email_placeholder": "E-mailadres toevoegen of contacten zoeken",
"send_invitations": "Uitnodigingen sturen naar deelnemers",
"status_summary": "{accepted} geaccepteerd, {pending} in afwachting",
"cancel_notification": "Deelnemers worden op de hoogte gebracht van de annulering",
"you_organizer": "Je bent de organisator",
"you_attendee": "Je bent een deelnemer",
"no_participants": "Geen deelnemers",
"count": "{count, plural, one {# deelnemer} other {# deelnemers}}"
},
"recurrence": {
"title": "Herhaling",
@@ -1128,7 +1154,10 @@
"event_move_error": "Evenement verplaatsen mislukt",
"alert_title": "Aankomend evenement",
"alert_now": "Begint nu",
"alert_in_minutes": "Over {count} min"
"alert_in_minutes": "Over {count} min",
"invitation_sent": "Uitnodigingen verzonden",
"rsvp_updated": "Reactie bijgewerkt",
"rsvp_error": "Reactie kon niet worden bijgewerkt"
},
"status": {
"loading_calendars": "Agenda's laden...",
+31 -2
View File
@@ -225,6 +225,23 @@
"success_mailto": "Solicitação de cancelamento enviada para seu cliente de e-mail",
"error": "Não foi possível cancelar a inscrição",
"dismiss": "Dispensar"
},
"calendar_invitation": {
"loading": "Carregando detalhes do evento…",
"title": "Convite de calendário",
"cancelled_title": "Evento cancelado",
"organizer": "Organizado por {name}",
"attendees": "{count, plural, one {# participante} other {# participantes}}",
"accept": "Aceitar",
"maybe": "Talvez",
"decline": "Recusar",
"add_to_calendar": "Adicionar ao calendário",
"added": "Adicionado ao calendário",
"rsvp_sent": "Resposta enviada",
"parse_error": "Não foi possível ler o convite",
"no_calendar": "Calendário não disponível",
"select_calendar": "Selecionar calendário",
"already_in_calendar": "Já está no seu calendário"
}
},
"email_composer": {
@@ -1066,7 +1083,16 @@
"accepted": "Aceito",
"declined": "Recusado",
"tentative": "Provisório",
"needs_action": "Aguardando resposta"
"needs_action": "Aguardando resposta",
"remove": "Remover",
"email_placeholder": "Adicionar endereço de e-mail ou pesquisar contatos",
"send_invitations": "Enviar convites aos participantes",
"status_summary": "{accepted} aceito(s), {pending} pendente(s)",
"cancel_notification": "Os participantes serão notificados do cancelamento",
"you_organizer": "Você é o organizador",
"you_attendee": "Você é um participante",
"no_participants": "Sem participantes",
"count": "{count, plural, one {# participante} other {# participantes}}"
},
"recurrence": {
"title": "Recorrência",
@@ -1128,7 +1154,10 @@
"event_move_error": "Falha ao mover o evento",
"alert_title": "Evento próximo",
"alert_now": "Começa agora",
"alert_in_minutes": "Em {count} min"
"alert_in_minutes": "Em {count} min",
"invitation_sent": "Convites enviados",
"rsvp_updated": "Resposta atualizada",
"rsvp_error": "Falha ao atualizar resposta"
},
"status": {
"loading_calendars": "Carregando calendários...",