diff --git a/Telegram/Resources/export_html/css/style.css b/Telegram/Resources/export_html/css/style.css index 63c444d2a..31136ab9c 100644 --- a/Telegram/Resources/export_html/css/style.css +++ b/Telegram/Resources/export_html/css/style.css @@ -2,6 +2,34 @@ body { margin: 0; font: 12px/18px 'Open Sans',"Lucida Grande","Lucida Sans Unicode",Arial,Helvetica,Verdana,sans-serif; } +strong { + font-weight: 700; +} +code, kbd, pre, samp { + font-family: Menlo,Monaco,Consolas,"Courier New",monospace; +} +code { + padding: 2px 4px; + font-size: 90%; + color: #c7254e; + background-color: #f9f2f4; + border-radius: 4px; +} +pre { + display: block; + margin: 0; + line-height: 1.42857143; + word-break: break-all; + word-wrap: break-word; + color: #333; + background-color: #f5f5f5; + border-radius: 4px; + overflow: auto; + padding: 3px; + border: 1px solid #eee; + max-height: none; + font-size: inherit; +} .clearfix:after { content: " "; visibility: hidden; @@ -38,13 +66,13 @@ body { border-radius: 0 !important; } .page_header a.content { - background-image: url(../images/back.png); background-repeat: no-repeat; background-position: 24px 21px; background-size: 24px 24px; } .bold { color: #212121; + font-weight: 700; } .details { color: #70777b; @@ -52,7 +80,6 @@ body { .page_header .content .text { padding: 24px 24px 22px 24px; font-size: 22px; - font-weight: 700; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; @@ -90,28 +117,47 @@ body { text-transform: uppercase; user-select: none; } -.userpic1 { +.color_red, +.userpic1, +.media_call .thumb, +.media_file .thumb, +.media_live_location .thumb { background-color: #ff5555; } -.userpic2 { +.color_green, +.userpic2, +.media_call.success .thumb { background-color: #64bf47; } -.userpic3 { +.color_yellow, +.userpic3, +.media_venue .thumb { background-color: #ffab00; } -.userpic4 { +.color_blue, +.userpic4, +.media_audio_file .thumb, +.media_voice_message .thumb { background-color: #4f9cd9; } -.userpic5 { +.color_purple, +.userpic5, +.media_game .thumb { background-color: #9884e8; } -.userpic6 { +.color_pink, +.userpic6, +.media_invoice .thumb { background-color: #e671a5; } -.userpic7 { +.color_sea, +.userpic7, +.media_location .thumb { background-color: #47bcd1; } -.userpic8 { +.color_orange, +.userpic8, +.media_contact .thumb { background-color: #ff8c44; } .personal_info { @@ -162,54 +208,6 @@ a.block_link:hover { .section .label { padding: 15px 0 0 82px; font-size: 15px; - font-weight: 700; -} -.section.calls { - background-image: url(../images/calls.png); -} -.section.chats { - background-image: url(../images/chats.png); -} -.section.contacts { - background-image: url(../images/contacts.png); -} -.section.frequent { - background-image: url(../images/frequent.png); -} -.section.photos { - background-image: url(../images/photos.png); -} -.section.sessions { - background-image: url(../images/sessions.png); -} -.section.web { - background-image: url(../images/web.png); -} -@media only screen and (min--moz-device-pixel-ratio: 2), only screen and (-o-min-device-pixel-ratio: 2/1), only screen and (-webkit-min-device-pixel-ratio: 2), only screen and (min-device-pixel-ratio: 2) { -.section.calls { - background-image: url(../images/calls@2x.png); -} -.section.chats { - background-image: url(../images/chats@2x.png); -} -.section.contacts { - background-image: url(../images/contacts@2x.png); -} -.section.frequent { - background-image: url(../images/frequent@2x.png); -} -.section.photos { - background-image: url(../images/photos@2x.png); -} -.section.sessions { - background-image: url(../images/sessions@2x.png); -} -.section.web { - background-image: url(../images/web@2x.png); -} -.page_header a.content { - background-image: url(../images/back@2x.png); -} } .list_page .page_about { padding: 16px 24px 0; @@ -229,7 +227,6 @@ a.block_link:hover { } .list_page .entry .name { padding: 4px 0 2px; - font-weight: 700; font-size: 14px; } .list_page .entry .subname { @@ -248,7 +245,7 @@ a.block_link:hover { .service { padding: 10px 24px; } -.service .content { +.service .body { text-align: center; } .service .userpic_wrap { @@ -260,3 +257,172 @@ a.block_link:hover { .service .userpic .initials { font-size: 24px; } +.message .userpic .initials { + font-size: 16px; +} +.default { + padding: 10px 0 10px; +} +.default.joined { + padding-top: 0; +} +.default .from_name { + color: #3892db; + font-weight: 700; + padding-bottom: 5px; +} +.default .from_name .details { + font-weight: normal; +} +.default .body { + margin-left: 60px; +} +.default .text { + word-wrap: break-word; + line-height: 150%; +} +.default .reply_to, +.default .media_wrap { + padding-bottom: 5px; +} +.default .media { + margin: 0 -10px; + padding: 5px 10px; +} +.default .media .thumb { + width: 48px; + height: 48px; + border-radius: 50%; + background-repeat: no-repeat; + background-position: 12px 12px; + background-size: 24px 24px; +} +.default .media .title { + padding-top: 4px; + font-size: 14px; +} +.default .media .description { + color: #000000; + padding-top: 4px; + font-size: 13px; +} +.default .media .status { + padding-top: 4px; + font-size: 13px; +} + +.section.calls { + background-image: url(../images/section_calls.png); +} +.section.chats { + background-image: url(../images/section_chats.png); +} +.section.contacts { + background-image: url(../images/section_contacts.png); +} +.section.frequent { + background-image: url(../images/section_frequent.png); +} +.section.photos { + background-image: url(../images/section_photos.png); +} +.section.sessions { + background-image: url(../images/section_sessions.png); +} +.section.web { + background-image: url(../images/section_web.png); +} +.section.leftchats { + background-image: url(../images/section_leftchats.png); +} +.section.other { + background-image: url(../images/section_other.png) +} +.page_header a.content { + background-image: url(../images/back.png); +} +.media_call .thumb { + background-image: url(../images/media_call.png) +} +.media_contact .thumb { + background-image: url(../images/media_contact.png) +} +.media_file .thumb { + background-image: url(../images/media_file.png) +} +.media_game .thumb { + background-image: url(../images/media_game.png) +} +.media_live_location .thumb, +.media_location .thumb, +.media_venue .thumb { + background-image: url(../images/media_location.png) +} +.media_audio_file .thumb { + background-image: url(../images/media_music.png) +} +.media_invoice .thumb { + background-image: url(../images/media_shop.png) +} +.media_voice_message .thumb { + background-image: url(../images/media_voice.png) +} + +@media only screen and (min--moz-device-pixel-ratio: 2), only screen and (-o-min-device-pixel-ratio: 2/1), only screen and (-webkit-min-device-pixel-ratio: 2), only screen and (min-device-pixel-ratio: 2) { +.section.calls { + background-image: url(../images/section_calls@2x.png); +} +.section.chats { + background-image: url(../images/section_chats@2x.png); +} +.section.contacts { + background-image: url(../images/section_contacts@2x.png); +} +.section.frequent { + background-image: url(../images/section_frequent@2x.png); +} +.section.photos { + background-image: url(../images/section_photos@2x.png); +} +.section.sessions { + background-image: url(../images/section_sessions@2x.png); +} +.section.web { + background-image: url(../images/section_web@2x.png); +} +.section.leftchats { + background-image: url(../images/section_leftchats@2x.png); +} +.section.other { + background-image: url(../images/section_other@2x.png); +} +.page_header a.content { + background-image: url(../images/back@2x.png); +} +.media_call .thumb { + background-image: url(../images/media_call@2x.png) +} +.media_contact .thumb { + background-image: url(../images/media_contact@2x.png) +} +.media_file .thumb { + background-image: url(../images/media_file@2x.png) +} +.media_game .thumb { + background-image: url(../images/media_game@2x.png) +} +.media_live_location .thumb, +.media_location .thumb, +.media_venue .thumb { + background-image: url(../images/media_location@2x.png) +} +.media_audio_file .thumb { + background-image: url(../images/media_music@2x.png) +} +.media_invoice .thumb { + background-image: url(../images/media_shop@2x.png) +} +.media_voice_message .thumb { + background-image: url(../images/media_voice@2x.png) +} +} diff --git a/Telegram/Resources/export_html/images/media_call.png b/Telegram/Resources/export_html/images/media_call.png new file mode 100644 index 000000000..9614b9562 Binary files /dev/null and b/Telegram/Resources/export_html/images/media_call.png differ diff --git a/Telegram/Resources/export_html/images/media_call@2x.png b/Telegram/Resources/export_html/images/media_call@2x.png new file mode 100644 index 000000000..92cdbe618 Binary files /dev/null and b/Telegram/Resources/export_html/images/media_call@2x.png differ diff --git a/Telegram/Resources/export_html/images/media_contact.png b/Telegram/Resources/export_html/images/media_contact.png new file mode 100644 index 000000000..be79172d5 Binary files /dev/null and b/Telegram/Resources/export_html/images/media_contact.png differ diff --git a/Telegram/Resources/export_html/images/media_contact@2x.png b/Telegram/Resources/export_html/images/media_contact@2x.png new file mode 100644 index 000000000..e4be8b037 Binary files /dev/null and b/Telegram/Resources/export_html/images/media_contact@2x.png differ diff --git a/Telegram/Resources/export_html/images/media_file.png b/Telegram/Resources/export_html/images/media_file.png new file mode 100644 index 000000000..3a5bde66c Binary files /dev/null and b/Telegram/Resources/export_html/images/media_file.png differ diff --git a/Telegram/Resources/export_html/images/media_file@2x.png b/Telegram/Resources/export_html/images/media_file@2x.png new file mode 100644 index 000000000..ad2c6cc2c Binary files /dev/null and b/Telegram/Resources/export_html/images/media_file@2x.png differ diff --git a/Telegram/Resources/export_html/images/media_game.png b/Telegram/Resources/export_html/images/media_game.png new file mode 100644 index 000000000..cad8ab854 Binary files /dev/null and b/Telegram/Resources/export_html/images/media_game.png differ diff --git a/Telegram/Resources/export_html/images/media_game@2x.png b/Telegram/Resources/export_html/images/media_game@2x.png new file mode 100644 index 000000000..09c2eb395 Binary files /dev/null and b/Telegram/Resources/export_html/images/media_game@2x.png differ diff --git a/Telegram/Resources/export_html/images/media_location.png b/Telegram/Resources/export_html/images/media_location.png new file mode 100644 index 000000000..ab8080b7e Binary files /dev/null and b/Telegram/Resources/export_html/images/media_location.png differ diff --git a/Telegram/Resources/export_html/images/media_location@2x.png b/Telegram/Resources/export_html/images/media_location@2x.png new file mode 100644 index 000000000..11d88d529 Binary files /dev/null and b/Telegram/Resources/export_html/images/media_location@2x.png differ diff --git a/Telegram/Resources/export_html/images/media_music.png b/Telegram/Resources/export_html/images/media_music.png new file mode 100644 index 000000000..057a694b4 Binary files /dev/null and b/Telegram/Resources/export_html/images/media_music.png differ diff --git a/Telegram/Resources/export_html/images/media_music@2x.png b/Telegram/Resources/export_html/images/media_music@2x.png new file mode 100644 index 000000000..20c805644 Binary files /dev/null and b/Telegram/Resources/export_html/images/media_music@2x.png differ diff --git a/Telegram/Resources/export_html/images/media_shop.png b/Telegram/Resources/export_html/images/media_shop.png new file mode 100644 index 000000000..4a92ce83e Binary files /dev/null and b/Telegram/Resources/export_html/images/media_shop.png differ diff --git a/Telegram/Resources/export_html/images/media_shop@2x.png b/Telegram/Resources/export_html/images/media_shop@2x.png new file mode 100644 index 000000000..9cfe5512b Binary files /dev/null and b/Telegram/Resources/export_html/images/media_shop@2x.png differ diff --git a/Telegram/Resources/export_html/images/media_voice.png b/Telegram/Resources/export_html/images/media_voice.png new file mode 100644 index 000000000..af33c2ed8 Binary files /dev/null and b/Telegram/Resources/export_html/images/media_voice.png differ diff --git a/Telegram/Resources/export_html/images/media_voice@2x.png b/Telegram/Resources/export_html/images/media_voice@2x.png new file mode 100644 index 000000000..5356a377c Binary files /dev/null and b/Telegram/Resources/export_html/images/media_voice@2x.png differ diff --git a/Telegram/Resources/export_html/images/calls.png b/Telegram/Resources/export_html/images/section_calls.png similarity index 100% rename from Telegram/Resources/export_html/images/calls.png rename to Telegram/Resources/export_html/images/section_calls.png diff --git a/Telegram/Resources/export_html/images/calls@2x.png b/Telegram/Resources/export_html/images/section_calls@2x.png similarity index 100% rename from Telegram/Resources/export_html/images/calls@2x.png rename to Telegram/Resources/export_html/images/section_calls@2x.png diff --git a/Telegram/Resources/export_html/images/chats.png b/Telegram/Resources/export_html/images/section_chats.png similarity index 100% rename from Telegram/Resources/export_html/images/chats.png rename to Telegram/Resources/export_html/images/section_chats.png diff --git a/Telegram/Resources/export_html/images/chats@2x.png b/Telegram/Resources/export_html/images/section_chats@2x.png similarity index 100% rename from Telegram/Resources/export_html/images/chats@2x.png rename to Telegram/Resources/export_html/images/section_chats@2x.png diff --git a/Telegram/Resources/export_html/images/contacts.png b/Telegram/Resources/export_html/images/section_contacts.png similarity index 100% rename from Telegram/Resources/export_html/images/contacts.png rename to Telegram/Resources/export_html/images/section_contacts.png diff --git a/Telegram/Resources/export_html/images/contacts@2x.png b/Telegram/Resources/export_html/images/section_contacts@2x.png similarity index 100% rename from Telegram/Resources/export_html/images/contacts@2x.png rename to Telegram/Resources/export_html/images/section_contacts@2x.png diff --git a/Telegram/Resources/export_html/images/frequent.png b/Telegram/Resources/export_html/images/section_frequent.png similarity index 100% rename from Telegram/Resources/export_html/images/frequent.png rename to Telegram/Resources/export_html/images/section_frequent.png diff --git a/Telegram/Resources/export_html/images/frequent@2x.png b/Telegram/Resources/export_html/images/section_frequent@2x.png similarity index 100% rename from Telegram/Resources/export_html/images/frequent@2x.png rename to Telegram/Resources/export_html/images/section_frequent@2x.png diff --git a/Telegram/Resources/export_html/images/section_leftchats.png b/Telegram/Resources/export_html/images/section_leftchats.png new file mode 100644 index 000000000..350d22cd4 Binary files /dev/null and b/Telegram/Resources/export_html/images/section_leftchats.png differ diff --git a/Telegram/Resources/export_html/images/section_leftchats@2x.png b/Telegram/Resources/export_html/images/section_leftchats@2x.png new file mode 100644 index 000000000..b33520d10 Binary files /dev/null and b/Telegram/Resources/export_html/images/section_leftchats@2x.png differ diff --git a/Telegram/Resources/export_html/images/section_other.png b/Telegram/Resources/export_html/images/section_other.png new file mode 100644 index 000000000..a60ed7a40 Binary files /dev/null and b/Telegram/Resources/export_html/images/section_other.png differ diff --git a/Telegram/Resources/export_html/images/section_other@2x.png b/Telegram/Resources/export_html/images/section_other@2x.png new file mode 100644 index 000000000..497fb3338 Binary files /dev/null and b/Telegram/Resources/export_html/images/section_other@2x.png differ diff --git a/Telegram/Resources/export_html/images/photos.png b/Telegram/Resources/export_html/images/section_photos.png similarity index 100% rename from Telegram/Resources/export_html/images/photos.png rename to Telegram/Resources/export_html/images/section_photos.png diff --git a/Telegram/Resources/export_html/images/photos@2x.png b/Telegram/Resources/export_html/images/section_photos@2x.png similarity index 100% rename from Telegram/Resources/export_html/images/photos@2x.png rename to Telegram/Resources/export_html/images/section_photos@2x.png diff --git a/Telegram/Resources/export_html/images/sessions.png b/Telegram/Resources/export_html/images/section_sessions.png similarity index 100% rename from Telegram/Resources/export_html/images/sessions.png rename to Telegram/Resources/export_html/images/section_sessions.png diff --git a/Telegram/Resources/export_html/images/sessions@2x.png b/Telegram/Resources/export_html/images/section_sessions@2x.png similarity index 100% rename from Telegram/Resources/export_html/images/sessions@2x.png rename to Telegram/Resources/export_html/images/section_sessions@2x.png diff --git a/Telegram/Resources/export_html/images/web.png b/Telegram/Resources/export_html/images/section_web.png similarity index 100% rename from Telegram/Resources/export_html/images/web.png rename to Telegram/Resources/export_html/images/section_web.png diff --git a/Telegram/Resources/export_html/images/web@2x.png b/Telegram/Resources/export_html/images/section_web@2x.png similarity index 100% rename from Telegram/Resources/export_html/images/web@2x.png rename to Telegram/Resources/export_html/images/section_web@2x.png diff --git a/Telegram/Resources/qrc/telegram.qrc b/Telegram/Resources/qrc/telegram.qrc index ab56db558..00763851f 100644 --- a/Telegram/Resources/qrc/telegram.qrc +++ b/Telegram/Resources/qrc/telegram.qrc @@ -3,20 +3,40 @@ ../export_html/css/style.css ../export_html/images/back.png ../export_html/images/back@2x.png - ../export_html/images/calls.png - ../export_html/images/calls@2x.png - ../export_html/images/chats.png - ../export_html/images/chats@2x.png - ../export_html/images/contacts.png - ../export_html/images/contacts@2x.png - ../export_html/images/frequent.png - ../export_html/images/frequent@2x.png - ../export_html/images/photos.png - ../export_html/images/photos@2x.png - ../export_html/images/sessions.png - ../export_html/images/sessions@2x.png - ../export_html/images/web.png - ../export_html/images/web@2x.png + ../export_html/images/media_call.png + ../export_html/images/media_call@2x.png + ../export_html/images/media_contact.png + ../export_html/images/media_contact@2x.png + ../export_html/images/media_file.png + ../export_html/images/media_file@2x.png + ../export_html/images/media_game.png + ../export_html/images/media_game@2x.png + ../export_html/images/media_location.png + ../export_html/images/media_location@2x.png + ../export_html/images/media_music.png + ../export_html/images/media_music@2x.png + ../export_html/images/media_shop.png + ../export_html/images/media_shop@2x.png + ../export_html/images/media_voice.png + ../export_html/images/media_voice@2x.png + ../export_html/images/section_calls.png + ../export_html/images/section_calls@2x.png + ../export_html/images/section_chats.png + ../export_html/images/section_chats@2x.png + ../export_html/images/section_contacts.png + ../export_html/images/section_contacts@2x.png + ../export_html/images/section_frequent.png + ../export_html/images/section_frequent@2x.png + ../export_html/images/section_leftchats.png + ../export_html/images/section_leftchats@2x.png + ../export_html/images/section_other.png + ../export_html/images/section_other@2x.png + ../export_html/images/section_photos.png + ../export_html/images/section_photos@2x.png + ../export_html/images/section_sessions.png + ../export_html/images/section_sessions@2x.png + ../export_html/images/section_web.png + ../export_html/images/section_web@2x.png ../fonts/OpenSans-Regular.ttf diff --git a/Telegram/SourceFiles/export/data/export_data_types.cpp b/Telegram/SourceFiles/export/data/export_data_types.cpp index c513494fd..eba6f47c4 100644 --- a/Telegram/SourceFiles/export/data/export_data_types.cpp +++ b/Telegram/SourceFiles/export/data/export_data_types.cpp @@ -20,6 +20,7 @@ QString formatPhone(QString phone); } // namespace App QString FillAmountAndCurrency(uint64 amount, const QString ¤cy); QString formatSizeText(qint64 size); +QString formatDurationText(qint64 duration); namespace Export { namespace Data { @@ -940,19 +941,32 @@ Message ParseMessage( const MTPMessage &data, const QString &mediaFolder) { auto result = Message(); - data.match([&](const MTPDmessage &data) { + data.match([&](const auto &data) { result.id = data.vid.v; - const auto peerId = ParsePeerId(data.vto_id); - if (IsChatPeerId(peerId)) { - result.chatId = BarePeerId(peerId); + if constexpr (!MTPDmessageEmpty::Is()) { + result.toId = ParsePeerId(data.vto_id); + const auto peerId = (!data.is_out() + && data.has_from_id() + && data.vto_id.type() == mtpc_peerUser) + ? UserPeerId(data.vfrom_id.v) + : result.toId; + if (IsChatPeerId(peerId)) { + result.chatId = BarePeerId(peerId); + } + if (data.has_from_id()) { + result.fromId = data.vfrom_id.v; + } + if (data.has_reply_to_msg_id()) { + result.replyToMsgId = data.vreply_to_msg_id.v; + } + result.date = data.vdate.v; + result.out = data.is_out(); } - result.date = data.vdate.v; + }); + data.match([&](const MTPDmessage &data) { if (data.has_edit_date()) { result.edited = data.vedit_date.v; } - if (data.has_from_id()) { - result.fromId = data.vfrom_id.v; - } if (data.has_fwd_from()) { result.forwardedFromId = data.vfwd_from.match( [](const MTPDmessageFwdHeader &data) { @@ -963,6 +977,10 @@ Message ParseMessage( } return PeerId(0); }); + result.forwardedDate = data.vfwd_from.match( + [](const MTPDmessageFwdHeader &data) { + return data.vdate.v; + }); result.savedFromChatId = data.vfwd_from.match( [](const MTPDmessageFwdHeader &data) { if (data.has_saved_from_peer()) { @@ -998,22 +1016,10 @@ Message ParseMessage( ? data.ventities.v : QVector{})); }, [&](const MTPDmessageService &data) { - result.id = data.vid.v; - const auto peerId = ParsePeerId(data.vto_id); - if (IsChatPeerId(peerId)) { - result.chatId = BarePeerId(peerId); - } - result.date = data.vdate.v; result.action = ParseServiceAction( context, data.vaction, mediaFolder); - if (data.has_from_id()) { - result.fromId = data.vfrom_id.v; - } - if (data.has_reply_to_msg_id()) { - result.replyToMsgId = data.vreply_to_msg_id.v; - } }, [&](const MTPDmessageEmpty &data) { result.id = data.vid.v; }); @@ -1373,9 +1379,9 @@ Utf8String FormatDateTime( const auto value = QDateTime::fromTime_t(date); return (QString("%1") + dateSeparator + "%2" + dateSeparator + "%3" + separator + "%4" + timeSeparator + "%5" + timeSeparator + "%6" - ).arg(value.date().year() - ).arg(value.date().month(), 2, 10, QChar('0') ).arg(value.date().day(), 2, 10, QChar('0') + ).arg(value.date().month(), 2, 10, QChar('0') + ).arg(value.date().year() ).arg(value.time().hour(), 2, 10, QChar('0') ).arg(value.time().minute(), 2, 10, QChar('0') ).arg(value.time().second(), 2, 10, QChar('0') @@ -1392,5 +1398,9 @@ Utf8String FormatFileSize(int64 size) { return formatSizeText(size).toUtf8(); } +Utf8String FormatDuration(int64 seconds) { + return formatDurationText(seconds).toUtf8(); +} + } // namespace Data } // namespace Export diff --git a/Telegram/SourceFiles/export/data/export_data_types.h b/Telegram/SourceFiles/export/data/export_data_types.h index 7449f934a..7facf8ec7 100644 --- a/Telegram/SourceFiles/export/data/export_data_types.h +++ b/Telegram/SourceFiles/export/data/export_data_types.h @@ -26,6 +26,8 @@ using PeerId = uint64; PeerId UserPeerId(int32 userId); PeerId ChatPeerId(int32 chatId); int32 BarePeerId(PeerId peerId); +bool IsChatPeerId(PeerId peerId); +bool IsUserPeerId(PeerId peerId); int PeerColorIndex(int32 bareId); int ApplicationColorIndex(int applicationId); int DomainApplicationId(const Utf8String &data); @@ -462,7 +464,9 @@ struct Message { TimeId date = 0; TimeId edited = 0; int32 fromId = 0; + PeerId toId = 0; PeerId forwardedFromId = 0; + TimeId forwardedDate = 0; PeerId savedFromChatId = 0; Utf8String signature; int32 viaBotId = 0; @@ -470,6 +474,7 @@ struct Message { std::vector text; Media media; ServiceAction action; + bool out = false; File &file(); const File &file() const; @@ -546,6 +551,7 @@ Utf8String FormatDateTime( QChar separator = QChar(' ')); Utf8String FormatMoneyAmount(uint64 amount, const Utf8String ¤cy); Utf8String FormatFileSize(int64 size); +Utf8String FormatDuration(int64 seconds); } // namespace Data } // namespace Export diff --git a/Telegram/SourceFiles/export/output/export_output_abstract.cpp b/Telegram/SourceFiles/export/output/export_output_abstract.cpp index 4d8c20881..28c9831ed 100644 --- a/Telegram/SourceFiles/export/output/export_output_abstract.cpp +++ b/Telegram/SourceFiles/export/output/export_output_abstract.cpp @@ -197,7 +197,11 @@ Stats AbstractWriter::produceTestExample( message.id = counter(); message.date = prevdate(); message.edited = date(); - message.forwardedFromId = user.info.userId; + static auto count = 0; + if (++count % 3 == 0) { + message.forwardedFromId = Data::UserPeerId(user.info.userId); + message.forwardedDate = date(); + } message.fromId = user.info.userId; message.replyToMsgId = counter(); message.viaBotId = bot.info.userId; @@ -485,6 +489,5 @@ Stats AbstractWriter::produceTestExample( return result; } - } // namespace Output } // namespace Export diff --git a/Telegram/SourceFiles/export/output/export_output_html.cpp b/Telegram/SourceFiles/export/output/export_output_html.cpp index 0acaf11c2..e226fc0fd 100644 --- a/Telegram/SourceFiles/export/output/export_output_html.cpp +++ b/Telegram/SourceFiles/export/output/export_output_html.cpp @@ -22,12 +22,21 @@ constexpr auto kMessagesInFile = 1000; constexpr auto kPersonalUserpicSize = 90; constexpr auto kEntryUserpicSize = 48; constexpr auto kServiceMessagePhotoSize = 60; +constexpr auto kHistoryUserpicSize = 42; constexpr auto kSavedMessagesColorIndex = 3; +constexpr auto kJoinWithinSeconds = 900; const auto kLineBreak = QByteArrayLiteral("
"); using Context = details::HtmlContext; using UserpicData = details::UserpicData; +using PeersMap = details::PeersMap; +using MediaData = details::MediaData; + +bool IsGlobalLink(const QString &link) { + return link.startsWith(qstr("http://"), Qt::CaseInsensitive) + || link.startsWith(qstr("https://"), Qt::CaseInsensitive); +} QByteArray SerializeString(const QByteArray &value) { const auto size = value.size(); @@ -76,6 +85,19 @@ QByteArray SerializeString(const QByteArray &value) { return result; } +QByteArray SerializeList(const std::vector &values) { + const auto count = values.size(); + if (count == 1) { + return values[0]; + } else if (count > 1) { + auto result = values[0]; + for (auto i = 1; i != count - 1; ++i) { + result += ", " + values[i]; + } + return result + " and " + values[count - 1]; + } + return QByteArray(); +} QByteArray MakeLinks(const QByteArray &value) { const auto domain = QByteArray("https://telegram.org/"); auto result = QByteArray(); @@ -162,6 +184,55 @@ QByteArray JoinList( return result; } +QByteArray FormatText( + const std::vector &data, + const QString &internalLinksDomain) { + return JoinList(QByteArray(), ranges::view::all( + data + ) | ranges::view::transform([&](const Data::TextPart &part) { + const auto text = SerializeString(part.text); + using Type = Data::TextPart::Type; + switch (part.type) { + case Type::Text: return text; + case Type::Unknown: return text; + case Type::Mention: + return "" + text + ""; + case Type::Hashtag: return "" + text + ""; + case Type::BotCommand: return "" + text + ""; + case Type::Url: return "" + text + ""; + case Type::Email: return "" + text + ""; + case Type::Bold: return "" + text + ""; + case Type::Italic: return "" + text + ""; + case Type::Code: return "" + text + ""; + case Type::Pre: return "
" + text + "
"; + case Type::TextUrl: return "" + text + ""; + case Type::MentionName: return "" + text + ""; + case Type::Phone: return "" + text + ""; + case Type::Cashtag: return "" + text + ""; + } + Unexpected("Type in text entities serialization."); + }) | ranges::to_vector); +} + QByteArray SerializeKeyValue( std::vector> &&values) { auto result = QByteArray(); @@ -229,6 +300,13 @@ QByteArray FormatDateText(TimeId date) { + Data::NumberToString(parsed.year()); } +QByteArray FormatTimeText(TimeId date) { + const auto parsed = QDateTime::fromTime_t(date).time(); + return Data::NumberToString(parsed.hour(), 2) + + ':' + + Data::NumberToString(parsed.minute(), 2); +} + QByteArray SerializeLink( const Data::Utf8String &text, const QString &path) { @@ -248,6 +326,85 @@ struct UserpicData { QByteArray lastName; }; +class PeersMap { +public: + using PeerId = Data::PeerId; + using Peer = Data::Peer; + using User = Data::User; + using Chat = Data::Chat; + + PeersMap(const std::map &data); + + const Peer &peer(PeerId peerId) const; + const User &user(int32 userId) const; + const Chat &chat(int32 chatId) const; + + QByteArray wrapPeerName(PeerId peerId) const; + QByteArray wrapUserName(int32 userId) const; + QByteArray wrapUserNames(const std::vector &data) const; + +private: + const std::map &_data; + +}; + +struct MediaData { + QByteArray title; + QByteArray description; + QByteArray status; + QByteArray classes; + QString link; +}; + +PeersMap::PeersMap(const std::map &data) : _data(data) { +} + +auto PeersMap::peer(PeerId peerId) const -> const Peer & { + if (const auto i = _data.find(peerId); i != end(_data)) { + return i->second; + } + static auto empty = Peer{ User() }; + return empty; +} + +auto PeersMap::user(int32 userId) const -> const User & { + if (const auto result = peer(Data::UserPeerId(userId)).user()) { + return *result; + } + static auto empty = User(); + return empty; +} + +auto PeersMap::chat(int32 chatId) const -> const Chat & { + if (const auto result = peer(Data::ChatPeerId(chatId)).chat()) { + return *result; + } + static auto empty = Chat(); + return empty; +} + +QByteArray PeersMap::wrapPeerName(PeerId peerId) const { + const auto result = peer(peerId).name(); + return result.isEmpty() + ? QByteArray("Deleted") + : SerializeString(result); +} + +QByteArray PeersMap::wrapUserName(int32 userId) const { + const auto result = user(userId).name(); + return result.isEmpty() + ? QByteArray("Deleted Account") + : SerializeString(result); +} + +QByteArray PeersMap::wrapUserNames(const std::vector &data) const { + auto list = std::vector(); + for (const auto userId : data) { + list.push_back(wrapUserName(userId)); + } + return SerializeList(list); +} + QByteArray HtmlContext::pushTag( const QByteArray &tag, std::map &&attributes) { @@ -294,6 +451,18 @@ bool HtmlContext::empty() const { } // namespace details +struct HtmlWriter::MessageInfo { + enum class Type { + Service, + Default, + }; + Type type = Type::Service; + int32 fromId = 0; + TimeId date = 0; + Data::PeerId forwardedFromId = 0; + TimeId forwardedDate = 0; +}; + class HtmlWriter::Wrap { public: Wrap(const QString &path, const QString &base, Stats *stats); @@ -341,11 +510,17 @@ public: const QString &basePath, const QByteArray &text, const Data::Photo *photo = nullptr); - [[nodiscard]] QByteArray pushMessage( + [[nodiscard]] std::pair pushMessage( const Data::Message &message, + const MessageInfo *previous, const Data::DialogInfo &dialog, const QString &basePath, - const std::map &peers, + const PeersMap &peers, + const QString &internalLinksDomain); + [[nodiscard]] QByteArray pushMedia( + const Data::Message &message, + const QString &basePath, + const PeersMap &peers, const QString &internalLinksDomain); [[nodiscard]] Result writeBlock(const QByteArray &block); @@ -367,6 +542,19 @@ private: std::initializer_list details, const QByteArray &info); + [[nodiscard]] bool messageNeedsWrap( + const Data::Message &message, + const MessageInfo *previous) const; + [[nodiscard]] bool forwardedNeedsWrap( + const Data::Message &message, + const MessageInfo *previous) const; + + [[nodiscard]] MediaData prepareMediaData( + const Data::Message &message, + const QString &basePath, + const PeersMap &peers, + const QString &internalLinksDomain) const; + File _file; bool _closed = false; QByteArray _base; @@ -382,6 +570,15 @@ struct HtmlWriter::SavedSection { QString path; }; +void FillUserpicNames(UserpicData &data, const Data::Peer &peer) { + if (peer.user()) { + data.firstName = peer.user()->info.firstName; + data.lastName = peer.user()->info.lastName; + } else if (peer.chat()) { + data.firstName = peer.name(); + } +} + QByteArray ComposeName(const UserpicData &data, const QByteArray &empty) { return ((data.firstName.isEmpty() && data.lastName.isEmpty()) ? empty @@ -640,7 +837,7 @@ QByteArray HtmlWriter::Wrap::pushServiceMessage( { "class", "message service" }, { "id", "message" + Data::NumberToString(messageId) } }); - result.append(pushDiv("content details")); + result.append(pushDiv("body details")); result.append(serialized); result.append(popTag()); if (photo) { @@ -663,114 +860,30 @@ QByteArray HtmlWriter::Wrap::pushServiceMessage( return result; } -QByteArray HtmlWriter::Wrap::pushMessage( +auto HtmlWriter::Wrap::pushMessage( const Data::Message &message, + const MessageInfo *previous, const Data::DialogInfo &dialog, const QString &basePath, - const std::map &peers, - const QString &internalLinksDomain) { + const PeersMap &peers, + const QString &internalLinksDomain +) -> std::pair { using namespace Data; + auto info = MessageInfo(); + info.fromId = message.fromId; + info.date = message.date; + info.forwardedFromId = message.forwardedFromId; + info.forwardedDate = message.forwardedDate; if (message.media.content.is()) { - return pushServiceMessage( + return { info, pushServiceMessage( message.id, dialog, basePath, "This message is not supported by this version " - "of Telegram Desktop. Please update the application."); + "of Telegram Desktop. Please update the application.") }; } - const auto peer = [&](PeerId peerId) -> const Peer& { - if (const auto i = peers.find(peerId); i != end(peers)) { - return i->second; - } - static auto empty = Peer{ User() }; - return empty; - }; - const auto user = [&](int32 userId) -> const User& { - if (const auto result = peer(UserPeerId(userId)).user()) { - return *result; - } - static auto empty = User(); - return empty; - }; - const auto chat = [&](int32 chatId) -> const Chat& { - if (const auto result = peer(ChatPeerId(chatId)).chat()) { - return *result; - } - static auto empty = Chat(); - return empty; - }; - - auto values = std::vector>{ - { "ID", SerializeString(NumberToString(message.id)) }, - { "Date", SerializeString(FormatDateTime(message.date)) }, - { "Edited", SerializeString(FormatDateTime(message.edited)) }, - }; - const auto pushBare = [&]( - const QByteArray &key, - const QByteArray &value) { - values.emplace_back(key, value); - }; - const auto push = [&](const QByteArray &key, const QByteArray &value) { - if (!value.isEmpty()) { - pushBare(key, SerializeString(value)); - } - }; - const auto wrapPeerName = [&](PeerId peerId) { - const auto result = peer(peerId).name(); - return result.isEmpty() ? QByteArray("(deleted peer)") : result; - }; - const auto wrapUserName = [&](int32 userId) { - const auto result = user(userId).name(); - return result.isEmpty() - ? QByteArray("Deleted Account") - : SerializeString(result); - }; - const auto pushFrom = [&](const QByteArray &label = "From") { - if (message.fromId) { - push(label, wrapUserName(message.fromId)); - } - }; - const auto pushReplyToMsgId = [&]( - const QByteArray &label = "Reply to message") { - if (message.replyToMsgId) { - push(label, "ID-" + NumberToString(message.replyToMsgId)); - } - }; - const auto wrapList = [&](const std::vector &values) { - const auto count = values.size(); - if (count == 1) { - return values[0]; - } else if (count > 1) { - auto result = values[0]; - for (auto i = 1; i != count - 1; ++i) { - result += ", " + values[i]; - } - return result + " and " + values[count - 1]; - } - return QByteArray(); - }; - const auto wrapUserNames = [&](const std::vector &data) { - auto list = std::vector(); - for (const auto userId : data) { - list.push_back(wrapUserName(userId)); - } - return wrapList(list); - }; - const auto pushActor = [&] { - pushFrom("Actor"); - }; - const auto pushAction = [&](const QByteArray &action) { - push("Action", action); - }; - const auto pushTTL = [&]( - const QByteArray &label = "Self destruct period") { - if (const auto ttl = message.media.ttl) { - push(label, NumberToString(ttl) + " sec."); - } - }; - using SkipReason = Data::File::SkipReason; const auto formatPath = [&]( const Data::File &file, @@ -798,19 +911,6 @@ QByteArray HtmlWriter::Wrap::pushMessage( } Unexpected("Skip reason while writing file path."); }; - const auto pushPath = [&]( - const Data::File &file, - const QByteArray &label, - const QByteArray &name = QByteArray()) { - pushBare(label, formatPath(file, label, name)); - }; - const auto pushPhoto = [&](const Image &image) { - pushPath(image.file, "Photo"); - if (image.width && image.height) { - push("Width", NumberToString(image.width)); - push("Height", NumberToString(image.height)); - } - }; const auto wrapReplyToLink = [&](const QByteArray &text) { return ""; }; - const auto serviceFrom = wrapUserName(message.fromId); + const auto serviceFrom = peers.wrapUserName(message.fromId); const auto serviceText = message.action.content.match( [&](const ActionChatCreate &data) { return serviceFrom + " created group «" + data.title + "»" + (data.userIds.empty() ? QByteArray() - : " with members " + wrapUserNames(data.userIds)); + : " with members " + peers.wrapUserNames(data.userIds)); }, [&](const ActionChatEditTitle &data) { return serviceFrom + " changed group title to «" + data.title + "»"; @@ -838,15 +938,15 @@ QByteArray HtmlWriter::Wrap::pushMessage( }, [&](const ActionChatAddUser &data) { return serviceFrom + " invited " - + wrapUserNames(data.userIds); + + peers.wrapUserNames(data.userIds); }, [&](const ActionChatDeleteUser &data) { return serviceFrom + " removed " - + wrapUserName(data.userId); + + peers.wrapUserName(data.userId); }, [&](const ActionChatJoinedByLink &data) { return serviceFrom + " joined group by link from " - + wrapUserName(data.inviterId); + + peers.wrapUserName(data.inviterId); }, [&](const ActionChannelCreate &data) { return "Channel «" + data.title + "» created"; }, [&](const ActionChatMigrateTo &data) { @@ -907,7 +1007,8 @@ QByteArray HtmlWriter::Wrap::pushMessage( return ""; }()); } - return "You have sent the following documents: " + wrapList(list); + return "You have sent the following documents: " + + SerializeList(list); }, [](const base::none_type &) { return QByteArray(); }); if (!serviceText.isEmpty()) { @@ -915,164 +1016,360 @@ QByteArray HtmlWriter::Wrap::pushMessage( const auto photo = content.is() ? &content.get_unchecked().photo : nullptr; - return pushServiceMessage( + return { info, pushServiceMessage( message.id, dialog, basePath, serviceText, - photo); + photo) }; + } + info.type = MessageInfo::Type::Default; + + const auto wrap = messageNeedsWrap(message, previous); + const auto fromPeerId = message.fromId + ? UserPeerId(message.fromId) + : ChatPeerId(message.chatId); + auto userpic = UserpicData(); + userpic.colorIndex = PeerColorIndex(BarePeerId(fromPeerId)); + userpic.pixelSize = kHistoryUserpicSize; + FillUserpicNames(userpic, peers.peer(fromPeerId)); + + const auto via = [&] { + if (message.viaBotId) { + const auto &user = peers.user(message.viaBotId); + if (!user.username.isEmpty()) { + return SerializeString(user.username); + } + } + return QByteArray(); + }(); + + const auto className = wrap + ? "message default clearfix" + : "message default clearfix joined"; + auto block = pushTag("div", { + { "class", className }, + { "id", "message" + NumberToString(message.id) } + }); + if (wrap) { + block.append(pushDiv("pull_left userpic_wrap")); + block.append(pushUserpic(userpic)); + block.append(popTag()); + } + block.append(pushDiv("body")); + block.append(pushTag("div", { + { "class", "pull_right date details" }, + { "title", FormatDateTime(message.date) }, + })); + block.append(FormatTimeText(message.date)); + block.append(popTag()); + if (wrap) { + block.append(pushDiv("from_name")); + block.append(SerializeString( + ComposeName(userpic, "Deleted Account"))); + if (!via.isEmpty() && !message.forwardedFromId) { + block.append(" via @" + via); + } + block.append(popTag()); + } + if (message.forwardedFromId) { + auto forwardedUserpic = UserpicData(); + forwardedUserpic.colorIndex = PeerColorIndex( + BarePeerId(message.forwardedFromId)); + forwardedUserpic.pixelSize = kHistoryUserpicSize; + FillUserpicNames( + forwardedUserpic, + peers.peer(message.forwardedFromId)); + + const auto forwardedWrap = forwardedNeedsWrap(message, previous); + if (forwardedWrap) { + block.append(pushDiv("pull_left forwarded userpic_wrap")); + block.append(pushUserpic(forwardedUserpic)); + block.append(popTag()); + } + block.append(pushDiv("forwarded body")); + if (forwardedWrap) { + block.append(pushDiv("from_name")); + block.append(SerializeString( + ComposeName(forwardedUserpic, "Deleted Account"))); + if (!via.isEmpty()) { + block.append(" via @" + via); + } + block.append(pushTag("span", { + { "class", "details" }, + { "inline", "" } + })); + block.append(' ' + FormatDateTime(message.forwardedDate)); + block.append(popTag()); + block.append(popTag()); + } + } + if (message.replyToMsgId) { + block.append(pushDiv("reply_to details")); + block.append("In reply to "); + block.append(wrapReplyToLink("this message")); + block.append(popTag()); } - if (!message.action.content) { - pushFrom(); - push("Author", message.signature); - if (message.forwardedFromId) { - push("Forwarded from", wrapPeerName(message.forwardedFromId)); - } - if (message.savedFromChatId) { - push("Saved from", wrapPeerName(message.savedFromChatId)); - } - pushReplyToMsgId(); - if (message.viaBotId) { - push("Via", user(message.viaBotId).username); + block.append(pushMedia(message, basePath, peers, internalLinksDomain)); + + const auto text = FormatText(message.text, internalLinksDomain); + if (!text.isEmpty()) { + block.append(pushDiv("text")); + block.append(text); + block.append(popTag()); + } + if (!message.signature.isEmpty()) { + block.append(pushDiv("signature details")); + block.append(SerializeString(message.signature)); + block.append(popTag()); + } + if (message.forwardedFromId) { + block.append(popTag()); + } + block.append(popTag()); + block.append(popTag()); + + return { info, block }; +} + +bool HtmlWriter::Wrap::messageNeedsWrap( + const Data::Message &message, + const MessageInfo *previous) const { + if (!previous) { + return true; + } else if (previous->type != MessageInfo::Type::Default) { + return true; + } else if (!message.fromId || previous->fromId != message.fromId) { + return true; + } else if (QDateTime::fromTime_t(previous->date).date() + != QDateTime::fromTime_t(message.date).date()) { + return true; + } else if (!message.forwardedFromId != !previous->forwardedFromId) { + return true; + } else if (std::abs(message.date - previous->date) + > (message.forwardedFromId ? 1 : kJoinWithinSeconds)) { + return true; + } + return false; +} + +QByteArray HtmlWriter::Wrap::pushMedia( + const Data::Message &message, + const QString &basePath, + const PeersMap &peers, + const QString &internalLinksDomain) { + const auto data = prepareMediaData( + message, + basePath, + peers, + internalLinksDomain); + if (data.classes.isEmpty()) { + return QByteArray(); + } + auto result = pushDiv("media_wrap clearfix"); + if (data.link.isEmpty()) { + result.append(pushDiv("media clearfix pull_left " + data.classes)); + } else { + result.append(pushTag("a", { + { + "class", + "media clearfix pull_left block_link " + data.classes + }, + { + "href", + (IsGlobalLink(data.link) + ? data.link.toUtf8() + : relativePath(data.link).toUtf8()) + } + })); + } + result.append(pushDiv("thumb pull_left")); + result.append(popTag()); + result.append(pushDiv("body")); + if (!data.title.isEmpty()) { + result.append(pushDiv("title bold")); + result.append(SerializeString(data.title)); + result.append(popTag()); + } + if (!data.description.isEmpty()) { + result.append(pushDiv("description")); + result.append(SerializeString(data.description)); + result.append(popTag()); + } + if (!data.status.isEmpty()) { + result.append(pushDiv("status details")); + result.append(SerializeString(data.status)); + result.append(popTag()); + } + result.append(popTag()); + result.append(popTag()); + result.append(popTag()); + return result; +} + +MediaData HtmlWriter::Wrap::prepareMediaData( + const Data::Message &message, + const QString &basePath, + const PeersMap &peers, + const QString &internalLinksDomain) const { + using namespace Data; + + auto result = MediaData(); + const auto &action = message.action; + if (const auto call = base::get_if(&action.content)) { + result.classes = "media_call"; + result.title = peers.peer(message.toId).name(); + result.status = [&] { + using Reason = ActionPhoneCall::DiscardReason; + const auto reason = call->discardReason; + if (message.out) { + return reason == Reason::Missed ? "Cancelled" : "Outgoing"; + } else if (reason == Reason::Missed) { + return "Missed"; + } else if (reason == Reason::Busy) { + return "Declined"; + } + return "Incoming"; + }(); + if (call->duration > 0) { + result.classes += " success"; + result.status += " (" + + NumberToString(call->duration) + + " seconds)"; } + return result; } message.media.content.match([&](const Photo &photo) { - pushPhoto(photo.image); - pushTTL(); + // #TODO export: photo + self destruct (ttl) + result.title = "Photo"; + result.status = NumberToString(photo.image.width) + + "x" + + NumberToString(photo.image.height); + result.classes = "media_file"; // #TODO export + result.link = FormatFilePath(photo.image.file); }, [&](const Document &data) { - const auto pushMyPath = [&](const QByteArray &label) { - return pushPath(data.file, label); - }; + // #TODO export: sticker + thumb (video, video message) + self destruct (ttl) + result.link = FormatFilePath(data.file); if (data.isSticker) { - pushMyPath("Sticker"); - push("Emoji", data.stickerEmoji); + result.title = "Sticker"; + result.status = data.stickerEmoji; + result.classes = "media_file"; // #TODO export } else if (data.isVideoMessage) { - pushMyPath("Video message"); + result.title = "Video message"; + result.status = FormatDuration(data.duration); + result.classes = "media_file"; // #TODO export } else if (data.isVoiceMessage) { - pushMyPath("Voice message"); + result.title = "Voice message"; + result.status = FormatDuration(data.duration); + result.classes = "media_voice_message"; } else if (data.isAnimated) { - pushMyPath("Animation"); + result.title = "Animation"; + result.status = FormatFileSize(data.duration); + result.classes = "media_file"; // #TODO export } else if (data.isVideoFile) { - pushMyPath("Video file"); + result.title = "Video file"; + result.status = FormatDuration(data.duration); + result.classes = "media_file"; // #TODO export } else if (data.isAudioFile) { - pushMyPath("Audio file"); - push("Performer", data.songPerformer); - push("Title", data.songTitle); + result.title = (data.songPerformer.isEmpty() + || data.songTitle.isEmpty()) + ? QByteArray("Audio file") + : data.songPerformer + " \xe2\x80\x93 " + data.songTitle; + result.status = FormatDuration(data.duration); + result.classes = "media_audio_file"; } else { - pushMyPath("File"); + result.title = data.name.isEmpty() + ? QByteArray("File") + : data.name; + result.status = FormatFileSize(data.duration); + result.classes = "media_file"; } - if (!data.isSticker) { - push("Mime type", data.mime); - } - if (data.duration) { - push("Duration", NumberToString(data.duration) + " sec."); - } - if (data.width && data.height) { - push("Width", NumberToString(data.width)); - push("Height", NumberToString(data.height)); - } - pushTTL(); }, [&](const SharedContact &data) { - pushBare("Contact information", SerializeBlockquote({ - { "First name", data.info.firstName }, - { "Last name", data.info.lastName }, - { "Phone number", FormatPhoneNumber(data.info.phoneNumber) }, - { "vCard", (data.vcard.content.isEmpty() - ? QByteArray() - : formatPath(data.vcard, "vCard")) } - })); + result.title = data.info.firstName + ' ' + data.info.lastName; + result.classes = "media_contact"; + result.status = FormatPhoneNumber(data.info.phoneNumber); + if (!data.vcard.content.isEmpty()) { + result.status += " - vCard"; + result.link = FormatFilePath(data.vcard); + } }, [&](const GeoPoint &data) { - pushBare("Location", data.valid ? SerializeBlockquote({ - { "Latitude", NumberToString(data.latitude) }, - { "Longitude", NumberToString(data.longitude) }, - }) : QByteArray("(empty value)")); - pushTTL("Live location period"); + if (message.media.ttl) { + result.classes = "media_live_location"; + result.title = "Live location"; + result.status = ""; + } else { + result.classes = "media_location"; + result.title = "Location"; + } + if (data.valid) { + const auto latitude = NumberToString(data.latitude); + const auto longitude = NumberToString(data.longitude); + const auto coords = latitude + ',' + longitude; + result.status = latitude + ", " + longitude; + result.link = "https://maps.google.com/maps?q=" + + coords + + "&ll=" + + coords + + "&z=16"; + } }, [&](const Venue &data) { - push("Place name", data.title); - push("Address", data.address); + result.classes = "media_venue"; + result.title = data.title; + result.description = data.address; if (data.point.valid) { - pushBare("Location", SerializeBlockquote({ - { "Latitude", NumberToString(data.point.latitude) }, - { "Longitude", NumberToString(data.point.longitude) }, - })); + const auto latitude = NumberToString(data.point.latitude); + const auto longitude = NumberToString(data.point.longitude); + const auto coords = latitude + ',' + longitude; + result.link = "https://maps.google.com/maps?q=" + + coords + + "&ll=" + + coords + + "&z=16"; } }, [&](const Game &data) { - push("Game", data.title); - push("Description", data.description); + result.classes = "media_game"; + result.title = data.title; + result.description = data.description; if (data.botId != 0 && !data.shortName.isEmpty()) { - const auto bot = user(data.botId); + const auto bot = peers.user(data.botId); if (bot.isBot && !bot.username.isEmpty()) { - push("Link", internalLinksDomain.toUtf8() + const auto link = internalLinksDomain.toUtf8() + bot.username + "?game=" - + data.shortName); + + data.shortName; + result.link = link; + result.status = link; } } }, [&](const Invoice &data) { - pushBare("Invoice", SerializeBlockquote({ - { "Title", data.title }, - { "Description", data.description }, - { - "Amount", - Data::FormatMoneyAmount(data.amount, data.currency) - }, - { "Receipt message", (data.receiptMsgId - ? "ID-" + NumberToString(data.receiptMsgId) - : QByteArray()) } - })); + result.classes = "media_invoice"; + result.title = data.title; + result.description = data.description; + result.status = Data::FormatMoneyAmount(data.amount, data.currency); }, [](const UnsupportedMedia &data) { Unexpected("Unsupported message."); }, [](const base::none_type &) {}); + return result; +} - auto value = JoinList(QByteArray(), ranges::view::all( - message.text - ) | ranges::view::transform([&](const Data::TextPart &part) { - const auto text = SerializeString(part.text); - using Type = Data::TextPart::Type; - switch (part.type) { - case Type::Text: return text; - case Type::Unknown: return text; - case Type::Mention: - return "" + text + ""; - case Type::Hashtag: return "" + text + ""; - case Type::BotCommand: return "" + text + ""; - case Type::Url: return "" + text + ""; - case Type::Email: return "" + text + ""; - case Type::Bold: return "" + text + ""; - case Type::Italic: return "" + text + ""; - case Type::Code: return "" + text + ""; - case Type::Pre: return "
" + text + "
"; - case Type::TextUrl: return "" + text + ""; - case Type::MentionName: return "" + text + ""; - case Type::Phone: return "" + text + ""; - case Type::Cashtag: return "" + text + ""; - } - Unexpected("Type in text entities serialization."); - }) | ranges::to_vector); - pushBare("Text", value); +bool HtmlWriter::Wrap::forwardedNeedsWrap( + const Data::Message &message, + const MessageInfo *previous) const { + Expects(message.forwardedFromId != 0); - return SerializeKeyValue(std::move(values)); + if (messageNeedsWrap(message, previous)) { + return true; + } else if (message.forwardedFromId != previous->forwardedFromId) { + return true; + } else if (Data::IsChatPeerId(message.forwardedFromId)) { + return true; + } else if (abs(message.forwardedDate - previous->forwardedDate) + > kJoinWithinSeconds) { + return true; + } + return false; } Result HtmlWriter::Wrap::close() { @@ -1149,13 +1446,23 @@ Result HtmlWriter::start( const auto files = { "css/style.css", "images/back.png", - "images/calls.png", - "images/chats.png", - "images/contacts.png", - "images/frequent.png", - "images/photos.png", - "images/sessions.png", - "images/web.png", + "images/media_call.png", + "images/media_contact.png", + "images/media_file.png", + "images/media_game.png", + "images/media_location.png", + "images/media_music.png", + "images/media_shop.png", + "images/media_voice.png", + "images/section_calls.png", + "images/section_chats.png", + "images/section_contacts.png", + "images/section_frequent.png", + "images/section_leftchats.png", + "images/section_other.png", + "images/section_photos.png", + "images/section_sessions.png", + "images/section_web.png", }; for (const auto path : files) { const auto name = QString(path); @@ -1446,9 +1753,7 @@ Result HtmlWriter::writeFrequentContacts(const Data::ContactsList &data) { userpic.lastName = lastName; block.append(file->pushListEntry( userpic, - ((name.isEmpty() && lastName.isEmpty()) - ? QByteArray("Deleted Account") - : (name + ' ' + lastName)), + ComposeName(userpic, "Deleted Account"), "Rating: " + Data::NumberToString(top.rating), category)); } @@ -1586,18 +1891,20 @@ Result HtmlWriter::writeWebSessions(const Data::SessionsList &data) { Result HtmlWriter::writeOtherData(const Data::File &data) { Expects(_summary != nullptr); - const auto header = SerializeLink( + pushSection( + 7, "Other data", - _summary->relativePath(data)) - + kLineBreak - + kLineBreak; - return _summary->writeBlock(header); + "other", + 1, + data.relativePath); + return Result::Success(); } Result HtmlWriter::writeDialogsStart(const Data::DialogsInfo &data) { return writeChatsStart( data, "Chats", + "chats", _environment.aboutChats, "lists/chats.html"); } @@ -1622,6 +1929,7 @@ Result HtmlWriter::writeLeftChannelsStart(const Data::DialogsInfo &data) { return writeChatsStart( data, "Left chats", + "leftchats", _environment.aboutLeftChats, "lists/left_chats.html"); } @@ -1645,6 +1953,7 @@ Result HtmlWriter::writeLeftChannelsEnd() { Result HtmlWriter::writeChatsStart( const Data::DialogsInfo &data, const QByteArray &listName, + const QByteArray &buttonClass, const QByteArray &about, const QString &fileName) { Expects(_summary != nullptr); @@ -1672,7 +1981,7 @@ Result HtmlWriter::writeChatsStart( pushSection( 0, listName, - "chats", + buttonClass, data.list.size(), fileName); return writeSections(); @@ -1687,7 +1996,7 @@ Result HtmlWriter::writeChatStart(const Data::DialogInfo &data) { _chat = fileWithRelativePath(data.relativePath + messagesFile(0)); _messagesCount = 0; _dateMessageId = 0; - _lastMessageDate = 0; + _lastMessageInfo = nullptr; _dialog = data; return Result::Success(); } @@ -1697,8 +2006,12 @@ Result HtmlWriter::writeChatSlice(const Data::MessagesSlice &data) { Expects(!data.list.empty()); if (_chat->empty()) { + const auto name = (_dialog.name.isEmpty() + && _dialog.lastName.isEmpty()) + ? QByteArray("Deleted Account") + : (_dialog.name + ' ' + _dialog.lastName); auto block = _chat->pushHeader( - _dialog.name + ' ' + _dialog.lastName, + name, _dialogsRelativePath); block.append(_chat->pushDiv("page_body chat_page")); block.append(_chat->pushDiv("history")); @@ -1716,24 +2029,30 @@ Result HtmlWriter::writeChatSlice(const Data::MessagesSlice &data) { } } + auto previous = _lastMessageInfo.get(); + auto saved = MessageInfo(); auto block = QByteArray(); for (const auto &message : data.list) { const auto date = message.date; - if (DisplayDate(date, _lastMessageDate)) { + if (DisplayDate(date, previous ? previous->date : 0)) { block.append(_chat->pushServiceMessage( --_dateMessageId, _dialog, _settings.path, FormatDateText(date))); } - block.append(_chat->pushMessage( + const auto [info, content] = _chat->pushMessage( message, + previous, _dialog, _settings.path, data.peers, - _environment.internalLinksDomain)); - _lastMessageDate = date; + _environment.internalLinksDomain); + block.append(content); + saved = info; + previous = &saved; } + _lastMessageInfo = std::make_unique(saved); return _chat->writeBlock(block); } @@ -1791,6 +2110,8 @@ Result HtmlWriter::writeChatEnd() { const auto CountString = [](int count, bool outgoing) -> QByteArray { if (count == 1) { return outgoing ? "1 outgoing message" : "1 message"; + } else if (!count) { + return outgoing ? "No outgoing messages" : "No messages"; } return Data::NumberToString(count) + (outgoing ? " outgoing messages" : " messages"); @@ -1805,9 +2126,7 @@ Result HtmlWriter::writeChatEnd() { userpic.lastName = LastNameString(_dialog); return _chats->writeBlock(_chats->pushListEntry( userpic, - ((userpic.firstName.isEmpty() && userpic.lastName.isEmpty()) - ? QByteArray(DeletedString(_dialog.type)) - : (userpic.firstName + ' ' + userpic.lastName)), + ComposeName(userpic, DeletedString(_dialog.type)), CountString(_messagesCount, _dialog.onlyMyMessages), TypeString(_dialog.type), (_messagesCount > 0 diff --git a/Telegram/SourceFiles/export/output/export_output_html.h b/Telegram/SourceFiles/export/output/export_output_html.h index 6a8cfc2b6..4614ae53e 100644 --- a/Telegram/SourceFiles/export/output/export_output_html.h +++ b/Telegram/SourceFiles/export/output/export_output_html.h @@ -35,6 +35,8 @@ private: }; struct UserpicData; +struct PeersMap; +struct MediaData; } // namespace details @@ -84,7 +86,9 @@ public: private: using Context = details::HtmlContext; using UserpicData = details::UserpicData; + using MediaData = details::MediaData; class Wrap; + struct MessageInfo; [[nodiscard]] Result copyFile( const QString &source, @@ -104,6 +108,7 @@ private: [[nodiscard]] Result writeChatsStart( const Data::DialogsInfo &data, const QByteArray &listName, + const QByteArray &buttonClass, const QByteArray &about, const QString &fileName); [[nodiscard]] Result writeChatStart(const Data::DialogInfo &data); @@ -153,7 +158,7 @@ private: Data::DialogInfo _dialog; int _messagesCount = 0; - TimeId _lastMessageDate = 0; + std::unique_ptr _lastMessageInfo; int _dateMessageId = 0; std::unique_ptr _chats; std::unique_ptr _chat; diff --git a/Telegram/SourceFiles/mainwidget.cpp b/Telegram/SourceFiles/mainwidget.cpp index 9d1ff0508..96585a137 100644 --- a/Telegram/SourceFiles/mainwidget.cpp +++ b/Telegram/SourceFiles/mainwidget.cpp @@ -261,9 +261,9 @@ MainWidget::MainWidget( Messenger::Instance().mtp()->setUpdatesHandler(rpcDone(&MainWidget::updateReceived)); Messenger::Instance().mtp()->setGlobalFailHandler(rpcFail(&MainWidget::updateFail)); - Export::Output::HtmlWriter writer; - writer.produceTestExample(psDownloadPath(), Export::View::PrepareEnvironment()); - crl::on_main([] { App::quit(); }); + //Export::Output::HtmlWriter writer; + //writer.produceTestExample(psDownloadPath(), Export::View::PrepareEnvironment()); + //crl::on_main([] { App::quit(); }); _ptsWaiter.setRequesting(true); updateScrollColors();