diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index 1686295049..ec658928a1 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -5542,6 +5542,38 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_text_copied" = "Text copied to clipboard."; "lng_code_copied" = "Code copied to clipboard."; +"lng_date_copied" = "Date copied to clipboard."; + +"lng_date_relative_now" = "now"; + +"lng_date_relative_seconds_ago#one" = "{count} second ago"; +"lng_date_relative_seconds_ago#other" = "{count} seconds ago"; +"lng_date_relative_minutes_ago#one" = "{count} minute ago"; +"lng_date_relative_minutes_ago#other" = "{count} minutes ago"; +"lng_date_relative_hours_ago#one" = "{count} hour ago"; +"lng_date_relative_hours_ago#other" = "{count} hours ago"; +"lng_date_relative_days_ago#one" = "{count} day ago"; +"lng_date_relative_days_ago#other" = "{count} days ago"; +"lng_date_relative_months_ago#one" = "{count} month ago"; +"lng_date_relative_months_ago#other" = "{count} months ago"; +"lng_date_relative_years_ago#one" = "{count} year ago"; +"lng_date_relative_years_ago#other" = "{count} years ago"; + +"lng_date_relative_in_seconds#one" = "in {count} second"; +"lng_date_relative_in_seconds#other" = "in {count} seconds"; +"lng_date_relative_in_minutes#one" = "in {count} minute"; +"lng_date_relative_in_minutes#other" = "in {count} minutes"; +"lng_date_relative_in_hours#one" = "in {count} hour"; +"lng_date_relative_in_hours#other" = "in {count} hours"; +"lng_date_relative_in_days#one" = "in {count} day"; +"lng_date_relative_in_days#other" = "in {count} days"; +"lng_date_relative_in_months#one" = "in {count} month"; +"lng_date_relative_in_months#other" = "in {count} months"; +"lng_date_relative_in_years#one" = "in {count} year"; +"lng_date_relative_in_years#other" = "in {count} years"; + +"lng_context_copy_date" = "Copy Date"; +"lng_context_set_reminder" = "Set a Reminder"; "lng_spellchecker_submenu" = "Spelling"; "lng_spellchecker_add" = "Add to Dictionary"; diff --git a/Telegram/SourceFiles/api/api_text_entities.cpp b/Telegram/SourceFiles/api/api_text_entities.cpp index ae9e30c22e..3f26b832a8 100644 --- a/Telegram/SourceFiles/api/api_text_entities.cpp +++ b/Telegram/SourceFiles/api/api_text_entities.cpp @@ -239,6 +239,31 @@ EntitiesInText EntitiesFromMTP( d.is_collapsed() ? u"1"_q : QString(), }); }, [&](const MTPDmessageEntityFormattedDate &d) { + auto flags = FormattedDateFlags(); + if (d.is_relative()) { + flags |= FormattedDateFlag::Relative; + } + if (d.is_short_time()) { + flags |= FormattedDateFlag::ShortTime; + } + if (d.is_long_time()) { + flags |= FormattedDateFlag::LongTime; + } + if (d.is_short_date()) { + flags |= FormattedDateFlag::ShortDate; + } + if (d.is_long_date()) { + flags |= FormattedDateFlag::LongDate; + } + if (d.is_day_of_week()) { + flags |= FormattedDateFlag::DayOfWeek; + } + result.push_back({ + EntityType::FormattedDate, + d.voffset().v, + d.vlength().v, + SerializeFormattedDateData(d.vdate().v, flags), + }); }); } return result; @@ -359,6 +384,37 @@ MTPVector EntitiesToMTP( v.push_back(*valid); } } break; + case EntityType::FormattedDate: { + const auto [date, dateFlags] = DeserializeFormattedDateData( + entity.data()); + if (date) { + using Flag = MTPDmessageEntityFormattedDate::Flag; + auto mtpFlags = MTPDmessageEntityFormattedDate::Flags(); + if (dateFlags & FormattedDateFlag::Relative) { + mtpFlags |= Flag::f_relative; + } + if (dateFlags & FormattedDateFlag::ShortTime) { + mtpFlags |= Flag::f_short_time; + } + if (dateFlags & FormattedDateFlag::LongTime) { + mtpFlags |= Flag::f_long_time; + } + if (dateFlags & FormattedDateFlag::ShortDate) { + mtpFlags |= Flag::f_short_date; + } + if (dateFlags & FormattedDateFlag::LongDate) { + mtpFlags |= Flag::f_long_date; + } + if (dateFlags & FormattedDateFlag::DayOfWeek) { + mtpFlags |= Flag::f_day_of_week; + } + v.push_back(MTP_messageEntityFormattedDate( + MTP_flags(mtpFlags), + offset, + length, + MTP_int(date))); + } + } break; } } return MTP_vector(std::move(v)); diff --git a/Telegram/SourceFiles/core/click_handler_types.cpp b/Telegram/SourceFiles/core/click_handler_types.cpp index 824f7816b2..fc9f211d1b 100644 --- a/Telegram/SourceFiles/core/click_handler_types.cpp +++ b/Telegram/SourceFiles/core/click_handler_types.cpp @@ -29,8 +29,16 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "window/window_controller.h" #include "window/window_session_controller.h" #include "window/window_session_controller_link_info.h" +#include "apiwrap.h" +#include "history/view/history_view_schedule_box.h" +#include "menu/menu_send.h" +#include "data/data_types.h" #include "styles/style_calls.h" // groupCallBoxLabel #include "styles/style_layers.h" +#include "styles/style_menu_icons.h" + +#include +#include namespace { @@ -438,3 +446,81 @@ auto MonospaceClickHandler::getTextEntity() const -> TextEntity { QString MonospaceClickHandler::url() const { return _text; } + +FormattedDateClickHandler::FormattedDateClickHandler( + int32 date, + FormattedDateFlags flags) +: _date(date) +, _entityData(SerializeFormattedDateData(date, flags)) { +} + +void FormattedDateClickHandler::onClick(ClickContext context) const { + if (context.button != Qt::LeftButton) { + return; + } + const auto my = context.other.value(); + const auto controller = my.sessionWindow.get(); + if (!controller) { + return; + } + const auto menu = Ui::CreateChild( + controller->content(), + st::popupMenuWithIcons); + + const auto date = _date; + const auto show = controller->uiShow(); + + menu->addAction( + tr::lng_context_copy_date(tr::now), + [date, show] { + const auto dateTime = QDateTime::fromSecsSinceEpoch(date); + const auto text = QLocale().toString( + dateTime, + QLocale::LongFormat); + TextUtilities::SetClipboardText( + TextForMimeData::Simple(text)); + show->showToast(tr::lng_date_copied(tr::now)); + }, + &st::menuIconCopy); + + const auto itemId = my.itemId; + const auto &owner = controller->session().data(); + const auto item = owner.message(itemId); + const auto canForward = item + && !item->forbidsForward() + && item->history()->peer->allowsForwarding(); + if (canForward) { + menu->addAction( + tr::lng_context_set_reminder(tr::now), + [itemId, show] { + const auto session = &show->session(); + const auto item = session->data().message(itemId); + if (!item) { + return; + } + const auto self = session->user(); + const auto history = self->owner().history(self); + show->showBox(HistoryView::PrepareScheduleBox( + session, + show, + SendMenu::Details{ .type = SendMenu::Type::Reminder }, + [=](Api::SendOptions options) { + auto action = Api::SendAction(history, options); + action.clearDraft = false; + action.generateLocal = false; + session->api().forwardMessages( + Data::ResolvedForwardDraft{ + .items = { item }, + }, + action); + })); + }, + &st::menuIconSchedule); + } + + menu->popup(QCursor::pos()); +} + +auto FormattedDateClickHandler::getTextEntity() const -> TextEntity { + return { EntityType::FormattedDate, _entityData }; +} diff --git a/Telegram/SourceFiles/core/click_handler_types.h b/Telegram/SourceFiles/core/click_handler_types.h index a1cd3bd35b..1092ab6f8d 100644 --- a/Telegram/SourceFiles/core/click_handler_types.h +++ b/Telegram/SourceFiles/core/click_handler_types.h @@ -8,6 +8,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #pragma once #include "ui/basic_click_handlers.h" +#include "ui/text/text_entity.h" #include "data/data_msg_id.h" constexpr auto kPeerLinkPeerIdProperty = 0x01; @@ -237,3 +238,16 @@ private: const TextEntity _entity; }; + +class FormattedDateClickHandler : public ClickHandler { +public: + FormattedDateClickHandler(int32 date, FormattedDateFlags flags); + + void onClick(ClickContext context) const override; + TextEntity getTextEntity() const override; + +private: + int32 _date = 0; + QString _entityData; + +}; diff --git a/Telegram/SourceFiles/core/ui_integration.cpp b/Telegram/SourceFiles/core/ui_integration.cpp index 9cdaaa3b39..352089e02a 100644 --- a/Telegram/SourceFiles/core/ui_integration.cpp +++ b/Telegram/SourceFiles/core/ui_integration.cpp @@ -32,6 +32,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "window/window_controller.h" #include "window/window_session_controller.h" #include "mainwindow.h" +#include "base/unixtime.h" + +#include +#include namespace Core { namespace { @@ -114,6 +118,129 @@ const auto kBadPrefix = u"http://"_q; return cWorkingDir() + "tdata/angle_backend"; } +[[nodiscard]] Ui::Text::FormattedDateResult FormatDateRelative(TimeId date) { + const auto now = base::unixtime::now(); + const auto delta = int64(date) - int64(now); + const auto absDelta = std::abs(delta); + const auto future = (delta > 0); + auto text = QString(); + auto nextUpdate = int32(0); + + if (absDelta < 1) { + text = tr::lng_date_relative_now(tr::now); + nextUpdate = now + 1; + } else if (absDelta < 60) { + const auto count = int(absDelta); + text = (future + ? tr::lng_date_relative_in_seconds + : tr::lng_date_relative_seconds_ago)(tr::now, lt_count, count); + nextUpdate = now + 1; + } else if (absDelta < 3600) { + const auto count = int(absDelta / 60); + text = (future + ? tr::lng_date_relative_in_minutes + : tr::lng_date_relative_minutes_ago)(tr::now, lt_count, count); + nextUpdate = future + ? ((count > 1) + ? (date - (count - 1) * 60) + : (date - 59)) + : (date + (count + 1) * 60); + } else if (absDelta < 86400) { + const auto count = int(absDelta / 3600); + text = (future + ? tr::lng_date_relative_in_hours + : tr::lng_date_relative_hours_ago)(tr::now, lt_count, count); + nextUpdate = future + ? ((count > 1) + ? (date - (count - 1) * 3600) + : (date - 3599)) + : (date + (count + 1) * 3600); + } else if (absDelta < 30 * 86400) { + const auto count = int(absDelta / 86400); + text = (future + ? tr::lng_date_relative_in_days + : tr::lng_date_relative_days_ago)(tr::now, lt_count, count); + nextUpdate = future + ? ((count > 1) + ? (date - (count - 1) * 86400) + : (date - 86399)) + : (date + (count + 1) * 86400); + } else if (absDelta < 365 * 86400) { + const auto count = int(absDelta / (30 * 86400)); + text = (future + ? tr::lng_date_relative_in_months + : tr::lng_date_relative_months_ago)(tr::now, lt_count, count); + nextUpdate = future + ? ((count > 1) + ? (date - (count - 1) * 30 * 86400) + : (date - 30 * 86400 + 1)) + : (date + (count + 1) * 30 * 86400); + } else { + const auto count = int(absDelta / (365 * 86400)); + text = (future + ? tr::lng_date_relative_in_years + : tr::lng_date_relative_years_ago)(tr::now, lt_count, count); + nextUpdate = future + ? ((count > 1) + ? (date - (count - 1) * 365 * 86400) + : (date - 365 * 86400 + 1)) + : (date + (count + 1) * 365 * 86400); + } + return { text, nextUpdate }; +} + +[[nodiscard]] Ui::Text::FormattedDateResult FormatDateWithFlags( + TimeId date, + FormattedDateFlags flags) { + if (flags & FormattedDateFlag::Relative) { + return FormatDateRelative(date); + } + const auto dateTime = QDateTime::fromSecsSinceEpoch(date); + const auto locale = QLocale(); + auto parts = QStringList(); + const auto hasDayOfWeek = (flags + & FormattedDateFlag::DayOfWeek); + const auto hasShortDate = (flags + & FormattedDateFlag::ShortDate); + const auto hasLongDate = (flags + & FormattedDateFlag::LongDate); + const auto hasShortTime = (flags + & FormattedDateFlag::ShortTime); + const auto hasLongTime = (flags + & FormattedDateFlag::LongTime); + if (hasDayOfWeek && !hasShortDate) { + parts.push_back(locale.dayName( + dateTime.date().dayOfWeek())); + } else if (hasDayOfWeek) { + parts.push_back(locale.dayName( + dateTime.date().dayOfWeek(), + QLocale::ShortFormat)); + } + if (hasLongDate) { + parts.push_back(locale.toString( + dateTime.date(), + QLocale::LongFormat)); + } else if (hasShortDate) { + parts.push_back(locale.toString( + dateTime.date(), + QLocale::ShortFormat)); + } + if (hasLongTime) { + parts.push_back(locale.toString( + dateTime.time(), + QLocale::LongFormat)); + } else if (hasShortTime) { + parts.push_back(locale.toString( + dateTime.time(), + QLocale::ShortFormat)); + } + auto text = parts.join(u" "_q); + if (text.isEmpty()) { + text = locale.toString(dateTime, QLocale::ShortFormat); + } + return { text, 0 }; +} + } // namespace Ui::Text::MarkedContext TextContext(TextContextArgs &&args) { @@ -146,6 +273,7 @@ Ui::Text::MarkedContext TextContext(TextContextArgs &&args) { return { .repaint = std::move(args.repaint), .customEmojiFactory = std::move(factory), + .formattedDateFactory = FormatDateWithFlags, .other = std::move(args.details), }; } @@ -267,6 +395,12 @@ std::shared_ptr UiIntegration::createLinkHandler( return (my && my->session) ? std::make_shared(my->session, data.text) : nullptr; + case EntityType::FormattedDate: { + const auto [date, flags] = DeserializeFormattedDateData(data.data); + if (date) { + return std::make_shared(date, flags); + } + } break; } return Integration::createLinkHandler(data, context); } diff --git a/Telegram/SourceFiles/data/data_session.cpp b/Telegram/SourceFiles/data/data_session.cpp index a4723c828a..edc69c5062 100644 --- a/Telegram/SourceFiles/data/data_session.cpp +++ b/Telegram/SourceFiles/data/data_session.cpp @@ -235,6 +235,7 @@ Session::Session(not_null session) , _contactsList(Dialogs::SortMode::Name) , _contactsNoChatsList(Dialogs::SortMode::Name) , _ttlCheckTimer([=] { checkTTLs(); }) +, _formattedDateTimer([=] { checkFormattedDateUpdates(); }) , _selfDestructTimer([=] { checkSelfDestructItems(); }) , _pollsClosingTimer([=] { checkPollsClosings(); }) , _watchForOfflineTimer([=] { checkLocalUsersWentOffline(); }) @@ -2805,6 +2806,49 @@ void Session::checkTTLs() { scheduleNextTTLs(); } +void Session::registerFormattedDateUpdate( + TimeId when, + not_null view) { + _formattedDateUpdates[when].push_back( + base::make_weak(view.get())); + const auto nearest = _formattedDateUpdates.begin()->first; + if (nearest < when && _formattedDateTimer.isActive()) { + return; + } + scheduleNextFormattedDateUpdate(); +} + +void Session::scheduleNextFormattedDateUpdate() { + if (_formattedDateUpdates.empty()) { + return; + } + const auto nearest = _formattedDateUpdates.begin()->first; + const auto now = base::unixtime::now(); + const auto maxTimeout = TimeId(86400); + const auto timeout = std::min( + std::max(now, nearest) - now, + maxTimeout); + _formattedDateTimer.callOnce(timeout * crl::time(1000)); +} + +void Session::checkFormattedDateUpdates() { + _formattedDateTimer.cancel(); + const auto now = base::unixtime::now(); + auto expired = std::vector>(); + while (!_formattedDateUpdates.empty() + && _formattedDateUpdates.begin()->first <= now) { + auto &list = _formattedDateUpdates.begin()->second; + expired.insert(expired.end(), list.begin(), list.end()); + _formattedDateUpdates.erase(_formattedDateUpdates.begin()); + } + for (const auto &weak : expired) { + if (const auto strong = weak.get()) { + requestItemTextRefresh(strong->data()); + } + } + scheduleNextFormattedDateUpdate(); +} + void Session::processMessagesDeleted( PeerId peerId, const QVector &data) { diff --git a/Telegram/SourceFiles/data/data_session.h b/Telegram/SourceFiles/data/data_session.h index b64fefd748..403c6a4885 100644 --- a/Telegram/SourceFiles/data/data_session.h +++ b/Telegram/SourceFiles/data/data_session.h @@ -528,6 +528,10 @@ public: void registerMessageTTL(TimeId when, not_null item); void unregisterMessageTTL(TimeId when, not_null item); + void registerFormattedDateUpdate( + TimeId when, + not_null view); + // Returns true if item found and it is not detached. bool updateExistingMessage(const MTPDmessage &data); void updateEditedMessage(const MTPMessage &data); @@ -958,6 +962,9 @@ private: void scheduleNextTTLs(); void checkTTLs(); + void scheduleNextFormattedDateUpdate(); + void checkFormattedDateUpdates(); + int computeUnreadBadge(const Dialogs::UnreadState &state) const; bool computeUnreadBadgeMuted(const Dialogs::UnreadState &state) const; @@ -1147,6 +1154,9 @@ private: std::map>> _ttlMessages; base::Timer _ttlCheckTimer; + std::map>> _formattedDateUpdates; + base::Timer _formattedDateTimer; + std::unordered_map> _nonChannelMessages; base::flat_map _messageByRandomId; diff --git a/Telegram/SourceFiles/history/view/history_view_element.cpp b/Telegram/SourceFiles/history/view/history_view_element.cpp index e0a9731b8f..edf0e3f692 100644 --- a/Telegram/SourceFiles/history/view/history_view_element.cpp +++ b/Telegram/SourceFiles/history/view/history_view_element.cpp @@ -444,7 +444,7 @@ int KeyboardStyle::minButtonWidth( break; } if (modification.added) { - ++result.to; + result.to += modification.added; } const auto shiftTo = std::min( int(modification.skipped), @@ -452,7 +452,7 @@ int KeyboardStyle::minButtonWidth( result.to -= shiftTo; if (modification.position <= result.from) { if (modification.added) { - ++result.from; + result.from += modification.added; } const auto shiftFrom = std::min( int(modification.skipped), @@ -1866,6 +1866,9 @@ void Element::setTextWithLinks( } } InitElementTextPart(this, _text); + if (const auto next = _text.nextFormattedDateUpdate()) { + history()->session().data().registerFormattedDateUpdate(next, this); + } _textWidth = -1; _textHeight = 0; } @@ -2668,12 +2671,16 @@ SelectedQuote Element::FindSelectedQuote( modified.from += modification.skipped; if (modification.added && modification.position < selection.from) { - --modified.from; + modified.from = uint16(std::max( + 0, + int(modified.from) - int(modification.added))); } } modified.to += modification.skipped; if (modification.added && modified.to > modified.from) { - --modified.to; + modified.to = uint16(std::max( + int(modified.from), + int(modified.to) - int(modification.added))); } } auto result = item->originalText(); diff --git a/Telegram/lib_ui b/Telegram/lib_ui index ed74b00b7e..1bb25b4a5a 160000 --- a/Telegram/lib_ui +++ b/Telegram/lib_ui @@ -1 +1 @@ -Subproject commit ed74b00b7e0cc2564a965ac41ec02c136f451b33 +Subproject commit 1bb25b4a5a00fa15be5f112796f89758de12a84b