Merge branch 'fix/ics_export' into 'master'

Generate ICS manually for calendar export with user friendly information

See merge request BC/public/linphone-desktop!1658
This commit is contained in:
Christophe Deschamps 2026-01-24 07:03:50 +00:00
commit 2e5dc17570
2 changed files with 114 additions and 11 deletions

View file

@ -29,6 +29,7 @@
#include "tool/thread/SafeConnection.hpp"
#include <QDesktopServices>
#include <QRegularExpression>
DEFINE_ABSTRACT_OBJECT(ConferenceInfoCore)
@ -201,8 +202,7 @@ void ConferenceInfoCore::setSelf(QSharedPointer<ConferenceInfoCore> me) {
if (state == linphone::ConferenceScheduler::State::Ready) {
uri = mConferenceInfoModel->getConferenceScheduler()->getUri();
emit CoreModel::getInstance() -> conferenceInfoReceived(
CoreModel::getInstance()->getCore(),
mConferenceInfoModel->getConferenceInfo());
CoreModel::getInstance()->getCore(), mConferenceInfoModel->getConferenceInfo());
}
mConfInfoModelConnection->invokeToCore([this, state = LinphoneEnums::fromLinphone(state),
infoState = LinphoneEnums::fromLinphone(confInfoState),
@ -659,13 +659,116 @@ bool ConferenceInfoCore::isAllDayConf() const {
mEndDateTime.time().minute() == 59;
}
void ConferenceInfoCore::exportConferenceToICS() const {
QString filePath(Paths::getAppLocalDirPath() + "conference.ics");
QFile file(filePath);
if (file.open(QIODevice::WriteOnly | QIODevice::Text)) {
QTextStream out(&file);
out << mIcalendarString;
file.close();
void ConferenceInfoCore::exportConferenceToICS() {
// Collect participant addresses
QStringList participantAddresses;
for (const auto &participant : mParticipants) {
auto map = participant.toMap();
QString address = map["address"].toString();
if (!address.isEmpty()) {
participantAddresses.append(address);
}
}
QDesktopServices::openUrl(QUrl::fromLocalFile(filePath));
// Copy data needed for ICS generation
QString uri = mUri;
QString organizerAddress = mOrganizerAddress;
QString organizerName = mOrganizerName;
QString subject = mSubject;
QString description = mDescription;
QDateTime dateTime = mDateTime;
QDateTime endDateTime = mEndDateTime;
// Generate ICS on model thread (for display name lookup) then open file
App::postModelAsync(
[participantAddresses, uri, organizerAddress, organizerName, subject, description, dateTime, endDateTime]() {
// Helper lambda to escape special characters in ICS text fields
auto escapeIcsText = [](const QString &text) {
QString escaped = text;
escaped.replace("\\", "\\\\");
escaped.replace(";", "\\;");
escaped.replace(",", "\\,");
escaped.replace("\n", "\\n");
return escaped;
};
// Helper lambda to format datetime in ICS format (UTC)
auto formatIcsDateTime = [](const QDateTime &dt) { return dt.toUTC().toString("yyyyMMdd'T'HHmmss'Z'"); };
// Generate a unique UID based on URI or datetime + organizer
QString uid;
if (!uri.isEmpty()) {
uid = uri;
uid.replace("sip:", "").replace("@", "-at-");
} else {
uid = dateTime.toUTC().toString("yyyyMMddHHmmss") + "-" + organizerAddress;
uid.replace("sip:", "").replace("@", "-at-");
}
// Build the ICS content
QString icsContent;
QTextStream out(&icsContent);
out << "BEGIN:VCALENDAR\r\n";
out << "VERSION:2.0\r\n";
out << "PRODID:-//Titanium Comms//EN\r\n";
out << "METHOD:REQUEST\r\n";
out << "BEGIN:VEVENT\r\n";
// UID and timestamps
out << "UID:" << uid << "\r\n";
out << "DTSTAMP:" << formatIcsDateTime(QDateTime::currentDateTimeUtc()) << "\r\n";
out << "DTSTART:" << formatIcsDateTime(dateTime) << "\r\n";
out << "DTEND:" << formatIcsDateTime(endDateTime) << "\r\n";
// Organizer
if (!organizerAddress.isEmpty()) {
out << "ORGANIZER";
if (!organizerName.isEmpty()) {
out << ";CN=" << escapeIcsText(organizerName);
}
out << ":" << organizerAddress << "\r\n";
}
// Attendees/Participants
for (const QString &address : participantAddresses) {
QString displayName = ToolModel::getDisplayName(address);
out << "ATTENDEE;ROLE=REQ-PARTICIPANT;PARTSTAT=NEEDS-ACTION;RSVP=TRUE";
if (!displayName.isEmpty()) {
out << ";CN=" << escapeIcsText(displayName);
}
out << ":" << address << "\r\n";
}
// Subject/Summary
if (!subject.isEmpty()) {
out << "SUMMARY:" << escapeIcsText(subject) << "\r\n";
}
// Description
if (!description.isEmpty()) {
out << "DESCRIPTION:" << escapeIcsText(description) << "\r\n";
}
// Location (conference URI)
if (!uri.isEmpty()) {
out << "LOCATION:" << uri << "\r\n";
out << "URL:" << uri << "\r\n";
}
out << "STATUS:CONFIRMED\r\n";
out << "SEQUENCE:0\r\n";
out << "END:VEVENT\r\n";
out << "END:VCALENDAR\r\n";
// Write the file and open it
QString filePath(Paths::getAppLocalDirPath() + "conference.ics");
QFile file(filePath);
if (file.open(QIODevice::WriteOnly | QIODevice::Text)) {
QTextStream fileOut(&file);
fileOut << icsContent;
file.close();
}
QDesktopServices::openUrl(QUrl::fromLocalFile(filePath));
});
}

View file

@ -135,7 +135,7 @@ public:
Q_INVOKABLE bool isAllDayConf() const;
Q_INVOKABLE void exportConferenceToICS() const;
Q_INVOKABLE void exportConferenceToICS();
signals:
void dateTimeChanged();