Skip to content

Commit 90129c1

Browse files
committed
Initial formatted date support.
1 parent e5804fb commit 90129c1

File tree

9 files changed

+388
-5
lines changed

9 files changed

+388
-5
lines changed

‎Telegram/Resources/langs/lang.strings‎

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5542,6 +5542,38 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
55425542

55435543
"lng_text_copied" = "Text copied to clipboard.";
55445544
"lng_code_copied" = "Code copied to clipboard.";
5545+
"lng_date_copied" = "Date copied to clipboard.";
5546+
5547+
"lng_date_relative_now" = "now";
5548+
5549+
"lng_date_relative_seconds_ago#one" = "{count} second ago";
5550+
"lng_date_relative_seconds_ago#other" = "{count} seconds ago";
5551+
"lng_date_relative_minutes_ago#one" = "{count} minute ago";
5552+
"lng_date_relative_minutes_ago#other" = "{count} minutes ago";
5553+
"lng_date_relative_hours_ago#one" = "{count} hour ago";
5554+
"lng_date_relative_hours_ago#other" = "{count} hours ago";
5555+
"lng_date_relative_days_ago#one" = "{count} day ago";
5556+
"lng_date_relative_days_ago#other" = "{count} days ago";
5557+
"lng_date_relative_months_ago#one" = "{count} month ago";
5558+
"lng_date_relative_months_ago#other" = "{count} months ago";
5559+
"lng_date_relative_years_ago#one" = "{count} year ago";
5560+
"lng_date_relative_years_ago#other" = "{count} years ago";
5561+
5562+
"lng_date_relative_in_seconds#one" = "in {count} second";
5563+
"lng_date_relative_in_seconds#other" = "in {count} seconds";
5564+
"lng_date_relative_in_minutes#one" = "in {count} minute";
5565+
"lng_date_relative_in_minutes#other" = "in {count} minutes";
5566+
"lng_date_relative_in_hours#one" = "in {count} hour";
5567+
"lng_date_relative_in_hours#other" = "in {count} hours";
5568+
"lng_date_relative_in_days#one" = "in {count} day";
5569+
"lng_date_relative_in_days#other" = "in {count} days";
5570+
"lng_date_relative_in_months#one" = "in {count} month";
5571+
"lng_date_relative_in_months#other" = "in {count} months";
5572+
"lng_date_relative_in_years#one" = "in {count} year";
5573+
"lng_date_relative_in_years#other" = "in {count} years";
5574+
5575+
"lng_context_copy_date" = "Copy Date";
5576+
"lng_context_set_reminder" = "Set a Reminder";
55455577

55465578
"lng_spellchecker_submenu" = "Spelling";
55475579
"lng_spellchecker_add" = "Add to Dictionary";

‎Telegram/SourceFiles/api/api_text_entities.cpp‎

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,31 @@ EntitiesInText EntitiesFromMTP(
239239
d.is_collapsed() ? u"1"_q : QString(),
240240
});
241241
}, [&](const MTPDmessageEntityFormattedDate &d) {
242+
auto flags = FormattedDateFlags();
243+
if (d.is_relative()) {
244+
flags |= FormattedDateFlag::Relative;
245+
}
246+
if (d.is_short_time()) {
247+
flags |= FormattedDateFlag::ShortTime;
248+
}
249+
if (d.is_long_time()) {
250+
flags |= FormattedDateFlag::LongTime;
251+
}
252+
if (d.is_short_date()) {
253+
flags |= FormattedDateFlag::ShortDate;
254+
}
255+
if (d.is_long_date()) {
256+
flags |= FormattedDateFlag::LongDate;
257+
}
258+
if (d.is_day_of_week()) {
259+
flags |= FormattedDateFlag::DayOfWeek;
260+
}
261+
result.push_back({
262+
EntityType::FormattedDate,
263+
d.voffset().v,
264+
d.vlength().v,
265+
SerializeFormattedDateData(d.vdate().v, flags),
266+
});
242267
});
243268
}
244269
return result;
@@ -359,6 +384,37 @@ MTPVector<MTPMessageEntity> EntitiesToMTP(
359384
v.push_back(*valid);
360385
}
361386
} break;
387+
case EntityType::FormattedDate: {
388+
const auto [date, dateFlags] = DeserializeFormattedDateData(
389+
entity.data());
390+
if (date) {
391+
using Flag = MTPDmessageEntityFormattedDate::Flag;
392+
auto mtpFlags = MTPDmessageEntityFormattedDate::Flags();
393+
if (dateFlags & FormattedDateFlag::Relative) {
394+
mtpFlags |= Flag::f_relative;
395+
}
396+
if (dateFlags & FormattedDateFlag::ShortTime) {
397+
mtpFlags |= Flag::f_short_time;
398+
}
399+
if (dateFlags & FormattedDateFlag::LongTime) {
400+
mtpFlags |= Flag::f_long_time;
401+
}
402+
if (dateFlags & FormattedDateFlag::ShortDate) {
403+
mtpFlags |= Flag::f_short_date;
404+
}
405+
if (dateFlags & FormattedDateFlag::LongDate) {
406+
mtpFlags |= Flag::f_long_date;
407+
}
408+
if (dateFlags & FormattedDateFlag::DayOfWeek) {
409+
mtpFlags |= Flag::f_day_of_week;
410+
}
411+
v.push_back(MTP_messageEntityFormattedDate(
412+
MTP_flags(mtpFlags),
413+
offset,
414+
length,
415+
MTP_int(date)));
416+
}
417+
} break;
362418
}
363419
}
364420
return MTP_vector<MTPMessageEntity>(std::move(v));

