Initial formatted date support.

This commit is contained in:
John Preston
2026-02-23 18:03:38 +04:00
parent e5804fb213
commit 90129c10d8
9 changed files with 388 additions and 5 deletions
+32
View File
@@ -5542,6 +5542,38 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_text_copied" = "Text copied to clipboard."; "lng_text_copied" = "Text copied to clipboard.";
"lng_code_copied" = "Code 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_submenu" = "Spelling";
"lng_spellchecker_add" = "Add to Dictionary"; "lng_spellchecker_add" = "Add to Dictionary";
@@ -239,6 +239,31 @@ EntitiesInText EntitiesFromMTP(
d.is_collapsed() ? u"1"_q : QString(), d.is_collapsed() ? u"1"_q : QString(),
}); });
}, [&](const MTPDmessageEntityFormattedDate &d) { }, [&](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; return result;
@@ -359,6 +384,37 @@ MTPVector<MTPMessageEntity> EntitiesToMTP(
v.push_back(*valid); v.push_back(*valid);
} }
} break; } 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<MTPMessageEntity>(std::move(v)); return MTP_vector<MTPMessageEntity>(std::move(v));
@@ -29,8 +29,16 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "window/window_controller.h" #include "window/window_controller.h"
#include "window/window_session_controller.h" #include "window/window_session_controller.h"
#include "window/window_session_controller_link_info.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_calls.h" // groupCallBoxLabel
#include "styles/style_layers.h" #include "styles/style_layers.h"
#include "styles/style_menu_icons.h"
#include <QtCore/QDateTime>
#include <QtCore/QLocale>
namespace { namespace {
@@ -438,3 +446,81 @@ auto MonospaceClickHandler::getTextEntity() const -> TextEntity {
QString MonospaceClickHandler::url() const { QString MonospaceClickHandler::url() const {
return _text; 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<ClickHandlerContext>();
const auto controller = my.sessionWindow.get();
if (!controller) {
return;
}
const auto menu = Ui::CreateChild<Ui::PopupMenu>(
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 };
}
@@ -8,6 +8,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#pragma once #pragma once
#include "ui/basic_click_handlers.h" #include "ui/basic_click_handlers.h"
#include "ui/text/text_entity.h"
#include "data/data_msg_id.h" #include "data/data_msg_id.h"
constexpr auto kPeerLinkPeerIdProperty = 0x01; constexpr auto kPeerLinkPeerIdProperty = 0x01;
@@ -237,3 +238,16 @@ private:
const TextEntity _entity; 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;
};
@@ -32,6 +32,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "window/window_controller.h" #include "window/window_controller.h"
#include "window/window_session_controller.h" #include "window/window_session_controller.h"
#include "mainwindow.h" #include "mainwindow.h"
#include "base/unixtime.h"
#include <QtCore/QDateTime>
#include <QtCore/QLocale>
namespace Core { namespace Core {
namespace { namespace {
@@ -114,6 +118,129 @@ const auto kBadPrefix = u"http://"_q;
return cWorkingDir() + "tdata/angle_backend"; 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 } // namespace
Ui::Text::MarkedContext TextContext(TextContextArgs &&args) { Ui::Text::MarkedContext TextContext(TextContextArgs &&args) {
@@ -146,6 +273,7 @@ Ui::Text::MarkedContext TextContext(TextContextArgs &&args) {
return { return {
.repaint = std::move(args.repaint), .repaint = std::move(args.repaint),
.customEmojiFactory = std::move(factory), .customEmojiFactory = std::move(factory),
.formattedDateFactory = FormatDateWithFlags,
.other = std::move(args.details), .other = std::move(args.details),
}; };
} }
@@ -267,6 +395,12 @@ std::shared_ptr<ClickHandler> UiIntegration::createLinkHandler(
return (my && my->session) return (my && my->session)
? std::make_shared<BankCardClickHandler>(my->session, data.text) ? std::make_shared<BankCardClickHandler>(my->session, data.text)
: nullptr; : nullptr;
case EntityType::FormattedDate: {
const auto [date, flags] = DeserializeFormattedDateData(data.data);
if (date) {
return std::make_shared<FormattedDateClickHandler>(date, flags);
}
} break;
} }
return Integration::createLinkHandler(data, context); return Integration::createLinkHandler(data, context);
} }
@@ -235,6 +235,7 @@ Session::Session(not_null<Main::Session*> session)
, _contactsList(Dialogs::SortMode::Name) , _contactsList(Dialogs::SortMode::Name)
, _contactsNoChatsList(Dialogs::SortMode::Name) , _contactsNoChatsList(Dialogs::SortMode::Name)
, _ttlCheckTimer([=] { checkTTLs(); }) , _ttlCheckTimer([=] { checkTTLs(); })
, _formattedDateTimer([=] { checkFormattedDateUpdates(); })
, _selfDestructTimer([=] { checkSelfDestructItems(); }) , _selfDestructTimer([=] { checkSelfDestructItems(); })
, _pollsClosingTimer([=] { checkPollsClosings(); }) , _pollsClosingTimer([=] { checkPollsClosings(); })
, _watchForOfflineTimer([=] { checkLocalUsersWentOffline(); }) , _watchForOfflineTimer([=] { checkLocalUsersWentOffline(); })
@@ -2805,6 +2806,49 @@ void Session::checkTTLs() {
scheduleNextTTLs(); scheduleNextTTLs();
} }
void Session::registerFormattedDateUpdate(
TimeId when,
not_null<HistoryView::Element*> 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<base::weak_ptr<HistoryView::Element>>();
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( void Session::processMessagesDeleted(
PeerId peerId, PeerId peerId,
const QVector<MTPint> &data) { const QVector<MTPint> &data) {
+10
View File
@@ -528,6 +528,10 @@ public:
void registerMessageTTL(TimeId when, not_null<HistoryItem*> item); void registerMessageTTL(TimeId when, not_null<HistoryItem*> item);
void unregisterMessageTTL(TimeId when, not_null<HistoryItem*> item); void unregisterMessageTTL(TimeId when, not_null<HistoryItem*> item);
void registerFormattedDateUpdate(
TimeId when,
not_null<HistoryView::Element*> view);
// Returns true if item found and it is not detached. // Returns true if item found and it is not detached.
bool updateExistingMessage(const MTPDmessage &data); bool updateExistingMessage(const MTPDmessage &data);
void updateEditedMessage(const MTPMessage &data); void updateEditedMessage(const MTPMessage &data);
@@ -958,6 +962,9 @@ private:
void scheduleNextTTLs(); void scheduleNextTTLs();
void checkTTLs(); void checkTTLs();
void scheduleNextFormattedDateUpdate();
void checkFormattedDateUpdates();
int computeUnreadBadge(const Dialogs::UnreadState &state) const; int computeUnreadBadge(const Dialogs::UnreadState &state) const;
bool computeUnreadBadgeMuted(const Dialogs::UnreadState &state) const; bool computeUnreadBadgeMuted(const Dialogs::UnreadState &state) const;
@@ -1147,6 +1154,9 @@ private:
std::map<TimeId, base::flat_set<not_null<HistoryItem*>>> _ttlMessages; std::map<TimeId, base::flat_set<not_null<HistoryItem*>>> _ttlMessages;
base::Timer _ttlCheckTimer; base::Timer _ttlCheckTimer;
std::map<TimeId, std::vector<base::weak_ptr<HistoryView::Element>>> _formattedDateUpdates;
base::Timer _formattedDateTimer;
std::unordered_map<MsgId, not_null<HistoryItem*>> _nonChannelMessages; std::unordered_map<MsgId, not_null<HistoryItem*>> _nonChannelMessages;
base::flat_map<uint64, FullMsgId> _messageByRandomId; base::flat_map<uint64, FullMsgId> _messageByRandomId;
@@ -444,7 +444,7 @@ int KeyboardStyle::minButtonWidth(
break; break;
} }
if (modification.added) { if (modification.added) {
++result.to; result.to += modification.added;
} }
const auto shiftTo = std::min( const auto shiftTo = std::min(
int(modification.skipped), int(modification.skipped),
@@ -452,7 +452,7 @@ int KeyboardStyle::minButtonWidth(
result.to -= shiftTo; result.to -= shiftTo;
if (modification.position <= result.from) { if (modification.position <= result.from) {
if (modification.added) { if (modification.added) {
++result.from; result.from += modification.added;
} }
const auto shiftFrom = std::min( const auto shiftFrom = std::min(
int(modification.skipped), int(modification.skipped),
@@ -1866,6 +1866,9 @@ void Element::setTextWithLinks(
} }
} }
InitElementTextPart(this, _text); InitElementTextPart(this, _text);
if (const auto next = _text.nextFormattedDateUpdate()) {
history()->session().data().registerFormattedDateUpdate(next, this);
}
_textWidth = -1; _textWidth = -1;
_textHeight = 0; _textHeight = 0;
} }
@@ -2668,12 +2671,16 @@ SelectedQuote Element::FindSelectedQuote(
modified.from += modification.skipped; modified.from += modification.skipped;
if (modification.added if (modification.added
&& modification.position < selection.from) { && modification.position < selection.from) {
--modified.from; modified.from = uint16(std::max(
0,
int(modified.from) - int(modification.added)));
} }
} }
modified.to += modification.skipped; modified.to += modification.skipped;
if (modification.added && modified.to > modified.from) { 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(); auto result = item->originalText();