‎Telegram/SourceFiles/core/click_handler_types.cpp‎

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,16 @@ For license and copyright information please follow this link:
2929
#include "window/window_controller.h"
3030
#include "window/window_session_controller.h"
3131
#include "window/window_session_controller_link_info.h"
32+
#include "apiwrap.h"
33+
#include "history/view/history_view_schedule_box.h"
34+
#include "menu/menu_send.h"
35+
#include "data/data_types.h"
3236
#include "styles/style_calls.h" // groupCallBoxLabel
3337
#include "styles/style_layers.h"
38+
#include "styles/style_menu_icons.h"
39+
40+
#include <QtCore/QDateTime>
41+
#include <QtCore/QLocale>
3442

3543
namespace {
3644

@@ -438,3 +446,81 @@ auto MonospaceClickHandler::getTextEntity() const -> TextEntity {
438446
QString MonospaceClickHandler::url() const {
439447
return _text;
440448
}
449+
450+
FormattedDateClickHandler::FormattedDateClickHandler(
451+
int32 date,
452+
FormattedDateFlags flags)
453+
: _date(date)
454+
, _entityData(SerializeFormattedDateData(date, flags)) {
455+
}
456+
457+
void FormattedDateClickHandler::onClick(ClickContext context) const {
458+
if (context.button != Qt::LeftButton) {
459+
return;
460+
}
461+
const auto my = context.other.value<ClickHandlerContext>();
462+
const auto controller = my.sessionWindow.get();
463+
if (!controller) {
464+
return;
465+
}
466+
const auto menu = Ui::CreateChild<Ui::PopupMenu>(
467+
controller->content(),
468+
st::popupMenuWithIcons);
469+
470+
const auto date = _date;
471+
const auto show = controller->uiShow();
472+
473+
menu->addAction(
474+
tr::lng_context_copy_date(tr::now),
475+
[date, show] {
476+
const auto dateTime = QDateTime::fromSecsSinceEpoch(date);
477+
const auto text = QLocale().toString(
478+
dateTime,
479+
QLocale::LongFormat);
480+
TextUtilities::SetClipboardText(
481+
TextForMimeData::Simple(text));
482+
show->showToast(tr::lng_date_copied(tr::now));
483+
},
484+
&st::menuIconCopy);
485+
486+
const auto itemId = my.itemId;
487+
const auto &owner = controller->session().data();
488+
const auto item = owner.message(itemId);
489+
const auto canForward = item
490+
&& !item->forbidsForward()
491+
&& item->history()->peer->allowsForwarding();
492+
if (canForward) {
493+
menu->addAction(
494+
tr::lng_context_set_reminder(tr::now),
495+
[itemId, show] {
496+
const auto session = &show->session();
497+
const auto item = session->data().message(itemId);
498+
if (!item) {
499+
return;
500+
}
501+
const auto self = session->user();
502+
const auto history = self->owner().history(self);
503+
show->showBox(HistoryView::PrepareScheduleBox(
504+
session,
505+
show,
506+
SendMenu::Details{ .type = SendMenu::Type::Reminder },
507+
[=](Api::SendOptions options) {
508+
auto action = Api::SendAction(history, options);
509+
action.clearDraft = false;
510+
action.generateLocal = false;
511+
session->api().forwardMessages(
512+
Data::ResolvedForwardDraft{
513+
.items = { item },
514+
},
515+
action);
516+
}));
517+
},
518+
&st::menuIconSchedule);
519+
}
520+
521+
menu->popup(QCursor::pos());
522+
}
523+
524+
auto FormattedDateClickHandler::getTextEntity() const -> TextEntity {
525+
return { EntityType::FormattedDate, _entityData };
526+
}

‎Telegram/SourceFiles/core/click_handler_types.h‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ For license and copyright information please follow this link:
88
#pragma once
99

1010
#include "ui/basic_click_handlers.h"
11+
#include "ui/text/text_entity.h"
1112
#include "data/data_msg_id.h"
1213

1314
constexpr auto kPeerLinkPeerIdProperty = 0x01;
@@ -237,3 +238,16 @@ class MonospaceClickHandler : public TextClickHandler {
237238
const TextEntity _entity;
238239

239240
};
241+
242+
class FormattedDateClickHandler : public ClickHandler {
243+
public:
244+
FormattedDateClickHandler(int32 date, FormattedDateFlags flags);
245+
246+
void onClick(ClickContext context) const override;
247+
TextEntity getTextEntity() const override;
248+
249+
private:
250+
int32 _date = 0;
251+
QString _entityData;
252+
253+
};

‎Telegram/SourceFiles/core/ui_integration.cpp‎

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,10 @@ For license and copyright information please follow this link:
3232
#include "window/window_controller.h"
3333
#include "window/window_session_controller.h"
3434
#include "mainwindow.h"
35+
#include "base/unixtime.h"
36+
37+
#include <QtCore/QDateTime>
38+
#include <QtCore/QLocale>
3539

3640
namespace Core {
3741
namespace {
@@ -114,6 +118,129 @@ const auto kBadPrefix = u"http://"_q;
114118
return cWorkingDir() + "tdata/angle_backend";
115119
}
116120

121+
[[nodiscard]] Ui::Text::FormattedDateResult FormatDateRelative(TimeId date) {
122+
const auto now = base::unixtime::now();
123+
const auto delta = int64(date) - int64(now);
124+
const auto absDelta = std::abs(delta);
125+
const auto future = (delta > 0);
126+
auto text = QString();
127+
auto nextUpdate = int32(0);
128+
129+
if (absDelta < 1) {
130+
text = tr::lng_date_relative_now(tr::now);
131+
nextUpdate = now + 1;
132+
} else if (absDelta < 60) {
133+
const auto count = int(absDelta);
134+
text = (future
135+
? tr::lng_date_relative_in_seconds
136+
: tr::lng_date_relative_seconds_ago)(tr::now, lt_count, count);
137+
nextUpdate = now + 1;
138+
} else if (absDelta < 3600) {
139+
const auto count = int(absDelta / 60);
140+
text = (future
141+
? tr::lng_date_relative_in_minutes
142+
: tr::lng_date_relative_minutes_ago)(tr::now, lt_count, count);
143+
nextUpdate = future
144+
? ((count > 1)
145+
? (date - (count - 1) * 60)
146+
: (date - 59))
147+
: (date + (count + 1) * 60);
148+
} else if (absDelta < 86400) {
149+
const auto count = int(absDelta / 3600);
150+
text = (future
151+
? tr::lng_date_relative_in_hours
152+
: tr::lng_date_relative_hours_ago)(tr::now, lt_count, count);
153+
nextUpdate = future
154+
? ((count > 1)
155+
? (date - (count - 1) * 3600)
156+
: (date - 3599))
157+
: (date + (count + 1) * 3600);
158+
} else if (absDelta < 30 * 86400) {
159+
const auto count = int(absDelta / 86400);
160+
text = (future
161+
? tr::lng_date_relative_in_days
162+
: tr::lng_date_relative_days_ago)(tr::now, lt_count, count);
163+
nextUpdate = future
164+
? ((count > 1)
165+
? (date - (count - 1) * 86400)
166+
: (date - 86399))
167+
: (date + (count + 1) * 86400);
168+
} else if (absDelta < 365 * 86400) {
169+
const auto count = int(absDelta / (30 * 86400));
170+
text = (future
171+
? tr::lng_date_relative_in_months
172+
: tr::lng_date_relative_months_ago)(tr::now, lt_count, count);
173+
nextUpdate = future
174+
? ((count > 1)
175+
? (date - (count - 1) * 30 * 86400)
176+
: (date - 30 * 86400 + 1))
177+
: (date + (count + 1) * 30 * 86400);
178+
} else {
179+
const auto count = int(absDelta / (365 * 86400));
180+
text = (future
181+
? tr::lng_date_relative_in_years
182+
: tr::lng_date_relative_years_ago)(tr::now, lt_count, count);
183+
nextUpdate = future
184+
? ((count > 1)
185+
? (date - (count - 1) * 365 * 86400)
186+
: (date - 365 * 86400 + 1))
187+
: (date + (count + 1) * 365 * 86400);
188+
}
189+
return { text, nextUpdate };
190+
}
191+
192+
[[nodiscard]] Ui::Text::FormattedDateResult FormatDateWithFlags(
193+
TimeId date,
194+
FormattedDateFlags flags) {
195+
if (flags & FormattedDateFlag::Relative) {
196+
return FormatDateRelative(date);
197+
}
198+
const auto dateTime = QDateTime::fromSecsSinceEpoch(date);
199+
const auto locale = QLocale();
200+
auto parts = QStringList();
201+
const auto hasDayOfWeek = (flags
202+
& FormattedDateFlag::DayOfWeek);
203+
const auto hasShortDate = (flags
204+
& FormattedDateFlag::ShortDate);
205+
const auto hasLongDate = (flags
206+
& FormattedDateFlag::LongDate);
207+
const auto hasShortTime = (flags
208+
& FormattedDateFlag::ShortTime);
209+
const auto hasLongTime = (flags
210+
& FormattedDateFlag::LongTime);
211+
if (hasDayOfWeek && !hasShortDate) {
212+
parts.push_back(locale.dayName(
213+
dateTime.date().dayOfWeek()));
214+
} else if (hasDayOfWeek) {
215+
parts.push_back(locale.dayName(
216+
dateTime.date().dayOfWeek(),
217+
QLocale::ShortFormat));
218+
}
219+
if (hasLongDate) {
220+
parts.push_back(locale.toString(
221+
dateTime.date(),
222+
QLocale::LongFormat));
223+
} else if (hasShortDate) {
224+
parts.push_back(locale.toString(
225+
dateTime.date(),
226+
QLocale::ShortFormat));
227+
}
228+
if (hasLongTime) {
229+
parts.push_back(locale.toString(
230+
dateTime.time(),
231+
QLocale::LongFormat));
232+
} else if (hasShortTime) {
233+
parts.push_back(locale.toString(
234+
dateTime.time(),
235+
QLocale::ShortFormat));
236+
}
237+
auto text = parts.join(u" "_q);
238+
if (text.isEmpty()) {
239+
text = locale.toString(dateTime, QLocale::ShortFormat);
240+
}
241+
return { text, 0 };
242+
}
243+
117244
} // namespace
118245

119246
Ui::Text::MarkedContext TextContext(TextContextArgs &&args) {
@@ -146,6 +273,7 @@ Ui::Text::MarkedContext TextContext(TextContextArgs &&args) {
146273
return {
147274
.repaint = std::move(args.repaint),
148275
.customEmojiFactory = std::move(factory),
276+
.formattedDateFactory = FormatDateWithFlags,
149277
.other = std::move(args.details),
150278
};
151279
}
@@ -267,6 +395,12 @@ std::shared_ptr<ClickHandler> UiIntegration::createLinkHandler(
267395
return (my && my->session)
268396
? std::make_shared<BankCardClickHandler>(my->session, data.text)
269397
: nullptr;
398+
case EntityType::FormattedDate: {
399+
const auto [date, flags] = DeserializeFormattedDateData(data.data);
400+
if (date) {
401+
return std::make_shared<FormattedDateClickHandler>(date, flags);
402+
}
403+
} break;
270404
}
271405
return Integration::createLinkHandler(data, context);
272406
}

0 commit comments

Comments
 (0)