init
Some checks failed
Docker. / Ubuntu (push) Has been cancelled
User-agent updater. / User-agent (push) Failing after 15s
Lock Threads / lock (push) Failing after 10s
Waiting for answer. / waiting-for-answer (push) Failing after 22s
Close stale issues and PRs / stale (push) Successful in 13s
Needs user action. / needs-user-action (push) Failing after 8s
Can't reproduce. / cant-reproduce (push) Failing after 8s
Some checks failed
Docker. / Ubuntu (push) Has been cancelled
User-agent updater. / User-agent (push) Failing after 15s
Lock Threads / lock (push) Failing after 10s
Waiting for answer. / waiting-for-answer (push) Failing after 22s
Close stale issues and PRs / stale (push) Successful in 13s
Needs user action. / needs-user-action (push) Failing after 8s
Can't reproduce. / cant-reproduce (push) Failing after 8s
This commit is contained in:
919
Telegram/SourceFiles/dialogs/dialogs.style
Normal file
919
Telegram/SourceFiles/dialogs/dialogs.style
Normal file
@@ -0,0 +1,919 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
using "ui/basic.style";
|
||||
|
||||
using "ui/layers/layers.style"; // boxRoundShadow
|
||||
using "ui/widgets/widgets.style";
|
||||
|
||||
DialogRow {
|
||||
height: pixels;
|
||||
padding: margins;
|
||||
photoSize: pixels;
|
||||
nameLeft: pixels;
|
||||
nameTop: pixels;
|
||||
textLeft: pixels;
|
||||
textTop: pixels;
|
||||
topicsSkip: pixels;
|
||||
topicsSkipBig: pixels;
|
||||
topicsHeight: pixels;
|
||||
unreadMarkDiameter: pixels;
|
||||
tagTop: pixels;
|
||||
}
|
||||
DialogRightButton {
|
||||
button: RoundButton;
|
||||
margin: margins;
|
||||
}
|
||||
|
||||
ThreeStateIcon {
|
||||
icon: icon;
|
||||
over: icon;
|
||||
active: icon;
|
||||
}
|
||||
|
||||
VerifiedBadge {
|
||||
color: color;
|
||||
height: pixels;
|
||||
}
|
||||
|
||||
ForumTopicIcon {
|
||||
size: pixels;
|
||||
font: font;
|
||||
textTop: pixels;
|
||||
}
|
||||
|
||||
DialogsMiniIcon {
|
||||
icon: ThreeStateIcon;
|
||||
skipText: pixels;
|
||||
skipMedia: pixels;
|
||||
}
|
||||
|
||||
defaultForumTopicIcon: ForumTopicIcon {
|
||||
size: 21px;
|
||||
font: font(bold 11px);
|
||||
textTop: 2px;
|
||||
}
|
||||
normalForumTopicIcon: ForumTopicIcon {
|
||||
size: 19px;
|
||||
font: font(bold 10px);
|
||||
textTop: 2px;
|
||||
}
|
||||
largeForumTopicIcon: ForumTopicIcon {
|
||||
size: 26px;
|
||||
font: font(bold 13px);
|
||||
textTop: 3px;
|
||||
}
|
||||
infoForumTopicIcon: ForumTopicIcon {
|
||||
size: 32px;
|
||||
font: font(bold 15px);
|
||||
textTop: 4px;
|
||||
}
|
||||
|
||||
dialogsUnreadFont: font(12px bold);
|
||||
dialogsUnreadHeight: 19px;
|
||||
dialogsUnreadPadding: 5px;
|
||||
|
||||
dialogsRipple: RippleAnimation(defaultRippleAnimation) {
|
||||
color: dialogsRippleBg;
|
||||
}
|
||||
|
||||
dialogsTextFont: normalFont;
|
||||
dialogsTextStyle: defaultTextStyle;
|
||||
dialogsDateFont: font(13px);
|
||||
dialogsDateSkip: 5px;
|
||||
|
||||
dialogsRowHeight: 62px;
|
||||
dialogsFilterPadding: point(7px, 7px);
|
||||
dialogsFilterSkip: 4px;
|
||||
|
||||
defaultDialogRow: DialogRow {
|
||||
height: dialogsRowHeight;
|
||||
padding: margins(10px, 8px, 10px, 8px);
|
||||
photoSize: 46px;
|
||||
nameLeft: 68px;
|
||||
nameTop: 10px;
|
||||
textLeft: 68px;
|
||||
textTop: 34px;
|
||||
}
|
||||
taggedDialogRow: DialogRow(defaultDialogRow) {
|
||||
height: 72px;
|
||||
textTop: 30px;
|
||||
tagTop: 52px;
|
||||
}
|
||||
forumDialogRow: DialogRow(defaultDialogRow) {
|
||||
height: 80px;
|
||||
textTop: 32px;
|
||||
topicsSkip: 8px;
|
||||
topicsSkipBig: 14px;
|
||||
topicsHeight: 21px;
|
||||
}
|
||||
taggedForumDialogRow: DialogRow(forumDialogRow) {
|
||||
height: 96px;
|
||||
tagTop: 77px;
|
||||
}
|
||||
dialogRowFilterTagSkip: 4px;
|
||||
dialogRowFilterTagStyle: TextStyle(defaultTextStyle) {
|
||||
font: font(10px);
|
||||
}
|
||||
dialogRowOpenBot: DialogRightButton {
|
||||
button: RoundButton(defaultActiveButton) {
|
||||
height: 20px;
|
||||
textTop: 1px;
|
||||
}
|
||||
margin: margins(0px, 32px, 10px, 0px);
|
||||
}
|
||||
dialogRowOpenBotRecent: DialogRightButton(dialogRowOpenBot) {
|
||||
margin: margins(0px, 32px, 16px, 0px);
|
||||
}
|
||||
dialogsTopBarRightButton: RoundButton(defaultActiveButton) {
|
||||
width: -16px;
|
||||
height: 22px;
|
||||
textTop: 2px;
|
||||
}
|
||||
|
||||
forumDialogJumpArrow: icon{{ "dialogs/dialogs_topic_arrow", dialogsTextFg }};
|
||||
forumDialogJumpArrowOver: icon{{ "dialogs/dialogs_topic_arrow", dialogsTextFgOver }};
|
||||
forumDialogJumpArrowSkip: 8px;
|
||||
forumDialogJumpArrowPosition: point(3px, 3px);
|
||||
forumDialogJumpPadding: margins(8px, 3px, 8px, 3px);
|
||||
forumDialogJumpRadius: 11px;
|
||||
|
||||
dialogsOnlineBadgeStroke: 2px;
|
||||
dialogsOnlineBadgeSize: 10px;
|
||||
dialogsOnlineBadgeSkip: point(0px, 2px);
|
||||
dialogsOnlineBadgeDuration: 150;
|
||||
|
||||
dialogsCallBadgeSize: 16px;
|
||||
dialogsCallBadgeSkip: point(-3px, -3px);
|
||||
|
||||
dialogsSubscriptionBadgeSize: 16px;
|
||||
dialogsSubscriptionBadgeSkip: point(-4px, -4px);
|
||||
|
||||
dialogsTTLBadgeSize: 20px;
|
||||
dialogsTTLBadgeInnerMargins: margins(2px, 2px, 2px, 2px);
|
||||
// Relative to a photo place, not a whole userpic place.
|
||||
dialogsTTLBadgeSkip: point(1px, 1px);
|
||||
|
||||
dialogsSpeakingStrokeNumerator: 16px;
|
||||
dialogsSpeakingDenominator: 8.;
|
||||
|
||||
dialogsImportantBarHeight: 37px;
|
||||
|
||||
dialogsWidthDuration: universalDuration;
|
||||
dialogsTextWidthMin: 150px;
|
||||
|
||||
dialogsTextPalette: TextPalette(defaultTextPalette) {
|
||||
linkFg: dialogsTextFgService;
|
||||
monoFg: dialogsTextFg;
|
||||
spoilerFg: dialogsTextFg;
|
||||
}
|
||||
dialogsTextPaletteOver: TextPalette(defaultTextPalette) {
|
||||
linkFg: dialogsTextFgServiceOver;
|
||||
monoFg: dialogsTextFgOver;
|
||||
spoilerFg: dialogsTextFgOver;
|
||||
}
|
||||
dialogsTextPaletteActive: TextPalette(defaultTextPalette) {
|
||||
linkFg: dialogsTextFgServiceActive;
|
||||
monoFg: dialogsTextFgActive;
|
||||
spoilerFg: dialogsTextFgActive;
|
||||
}
|
||||
dialogsTextPaletteDraft: TextPalette(defaultTextPalette) {
|
||||
linkFg: dialogsDraftFg;
|
||||
monoFg: dialogsTextFg;
|
||||
spoilerFg: dialogsTextFg;
|
||||
}
|
||||
dialogsTextPaletteDraftOver: TextPalette(defaultTextPalette) {
|
||||
linkFg: dialogsDraftFgOver;
|
||||
monoFg: dialogsTextFgOver;
|
||||
spoilerFg: dialogsTextFgOver;
|
||||
}
|
||||
dialogsTextPaletteDraftActive: TextPalette(defaultTextPalette) {
|
||||
linkFg: dialogsDraftFgActive;
|
||||
monoFg: dialogsTextFgActive;
|
||||
spoilerFg: dialogsTextFgActive;
|
||||
}
|
||||
dialogsTextPaletteTaken: TextPalette(defaultTextPalette) {
|
||||
linkFg: boxTextFgGood;
|
||||
monoFg: dialogsTextFg;
|
||||
spoilerFg: dialogsTextFg;
|
||||
}
|
||||
dialogsTextPaletteTakenOver: TextPalette(defaultTextPalette) {
|
||||
linkFg: boxTextFgGood;
|
||||
monoFg: dialogsTextFgOver;
|
||||
spoilerFg: dialogsTextFgOver;
|
||||
}
|
||||
dialogsTextPaletteTakenActive: TextPalette(defaultTextPalette) {
|
||||
linkFg: dialogsDraftFgActive;
|
||||
monoFg: dialogsTextFgActive;
|
||||
spoilerFg: dialogsTextFgActive;
|
||||
}
|
||||
dialogsTextPaletteArchive: TextPalette(defaultTextPalette) {
|
||||
linkFg: dialogsArchiveFg;
|
||||
monoFg: dialogsArchiveFg;
|
||||
spoilerFg: dialogsArchiveFg;
|
||||
}
|
||||
dialogsTextPaletteArchiveOver: TextPalette(defaultTextPalette) {
|
||||
linkFg: dialogsArchiveFgOver;
|
||||
monoFg: dialogsArchiveFgOver;
|
||||
spoilerFg: dialogsArchiveFgOver;
|
||||
}
|
||||
dialogsTextPaletteArchiveActive: TextPalette(defaultTextPalette) {
|
||||
linkFg: dialogsTextFgActive;
|
||||
monoFg: dialogsTextFgActive;
|
||||
spoilerFg: dialogsTextFgActive;
|
||||
}
|
||||
dialogsTextPaletteInTopic: TextPalette(defaultTextPalette) {
|
||||
linkFg: dialogsNameFg;
|
||||
monoFg: dialogsTextFg;
|
||||
spoilerFg: dialogsTextFg;
|
||||
}
|
||||
dialogsTextPaletteInTopicOver: TextPalette(defaultTextPalette) {
|
||||
linkFg: dialogsNameFgOver;
|
||||
monoFg: dialogsTextFgOver;
|
||||
spoilerFg: dialogsTextFgOver;
|
||||
}
|
||||
dialogsTextPaletteInTopicActive: TextPalette(defaultTextPalette) {
|
||||
linkFg: dialogsNameFgActive;
|
||||
monoFg: dialogsTextFgActive;
|
||||
spoilerFg: dialogsTextFgActive;
|
||||
}
|
||||
|
||||
dialogsEmptyHeight: 160px;
|
||||
dialogsEmptySkip: 2px;
|
||||
dialogsEmptyLabel: FlatLabel(defaultFlatLabel) {
|
||||
minWidth: 32px;
|
||||
align: align(top);
|
||||
textFg: windowSubTextFg;
|
||||
}
|
||||
dialogEmptyButton: RoundButton(defaultActiveButton) {
|
||||
}
|
||||
dialogEmptyButtonSkip: 12px;
|
||||
dialogEmptyButtonLabel: FlatLabel(defaultFlatLabel) {
|
||||
style: TextStyle(defaultTextStyle) {
|
||||
font: font(boxFontSize semibold);
|
||||
}
|
||||
minWidth: 32px;
|
||||
align: align(top);
|
||||
textFg: windowFg;
|
||||
}
|
||||
|
||||
dialogsMenuToggle: IconButton {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
|
||||
icon: icon {{ "dialogs/dialogs_menu", dialogsMenuIconFg }};
|
||||
iconOver: icon {{ "dialogs/dialogs_menu", dialogsMenuIconFgOver }};
|
||||
iconPosition: point(-1px, -1px);
|
||||
|
||||
rippleAreaPosition: point(0px, 0px);
|
||||
rippleAreaSize: 40px;
|
||||
ripple: defaultRippleAnimationBgOver;
|
||||
}
|
||||
dialogsMenuToggleUnread: icon {
|
||||
{ "dialogs/dialogs_menu_unread", dialogsMenuIconFg },
|
||||
{ "dialogs/dialogs_menu_unread_dot", dialogsUnreadBg },
|
||||
};
|
||||
dialogsMenuToggleUnreadMuted: icon {
|
||||
{ "dialogs/dialogs_menu_unread", dialogsMenuIconFg },
|
||||
{ "dialogs/dialogs_menu_unread_dot", dialogsMenuIconFg },
|
||||
};
|
||||
|
||||
dialogsLock: IconButton {
|
||||
width: 36px;
|
||||
height: 38px;
|
||||
|
||||
icon: icon {{ "dialogs/dialogs_lock_off", dialogsMenuIconFg }};
|
||||
iconOver: icon {{ "dialogs/dialogs_lock_off", dialogsMenuIconFgOver }};
|
||||
iconPosition: point(-1px, -1px);
|
||||
|
||||
ripple: emptyRippleAnimation;
|
||||
}
|
||||
dialogsUnlockIcon: icon {{ "dialogs/dialogs_lock_on", dialogsMenuIconFg }};
|
||||
dialogsUnlockIconOver: icon {{ "dialogs/dialogs_lock_on", dialogsMenuIconFgOver }};
|
||||
dialogsCalendar: IconButton {
|
||||
width: 32px;
|
||||
height: 35px;
|
||||
|
||||
icon: icon {{ "dialogs/dialogs_calendar", dialogsMenuIconFg }};
|
||||
iconOver: icon {{ "dialogs/dialogs_calendar", dialogsMenuIconFgOver }};
|
||||
iconPosition: point(1px, 6px);
|
||||
}
|
||||
dialogsSearchFrom: IconButton(dialogsCalendar) {
|
||||
width: 29px;
|
||||
icon: icon {{ "dialogs/dialogs_search_from", dialogsMenuIconFg }};
|
||||
iconOver: icon {{ "dialogs/dialogs_search_from", dialogsMenuIconFgOver }};
|
||||
}
|
||||
dialogsSearchForNarrowFilters: IconButton(dialogsMenuToggle) {
|
||||
icon: icon {{ "top_bar_search", menuIconFg }};
|
||||
iconOver: icon {{ "top_bar_search", menuIconFgOver }};
|
||||
iconPosition: point(4px, 4px);
|
||||
}
|
||||
|
||||
dialogsFilter: InputField(defaultInputField) {
|
||||
textBg: filterInputInactiveBg;
|
||||
textBgActive: filterInputActiveBg;
|
||||
textMargins: margins(12px, 8px, 30px, 5px);
|
||||
|
||||
placeholderFg: placeholderFg;
|
||||
placeholderFgActive: placeholderFgActive;
|
||||
placeholderFgError: placeholderFgActive;
|
||||
placeholderMargins: margins(5px, 0px, 2px, 0px);
|
||||
placeholderScale: 0.;
|
||||
placeholderShift: -50px;
|
||||
placeholderFont: normalFont;
|
||||
|
||||
borderFg: filterInputInactiveBg;
|
||||
borderFgActive: windowBgRipple;
|
||||
borderFgError: activeLineFgError;
|
||||
|
||||
border: 3px;
|
||||
borderActive: 2px;
|
||||
borderRadius: 18px;
|
||||
borderDenominator: 2;
|
||||
|
||||
style: defaultTextStyle;
|
||||
|
||||
heightMin: 35px;
|
||||
}
|
||||
dialogsCancelSearchInPeer: IconButton(dialogsMenuToggle) {
|
||||
icon: icon {{ "dialogs/dialogs_cancel_search", dialogsMenuIconFg }};
|
||||
iconOver: icon {{ "dialogs/dialogs_cancel_search", dialogsMenuIconFgOver }};
|
||||
iconPosition: point(11px, 11px);
|
||||
rippleAreaPosition: point(3px, 3px);
|
||||
rippleAreaSize: 34px;
|
||||
}
|
||||
dialogsCancelSearch: CrossButton {
|
||||
width: 35px;
|
||||
height: 35px;
|
||||
|
||||
cross: CrossAnimation {
|
||||
size: 35px;
|
||||
skip: 12px;
|
||||
stroke: 1.5;
|
||||
minScale: 0.3;
|
||||
}
|
||||
crossFg: dialogsMenuIconFg;
|
||||
crossFgOver: dialogsMenuIconFgOver;
|
||||
crossPosition: point(0px, 0px);
|
||||
|
||||
duration: 150;
|
||||
loadingPeriod: 1000;
|
||||
ripple: emptyRippleAnimation;
|
||||
}
|
||||
|
||||
dialogCalendar: IconButton(dialogsMenuToggle) {
|
||||
icon: icon {{ "dialogs/dialogs_calendar", lightButtonFg }};
|
||||
iconOver: icon {{ "dialogs/dialogs_calendar", lightButtonFgOver }};
|
||||
iconPosition: point(8px, 9px);
|
||||
|
||||
rippleAreaPosition: point(3px, 3px);
|
||||
rippleAreaSize: 34px;
|
||||
}
|
||||
dialogSearchFrom: IconButton(dialogCalendar) {
|
||||
icon: icon {{ "dialogs/dialogs_search_from", lightButtonFg }};
|
||||
iconOver: icon {{ "dialogs/dialogs_search_from", lightButtonFgOver }};
|
||||
iconPosition: point(9px, 8px);
|
||||
}
|
||||
|
||||
dialogsChatTypeSkip: 3px;
|
||||
dialogsChatIcon: ThreeStateIcon {
|
||||
icon: icon {{ "dialogs/dialogs_chat", dialogsChatIconFg, point(1px, 4px) }};
|
||||
over: icon {{ "dialogs/dialogs_chat", dialogsChatIconFgOver, point(1px, 4px) }};
|
||||
active: icon {{ "dialogs/dialogs_chat", dialogsChatIconFgActive, point(1px, 4px) }};
|
||||
}
|
||||
dialogsChannelIcon: ThreeStateIcon {
|
||||
icon: icon {{ "dialogs/dialogs_channel", dialogsChatIconFg, point(3px, 4px) }};
|
||||
over: icon {{ "dialogs/dialogs_channel", dialogsChatIconFgOver, point(3px, 4px) }};
|
||||
active: icon {{ "dialogs/dialogs_channel", dialogsChatIconFgActive, point(3px, 4px) }};
|
||||
}
|
||||
dialogsBotIcon: ThreeStateIcon {
|
||||
icon: icon {{ "dialogs/dialogs_bot", dialogsChatIconFg, point(1px, 3px) }};
|
||||
over: icon {{ "dialogs/dialogs_bot", dialogsChatIconFgOver, point(1px, 3px) }};
|
||||
active: icon {{ "dialogs/dialogs_bot", dialogsChatIconFgActive, point(1px, 3px) }};
|
||||
}
|
||||
dialogsForumIcon: ThreeStateIcon {
|
||||
icon: icon {{ "dialogs/dialogs_forum", dialogsChatIconFg, point(1px, 4px) }};
|
||||
over: icon {{ "dialogs/dialogs_forum", dialogsChatIconFgOver, point(1px, 4px) }};
|
||||
active: icon {{ "dialogs/dialogs_forum", dialogsChatIconFgActive, point(1px, 4px) }};
|
||||
}
|
||||
dialogsArchiveUserpic: icon {{ "archive_userpic", historyPeerUserpicFg }};
|
||||
dialogsRepliesUserpic: icon {{ "replies_userpic", historyPeerUserpicFg }};
|
||||
dialogsInaccessibleUserpic: icon {{ "dialogs/inaccessible_userpic", historyPeerUserpicFg }};
|
||||
dialogsHiddenAuthorUserpic: icon {{ "dialogs/avatar_hidden", premiumButtonFg }};
|
||||
dialogsMyNotesUserpic: icon {{ "dialogs/avatar_notes", historyPeerUserpicFg }};
|
||||
|
||||
dialogsSendStateSkip: 20px;
|
||||
dialogsSendingIcon: ThreeStateIcon {
|
||||
icon: icon {{ "dialogs/dialogs_sending", dialogsSendingIconFg, point(8px, 4px) }};
|
||||
over: icon {{ "dialogs/dialogs_sending", dialogsSendingIconFgOver, point(8px, 4px) }};
|
||||
active: icon {{ "dialogs/dialogs_sending", dialogsSendingIconFgActive, point(8px, 4px) }};
|
||||
}
|
||||
dialogsSentIcon: ThreeStateIcon {
|
||||
icon: icon {{ "dialogs/dialogs_sent", dialogsSentIconFg, point(10px, 4px) }};
|
||||
over: icon {{ "dialogs/dialogs_sent", dialogsSentIconFgOver, point(10px, 4px) }};
|
||||
active: icon {{ "dialogs/dialogs_sent", dialogsSentIconFgActive, point(10px, 4px) }};
|
||||
}
|
||||
dialogsReceivedIcon: ThreeStateIcon {
|
||||
icon: icon {{ "dialogs/dialogs_received", dialogsSentIconFg, point(5px, 4px) }};
|
||||
over: icon {{ "dialogs/dialogs_received", dialogsSentIconFgOver, point(5px, 4px) }};
|
||||
active: icon {{ "dialogs/dialogs_received", dialogsSentIconFgActive, point(5px, 4px) }};
|
||||
}
|
||||
dialogsPinnedIcon: ThreeStateIcon {
|
||||
icon: icon {{ "dialogs/dialogs_pinned", dialogsUnreadBgMuted }};
|
||||
over: icon {{ "dialogs/dialogs_pinned", dialogsUnreadBgMutedOver }};
|
||||
active: icon {{ "dialogs/dialogs_pinned", dialogsUnreadBgMutedActive }};
|
||||
}
|
||||
dialogsLockIcon: ThreeStateIcon {
|
||||
icon: icon {{ "emoji/premium_lock", dialogsUnreadBgMuted, point(4px, 0px) }};
|
||||
over: icon {{ "emoji/premium_lock", dialogsUnreadBgMutedOver, point(4px, 0px) }};
|
||||
active: icon {{ "emoji/premium_lock", dialogsUnreadBgMutedActive, point(4px, 0px) }};
|
||||
}
|
||||
|
||||
dialogsVerifiedColors: VerifiedBadge {
|
||||
height: 20px;
|
||||
color: dialogsVerifiedIconBg;
|
||||
}
|
||||
dialogsVerifiedColorsOver: VerifiedBadge(dialogsVerifiedColors) {
|
||||
color: dialogsVerifiedIconBgOver;
|
||||
}
|
||||
dialogsVerifiedColorsActive: VerifiedBadge(dialogsVerifiedColors) {
|
||||
color: dialogsVerifiedIconBgActive;
|
||||
}
|
||||
|
||||
dialogsVerifiedIcon: icon {
|
||||
{ "dialogs/dialogs_verified_star", dialogsVerifiedIconBg },
|
||||
{ "dialogs/dialogs_verified_check", dialogsVerifiedIconFg },
|
||||
};
|
||||
dialogsVerifiedIconOver: icon {
|
||||
{ "dialogs/dialogs_verified_star", dialogsVerifiedIconBgOver },
|
||||
{ "dialogs/dialogs_verified_check", dialogsVerifiedIconFgOver },
|
||||
};
|
||||
dialogsVerifiedIconActive: icon {
|
||||
{ "dialogs/dialogs_verified_star", dialogsVerifiedIconBgActive },
|
||||
{ "dialogs/dialogs_verified_check", dialogsVerifiedIconFgActive },
|
||||
};
|
||||
dialogsPremiumIcon: ThreeStateIcon {
|
||||
icon: icon {{ "dialogs/dialogs_premium", dialogsVerifiedIconBg }};
|
||||
over: icon {{ "dialogs/dialogs_premium", dialogsVerifiedIconBgOver }};
|
||||
active: icon {{ "dialogs/dialogs_premium", dialogsVerifiedIconBgActive }};
|
||||
}
|
||||
|
||||
historySendingIcon: icon {{ "dialogs/dialogs_sending", historySendingOutIconFg, point(5px, 5px) }};
|
||||
historySendingInvertedIcon: icon {{ "dialogs/dialogs_sending", historySendingInvertedIconFg, point(5px, 5px) }};
|
||||
historyViewsSendingIcon: icon {{ "dialogs/dialogs_sending", historySendingInIconFg, point(3px, 0px) }};
|
||||
historyViewsSendingInvertedIcon: icon {{ "dialogs/dialogs_sending", historySendingInvertedIconFg, point(3px, 0px) }};
|
||||
|
||||
dialogsUpdateButton: FlatButton {
|
||||
color: activeButtonFg;
|
||||
overColor: activeButtonFgOver;
|
||||
|
||||
bgColor: activeButtonBg;
|
||||
overBgColor: activeButtonBgOver;
|
||||
|
||||
width: -34px;
|
||||
height: 46px;
|
||||
|
||||
textTop: 14px;
|
||||
|
||||
font: semiboldFont;
|
||||
overFont: semiboldFont;
|
||||
|
||||
ripple: RippleAnimation(defaultRippleAnimation) {
|
||||
color: activeButtonBgRipple;
|
||||
}
|
||||
}
|
||||
dialogsInstallUpdate: icon {{ "install_update", activeButtonFg }};
|
||||
dialogsInstallUpdateOver: icon {{ "install_update", activeButtonFgOver }};
|
||||
dialogsInstallUpdateIconSkip: 7px;
|
||||
dialogsInstallUpdateIconSize: 20px;
|
||||
dialogsInstallUpdateIconInnerMargin: 5px;
|
||||
dialogsInstallUpdateIconSide1: 5px;
|
||||
dialogsInstallUpdateIconSide2: 3px;
|
||||
|
||||
dialogsLoadMoreButton: FlatButton(dialogsUpdateButton) {
|
||||
color: lightButtonFg;
|
||||
overColor: lightButtonFg;
|
||||
bgColor: lightButtonBg;
|
||||
overBgColor: lightButtonBgOver;
|
||||
ripple: RippleAnimation(defaultRippleAnimation) {
|
||||
color: lightButtonBgRipple;
|
||||
}
|
||||
|
||||
height: 36px;
|
||||
textTop: 9px;
|
||||
font: semiboldFont;
|
||||
overFont: semiboldFont;
|
||||
}
|
||||
dialogsLoadMore: icon {{ "install_update-flip_vertical", lightButtonFg }};
|
||||
dialogsLoadMoreLoading: InfiniteRadialAnimation(defaultInfiniteRadialAnimation) {
|
||||
color: lightButtonFg;
|
||||
thickness: 3px;
|
||||
size: size(12px, 12px);
|
||||
}
|
||||
|
||||
dialogsSearchInHeight: 38px;
|
||||
dialogsSearchInPhotoSize: 28px;
|
||||
dialogsSearchInPhotoPadding: 10px;
|
||||
dialogsSearchInSkip: 10px;
|
||||
dialogsSearchInNameTop: 9px;
|
||||
dialogsSearchInDownTop: 15px;
|
||||
dialogsSearchInDown: icon {{ "intro_country_dropdown", windowBoldFg }};
|
||||
dialogsSearchInDownSkip: 4px;
|
||||
dialogsSearchInMenu: PopupMenu(defaultPopupMenu) {
|
||||
shadow: boxRoundShadow;
|
||||
animation: PanelAnimation(defaultPanelAnimation) {
|
||||
shadow: boxRoundShadow;
|
||||
}
|
||||
scrollPadding: margins(0px, 0px, 0px, 0px);
|
||||
radius: 8px;
|
||||
menu: menuWithIcons;
|
||||
}
|
||||
dialogsSearchInCheck: icon {{ "player/player_check", mediaPlayerActiveFg }};
|
||||
dialogsSearchInCheckSkip: 8px;
|
||||
dialogsSearchFromStyle: defaultTextStyle;
|
||||
dialogsSearchFromPalette: TextPalette(defaultTextPalette) {
|
||||
linkFg: dialogsNameFg;
|
||||
}
|
||||
|
||||
dialogsScamPadding: margins(2px, 0px, 2px, 0px);
|
||||
dialogsScamFont: font(9px semibold);
|
||||
dialogsScamSkip: 4px;
|
||||
dialogsScamRadius: 2px;
|
||||
|
||||
dialogsMiniPreviewTop: 1px;
|
||||
dialogsMiniPreview: 16px;
|
||||
dialogsMiniPreviewRadius: 2px;
|
||||
dialogsMiniPreviewSkip: 2px;
|
||||
dialogsMiniPreviewRight: 3px;
|
||||
dialogsMiniPlay: icon{{ "dialogs/dialogs_mini_play", videoPlayIconFg }};
|
||||
|
||||
dialogsMiniForward: DialogsMiniIcon {
|
||||
icon: ThreeStateIcon {
|
||||
icon: icon {{ "mini_forward", dialogsTextFg, point(0px, 1px) }};
|
||||
over: icon {{ "mini_forward", dialogsTextFgOver, point(0px, 1px) }};
|
||||
active: icon {{ "mini_forward", dialogsTextFgActive, point(0px, 1px) }};
|
||||
}
|
||||
skipText: 1px;
|
||||
skipMedia: 2px;
|
||||
}
|
||||
|
||||
dialogsMiniReplyIcon: IconEmoji {
|
||||
icon: icon {{ "mini_forward-flip_horizontal", attentionButtonFg }};
|
||||
padding: margins(0px, 2px, 0px, 0px);
|
||||
}
|
||||
|
||||
dialogsMiniReplyStory: DialogsMiniIcon {
|
||||
icon: ThreeStateIcon {
|
||||
icon: icon {{ "mini_reply_story", dialogsTextFg, point(0px, 1px) }};
|
||||
over: icon {{ "mini_reply_story", dialogsTextFgOver, point(0px, 1px) }};
|
||||
active: icon {{ "mini_reply_story", dialogsTextFgActive, point(0px, 1px) }};
|
||||
}
|
||||
skipText: 4px;
|
||||
skipMedia: 5px;
|
||||
}
|
||||
|
||||
dialogsUnreadMention: ThreeStateIcon {
|
||||
icon: icon{{ "dialogs/dialogs_mention", dialogsUnreadFg }};
|
||||
over: icon{{ "dialogs/dialogs_mention", dialogsUnreadFgOver }};
|
||||
active: icon{{ "dialogs/dialogs_mention", dialogsUnreadFgActive }};
|
||||
}
|
||||
dialogsUnreadReaction: ThreeStateIcon {
|
||||
icon: icon{{ "dialogs/dialogs_reaction", dialogsUnreadFg }};
|
||||
over: icon{{ "dialogs/dialogs_reaction", dialogsUnreadFgOver }};
|
||||
active: icon{{ "dialogs/dialogs_reaction", dialogsUnreadFgActive }};
|
||||
}
|
||||
|
||||
downloadBarHeight: 46px;
|
||||
downloadArrow: icon{{ "fast_to_original", menuIconFg }};
|
||||
downloadArrowOver: icon{{ "fast_to_original", menuIconFgOver }};
|
||||
downloadArrowRight: 10px;
|
||||
downloadTitleLeft: 57px;
|
||||
downloadTitleTop: 4px;
|
||||
downloadInfoStyle: TextStyle(defaultTextStyle) {
|
||||
font: font(12px);
|
||||
}
|
||||
downloadInfoLeft: 57px;
|
||||
downloadInfoTop: 23px;
|
||||
downloadLoadingLeft: 15px;
|
||||
downloadLoadingSize: 24px;
|
||||
downloadLoadingLine: 2px;
|
||||
downloadIconDocument: icon {{ "dialogs/dialogs_downloads", windowFgActive }};
|
||||
downloadIconSize: 16px;
|
||||
downloadIconSizeDone: 20px;
|
||||
|
||||
forumTopicRow: DialogRow(defaultDialogRow) {
|
||||
height: 54px;
|
||||
padding: margins(8px, 7px, 10px, 7px);
|
||||
photoSize: 20px;
|
||||
nameLeft: 39px;
|
||||
nameTop: 7px;
|
||||
textLeft: 39px;
|
||||
textTop: 29px;
|
||||
unreadMarkDiameter: 8px;
|
||||
}
|
||||
forumTopicIconPosition: point(2px, 0px);
|
||||
editTopicTitleMargin: margins(70px, 2px, 22px, 18px);
|
||||
editTopicIconPosition: point(24px, 19px);
|
||||
editTopicMaxHeight: 408px;
|
||||
|
||||
chooseTopicListItem: PeerListItem(defaultPeerListItem) {
|
||||
height: 44px;
|
||||
photoSize: 20px;
|
||||
photoPosition: point(16px, 12px);
|
||||
namePosition: point(55px, 11px);
|
||||
nameStyle: TextStyle(defaultTextStyle) {
|
||||
font: font(14px semibold);
|
||||
}
|
||||
}
|
||||
chooseTopicList: PeerList(defaultPeerList) {
|
||||
item: chooseTopicListItem;
|
||||
}
|
||||
|
||||
DialogsStories {
|
||||
left: pixels;
|
||||
height: pixels;
|
||||
photo: pixels;
|
||||
photoLeft: pixels;
|
||||
photoTop: pixels;
|
||||
shift: pixels;
|
||||
lineTwice: pixels;
|
||||
lineReadTwice: pixels;
|
||||
nameLeft: pixels;
|
||||
nameRight: pixels;
|
||||
nameTop: pixels;
|
||||
nameStyle: TextStyle;
|
||||
}
|
||||
DialogsStoriesList {
|
||||
small: DialogsStories;
|
||||
full: DialogsStories;
|
||||
bg: color;
|
||||
readOpacity: double;
|
||||
fullClickable: int;
|
||||
}
|
||||
|
||||
dialogsStories: DialogsStories {
|
||||
left: 4px;
|
||||
height: 35px;
|
||||
photo: 21px;
|
||||
photoTop: 4px;
|
||||
photoLeft: 4px;
|
||||
shift: 16px;
|
||||
lineTwice: 3px;
|
||||
lineReadTwice: 0px;
|
||||
nameLeft: 11px;
|
||||
nameRight: 10px;
|
||||
nameTop: 3px;
|
||||
nameStyle: semiboldTextStyle;
|
||||
}
|
||||
|
||||
dialogsStoriesFull: DialogsStories {
|
||||
left: 4px;
|
||||
height: 77px;
|
||||
photo: 42px;
|
||||
photoLeft: 10px;
|
||||
photoTop: 9px;
|
||||
lineTwice: 4px;
|
||||
lineReadTwice: 2px;
|
||||
nameLeft: 0px;
|
||||
nameRight: 0px;
|
||||
nameTop: 56px;
|
||||
nameStyle: TextStyle(defaultTextStyle) {
|
||||
font: font(11px);
|
||||
}
|
||||
}
|
||||
topPeers: DialogsStories(dialogsStoriesFull) {
|
||||
photo: 46px;
|
||||
photoLeft: 10px;
|
||||
photoTop: 8px;
|
||||
nameLeft: 6px;
|
||||
}
|
||||
topPeersRadius: 4px;
|
||||
topPeersMargin: margins(3px, 3px, 3px, 4px);
|
||||
|
||||
recentPeersEmptySize: 100px;
|
||||
recentPeersEmptyMargin: margins(10px, 10px, 10px, 10px);
|
||||
recentPeersEmptySkip: 10px;
|
||||
recentPeersEmptyHeightMin: 220px;
|
||||
recentPeersItem: PeerListItem(defaultPeerListItem) {
|
||||
height: 56px;
|
||||
photoSize: 42px;
|
||||
photoPosition: point(10px, 7px);
|
||||
namePosition: point(64px, 9px);
|
||||
statusPosition: point(64px, 30px);
|
||||
button: OutlineButton(defaultPeerListButton) {
|
||||
textBg: contactsBg;
|
||||
textBgOver: contactsBgOver;
|
||||
ripple: defaultRippleAnimation;
|
||||
}
|
||||
statusFg: contactsStatusFg;
|
||||
statusFgOver: contactsStatusFgOver;
|
||||
statusFgActive: contactsStatusFgOnline;
|
||||
}
|
||||
recentPeersList: PeerList(defaultPeerList) {
|
||||
padding: margins(0px, 4px, 0px, 4px);
|
||||
item: recentPeersItem;
|
||||
}
|
||||
recentPeersItemActive: PeerListItem(recentPeersItem) {
|
||||
nameFg: dialogsNameFgActive;
|
||||
nameFgChecked: dialogsNameFgActive;
|
||||
button: OutlineButton(defaultPeerListButton) {
|
||||
textBg: dialogsBgActive;
|
||||
textBgOver: dialogsBgActive;
|
||||
ripple: RippleAnimation(defaultRippleAnimation) {
|
||||
color: dialogsRippleBgActive;
|
||||
}
|
||||
}
|
||||
statusFg: dialogsTextFgActive;
|
||||
statusFgOver: dialogsTextFgActive;
|
||||
statusFgActive: dialogsTextFgActive;
|
||||
}
|
||||
recentPeersSpecialName: PeerListItem(recentPeersItem) {
|
||||
namePosition: point(64px, 19px);
|
||||
}
|
||||
|
||||
dialogsTabsScroll: ScrollArea(defaultScrollArea) {
|
||||
barHidden: true;
|
||||
}
|
||||
dialogsSearchTabs: SettingsSlider(defaultSettingsSlider) {
|
||||
padding: 9px;
|
||||
height: 33px;
|
||||
barTop: 30px;
|
||||
barSkip: 0px;
|
||||
barStroke: 6px;
|
||||
barRadius: 2px;
|
||||
barFg: transparent;
|
||||
barSnapToLabel: true;
|
||||
strictSkip: 18px;
|
||||
labelTop: 7px;
|
||||
labelStyle: semiboldTextStyle;
|
||||
labelFg: windowSubTextFg;
|
||||
labelFgActive: lightButtonFg;
|
||||
rippleBottomSkip: 1px;
|
||||
rippleBg: windowBgOver;
|
||||
rippleBgActive: lightButtonBgOver;
|
||||
ripple: defaultRippleAnimation;
|
||||
}
|
||||
|
||||
chatsFiltersTabs: SettingsSlider(dialogsSearchTabs) {
|
||||
rippleBottomSkip: 0px;
|
||||
}
|
||||
|
||||
dialogsStoriesList: DialogsStoriesList {
|
||||
small: dialogsStories;
|
||||
full: dialogsStoriesFull;
|
||||
bg: dialogsBg;
|
||||
readOpacity: 0.6;
|
||||
fullClickable: 0;
|
||||
}
|
||||
dialogsStoriesListInfo: DialogsStoriesList(dialogsStoriesList) {
|
||||
bg: transparent;
|
||||
fullClickable: 1;
|
||||
}
|
||||
dialogsStoriesListMine: DialogsStoriesList(dialogsStoriesListInfo) {
|
||||
readOpacity: 1.;
|
||||
}
|
||||
dialogsStoriesTooltip: ImportantTooltip(defaultImportantTooltip) {
|
||||
padding: margins(0px, 0px, 0px, 0px);
|
||||
}
|
||||
dialogsStoriesTooltipLabel: defaultImportantTooltipLabel;
|
||||
dialogsStoriesTooltipMaxWidth: 200px;
|
||||
dialogsStoriesTooltipHide: IconButton(defaultIconButton) {
|
||||
width: 34px;
|
||||
height: 20px;
|
||||
iconPosition: point(-1px, -1px);
|
||||
icon: icon {{ "calls/video_tooltip", importantTooltipFg }};
|
||||
iconOver: icon {{ "calls/video_tooltip", importantTooltipFg }};
|
||||
ripple: emptyRippleAnimation;
|
||||
}
|
||||
|
||||
searchedBarHeight: 28px;
|
||||
searchedBarFont: normalFont;
|
||||
searchedBarPosition: point(14px, 5px);
|
||||
searchedBarLabel: FlatLabel(defaultFlatLabel) {
|
||||
textFg: searchedBarFg;
|
||||
margin: margins(14px, 5px, 14px, 5px);
|
||||
}
|
||||
searchedBarLink: LinkButton(defaultLinkButton) {
|
||||
color: searchedBarFg;
|
||||
overColor: searchedBarFg;
|
||||
padding: margins(14px, 5px, 14px, 5px);
|
||||
}
|
||||
|
||||
dialogsSearchTagSkip: point(8px, 4px);
|
||||
dialogsSearchTagBottom: 10px;
|
||||
dialogsSearchTagLocked: icon{{ "dialogs/mini_tag_lock", lightButtonFgOver }};
|
||||
dialogsSearchTagPromo: defaultTextStyle;
|
||||
dialogsSearchTagArrow: IconEmoji {
|
||||
icon: icon{{ "dialogs/mini_arrow", windowSubTextFg }};
|
||||
padding: margins(-6px, 3px, 0px, 0px);
|
||||
}
|
||||
dialogsSearchTagPromoLeft: 6px;
|
||||
dialogsSearchTagPromoRight: 1px;
|
||||
dialogsSearchTagPromoSkip: 6px;
|
||||
|
||||
dialogsPopularAppsPadding: margins(10px, 8px, 10px, 12px);
|
||||
dialogsPopularAppsAbout: FlatLabel(boxDividerLabel) {
|
||||
minWidth: 128px;
|
||||
}
|
||||
|
||||
dialogsQuickActionSize: 20px;
|
||||
dialogsQuickActionRippleSize: 80px;
|
||||
|
||||
dialogsSponsoredButton: DialogRightButton(dialogRowOpenBot) {
|
||||
button: RoundButton(defaultLightButton) {
|
||||
textFg: windowActiveTextFg;
|
||||
textFgOver: windowActiveTextFg;
|
||||
textBg: lightButtonBgOver;
|
||||
textBgOver: lightButtonBgOver;
|
||||
height: 20px;
|
||||
textTop: 1px;
|
||||
}
|
||||
margin: margins(0px, 9px, 10px, 0px);
|
||||
}
|
||||
|
||||
dialogsTopBarLeftPadding: 18px;
|
||||
dialogsTopBarSuggestionTitleStyle: TextStyle(defaultTextStyle) {
|
||||
font: font(semibold 12px);
|
||||
}
|
||||
dialogsTopBarSuggestionAboutStyle: TextStyle(defaultTextStyle) {
|
||||
font: font(11px);
|
||||
}
|
||||
dialogsSuggestionDeniedAuthLottie: size(70px, 70px);
|
||||
dialogsSuggestionDeniedAuthLottieCircle: size(90px, 90px);
|
||||
dialogsSuggestionDeniedAuthLottieMargins: margins(10px, 10px, 10px, 10px);
|
||||
|
||||
dialogsMiniQuoteIcon: icon{{ "chat/mini_quote", dialogsTextFg }};
|
||||
|
||||
postsSearchIntroTitle: FlatLabel(defaultFlatLabel) {
|
||||
textFg: windowBoldFg;
|
||||
minWidth: 64px;
|
||||
style: TextStyle(semiboldTextStyle) {
|
||||
font: font(semibold 16px);
|
||||
}
|
||||
align: align(top);
|
||||
}
|
||||
postsSearchIntroTitleMargin: margins(20px, 0px, 20px, 4px);
|
||||
postsSearchIntroSubtitle: FlatLabel(defaultFlatLabel) {
|
||||
textFg: windowSubTextFg;
|
||||
minWidth: 64px;
|
||||
align: align(top);
|
||||
}
|
||||
postsSearchIntroSubtitleMargin: margins(20px, 4px, 20px, 16px);
|
||||
postsSearchIntroButton: RoundButton(defaultActiveButton) {
|
||||
width: 200px;
|
||||
height: 42px;
|
||||
textTop: 12px;
|
||||
style: semiboldTextStyle;
|
||||
}
|
||||
postsSearchIntroFooter: FlatLabel(defaultFlatLabel) {
|
||||
textFg: windowSubTextFg;
|
||||
minWidth: 64px;
|
||||
align: align(top);
|
||||
style: TextStyle(defaultTextStyle) {
|
||||
font: font(12px);
|
||||
}
|
||||
}
|
||||
postsSearchIntroFooterMargin: margins(20px, 12px, 20px, 0px);
|
||||
postsSearchIcon: icon {{ "box_search", windowFgActive }};
|
||||
postsSearchIconPadding: margins(12px, 1px, 6px, 0px);
|
||||
postsSearchArrow: icon {{ "dialogs/mini_arrow", windowFgActive }};
|
||||
postsSearchArrowPadding: margins(2px, 1px, 8px, 0px);
|
||||
|
||||
dialogsUnconfirmedAuthPadding: margins(10px, 0px, 10px, 0px);
|
||||
dialogsUnconfirmedAuthTitle: FlatLabel(boxLabel) {
|
||||
minWidth: 70px;
|
||||
align: align(top);
|
||||
style: TextStyle(defaultTextStyle) {
|
||||
font: font(12px);
|
||||
}
|
||||
}
|
||||
dialogsUnconfirmedAuthAbout: FlatLabel(boxLabel) {
|
||||
minWidth: 70px;
|
||||
align: align(top);
|
||||
textFg: windowSubTextFg;
|
||||
style: TextStyle(defaultTextStyle) {
|
||||
font: font(11px);
|
||||
}
|
||||
}
|
||||
dialogsUnconfirmedAuthButton: RoundButton(defaultBoxButton) {
|
||||
width: -16px;
|
||||
height: 26px;
|
||||
textTop: 5px;
|
||||
radius: 10px;
|
||||
style: TextStyle(semiboldTextStyle) {
|
||||
font: font(11px semibold);
|
||||
}
|
||||
}
|
||||
dialogsUnconfirmedAuthButtonNo: RoundButton(dialogsUnconfirmedAuthButton) {
|
||||
textFg: attentionButtonFg;
|
||||
textFgOver: attentionButtonFgOver;
|
||||
textBgOver: attentionButtonBgOver;
|
||||
|
||||
ripple: RippleAnimation(defaultRippleAnimation) {
|
||||
color: attentionButtonBgRipple;
|
||||
}
|
||||
}
|
||||
155
Telegram/SourceFiles/dialogs/dialogs_common.h
Normal file
155
Telegram/SourceFiles/dialogs/dialogs_common.h
Normal file
@@ -0,0 +1,155 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#ifdef _DEBUG
|
||||
#include <QtCore/QDebug>
|
||||
#endif // _DEBUG
|
||||
|
||||
namespace style {
|
||||
struct DialogRightButton;
|
||||
} // namespace style
|
||||
|
||||
namespace Ui {
|
||||
class RippleAnimation;
|
||||
} // namespace Ui
|
||||
|
||||
namespace Dialogs {
|
||||
|
||||
class Row;
|
||||
|
||||
enum class SortMode {
|
||||
Date = 0x00,
|
||||
Name = 0x01,
|
||||
Add = 0x02,
|
||||
};
|
||||
|
||||
struct PositionChange {
|
||||
int from = -1;
|
||||
int to = -1;
|
||||
int height = 0;
|
||||
};
|
||||
|
||||
struct UnreadState {
|
||||
int messages = 0;
|
||||
int messagesMuted = 0;
|
||||
int chats = 0;
|
||||
int chatsMuted = 0;
|
||||
int marks = 0;
|
||||
int marksMuted = 0;
|
||||
int reactions = 0;
|
||||
int reactionsMuted = 0;
|
||||
int mentions = 0;
|
||||
bool known = false;
|
||||
|
||||
UnreadState &operator+=(const UnreadState &other) {
|
||||
messages += other.messages;
|
||||
messagesMuted += other.messagesMuted;
|
||||
chats += other.chats;
|
||||
chatsMuted += other.chatsMuted;
|
||||
marks += other.marks;
|
||||
marksMuted += other.marksMuted;
|
||||
reactions += other.reactions;
|
||||
reactionsMuted += other.reactionsMuted;
|
||||
mentions += other.mentions;
|
||||
return *this;
|
||||
}
|
||||
UnreadState &operator-=(const UnreadState &other) {
|
||||
messages -= other.messages;
|
||||
messagesMuted -= other.messagesMuted;
|
||||
chats -= other.chats;
|
||||
chatsMuted -= other.chatsMuted;
|
||||
marks -= other.marks;
|
||||
marksMuted -= other.marksMuted;
|
||||
reactions -= other.reactions;
|
||||
reactionsMuted -= other.reactionsMuted;
|
||||
mentions -= other.mentions;
|
||||
return *this;
|
||||
}
|
||||
};
|
||||
|
||||
inline UnreadState operator+(const UnreadState &a, const UnreadState &b) {
|
||||
auto result = a;
|
||||
result += b;
|
||||
return result;
|
||||
}
|
||||
|
||||
inline UnreadState operator-(const UnreadState &a, const UnreadState &b) {
|
||||
auto result = a;
|
||||
result -= b;
|
||||
return result;
|
||||
}
|
||||
|
||||
#ifdef _DEBUG
|
||||
inline QDebug operator<<(QDebug debug, const UnreadState &state) {
|
||||
return debug.nospace() << "UnreadState(messages:" << state.messages
|
||||
<< ", messagesMuted:" << state.messagesMuted
|
||||
<< ", chats:" << state.chats
|
||||
<< ", chatsMuted:" << state.chatsMuted
|
||||
<< ", marks:" << state.marks
|
||||
<< ", marksMuted:" << state.marksMuted
|
||||
<< ", reactions:" << state.reactions
|
||||
<< ", reactionsMuted:" << state.reactionsMuted
|
||||
<< ", mentions:" << state.mentions
|
||||
<< ", known:" << state.known << ")";
|
||||
}
|
||||
#endif // _DEBUG
|
||||
|
||||
struct BadgesState {
|
||||
int unreadCounter = 0;
|
||||
bool unread : 1 = false;
|
||||
bool unreadMuted : 1 = false;
|
||||
bool mention : 1 = false;
|
||||
bool mentionMuted : 1 = false;
|
||||
bool reaction : 1 = false;
|
||||
bool reactionMuted : 1 = false;
|
||||
|
||||
friend inline constexpr auto operator<=>(
|
||||
BadgesState,
|
||||
BadgesState) = default;
|
||||
friend inline constexpr bool operator==(
|
||||
BadgesState,
|
||||
BadgesState) = default;
|
||||
|
||||
[[nodiscard]] bool empty() const {
|
||||
return !unread && !mention && !reaction;
|
||||
}
|
||||
};
|
||||
|
||||
enum class CountInBadge : uchar {
|
||||
Default,
|
||||
Chats,
|
||||
Messages,
|
||||
};
|
||||
|
||||
enum class IncludeInBadge : uchar {
|
||||
Default,
|
||||
Unmuted,
|
||||
All,
|
||||
UnmutedOrAll,
|
||||
};
|
||||
|
||||
struct RowsByLetter {
|
||||
not_null<Row*> main;
|
||||
base::flat_map<QChar, not_null<Row*>> letters;
|
||||
};
|
||||
|
||||
struct RightButton final {
|
||||
const style::DialogRightButton *st = nullptr;
|
||||
QImage bg;
|
||||
QImage selectedBg;
|
||||
QImage activeBg;
|
||||
Ui::Text::String text;
|
||||
std::unique_ptr<Ui::RippleAnimation> ripple;
|
||||
|
||||
explicit operator bool() const {
|
||||
return st != nullptr;
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace Dialogs
|
||||
475
Telegram/SourceFiles/dialogs/dialogs_entry.cpp
Normal file
475
Telegram/SourceFiles/dialogs/dialogs_entry.cpp
Normal file
@@ -0,0 +1,475 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#include "dialogs/dialogs_entry.h"
|
||||
|
||||
#include "dialogs/dialogs_key.h"
|
||||
#include "dialogs/dialogs_indexed_list.h"
|
||||
#include "data/data_changes.h"
|
||||
#include "data/data_session.h"
|
||||
#include "data/data_folder.h"
|
||||
#include "data/data_forum_topic.h"
|
||||
#include "data/data_chat_filters.h"
|
||||
#include "data/data_saved_sublist.h"
|
||||
#include "core/application.h"
|
||||
#include "core/core_settings.h"
|
||||
#include "mainwidget.h"
|
||||
#include "main/main_session.h"
|
||||
#include "main/main_session_settings.h"
|
||||
#include "ui/text/text_options.h"
|
||||
#include "ui/ui_utility.h"
|
||||
#include "history/history.h"
|
||||
#include "history/history_item.h"
|
||||
#include "styles/style_dialogs.h" // st::dialogsTextWidthMin
|
||||
|
||||
namespace Dialogs {
|
||||
namespace {
|
||||
|
||||
auto DialogsPosToTopShift = 0;
|
||||
|
||||
uint64 DialogPosFromDate(TimeId date) {
|
||||
if (!date) {
|
||||
return 0;
|
||||
}
|
||||
return (uint64(date) << 32) | (++DialogsPosToTopShift);
|
||||
}
|
||||
|
||||
uint64 FixedOnTopDialogPos(int index) {
|
||||
return 0xFFFFFFFFFFFF000FULL - index;
|
||||
}
|
||||
|
||||
uint64 PinnedDialogPos(int pinnedIndex) {
|
||||
return 0xFFFFFFFF000000FFULL - pinnedIndex;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
BadgesState BadgesForUnread(
|
||||
const UnreadState &state,
|
||||
CountInBadge count,
|
||||
IncludeInBadge include) {
|
||||
const auto countMessages = (count == CountInBadge::Messages)
|
||||
|| ((count == CountInBadge::Default)
|
||||
&& Core::App().settings().countUnreadMessages());
|
||||
const auto counterFull = state.marks
|
||||
+ (countMessages ? state.messages : state.chats);
|
||||
const auto counterMuted = state.marksMuted
|
||||
+ (countMessages ? state.messagesMuted : state.chatsMuted);
|
||||
const auto unreadMuted = (counterFull <= counterMuted);
|
||||
|
||||
const auto includeMuted = (include == IncludeInBadge::All)
|
||||
|| (include == IncludeInBadge::UnmutedOrAll && unreadMuted)
|
||||
|| ((include == IncludeInBadge::Default)
|
||||
&& Core::App().settings().includeMutedCounter());
|
||||
|
||||
const auto marks = state.marks - (includeMuted ? 0 : state.marksMuted);
|
||||
const auto counter = counterFull - (includeMuted ? 0 : counterMuted);
|
||||
const auto mark = (counter == 1) && (marks == 1);
|
||||
return {
|
||||
.unreadCounter = mark ? 0 : counter,
|
||||
.unread = (counter > 0),
|
||||
.unreadMuted = includeMuted && (counter <= counterMuted),
|
||||
.mention = (state.mentions > 0),
|
||||
.reaction = (state.reactions > 0),
|
||||
.reactionMuted = (state.reactions <= state.reactionsMuted),
|
||||
};
|
||||
}
|
||||
|
||||
Entry::Entry(not_null<Data::Session*> owner, Type type)
|
||||
: _owner(owner)
|
||||
, _flags((type == Type::History)
|
||||
? (Flag::IsThread | Flag::IsHistory)
|
||||
: (type == Type::ForumTopic)
|
||||
? (Flag::IsThread | Flag::IsForumTopic)
|
||||
: (type == Type::SavedSublist)
|
||||
? (Flag::IsThread | Flag::IsSavedSublist)
|
||||
: Flag(0)) {
|
||||
}
|
||||
|
||||
Entry::~Entry() = default;
|
||||
|
||||
Data::Session &Entry::owner() const {
|
||||
return *_owner;
|
||||
}
|
||||
|
||||
Main::Session &Entry::session() const {
|
||||
return _owner->session();
|
||||
}
|
||||
|
||||
History *Entry::asHistory() {
|
||||
return (_flags & Flag::IsHistory)
|
||||
? static_cast<History*>(this)
|
||||
: nullptr;
|
||||
}
|
||||
|
||||
Data::Forum *Entry::asForum() {
|
||||
return (_flags & Flag::IsHistory)
|
||||
? static_cast<History*>(this)->peer->forum()
|
||||
: nullptr;
|
||||
}
|
||||
|
||||
Data::Folder *Entry::asFolder() {
|
||||
return (_flags & Flag::IsThread)
|
||||
? nullptr
|
||||
: static_cast<Data::Folder*>(this);
|
||||
}
|
||||
|
||||
Data::Thread *Entry::asThread() {
|
||||
return (_flags & Flag::IsThread)
|
||||
? static_cast<Data::Thread*>(this)
|
||||
: nullptr;
|
||||
}
|
||||
|
||||
Data::ForumTopic *Entry::asTopic() {
|
||||
return (_flags & Flag::IsForumTopic)
|
||||
? static_cast<Data::ForumTopic*>(this)
|
||||
: nullptr;
|
||||
}
|
||||
|
||||
Data::SavedSublist *Entry::asSublist() {
|
||||
return (_flags & Flag::IsSavedSublist)
|
||||
? static_cast<Data::SavedSublist*>(this)
|
||||
: nullptr;
|
||||
}
|
||||
|
||||
const History *Entry::asHistory() const {
|
||||
return const_cast<Entry*>(this)->asHistory();
|
||||
}
|
||||
|
||||
const Data::Forum *Entry::asForum() const {
|
||||
return const_cast<Entry*>(this)->asForum();
|
||||
}
|
||||
|
||||
const Data::Folder *Entry::asFolder() const {
|
||||
return const_cast<Entry*>(this)->asFolder();
|
||||
}
|
||||
|
||||
const Data::Thread *Entry::asThread() const {
|
||||
return const_cast<Entry*>(this)->asThread();
|
||||
}
|
||||
|
||||
const Data::ForumTopic *Entry::asTopic() const {
|
||||
return const_cast<Entry*>(this)->asTopic();
|
||||
}
|
||||
|
||||
const Data::SavedSublist *Entry::asSublist() const {
|
||||
return const_cast<Entry*>(this)->asSublist();
|
||||
}
|
||||
|
||||
void Entry::pinnedIndexChanged(FilterId filterId, int was, int now) {
|
||||
if (!filterId && session().supportMode()) {
|
||||
// Force reorder in support mode.
|
||||
_sortKeyInChatList = 0;
|
||||
}
|
||||
updateChatListSortPosition();
|
||||
updateChatListEntry();
|
||||
if ((was != 0) != (now != 0)) {
|
||||
changedChatListPinHook();
|
||||
}
|
||||
}
|
||||
|
||||
void Entry::cachePinnedIndex(FilterId filterId, int index) {
|
||||
const auto i = _pinnedIndex.find(filterId);
|
||||
const auto was = (i != end(_pinnedIndex)) ? i->second : 0;
|
||||
if (index == was) {
|
||||
return;
|
||||
}
|
||||
if (!index) {
|
||||
_pinnedIndex.erase(i);
|
||||
} else if (!was) {
|
||||
_pinnedIndex.emplace(filterId, index);
|
||||
} else {
|
||||
i->second = index;
|
||||
}
|
||||
pinnedIndexChanged(filterId, was, index);
|
||||
}
|
||||
|
||||
bool Entry::needUpdateInChatList() const {
|
||||
return inChatList() || shouldBeInChatList();
|
||||
}
|
||||
|
||||
void Entry::updateChatListSortPosition() {
|
||||
if (session().supportMode()
|
||||
&& _sortKeyInChatList != 0
|
||||
&& session().settings().supportFixChatsOrder()) {
|
||||
updateChatListEntry();
|
||||
return;
|
||||
}
|
||||
_sortKeyByDate = DialogPosFromDate(adjustedChatListTimeId());
|
||||
const auto fixedIndex = fixedOnTopIndex();
|
||||
_sortKeyInChatList = fixedIndex
|
||||
? FixedOnTopDialogPos(fixedIndex)
|
||||
: computeSortPosition(0);
|
||||
if (needUpdateInChatList()) {
|
||||
setChatListExistence(true);
|
||||
} else {
|
||||
_sortKeyInChatList = _sortKeyByDate = 0;
|
||||
}
|
||||
}
|
||||
|
||||
int Entry::lookupPinnedIndex(FilterId filterId) const {
|
||||
if (filterId) {
|
||||
const auto i = _pinnedIndex.find(filterId);
|
||||
return (i != end(_pinnedIndex)) ? i->second : 0;
|
||||
} else if (!_pinnedIndex.empty()) {
|
||||
return _pinnedIndex.front().first
|
||||
? 0
|
||||
: _pinnedIndex.front().second;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
uint64 Entry::computeSortPosition(FilterId filterId) const {
|
||||
const auto index = lookupPinnedIndex(filterId);
|
||||
return index ? PinnedDialogPos(index) : _sortKeyByDate;
|
||||
}
|
||||
|
||||
void Entry::updateChatListExistence() {
|
||||
if (const auto history = asHistory()) {
|
||||
if (history->peer->asMonoforum()) {
|
||||
if (!folderKnown()) {
|
||||
history->clearFolder();
|
||||
}
|
||||
}
|
||||
}
|
||||
setChatListExistence(shouldBeInChatList());
|
||||
}
|
||||
|
||||
void Entry::notifyUnreadStateChange(const UnreadState &wasState) {
|
||||
Expects(folderKnown());
|
||||
Expects(inChatList());
|
||||
|
||||
const auto nowState = chatListUnreadState();
|
||||
owner().chatsListFor(this)->unreadStateChanged(wasState, nowState);
|
||||
auto &filters = owner().chatsFilters();
|
||||
for (const auto &[filterId, links] : _chatListLinks) {
|
||||
if (filterId) {
|
||||
filters.chatsList(filterId)->unreadStateChanged(
|
||||
wasState,
|
||||
nowState);
|
||||
}
|
||||
}
|
||||
if (const auto history = asHistory()) {
|
||||
session().changes().historyUpdated(
|
||||
history,
|
||||
Data::HistoryUpdate::Flag::UnreadView);
|
||||
const auto isForFilters = [](UnreadState state) {
|
||||
return state.messages || state.marks || state.mentions;
|
||||
};
|
||||
if (isForFilters(wasState) != isForFilters(nowState)) {
|
||||
const auto wasTags = _tagColors.size();
|
||||
owner().chatsFilters().refreshHistory(history);
|
||||
|
||||
// Hack for History::fakeUnreadWhileOpened().
|
||||
if (!isForFilters(nowState)
|
||||
&& (wasTags > 0)
|
||||
&& (wasTags == _tagColors.size())) {
|
||||
auto updateRequested = false;
|
||||
for (const auto &filter : filters.list()) {
|
||||
if (!(filter.flags() & Data::ChatFilter::Flag::NoRead)
|
||||
|| !_chatListLinks.contains(filter.id())
|
||||
|| filter.contains(history, true)) {
|
||||
continue;
|
||||
}
|
||||
const auto wasTagsCount = _tagColors.size();
|
||||
setColorIndexForFilterId(filter.id(), std::nullopt);
|
||||
updateRequested |= (wasTagsCount != _tagColors.size());
|
||||
}
|
||||
if (updateRequested) {
|
||||
updateChatListEntryHeight();
|
||||
session().changes().peerUpdated(
|
||||
history->peer,
|
||||
Data::PeerUpdate::Flag::Name);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (const auto sublist = asSublist()) {
|
||||
session().changes().sublistUpdated(
|
||||
sublist,
|
||||
Data::SublistUpdate::Flag::UnreadView);
|
||||
}
|
||||
updateChatListEntryPostponed();
|
||||
}
|
||||
|
||||
const Ui::Text::String &Entry::chatListNameText() const {
|
||||
const auto version = chatListNameVersion();
|
||||
if (_chatListNameVersion < version) {
|
||||
_chatListNameVersion = version;
|
||||
_chatListNameText.setText(
|
||||
st::semiboldTextStyle,
|
||||
chatListName(),
|
||||
Ui::NameTextOptions());
|
||||
}
|
||||
return _chatListNameText;
|
||||
}
|
||||
|
||||
void Entry::setChatListExistence(bool exists) {
|
||||
if (exists && _sortKeyInChatList) {
|
||||
owner().refreshChatListEntry(this);
|
||||
updateChatListEntry();
|
||||
} else {
|
||||
owner().removeChatListEntry(this);
|
||||
}
|
||||
}
|
||||
|
||||
TimeId Entry::adjustedChatListTimeId() const {
|
||||
return chatListTimeId();
|
||||
}
|
||||
|
||||
void Entry::changedChatListPinHook() {
|
||||
}
|
||||
|
||||
RowsByLetter *Entry::chatListLinks(FilterId filterId) {
|
||||
const auto i = _chatListLinks.find(filterId);
|
||||
return (i != end(_chatListLinks)) ? &i->second : nullptr;
|
||||
}
|
||||
|
||||
const RowsByLetter *Entry::chatListLinks(FilterId filterId) const {
|
||||
const auto i = _chatListLinks.find(filterId);
|
||||
return (i != end(_chatListLinks)) ? &i->second : nullptr;
|
||||
}
|
||||
|
||||
not_null<Row*> Entry::mainChatListLink(FilterId filterId) const {
|
||||
const auto links = chatListLinks(filterId);
|
||||
Assert(links != nullptr);
|
||||
return links->main;
|
||||
}
|
||||
|
||||
Row *Entry::maybeMainChatListLink(FilterId filterId) const {
|
||||
const auto links = chatListLinks(filterId);
|
||||
return links ? links->main.get() : nullptr;
|
||||
}
|
||||
|
||||
PositionChange Entry::adjustByPosInChatList(
|
||||
FilterId filterId,
|
||||
not_null<MainList*> list) {
|
||||
const auto links = chatListLinks(filterId);
|
||||
Assert(links != nullptr);
|
||||
const auto from = links->main->top();
|
||||
list->indexed()->adjustByDate(*links);
|
||||
const auto to = links->main->top();
|
||||
return { .from = from, .to = to, .height = links->main->height() };
|
||||
}
|
||||
|
||||
void Entry::setChatListTimeId(TimeId date) {
|
||||
_timeId = date;
|
||||
updateChatListSortPosition();
|
||||
if (const auto folder = this->folder()) {
|
||||
folder->updateChatListSortPosition();
|
||||
}
|
||||
}
|
||||
|
||||
int Entry::posInChatList(FilterId filterId) const {
|
||||
return mainChatListLink(filterId)->index();
|
||||
}
|
||||
|
||||
void Entry::setColorIndexForFilterId(
|
||||
FilterId filterId,
|
||||
std::optional<uint8> colorIndex) {
|
||||
if (!filterId) {
|
||||
return;
|
||||
}
|
||||
if (colorIndex) {
|
||||
_tagColors[filterId] = *colorIndex;
|
||||
} else {
|
||||
_tagColors.remove(filterId);
|
||||
}
|
||||
}
|
||||
|
||||
not_null<Row*> Entry::addToChatList(
|
||||
FilterId filterId,
|
||||
not_null<MainList*> list) {
|
||||
if (filterId) {
|
||||
const auto &list = owner().chatsFilters().list();
|
||||
const auto it = ranges::find(list, filterId, &Data::ChatFilter::id);
|
||||
if (it != end(list)) {
|
||||
setColorIndexForFilterId(filterId, it->colorIndex());
|
||||
}
|
||||
}
|
||||
if (const auto main = maybeMainChatListLink(filterId)) {
|
||||
return main;
|
||||
}
|
||||
return _chatListLinks.emplace(
|
||||
filterId,
|
||||
list->addEntry(this)
|
||||
).first->second.main;
|
||||
}
|
||||
|
||||
void Entry::removeFromChatList(
|
||||
FilterId filterId,
|
||||
not_null<MainList*> list) {
|
||||
if (isPinnedDialog(filterId)) {
|
||||
owner().setChatPinned(this, filterId, false);
|
||||
}
|
||||
if (filterId) {
|
||||
const auto it = _tagColors.find(filterId);
|
||||
if (it != end(_tagColors)) {
|
||||
_tagColors.erase(it);
|
||||
}
|
||||
}
|
||||
|
||||
const auto i = _chatListLinks.find(filterId);
|
||||
if (i == end(_chatListLinks)) {
|
||||
return;
|
||||
}
|
||||
_chatListLinks.erase(i);
|
||||
list->removeEntry(this);
|
||||
}
|
||||
|
||||
void Entry::removeChatListEntryByLetter(FilterId filterId, QChar letter) {
|
||||
const auto i = _chatListLinks.find(filterId);
|
||||
if (i != end(_chatListLinks)) {
|
||||
i->second.letters.remove(letter);
|
||||
}
|
||||
}
|
||||
|
||||
void Entry::addChatListEntryByLetter(
|
||||
FilterId filterId,
|
||||
QChar letter,
|
||||
not_null<Row*> row) {
|
||||
const auto i = _chatListLinks.find(filterId);
|
||||
if (i != end(_chatListLinks)) {
|
||||
i->second.letters.emplace(letter, row);
|
||||
}
|
||||
}
|
||||
|
||||
void Entry::updateChatListEntry() {
|
||||
_flags &= ~Flag::UpdatePostponed;
|
||||
session().changes().entryUpdated(this, Data::EntryUpdate::Flag::Repaint);
|
||||
}
|
||||
|
||||
void Entry::updateChatListEntryPostponed() {
|
||||
if (_flags & Flag::UpdatePostponed) {
|
||||
return;
|
||||
}
|
||||
_flags |= Flag::UpdatePostponed;
|
||||
Ui::PostponeCall(this, [=] {
|
||||
if (_flags & Flag::UpdatePostponed) {
|
||||
updateChatListEntry();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void Entry::updateChatListEntryHeight() {
|
||||
session().changes().entryUpdated(this, Data::EntryUpdate::Flag::Height);
|
||||
}
|
||||
|
||||
bool Entry::hasChatsFilterTags(FilterId exclude) const {
|
||||
if (!owner().chatsFilters().tagsEnabled()) {
|
||||
return false;
|
||||
}
|
||||
if (exclude) {
|
||||
if (_tagColors.size() == 1) {
|
||||
if (_tagColors.begin()->first == exclude) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return !_tagColors.empty();
|
||||
}
|
||||
|
||||
} // namespace Dialogs
|
||||
217
Telegram/SourceFiles/dialogs/dialogs_entry.h
Normal file
217
Telegram/SourceFiles/dialogs/dialogs_entry.h
Normal file
@@ -0,0 +1,217 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "base/flat_map.h"
|
||||
#include "base/weak_ptr.h"
|
||||
#include "base/flags.h"
|
||||
#include "dialogs/dialogs_common.h"
|
||||
#include "ui/unread_badge.h"
|
||||
|
||||
class HistoryItem;
|
||||
class History;
|
||||
class UserData;
|
||||
|
||||
namespace Main {
|
||||
class Session;
|
||||
} // namespace Main
|
||||
|
||||
namespace Data {
|
||||
class Session;
|
||||
class Forum;
|
||||
class Folder;
|
||||
class ForumTopic;
|
||||
class SavedSublist;
|
||||
class SavedMessages;
|
||||
class Thread;
|
||||
} // namespace Data
|
||||
|
||||
namespace Ui {
|
||||
struct PeerUserpicView;
|
||||
} // namespace Ui
|
||||
|
||||
namespace Dialogs::Ui {
|
||||
using namespace ::Ui;
|
||||
struct PaintContext;
|
||||
} // namespace Dialogs::Ui
|
||||
|
||||
namespace Dialogs {
|
||||
|
||||
struct UnreadState;
|
||||
class Row;
|
||||
class IndexedList;
|
||||
class MainList;
|
||||
|
||||
[[nodiscard]] BadgesState BadgesForUnread(
|
||||
const UnreadState &state,
|
||||
CountInBadge count = CountInBadge::Default,
|
||||
IncludeInBadge include = IncludeInBadge::Default);
|
||||
|
||||
class Entry : public base::has_weak_ptr {
|
||||
public:
|
||||
enum class Type : uchar {
|
||||
History,
|
||||
Folder,
|
||||
ForumTopic,
|
||||
SavedSublist,
|
||||
};
|
||||
Entry(not_null<Data::Session*> owner, Type type);
|
||||
virtual ~Entry();
|
||||
|
||||
[[nodiscard]] Data::Session &owner() const;
|
||||
[[nodiscard]] Main::Session &session() const;
|
||||
|
||||
History *asHistory();
|
||||
Data::Forum *asForum();
|
||||
Data::Folder *asFolder();
|
||||
Data::Thread *asThread();
|
||||
Data::ForumTopic *asTopic();
|
||||
Data::SavedSublist *asSublist();
|
||||
|
||||
const History *asHistory() const;
|
||||
const Data::Forum *asForum() const;
|
||||
const Data::Folder *asFolder() const;
|
||||
const Data::Thread *asThread() const;
|
||||
const Data::ForumTopic *asTopic() const;
|
||||
const Data::SavedSublist *asSublist() const;
|
||||
|
||||
PositionChange adjustByPosInChatList(
|
||||
FilterId filterId,
|
||||
not_null<MainList*> list);
|
||||
[[nodiscard]] bool inChatList(FilterId filterId = 0) const {
|
||||
return _chatListLinks.contains(filterId);
|
||||
}
|
||||
RowsByLetter *chatListLinks(FilterId filterId);
|
||||
const RowsByLetter *chatListLinks(FilterId filterId) const;
|
||||
[[nodiscard]] int posInChatList(FilterId filterId) const;
|
||||
not_null<Row*> addToChatList(
|
||||
FilterId filterId,
|
||||
not_null<MainList*> list);
|
||||
void setColorIndexForFilterId(FilterId, std::optional<uint8>);
|
||||
void removeFromChatList(
|
||||
FilterId filterId,
|
||||
not_null<MainList*> list);
|
||||
void removeChatListEntryByLetter(FilterId filterId, QChar letter);
|
||||
void addChatListEntryByLetter(
|
||||
FilterId filterId,
|
||||
QChar letter,
|
||||
not_null<Row*> row);
|
||||
void updateChatListEntry();
|
||||
void updateChatListEntryPostponed();
|
||||
void updateChatListEntryHeight();
|
||||
[[nodiscard]] bool isPinnedDialog(FilterId filterId) const {
|
||||
return lookupPinnedIndex(filterId) != 0;
|
||||
}
|
||||
void cachePinnedIndex(FilterId filterId, int index);
|
||||
[[nodiscard]] uint64 sortKeyInChatList(FilterId filterId) const {
|
||||
return filterId
|
||||
? computeSortPosition(filterId)
|
||||
: _sortKeyInChatList;
|
||||
}
|
||||
void updateChatListSortPosition();
|
||||
void setChatListTimeId(TimeId date);
|
||||
virtual void updateChatListExistence();
|
||||
bool needUpdateInChatList() const;
|
||||
[[nodiscard]] virtual TimeId adjustedChatListTimeId() const;
|
||||
|
||||
[[nodiscard]] virtual int fixedOnTopIndex() const = 0;
|
||||
static constexpr auto kArchiveFixOnTopIndex = 1;
|
||||
static constexpr auto kTopPromotionFixOnTopIndex = 2;
|
||||
|
||||
[[nodiscard]] virtual bool shouldBeInChatList() const = 0;
|
||||
[[nodiscard]] virtual UnreadState chatListUnreadState() const = 0;
|
||||
[[nodiscard]] virtual BadgesState chatListBadgesState() const = 0;
|
||||
[[nodiscard]] virtual HistoryItem *chatListMessage() const = 0;
|
||||
[[nodiscard]] virtual bool chatListMessageKnown() const = 0;
|
||||
[[nodiscard]] virtual const QString &chatListName() const = 0;
|
||||
[[nodiscard]] virtual const QString &chatListNameSortKey() const = 0;
|
||||
[[nodiscard]] virtual int chatListNameVersion() const = 0;
|
||||
[[nodiscard]] virtual auto chatListNameWords() const
|
||||
-> const base::flat_set<QString> & = 0;
|
||||
[[nodiscard]] virtual auto chatListFirstLetters() const
|
||||
-> const base::flat_set<QChar> & = 0;
|
||||
|
||||
[[nodiscard]] virtual bool folderKnown() const {
|
||||
return true;
|
||||
}
|
||||
[[nodiscard]] virtual Data::Folder *folder() const {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
virtual void chatListPreloadData() = 0;
|
||||
virtual void paintUserpic(
|
||||
Painter &p,
|
||||
Ui::PeerUserpicView &view,
|
||||
const Ui::PaintContext &context) const = 0;
|
||||
|
||||
[[nodiscard]] TimeId chatListTimeId() const {
|
||||
return _timeId;
|
||||
}
|
||||
|
||||
[[nodiscard]] const Ui::Text::String &chatListNameText() const;
|
||||
[[nodiscard]] Ui::PeerBadge &chatListPeerBadge() const {
|
||||
return _chatListPeerBadge;
|
||||
}
|
||||
|
||||
[[nodiscard]] bool hasChatsFilterTags(FilterId exclude) const;
|
||||
protected:
|
||||
void notifyUnreadStateChange(const UnreadState &wasState);
|
||||
inline auto unreadStateChangeNotifier(bool required);
|
||||
|
||||
[[nodiscard]] int lookupPinnedIndex(FilterId filterId) const;
|
||||
|
||||
private:
|
||||
enum class Flag : uchar {
|
||||
IsThread = (1 << 0),
|
||||
IsHistory = (1 << 1),
|
||||
IsForumTopic = (1 << 2),
|
||||
IsSavedSublist = (1 << 3),
|
||||
UpdatePostponed = (1 << 4),
|
||||
InUnreadChangeBlock = (1 << 5),
|
||||
};
|
||||
friend inline constexpr bool is_flag_type(Flag) { return true; }
|
||||
using Flags = base::flags<Flag>;
|
||||
|
||||
virtual void changedChatListPinHook();
|
||||
void pinnedIndexChanged(FilterId filterId, int was, int now);
|
||||
[[nodiscard]] uint64 computeSortPosition(FilterId filterId) const;
|
||||
|
||||
void setChatListExistence(bool exists);
|
||||
not_null<Row*> mainChatListLink(FilterId filterId) const;
|
||||
Row *maybeMainChatListLink(FilterId filterId) const;
|
||||
|
||||
const not_null<Data::Session*> _owner;
|
||||
base::flat_map<FilterId, RowsByLetter> _chatListLinks;
|
||||
uint64 _sortKeyInChatList = 0;
|
||||
uint64 _sortKeyByDate = 0;
|
||||
base::flat_map<FilterId, int> _pinnedIndex;
|
||||
base::flat_map<FilterId, uint8> _tagColors;
|
||||
mutable Ui::PeerBadge _chatListPeerBadge;
|
||||
mutable Ui::Text::String _chatListNameText;
|
||||
mutable int _chatListNameVersion = 0;
|
||||
TimeId _timeId = 0;
|
||||
Flags _flags;
|
||||
|
||||
};
|
||||
|
||||
auto Entry::unreadStateChangeNotifier(bool required) {
|
||||
Expects(!(_flags & Flag::InUnreadChangeBlock));
|
||||
|
||||
_flags |= Flag::InUnreadChangeBlock;
|
||||
const auto notify = required && inChatList();
|
||||
const auto wasState = notify ? chatListUnreadState() : UnreadState();
|
||||
return gsl::finally([=, this] {
|
||||
_flags &= ~Flag::InUnreadChangeBlock;
|
||||
if (notify) {
|
||||
Assert(inChatList());
|
||||
notifyUnreadStateChange(wasState);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
} // namespace Dialogs
|
||||
262
Telegram/SourceFiles/dialogs/dialogs_indexed_list.cpp
Normal file
262
Telegram/SourceFiles/dialogs/dialogs_indexed_list.cpp
Normal file
@@ -0,0 +1,262 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#include "dialogs/dialogs_indexed_list.h"
|
||||
|
||||
#include "main/main_session.h"
|
||||
#include "data/data_session.h"
|
||||
#include "history/history.h"
|
||||
|
||||
namespace Dialogs {
|
||||
|
||||
IndexedList::IndexedList(SortMode sortMode, FilterId filterId)
|
||||
: _sortMode(sortMode)
|
||||
, _filterId(filterId)
|
||||
, _list(sortMode, filterId)
|
||||
, _empty(sortMode, filterId) {
|
||||
}
|
||||
|
||||
RowsByLetter IndexedList::addToEnd(Key key) {
|
||||
if (const auto row = _list.getRow(key)) {
|
||||
return { row };
|
||||
}
|
||||
|
||||
auto result = RowsByLetter{ _list.addToEnd(key) };
|
||||
for (const auto &ch : key.entry()->chatListFirstLetters()) {
|
||||
auto j = _index.find(ch);
|
||||
if (j == _index.cend()) {
|
||||
j = _index.emplace(ch, _sortMode, _filterId).first;
|
||||
}
|
||||
result.letters.emplace(ch, j->second.addToEnd(key));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
Row *IndexedList::addByName(Key key) {
|
||||
if (const auto row = _list.getRow(key)) {
|
||||
return row;
|
||||
}
|
||||
|
||||
const auto result = _list.addByName(key);
|
||||
for (const auto &ch : key.entry()->chatListFirstLetters()) {
|
||||
auto j = _index.find(ch);
|
||||
if (j == _index.cend()) {
|
||||
j = _index.emplace(ch, _sortMode, _filterId).first;
|
||||
}
|
||||
j->second.addByName(key);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
void IndexedList::adjustByDate(const RowsByLetter &links) {
|
||||
_list.adjustByDate(links.main);
|
||||
for (const auto &[ch, row] : links.letters) {
|
||||
if (auto it = _index.find(ch); it != _index.cend()) {
|
||||
it->second.adjustByDate(row);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool IndexedList::updateHeights(float64 narrowRatio) {
|
||||
return _list.updateHeights(narrowRatio);
|
||||
}
|
||||
|
||||
bool IndexedList::updateHeight(Key key, float64 narrowRatio) {
|
||||
return _list.updateHeight(key, narrowRatio);
|
||||
}
|
||||
|
||||
void IndexedList::moveToTop(Key key) {
|
||||
if (_list.moveToTop(key)) {
|
||||
for (const auto &ch : key.entry()->chatListFirstLetters()) {
|
||||
if (auto it = _index.find(ch); it != _index.cend()) {
|
||||
it->second.moveToTop(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void IndexedList::movePinned(Row *row, int deltaSign) {
|
||||
auto swapPinnedIndexWith = find(row);
|
||||
Assert(swapPinnedIndexWith != cend());
|
||||
if (deltaSign > 0) {
|
||||
++swapPinnedIndexWith;
|
||||
} else {
|
||||
Assert(swapPinnedIndexWith != cbegin());
|
||||
--swapPinnedIndexWith;
|
||||
}
|
||||
row->key().entry()->owner().reorderTwoPinnedChats(
|
||||
_filterId,
|
||||
row->key(),
|
||||
(*swapPinnedIndexWith)->key());
|
||||
}
|
||||
|
||||
void IndexedList::peerNameChanged(
|
||||
not_null<PeerData*> peer,
|
||||
const base::flat_set<QChar> &oldLetters) {
|
||||
Expects(_sortMode != SortMode::Date);
|
||||
|
||||
if (const auto history = peer->owner().historyLoaded(peer)) {
|
||||
if (_sortMode == SortMode::Name) {
|
||||
adjustByName(history, oldLetters);
|
||||
} else {
|
||||
adjustNames(FilterId(), history, oldLetters);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void IndexedList::peerNameChanged(
|
||||
FilterId filterId,
|
||||
not_null<PeerData*> peer,
|
||||
const base::flat_set<QChar> &oldLetters) {
|
||||
Expects(_sortMode == SortMode::Date);
|
||||
|
||||
if (const auto history = peer->owner().historyLoaded(peer)) {
|
||||
adjustNames(filterId, history, oldLetters);
|
||||
}
|
||||
}
|
||||
|
||||
void IndexedList::adjustByName(
|
||||
Key key,
|
||||
const base::flat_set<QChar> &oldLetters) {
|
||||
Expects(_sortMode == SortMode::Name);
|
||||
|
||||
const auto mainRow = _list.adjustByName(key);
|
||||
if (!mainRow) return;
|
||||
|
||||
auto toRemove = oldLetters;
|
||||
auto toAdd = base::flat_set<QChar>();
|
||||
for (const auto &ch : key.entry()->chatListFirstLetters()) {
|
||||
auto j = toRemove.find(ch);
|
||||
if (j == toRemove.cend()) {
|
||||
toAdd.insert(ch);
|
||||
} else {
|
||||
toRemove.erase(j);
|
||||
if (auto it = _index.find(ch); it != _index.cend()) {
|
||||
it->second.adjustByName(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (auto ch : toRemove) {
|
||||
if (auto it = _index.find(ch); it != _index.cend()) {
|
||||
it->second.remove(key, mainRow);
|
||||
}
|
||||
}
|
||||
if (!toAdd.empty()) {
|
||||
for (auto ch : toAdd) {
|
||||
auto j = _index.find(ch);
|
||||
if (j == _index.cend()) {
|
||||
j = _index.emplace(ch, _sortMode, _filterId).first;
|
||||
}
|
||||
j->second.addByName(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void IndexedList::adjustNames(
|
||||
FilterId filterId,
|
||||
not_null<History*> history,
|
||||
const base::flat_set<QChar> &oldLetters) {
|
||||
const auto key = Dialogs::Key(history);
|
||||
auto mainRow = _list.getRow(key);
|
||||
if (!mainRow) return;
|
||||
|
||||
auto toRemove = oldLetters;
|
||||
auto toAdd = base::flat_set<QChar>();
|
||||
for (const auto &ch : key.entry()->chatListFirstLetters()) {
|
||||
auto j = toRemove.find(ch);
|
||||
if (j == toRemove.cend()) {
|
||||
toAdd.insert(ch);
|
||||
} else {
|
||||
toRemove.erase(j);
|
||||
}
|
||||
}
|
||||
for (auto ch : toRemove) {
|
||||
if (_sortMode == SortMode::Date) {
|
||||
history->removeChatListEntryByLetter(filterId, ch);
|
||||
}
|
||||
if (auto it = _index.find(ch); it != _index.cend()) {
|
||||
it->second.remove(key, mainRow);
|
||||
}
|
||||
}
|
||||
for (auto ch : toAdd) {
|
||||
auto j = _index.find(ch);
|
||||
if (j == _index.cend()) {
|
||||
j = _index.emplace(ch, _sortMode, _filterId).first;
|
||||
}
|
||||
auto row = j->second.addToEnd(key);
|
||||
if (_sortMode == SortMode::Date) {
|
||||
history->addChatListEntryByLetter(filterId, ch, row);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void IndexedList::remove(Key key, Row *replacedBy) {
|
||||
if (_list.remove(key, replacedBy)) {
|
||||
for (const auto &ch : key.entry()->chatListFirstLetters()) {
|
||||
if (const auto it = _index.find(ch); it != _index.cend()) {
|
||||
it->second.remove(key, replacedBy);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void IndexedList::clear() {
|
||||
_list.clear();
|
||||
_index.clear();
|
||||
}
|
||||
|
||||
std::vector<not_null<Row*>> IndexedList::filtered(
|
||||
const QStringList &words) const {
|
||||
const auto minimal = [&]() -> const Dialogs::List* {
|
||||
if (empty()) {
|
||||
return nullptr;
|
||||
}
|
||||
auto result = (const Dialogs::List*)nullptr;
|
||||
for (const auto &word : words) {
|
||||
if (word.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
const auto found = filtered(word[0]);
|
||||
if (!found || found->empty()) {
|
||||
return nullptr;
|
||||
} else if (!result || result->size() > found->size()) {
|
||||
result = found;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}();
|
||||
auto result = std::vector<not_null<Row*>>();
|
||||
if (!minimal || minimal->empty()) {
|
||||
return result;
|
||||
}
|
||||
result.reserve(minimal->size());
|
||||
for (const auto &row : *minimal) {
|
||||
const auto &nameWords = row->entry()->chatListNameWords();
|
||||
const auto found = [&](const QString &word) {
|
||||
for (const auto &name : nameWords) {
|
||||
if (name.startsWith(word)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
const auto allFound = [&] {
|
||||
for (const auto &word : words) {
|
||||
if (!found(word)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}();
|
||||
if (allFound) {
|
||||
result.push_back(row);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace Dialogs
|
||||
103
Telegram/SourceFiles/dialogs/dialogs_indexed_list.h
Normal file
103
Telegram/SourceFiles/dialogs/dialogs_indexed_list.h
Normal file
@@ -0,0 +1,103 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "dialogs/dialogs_list.h"
|
||||
|
||||
class History;
|
||||
|
||||
namespace Dialogs {
|
||||
|
||||
struct RowsByLetter;
|
||||
class Row;
|
||||
|
||||
class IndexedList {
|
||||
public:
|
||||
IndexedList(SortMode sortMode, FilterId filterId = 0);
|
||||
|
||||
RowsByLetter addToEnd(Key key);
|
||||
Row *addByName(Key key);
|
||||
void adjustByDate(const RowsByLetter &links);
|
||||
void moveToTop(Key key);
|
||||
bool updateHeight(Key key, float64 narrowRatio);
|
||||
bool updateHeights(float64 narrowRatio);
|
||||
|
||||
// row must belong to this indexed list all().
|
||||
void movePinned(Row *row, int deltaSign);
|
||||
|
||||
// For sortMode != SortMode::Date && != Complex
|
||||
void peerNameChanged(
|
||||
not_null<PeerData*> peer,
|
||||
const base::flat_set<QChar> &oldChars);
|
||||
|
||||
//For sortMode == SortMode::Date || == Complex
|
||||
void peerNameChanged(
|
||||
FilterId filterId,
|
||||
not_null<PeerData*> peer,
|
||||
const base::flat_set<QChar> &oldChars);
|
||||
|
||||
void remove(Key key, Row *replacedBy = nullptr);
|
||||
void clear();
|
||||
|
||||
[[nodiscard]] const List &all() const {
|
||||
return _list;
|
||||
}
|
||||
[[nodiscard]] const List *filtered(QChar ch) const {
|
||||
const auto i = _index.find(ch);
|
||||
return (i != _index.end()) ? &i->second : nullptr;
|
||||
}
|
||||
[[nodiscard]] std::vector<not_null<Row*>> filtered(
|
||||
const QStringList &words) const;
|
||||
|
||||
// Part of List interface is duplicated here for all() list.
|
||||
[[nodiscard]] int size() const { return all().size(); }
|
||||
[[nodiscard]] bool empty() const { return all().empty(); }
|
||||
[[nodiscard]] int height() const { return all().height(); }
|
||||
[[nodiscard]] bool contains(Key key) const {
|
||||
return all().contains(key);
|
||||
}
|
||||
[[nodiscard]] Row *getRow(Key key) const { return all().getRow(key); }
|
||||
[[nodiscard]] Row *rowAtY(int y) const { return all().rowAtY(y); }
|
||||
|
||||
using iterator = List::iterator;
|
||||
using const_iterator = List::const_iterator;
|
||||
[[nodiscard]] const_iterator cbegin() const { return all().cbegin(); }
|
||||
[[nodiscard]] const_iterator cend() const { return all().cend(); }
|
||||
[[nodiscard]] const_iterator begin() const { return all().cbegin(); }
|
||||
[[nodiscard]] const_iterator end() const { return all().cend(); }
|
||||
[[nodiscard]] iterator begin() { return all().begin(); }
|
||||
[[nodiscard]] iterator end() { return all().end(); }
|
||||
[[nodiscard]] const_iterator cfind(Row *value) const {
|
||||
return all().cfind(value);
|
||||
}
|
||||
[[nodiscard]] const_iterator find(Row *value) const {
|
||||
return all().cfind(value);
|
||||
}
|
||||
[[nodiscard]] iterator find(Row *value) { return all().find(value); }
|
||||
[[nodiscard]] const_iterator findByY(int y) const {
|
||||
return all().findByY(y);
|
||||
}
|
||||
[[nodiscard]] iterator findByY(int y) { return all().findByY(y); }
|
||||
|
||||
private:
|
||||
void adjustByName(
|
||||
Key key,
|
||||
const base::flat_set<QChar> &oldChars);
|
||||
void adjustNames(
|
||||
FilterId filterId,
|
||||
not_null<History*> history,
|
||||
const base::flat_set<QChar> &oldChars);
|
||||
|
||||
SortMode _sortMode = SortMode();
|
||||
FilterId _filterId = 0;
|
||||
List _list, _empty;
|
||||
base::flat_map<QChar, List> _index;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Dialogs
|
||||
5590
Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp
Normal file
5590
Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp
Normal file
File diff suppressed because it is too large
Load Diff
689
Telegram/SourceFiles/dialogs/dialogs_inner_widget.h
Normal file
689
Telegram/SourceFiles/dialogs/dialogs_inner_widget.h
Normal file
@@ -0,0 +1,689 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "base/flags.h"
|
||||
#include "base/object_ptr.h"
|
||||
#include "base/timer.h"
|
||||
#include "dialogs/dialogs_key.h"
|
||||
#include "dialogs/ui/dialogs_quick_action_context.h"
|
||||
#include "data/data_messages.h"
|
||||
#include "ui/dragging_scroll_manager.h"
|
||||
#include "ui/effects/animations.h"
|
||||
#include "ui/rp_widget.h"
|
||||
#include "ui/userpic_view.h"
|
||||
|
||||
namespace style {
|
||||
struct DialogRow;
|
||||
struct DialogRightButton;
|
||||
} // namespace style
|
||||
|
||||
namespace Api {
|
||||
struct PeerSearchResult;
|
||||
} // namespace Api
|
||||
|
||||
namespace MTP {
|
||||
class Error;
|
||||
} // namespace MTP
|
||||
|
||||
namespace Lottie {
|
||||
class Icon;
|
||||
} // namespace Lottie
|
||||
|
||||
namespace Main {
|
||||
class Session;
|
||||
} // namespace Main
|
||||
|
||||
namespace Ui {
|
||||
class IconButton;
|
||||
class PopupMenu;
|
||||
class FlatLabel;
|
||||
class VerticalLayout;
|
||||
class RoundButton;
|
||||
struct ScrollToRequest;
|
||||
namespace Controls {
|
||||
enum class QuickDialogAction;
|
||||
} // namespace Controls
|
||||
} // namespace Ui
|
||||
|
||||
namespace Window {
|
||||
class SessionController;
|
||||
} // namespace Window
|
||||
|
||||
namespace Data {
|
||||
class ChatFilter;
|
||||
class Thread;
|
||||
class Folder;
|
||||
class Forum;
|
||||
class SavedMessages;
|
||||
struct ReactionId;
|
||||
} // namespace Data
|
||||
|
||||
namespace Dialogs::Ui {
|
||||
using namespace ::Ui;
|
||||
class VideoUserpic;
|
||||
struct PaintContext;
|
||||
struct TopicJumpCache;
|
||||
} // namespace Dialogs::Ui
|
||||
|
||||
namespace Dialogs {
|
||||
|
||||
class Row;
|
||||
class FakeRow;
|
||||
class IndexedList;
|
||||
class SearchTags;
|
||||
class SearchEmpty;
|
||||
class ChatSearchIn;
|
||||
enum class HashOrCashtag : uchar;
|
||||
struct RightButton;
|
||||
enum class ChatTypeFilter : uchar;
|
||||
|
||||
struct ChosenRow {
|
||||
Key key;
|
||||
Data::MessagePosition message;
|
||||
MsgId topicJumpRootId;
|
||||
PeerId sublistJumpPeerId;
|
||||
QByteArray sponsoredRandomId;
|
||||
bool userpicClick : 1 = false;
|
||||
bool filteredRow : 1 = false;
|
||||
bool newWindow : 1 = false;
|
||||
};
|
||||
|
||||
struct SearchRequestType {
|
||||
bool migrated : 1 = false;
|
||||
bool posts : 1 = false;
|
||||
bool start : 1 = false;
|
||||
bool peer : 1 = false;
|
||||
|
||||
friend inline constexpr auto operator<=>(
|
||||
SearchRequestType a,
|
||||
SearchRequestType b) = default;
|
||||
friend inline constexpr bool operator==(
|
||||
SearchRequestType a,
|
||||
SearchRequestType b) = default;
|
||||
};
|
||||
|
||||
enum class SearchRequestDelay : uchar {
|
||||
InCache,
|
||||
Instant,
|
||||
Delayed,
|
||||
};
|
||||
|
||||
enum class WidgetState {
|
||||
Default,
|
||||
Filtered,
|
||||
};
|
||||
|
||||
class InnerWidget final : public Ui::RpWidget {
|
||||
public:
|
||||
using ChatsFilterTagsKey = int64;
|
||||
|
||||
struct ChildListShown {
|
||||
PeerId peerId = 0;
|
||||
float64 shown = 0.;
|
||||
};
|
||||
InnerWidget(
|
||||
QWidget *parent,
|
||||
not_null<Window::SessionController*> controller,
|
||||
rpl::producer<ChildListShown> childListShown);
|
||||
|
||||
void searchReceived(
|
||||
std::vector<not_null<HistoryItem*>> result,
|
||||
HistoryItem *inject,
|
||||
SearchRequestType type,
|
||||
int fullCount);
|
||||
void peerSearchReceived(Api::PeerSearchResult result);
|
||||
|
||||
[[nodiscard]] FilterId filterId() const;
|
||||
|
||||
void clearSelection();
|
||||
|
||||
void changeOpenedFolder(Data::Folder *folder);
|
||||
void changeOpenedForum(Data::Forum *forum);
|
||||
void showSavedSublists();
|
||||
void selectSkip(int32 direction);
|
||||
void selectSkipPage(int32 pixels, int32 direction);
|
||||
|
||||
void dragLeft();
|
||||
void setNarrowRatio(float64 narrowRatio);
|
||||
|
||||
void clearFilter();
|
||||
void refresh(bool toTop = false);
|
||||
void refreshEmpty();
|
||||
void resizeEmpty();
|
||||
|
||||
void showPeerMenu();
|
||||
|
||||
[[nodiscard]] bool isUserpicPress() const;
|
||||
[[nodiscard]] bool isUserpicPressOnWide() const;
|
||||
void cancelChatPreview();
|
||||
bool scheduleChatPreview(QPoint positionOverride);
|
||||
bool showChatPreview();
|
||||
void chatPreviewShown(bool shown, RowDescriptor row = {});
|
||||
bool chooseRow(
|
||||
Qt::KeyboardModifiers modifiers = {},
|
||||
MsgId pressedTopicRootId = {},
|
||||
PeerId pressedSublistPeerId = {});
|
||||
|
||||
void scrollToEntry(const RowDescriptor &entry);
|
||||
|
||||
[[nodiscard]] Data::Folder *shownFolder() const;
|
||||
[[nodiscard]] Data::Forum *shownForum() const;
|
||||
|
||||
[[nodiscard]] WidgetState state() const;
|
||||
[[nodiscard]] not_null<const style::DialogRow*> st() const {
|
||||
return _st;
|
||||
}
|
||||
[[nodiscard]] bool hasFilteredResults() const;
|
||||
|
||||
void searchRequested(bool loading);
|
||||
void applySearchState(SearchState state);
|
||||
[[nodiscard]] auto searchTagsChanges() const
|
||||
-> rpl::producer<std::vector<Data::ReactionId>>;
|
||||
|
||||
void onHashtagFilterUpdate(QStringView newFilter);
|
||||
void appendToFiltered(Key key);
|
||||
|
||||
Data::Thread *updateFromParentDrag(QPoint globalPosition);
|
||||
|
||||
void setLoadMoreCallback(Fn<void()> callback);
|
||||
void setLoadMoreFilteredCallback(Fn<void()> callback);
|
||||
[[nodiscard]] rpl::producer<> listBottomReached() const;
|
||||
[[nodiscard]] auto changeSearchTabRequests() const
|
||||
-> rpl::producer<ChatSearchTab>;
|
||||
[[nodiscard]] auto changeSearchFilterRequests() const
|
||||
-> rpl::producer<ChatTypeFilter>;
|
||||
[[nodiscard]] rpl::producer<> cancelSearchRequests() const;
|
||||
[[nodiscard]] rpl::producer<> cancelSearchFromRequests() const;
|
||||
[[nodiscard]] rpl::producer<> changeSearchFromRequests() const;
|
||||
[[nodiscard]] rpl::producer<ChosenRow> chosenRow() const;
|
||||
[[nodiscard]] rpl::producer<> updated() const;
|
||||
|
||||
[[nodiscard]] rpl::producer<int> scrollByDeltaRequests() const;
|
||||
[[nodiscard]] rpl::producer<Ui::ScrollToRequest> mustScrollTo() const;
|
||||
[[nodiscard]] rpl::producer<Ui::ScrollToRequest> dialogMoved() const;
|
||||
[[nodiscard]] rpl::producer<SearchRequestDelay> searchRequests() const;
|
||||
[[nodiscard]] rpl::producer<QString> completeHashtagRequests() const;
|
||||
[[nodiscard]] rpl::producer<> refreshHashtagsRequests() const;
|
||||
|
||||
[[nodiscard]] RowDescriptor resolveChatNext(RowDescriptor from = {}) const;
|
||||
[[nodiscard]] RowDescriptor resolveChatPrevious(RowDescriptor from = {}) const;
|
||||
|
||||
~InnerWidget();
|
||||
|
||||
void parentGeometryChanged();
|
||||
|
||||
bool processTouchEvent(not_null<QTouchEvent*> e);
|
||||
[[nodiscard]] rpl::producer<> touchCancelRequests() const {
|
||||
return _touchCancelRequests.events();
|
||||
}
|
||||
|
||||
[[nodiscard]] rpl::producer<UserId> openBotMainAppRequests() const;
|
||||
|
||||
void setSwipeContextData(
|
||||
int64 key,
|
||||
std::optional<Ui::Controls::SwipeContextData> data);
|
||||
[[nodiscard]] int64 calcSwipeKey(int top);
|
||||
void prepareQuickAction(int64 key, Dialogs::Ui::QuickDialogAction);
|
||||
void clearQuickActions();
|
||||
|
||||
protected:
|
||||
void visibleTopBottomUpdated(
|
||||
int visibleTop,
|
||||
int visibleBottom) override;
|
||||
|
||||
void paintEvent(QPaintEvent *e) override;
|
||||
void mouseMoveEvent(QMouseEvent *e) override;
|
||||
void mousePressEvent(QMouseEvent *e) override;
|
||||
void mouseReleaseEvent(QMouseEvent *e) override;
|
||||
void resizeEvent(QResizeEvent *e) override;
|
||||
void enterEventHook(QEnterEvent *e) override;
|
||||
void leaveEventHook(QEvent *e) override;
|
||||
void contextMenuEvent(QContextMenuEvent *e) override;
|
||||
|
||||
private:
|
||||
struct CollapsedRow;
|
||||
struct HashtagResult;
|
||||
struct SponsoredSearchResult;
|
||||
struct PeerSearchResult;
|
||||
struct TagCache;
|
||||
|
||||
enum class JumpSkip {
|
||||
PreviousOrBegin,
|
||||
NextOrEnd,
|
||||
PreviousOrOriginal,
|
||||
NextOrOriginal,
|
||||
};
|
||||
|
||||
enum class EmptyState : uchar {
|
||||
None,
|
||||
Loading,
|
||||
NoContacts,
|
||||
EmptyFolder,
|
||||
EmptyForum,
|
||||
EmptySavedSublists,
|
||||
};
|
||||
|
||||
struct PinnedRow {
|
||||
anim::value yadd;
|
||||
crl::time animStartTime = 0;
|
||||
};
|
||||
|
||||
struct FilterResult {
|
||||
FilterResult(not_null<Row*> row) : row(row) {
|
||||
}
|
||||
|
||||
not_null<Row*> row;
|
||||
int top = 0;
|
||||
|
||||
[[nodiscard]] Key key() const;
|
||||
[[nodiscard]] int bottom() const;
|
||||
};
|
||||
|
||||
Main::Session &session() const;
|
||||
|
||||
void dialogRowReplaced(Row *oldRow, Row *newRow);
|
||||
|
||||
void setState(WidgetState state);
|
||||
void editOpenedFilter();
|
||||
void repaintCollapsedFolderRow(not_null<Data::Folder*> folder);
|
||||
void refreshWithCollapsedRows(bool toTop = false);
|
||||
bool needCollapsedRowsRefresh() const;
|
||||
bool chooseCollapsedRow(Qt::KeyboardModifiers modifiers);
|
||||
void switchToFilter(FilterId filterId);
|
||||
bool chooseHashtag();
|
||||
ChosenRow computeChosenRow() const;
|
||||
bool isRowActive(not_null<Row*> row, const RowDescriptor &entry) const;
|
||||
bool isSearchResultActive(
|
||||
not_null<FakeRow*> result,
|
||||
const RowDescriptor &entry) const;
|
||||
|
||||
void repaintDialogRow(FilterId filterId, not_null<Row*> row);
|
||||
void repaintDialogRow(RowDescriptor row);
|
||||
void refreshDialogRow(RowDescriptor row);
|
||||
bool updateEntryHeight(not_null<Entry*> entry);
|
||||
void showSponsoredMenu(int peerSearchIndex, QPoint globalPos);
|
||||
|
||||
void clearMouseSelection(bool clearSelection = false);
|
||||
void mousePressReleased(
|
||||
QPoint globalPosition,
|
||||
Qt::MouseButton button,
|
||||
Qt::KeyboardModifiers modifiers);
|
||||
void processGlobalForceClick(QPoint globalPosition);
|
||||
void clearIrrelevantState();
|
||||
void selectByMouse(QPoint globalPosition);
|
||||
void preloadRowsData();
|
||||
void scrollToItem(int top, int height);
|
||||
void scrollToDefaultSelected();
|
||||
void setCollapsedPressed(int pressed);
|
||||
void setPressed(
|
||||
Row *pressed,
|
||||
bool pressedTopicJump,
|
||||
bool pressedRightButton);
|
||||
void clearPressed();
|
||||
void setHashtagPressed(int pressed);
|
||||
void setFilteredPressed(
|
||||
int pressed,
|
||||
bool pressedTopicJump,
|
||||
bool pressedRightButton);
|
||||
void setPeerSearchPressed(int pressed, bool pressedRightButton);
|
||||
void setPreviewPressed(int pressed);
|
||||
void setSearchedPressed(int pressed);
|
||||
bool isPressed() const {
|
||||
return (_collapsedPressed >= 0)
|
||||
|| _pressed
|
||||
|| (_hashtagPressed >= 0)
|
||||
|| (_filteredPressed >= 0)
|
||||
|| (_peerSearchPressed >= 0)
|
||||
|| (_previewPressed >= 0)
|
||||
|| (_searchedPressed >= 0)
|
||||
|| _pressedMorePosts
|
||||
|| _pressedChatTypeFilter;
|
||||
}
|
||||
bool isSelected() const {
|
||||
return (_collapsedSelected >= 0)
|
||||
|| _selected
|
||||
|| (_hashtagSelected >= 0)
|
||||
|| (_filteredSelected >= 0)
|
||||
|| (_peerSearchSelected >= 0)
|
||||
|| (_previewSelected >= 0)
|
||||
|| (_searchedSelected >= 0)
|
||||
|| _selectedMorePosts
|
||||
|| _selectedChatTypeFilter;
|
||||
}
|
||||
bool uniqueSearchResults() const;
|
||||
bool hasHistoryInResults(not_null<History*> history) const;
|
||||
|
||||
int defaultRowTop(not_null<Row*> row) const;
|
||||
void setupOnlineStatusCheck();
|
||||
void jumpToTop();
|
||||
|
||||
void updateRowCornerStatusShown(not_null<History*> history);
|
||||
void repaintDialogRowCornerStatus(not_null<History*> history);
|
||||
|
||||
bool addBotAppRipple(QPoint origin, Fn<void()> updateCallback);
|
||||
bool addQuickActionRipple(not_null<Row*> row, Fn<void()> updateCallback);
|
||||
|
||||
bool addRightButtonRipple(QPoint origin, Fn<void()> updateCallback);
|
||||
|
||||
void setupShortcuts();
|
||||
RowDescriptor computeJump(
|
||||
const RowDescriptor &to,
|
||||
JumpSkip skip) const;
|
||||
bool jumpToDialogRow(RowDescriptor to);
|
||||
|
||||
RowDescriptor chatListEntryBefore(const RowDescriptor &which) const;
|
||||
RowDescriptor chatListEntryAfter(const RowDescriptor &which) const;
|
||||
RowDescriptor chatListEntryFirst() const;
|
||||
RowDescriptor chatListEntryLast() const;
|
||||
|
||||
void itemRemoved(not_null<const HistoryItem*> item);
|
||||
enum class UpdateRowSection {
|
||||
Default = (1 << 0),
|
||||
Filtered = (1 << 1),
|
||||
PeerSearch = (1 << 2),
|
||||
MessageSearch = (1 << 3),
|
||||
All = Default | Filtered | PeerSearch | MessageSearch,
|
||||
};
|
||||
using UpdateRowSections = base::flags<UpdateRowSection>;
|
||||
friend inline constexpr auto is_flag_type(UpdateRowSection) { return true; };
|
||||
|
||||
void updateSearchResult(not_null<PeerData*> peer);
|
||||
void updateDialogRow(
|
||||
RowDescriptor row,
|
||||
QRect updateRect = QRect(),
|
||||
UpdateRowSections sections = UpdateRowSection::All);
|
||||
void fillSupportSearchMenu(not_null<Ui::PopupMenu*> menu);
|
||||
void fillArchiveSearchMenu(not_null<Ui::PopupMenu*> menu);
|
||||
|
||||
void refreshShownList();
|
||||
[[nodiscard]] int skipTopHeight() const;
|
||||
[[nodiscard]] int collapsedRowsOffset() const;
|
||||
[[nodiscard]] int dialogsOffset() const;
|
||||
[[nodiscard]] int shownHeight(int till = -1) const;
|
||||
[[nodiscard]] int fixedOnTopCount() const;
|
||||
[[nodiscard]] int pinnedOffset() const;
|
||||
[[nodiscard]] int filteredOffset() const;
|
||||
[[nodiscard]] int filteredIndex(int y) const;
|
||||
[[nodiscard]] int filteredHeight(int till = -1) const;
|
||||
[[nodiscard]] int peerSearchOffset() const;
|
||||
[[nodiscard]] int searchInChatOffset() const;
|
||||
[[nodiscard]] int previewOffset() const;
|
||||
[[nodiscard]] int searchedOffset() const;
|
||||
[[nodiscard]] int searchInChatSkip() const;
|
||||
[[nodiscard]] int hashtagsOffset() const;
|
||||
|
||||
void paintCollapsedRows(
|
||||
Painter &p,
|
||||
QRect clip) const;
|
||||
void paintCollapsedRow(
|
||||
Painter &p,
|
||||
not_null<const CollapsedRow*> row,
|
||||
bool selected) const;
|
||||
void paintPeerSearchResult(
|
||||
Painter &p,
|
||||
not_null<const PeerSearchResult*> result,
|
||||
const Ui::PaintContext &context);
|
||||
void paintSearchTags(
|
||||
Painter &p,
|
||||
const Ui::PaintContext &context) const;
|
||||
//void paintSearchInChat(
|
||||
// Painter &p,
|
||||
// const Ui::PaintContext &context) const;
|
||||
//void paintSearchInPeer(
|
||||
// Painter &p,
|
||||
// not_null<PeerData*> peer,
|
||||
// Ui::PeerUserpicView &userpic,
|
||||
// int top,
|
||||
// const Ui::Text::String &text) const;
|
||||
//void paintSearchInSaved(
|
||||
// Painter &p,
|
||||
// int top,
|
||||
// const Ui::Text::String &text) const;
|
||||
//void paintSearchInReplies(
|
||||
// Painter &p,
|
||||
// int top,
|
||||
// const Ui::Text::String &text) const;
|
||||
//void paintSearchInTopic(
|
||||
// Painter &p,
|
||||
// const Ui::PaintContext &context,
|
||||
// not_null<Data::ForumTopic*> topic,
|
||||
// Ui::PeerUserpicView &userpic,
|
||||
// int top,
|
||||
// const Ui::Text::String &text) const;
|
||||
//template <typename PaintUserpic>
|
||||
//void paintSearchInFilter(
|
||||
// Painter &p,
|
||||
// PaintUserpic paintUserpic,
|
||||
// int top,
|
||||
// const style::icon *icon,
|
||||
// const Ui::Text::String &text) const;
|
||||
void updateSearchIn();
|
||||
void repaintSearchResult(int index);
|
||||
void repaintPreviewResult(int index);
|
||||
|
||||
[[nodiscard]] bool computeSearchWithPostsPreview() const;
|
||||
|
||||
Ui::VideoUserpic *validateVideoUserpic(not_null<Row*> row);
|
||||
Ui::VideoUserpic *validateVideoUserpic(not_null<History*> history);
|
||||
|
||||
Row *shownRowByKey(Key key);
|
||||
void clearSearchResults(bool alsoPeerSearchResults = true);
|
||||
void clearPeerSearchResults();
|
||||
void clearPreviewResults();
|
||||
void updateSelectedRow(Key key = Key());
|
||||
void trackResultsHistory(not_null<History*> history);
|
||||
|
||||
[[nodiscard]] QBrush currentBg() const;
|
||||
[[nodiscard]] RowDescriptor computeChatPreviewRow() const;
|
||||
|
||||
[[nodiscard]] const std::vector<Key> &pinnedChatsOrder() const;
|
||||
void checkReorderPinnedStart(QPoint localPosition);
|
||||
void startReorderPinned(QPoint localPosition);
|
||||
int updateReorderIndexGetCount();
|
||||
bool updateReorderPinned(QPoint localPosition);
|
||||
void finishReorderPinned();
|
||||
bool finishReorderOnRelease();
|
||||
void stopReorderPinned();
|
||||
int countPinnedIndex(Row *ofRow);
|
||||
void savePinnedOrder();
|
||||
bool pinnedShiftAnimationCallback(crl::time now);
|
||||
void handleChatListEntryRefreshes();
|
||||
void moveSearchIn();
|
||||
void dragPinnedFromTouch();
|
||||
[[nodiscard]] bool hasChatTypeFilter() const;
|
||||
|
||||
void saveChatsFilterScrollState(FilterId filterId);
|
||||
void restoreChatsFilterScrollState(FilterId filterId);
|
||||
|
||||
[[nodiscard]] not_null<Ui::QuickActionContext*> ensureQuickAction(
|
||||
int64 key);
|
||||
void deactivateQuickAction();
|
||||
|
||||
[[nodiscard]] bool lookupIsInBotAppButton(
|
||||
Row *row,
|
||||
QPoint localPosition);
|
||||
[[nodiscard]] bool lookupIsInRightButton(
|
||||
const RightButton &button,
|
||||
QPoint localPosition);
|
||||
[[nodiscard]] RightButton *maybeCacheRightButton(Row *row);
|
||||
void fillRightButton(
|
||||
RightButton &button,
|
||||
const TextWithEntities &text,
|
||||
const style::DialogRightButton &st);
|
||||
|
||||
[[nodiscard]] QImage *cacheChatsFilterTag(
|
||||
const Data::ChatFilter &filter,
|
||||
uint8 more,
|
||||
bool active);
|
||||
|
||||
const not_null<Window::SessionController*> _controller;
|
||||
|
||||
not_null<IndexedList*> _shownList;
|
||||
FilterId _filterId = 0;
|
||||
bool _mouseSelection = false;
|
||||
std::optional<QPoint> _lastMousePosition;
|
||||
int _lastRowLocalMouseX = -1;
|
||||
Qt::MouseButton _pressButton = Qt::LeftButton;
|
||||
|
||||
Data::Folder *_openedFolder = nullptr;
|
||||
Data::Forum *_openedForum = nullptr;
|
||||
rpl::lifetime _openedForumLifetime;
|
||||
|
||||
std::vector<std::unique_ptr<CollapsedRow>> _collapsedRows;
|
||||
not_null<const style::DialogRow*> _st;
|
||||
mutable std::unique_ptr<Ui::TopicJumpCache> _topicJumpCache;
|
||||
bool _selectedChatTypeFilter = false;
|
||||
bool _pressedChatTypeFilter = false;
|
||||
bool _selectedMorePosts = false;
|
||||
bool _pressedMorePosts = false;
|
||||
int _collapsedSelected = -1;
|
||||
int _collapsedPressed = -1;
|
||||
bool _skipTopDialog = false;
|
||||
Row *_selected = nullptr;
|
||||
Row *_pressed = nullptr;
|
||||
MsgId _pressedTopicJumpRootId;
|
||||
PeerId _pressedSublistJumpPeerId;
|
||||
bool _selectedTopicJump = false;
|
||||
bool _pressedTopicJump = false;
|
||||
|
||||
RightButton *_pressedRightButtonData = nullptr;
|
||||
bool _pressedRightButtonSponsored = false;
|
||||
bool _selectedRightButton = false;
|
||||
bool _pressedRightButton = false;
|
||||
|
||||
Row *_dragging = nullptr;
|
||||
int _draggingIndex = -1;
|
||||
int _aboveIndex = -1;
|
||||
QPoint _dragStart;
|
||||
std::vector<PinnedRow> _pinnedRows;
|
||||
Ui::Animations::Basic _pinnedShiftAnimation;
|
||||
base::flat_set<Key> _pinnedOnDragStart;
|
||||
|
||||
// Remember the last currently dragged row top shift for updating area.
|
||||
int _aboveTopShift = -1;
|
||||
int _narrowWidth = 0;
|
||||
|
||||
int _visibleTop = 0;
|
||||
int _visibleBottom = 0;
|
||||
QString _filter, _hashtagFilter;
|
||||
|
||||
std::vector<std::unique_ptr<HashtagResult>> _hashtagResults;
|
||||
int _hashtagSelected = -1;
|
||||
int _hashtagPressed = -1;
|
||||
bool _hashtagDeleteSelected = false;
|
||||
bool _hashtagDeletePressed = false;
|
||||
|
||||
std::vector<FilterResult> _filterResults;
|
||||
base::flat_map<Key, std::unique_ptr<Row>> _filterResultsGlobal;
|
||||
int _filteredSelected = -1;
|
||||
int _filteredPressed = -1;
|
||||
|
||||
EmptyState _emptyState = EmptyState::None;
|
||||
|
||||
base::flat_set<not_null<History*>> _trackedHistories;
|
||||
rpl::lifetime _trackedLifetime;
|
||||
|
||||
QString _peerSearchQuery;
|
||||
base::flat_set<not_null<PeerData*>> _sponsoredRemoved;
|
||||
std::vector<std::unique_ptr<PeerSearchResult>> _peerSearchResults;
|
||||
int _peerSearchSelected = -1;
|
||||
int _peerSearchPressed = -1;
|
||||
int _peerSearchMenu = -1;
|
||||
|
||||
std::vector<std::unique_ptr<FakeRow>> _previewResults;
|
||||
int _previewCount = 0;
|
||||
int _previewSelected = -1;
|
||||
int _previewPressed = -1;
|
||||
int _morePostsWidth = 0;
|
||||
int _chatTypeFilterWidth = 0;
|
||||
|
||||
std::vector<std::unique_ptr<FakeRow>> _searchResults;
|
||||
int _searchedCount = 0;
|
||||
int _searchedMigratedCount = 0;
|
||||
int _searchedSelected = -1;
|
||||
int _searchedPressed = -1;
|
||||
|
||||
WidgetState _state = WidgetState::Default;
|
||||
|
||||
std::unique_ptr<ChatSearchIn> _searchIn;
|
||||
rpl::event_stream<ChatSearchTab> _changeSearchTabRequests;
|
||||
rpl::event_stream<ChatTypeFilter> _changeSearchFilterRequests;
|
||||
rpl::event_stream<> _cancelSearchRequests;
|
||||
rpl::event_stream<> _cancelSearchFromRequests;
|
||||
rpl::event_stream<> _changeSearchFromRequests;
|
||||
object_ptr<Ui::RpWidget> _loadingAnimation = { nullptr };
|
||||
object_ptr<SearchEmpty> _searchEmpty = { nullptr };
|
||||
SearchState _searchEmptyState;
|
||||
object_ptr<Ui::FlatLabel> _empty = { nullptr };
|
||||
object_ptr<Ui::VerticalLayout> _emptyList = { nullptr };
|
||||
object_ptr<Ui::RoundButton> _emptyButton = { nullptr };
|
||||
|
||||
Ui::DraggingScrollManager _draggingScroll;
|
||||
|
||||
SearchState _searchState;
|
||||
HashOrCashtag _searchHashOrCashtag = {};
|
||||
bool _searchWithPostsPreview = false;
|
||||
History *_searchInMigrated = nullptr;
|
||||
PeerData *_searchFromShown = nullptr;
|
||||
Ui::Text::String _searchFromUserText;
|
||||
std::unique_ptr<SearchTags> _searchTags;
|
||||
int _searchTagsLeft = 0;
|
||||
RowDescriptor _menuRow;
|
||||
|
||||
base::flat_map<
|
||||
not_null<PeerData*>,
|
||||
std::unique_ptr<Ui::VideoUserpic>> _videoUserpics;
|
||||
|
||||
base::flat_map<FilterId, int> _chatsFilterScrollStates;
|
||||
|
||||
std::unordered_map<ChatsFilterTagsKey, TagCache> _chatsFilterTags;
|
||||
bool _waitingAllChatListEntryRefreshesForTags = false;
|
||||
rpl::lifetime _handleChatListEntryTagRefreshesLifetime;
|
||||
|
||||
std::unordered_map<PeerId, RightButton> _rightButtons;
|
||||
|
||||
Fn<void()> _loadMoreCallback;
|
||||
Fn<void()> _loadMoreFilteredCallback;
|
||||
rpl::event_stream<> _listBottomReached;
|
||||
rpl::event_stream<ChosenRow> _chosenRow;
|
||||
rpl::event_stream<> _updated;
|
||||
|
||||
rpl::event_stream<Ui::ScrollToRequest> _mustScrollTo;
|
||||
rpl::event_stream<Ui::ScrollToRequest> _dialogMoved;
|
||||
rpl::event_stream<SearchRequestDelay> _searchRequests;
|
||||
rpl::event_stream<QString> _completeHashtagRequests;
|
||||
rpl::event_stream<> _refreshHashtagsRequests;
|
||||
rpl::event_stream<UserId> _openBotMainAppRequests;
|
||||
|
||||
using QuickActionPtr = std::unique_ptr<Ui::QuickActionContext>;
|
||||
QuickActionPtr _activeQuickAction;
|
||||
std::vector<QuickActionPtr> _inactiveQuickActions;
|
||||
|
||||
RowDescriptor _chatPreviewRow;
|
||||
bool _chatPreviewScheduled = false;
|
||||
std::optional<QPoint> _chatPreviewTouchGlobal;
|
||||
base::Timer _touchDragPinnedTimer;
|
||||
std::optional<QPoint> _touchDragStartGlobal;
|
||||
std::optional<QPoint> _touchDragNowGlobal;
|
||||
rpl::event_stream<> _touchCancelRequests;
|
||||
|
||||
rpl::variable<ChildListShown> _childListShown;
|
||||
float64 _narrowRatio = 0.;
|
||||
bool _geometryInited = false;
|
||||
|
||||
Data::SavedMessages *_savedSublists = nullptr;
|
||||
|
||||
bool _searchLoading = false;
|
||||
bool _searchWaiting = false;
|
||||
|
||||
base::unique_qptr<Ui::PopupMenu> _menu;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Dialogs
|
||||
108
Telegram/SourceFiles/dialogs/dialogs_key.cpp
Normal file
108
Telegram/SourceFiles/dialogs/dialogs_key.cpp
Normal file
@@ -0,0 +1,108 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#include "dialogs/dialogs_key.h"
|
||||
|
||||
#include "data/data_folder.h"
|
||||
#include "data/data_forum_topic.h"
|
||||
#include "data/data_saved_sublist.h"
|
||||
#include "dialogs/ui/chat_search_in.h"
|
||||
#include "history/history.h"
|
||||
|
||||
namespace Dialogs {
|
||||
|
||||
Key::Key(History *history) : _value(history) {
|
||||
}
|
||||
|
||||
Key::Key(Data::Folder *folder) : _value(folder) {
|
||||
}
|
||||
|
||||
Key::Key(Data::Thread *thread) : _value(thread) {
|
||||
}
|
||||
|
||||
Key::Key(Data::ForumTopic *topic) : _value(topic) {
|
||||
}
|
||||
|
||||
Key::Key(Data::SavedSublist *sublist) : _value(sublist) {
|
||||
}
|
||||
|
||||
Key::Key(not_null<History*> history) : _value(history) {
|
||||
}
|
||||
|
||||
Key::Key(not_null<Data::Thread*> thread) : _value(thread) {
|
||||
}
|
||||
|
||||
Key::Key(not_null<Data::Folder*> folder) : _value(folder) {
|
||||
}
|
||||
|
||||
Key::Key(not_null<Data::ForumTopic*> topic) : _value(topic) {
|
||||
}
|
||||
|
||||
Key::Key(not_null<Data::SavedSublist*> sublist) : _value(sublist) {
|
||||
}
|
||||
|
||||
not_null<Entry*> Key::entry() const {
|
||||
Expects(_value != nullptr);
|
||||
|
||||
return _value;
|
||||
}
|
||||
|
||||
History *Key::history() const {
|
||||
return _value ? _value->asHistory() : nullptr;
|
||||
}
|
||||
|
||||
Data::Folder *Key::folder() const {
|
||||
return _value ? _value->asFolder() : nullptr;
|
||||
}
|
||||
|
||||
Data::ForumTopic *Key::topic() const {
|
||||
return _value ? _value->asTopic() : nullptr;
|
||||
}
|
||||
|
||||
Data::Thread *Key::thread() const {
|
||||
return _value ? _value->asThread() : nullptr;
|
||||
}
|
||||
|
||||
Data::SavedSublist *Key::sublist() const {
|
||||
return _value ? _value->asSublist() : nullptr;
|
||||
}
|
||||
|
||||
History *Key::owningHistory() const {
|
||||
if (const auto thread = this->thread()) {
|
||||
return thread->owningHistory();
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
PeerData *Key::peer() const {
|
||||
if (const auto history = owningHistory()) {
|
||||
return history->peer;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
[[nodiscard]] bool SearchState::empty() const {
|
||||
return !inChat
|
||||
&& tags.empty()
|
||||
&& QStringView(query).trimmed().isEmpty();
|
||||
}
|
||||
|
||||
ChatSearchTab SearchState::defaultTabForMe() const {
|
||||
return inChat.topic()
|
||||
? ChatSearchTab::ThisTopic
|
||||
: (inChat.history() || inChat.sublist())
|
||||
? ChatSearchTab::ThisPeer
|
||||
: ChatSearchTab::MyMessages;
|
||||
}
|
||||
|
||||
bool SearchState::filterChatsList() const {
|
||||
using Tab = ChatSearchTab;
|
||||
return !inChat // ThisPeer can be in opened forum.
|
||||
&& (tab == Tab::MyMessages || tab == Tab::ThisPeer);
|
||||
}
|
||||
|
||||
} // namespace Dialogs
|
||||
165
Telegram/SourceFiles/dialogs/dialogs_key.h
Normal file
165
Telegram/SourceFiles/dialogs/dialogs_key.h
Normal file
@@ -0,0 +1,165 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "base/qt/qt_compare.h"
|
||||
#include "data/data_message_reaction_id.h"
|
||||
|
||||
class History;
|
||||
class PeerData;
|
||||
|
||||
namespace Data {
|
||||
class Thread;
|
||||
class Folder;
|
||||
class ForumTopic;
|
||||
class SavedSublist;
|
||||
struct ReactionId;
|
||||
} // namespace Data
|
||||
|
||||
namespace Dialogs {
|
||||
|
||||
class Entry;
|
||||
enum class ChatSearchTab : uchar;
|
||||
|
||||
class Key {
|
||||
public:
|
||||
Key() = default;
|
||||
Key(Entry *entry) : _value(entry) {
|
||||
}
|
||||
Key(History *history);
|
||||
Key(Data::Folder *folder);
|
||||
Key(Data::Thread *thread);
|
||||
Key(Data::ForumTopic *topic);
|
||||
Key(Data::SavedSublist *sublist);
|
||||
Key(not_null<Entry*> entry) : _value(entry) {
|
||||
}
|
||||
Key(not_null<History*> history);
|
||||
Key(not_null<Data::Thread*> thread);
|
||||
Key(not_null<Data::Folder*> folder);
|
||||
Key(not_null<Data::ForumTopic*> topic);
|
||||
Key(not_null<Data::SavedSublist*> sublist);
|
||||
|
||||
explicit operator bool() const {
|
||||
return (_value != nullptr);
|
||||
}
|
||||
[[nodiscard]] not_null<Entry*> entry() const;
|
||||
[[nodiscard]] History *history() const;
|
||||
[[nodiscard]] Data::Folder *folder() const;
|
||||
[[nodiscard]] Data::ForumTopic *topic() const;
|
||||
[[nodiscard]] Data::Thread *thread() const;
|
||||
[[nodiscard]] History *owningHistory() const;
|
||||
[[nodiscard]] PeerData *peer() const;
|
||||
[[nodiscard]] Data::SavedSublist *sublist() const;
|
||||
|
||||
friend inline constexpr auto operator<=>(Key, Key) noexcept = default;
|
||||
|
||||
private:
|
||||
Entry *_value = nullptr;
|
||||
|
||||
};
|
||||
|
||||
struct RowDescriptor {
|
||||
RowDescriptor() = default;
|
||||
RowDescriptor(Key key, FullMsgId fullId) : key(key), fullId(fullId) {
|
||||
}
|
||||
|
||||
Key key;
|
||||
FullMsgId fullId;
|
||||
|
||||
};
|
||||
|
||||
inline bool operator==(const RowDescriptor &a, const RowDescriptor &b) {
|
||||
return (a.key == b.key)
|
||||
&& ((a.fullId == b.fullId) || (!a.fullId.msg && !b.fullId.msg));
|
||||
}
|
||||
|
||||
inline bool operator!=(const RowDescriptor &a, const RowDescriptor &b) {
|
||||
return !(a == b);
|
||||
}
|
||||
|
||||
inline bool operator<(const RowDescriptor &a, const RowDescriptor &b) {
|
||||
if (a.key < b.key) {
|
||||
return true;
|
||||
} else if (a.key > b.key) {
|
||||
return false;
|
||||
}
|
||||
return a.fullId < b.fullId;
|
||||
}
|
||||
|
||||
inline bool operator>(const RowDescriptor &a, const RowDescriptor &b) {
|
||||
return (b < a);
|
||||
}
|
||||
|
||||
inline bool operator<=(const RowDescriptor &a, const RowDescriptor &b) {
|
||||
return !(b < a);
|
||||
}
|
||||
|
||||
inline bool operator>=(const RowDescriptor &a, const RowDescriptor &b) {
|
||||
return !(a < b);
|
||||
}
|
||||
|
||||
struct EntryState {
|
||||
enum class Section {
|
||||
History,
|
||||
Profile,
|
||||
ChatsList,
|
||||
Scheduled,
|
||||
Pinned,
|
||||
Replies,
|
||||
SavedSublist,
|
||||
ContextMenu,
|
||||
SubsectionTabsMenu,
|
||||
ShortcutMessages,
|
||||
};
|
||||
|
||||
Key key;
|
||||
Section section = Section::History;
|
||||
FilterId filterId = 0;
|
||||
FullReplyTo currentReplyTo;
|
||||
SuggestOptions currentSuggest;
|
||||
|
||||
friend inline auto operator<=>(
|
||||
const EntryState&,
|
||||
const EntryState&) = default;
|
||||
friend inline bool operator==(
|
||||
const EntryState&,
|
||||
const EntryState&) = default;
|
||||
};
|
||||
|
||||
enum class ChatTypeFilter : uchar {
|
||||
All,
|
||||
Private,
|
||||
Groups,
|
||||
Channels,
|
||||
};
|
||||
|
||||
struct SearchState {
|
||||
Key inChat;
|
||||
PeerData *fromPeer = nullptr;
|
||||
std::vector<Data::ReactionId> tags;
|
||||
ChatSearchTab tab = {};
|
||||
ChatTypeFilter filter = ChatTypeFilter::All;
|
||||
QString query;
|
||||
|
||||
[[nodiscard]] bool empty() const;
|
||||
[[nodiscard]] ChatSearchTab defaultTabForMe() const;
|
||||
[[nodiscard]] bool filterChatsList() const;
|
||||
|
||||
explicit operator bool() const {
|
||||
return !empty();
|
||||
}
|
||||
|
||||
friend inline auto operator<=>(
|
||||
const SearchState&,
|
||||
const SearchState&) noexcept = default;
|
||||
friend inline bool operator==(
|
||||
const SearchState&,
|
||||
const SearchState&) = default;
|
||||
};
|
||||
|
||||
} // namespace Dialogs
|
||||
205
Telegram/SourceFiles/dialogs/dialogs_list.cpp
Normal file
205
Telegram/SourceFiles/dialogs/dialogs_list.cpp
Normal file
@@ -0,0 +1,205 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#include "dialogs/dialogs_list.h"
|
||||
|
||||
#include "dialogs/dialogs_entry.h"
|
||||
#include "dialogs/ui/dialogs_layout.h"
|
||||
#include "data/data_session.h"
|
||||
|
||||
namespace Dialogs {
|
||||
|
||||
List::List(SortMode sortMode, FilterId filterId)
|
||||
: _sortMode(sortMode)
|
||||
, _filterId(filterId) {
|
||||
}
|
||||
|
||||
List::const_iterator List::cfind(Row *value) const {
|
||||
return value
|
||||
? (cbegin() + value->index())
|
||||
: cend();
|
||||
}
|
||||
|
||||
not_null<Row*> List::addToEnd(Key key) {
|
||||
if (const auto result = getRow(key)) {
|
||||
return result;
|
||||
}
|
||||
const auto result = _rowByKey.emplace(
|
||||
key,
|
||||
std::make_unique<Row>(key, _rows.size(), height())
|
||||
).first->second.get();
|
||||
result->recountHeight(_narrowRatio, _filterId);
|
||||
_rows.emplace_back(result);
|
||||
if (_sortMode == SortMode::Date) {
|
||||
adjustByDate(result);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
Row *List::adjustByName(Key key) {
|
||||
Expects(_sortMode == SortMode::Name);
|
||||
|
||||
const auto row = getRow(key);
|
||||
if (!row) {
|
||||
return nullptr;
|
||||
}
|
||||
adjustByName(row);
|
||||
return row;
|
||||
}
|
||||
|
||||
not_null<Row*> List::addByName(Key key) {
|
||||
Expects(_sortMode == SortMode::Name);
|
||||
|
||||
const auto row = addToEnd(key);
|
||||
adjustByName(key);
|
||||
return row;
|
||||
}
|
||||
|
||||
void List::adjustByName(not_null<Row*> row) {
|
||||
Expects(row->index() >= 0 && row->index() < _rows.size());
|
||||
|
||||
const auto &key = row->entry()->chatListNameSortKey();
|
||||
const auto index = row->index();
|
||||
const auto i = _rows.begin() + index;
|
||||
const auto before = std::find_if(i + 1, _rows.end(), [&](Row *row) {
|
||||
return row->entry()->chatListNameSortKey().compare(key) >= 0;
|
||||
});
|
||||
if (before != i + 1) {
|
||||
rotate(i, i + 1, before);
|
||||
} else if (i != _rows.begin()) {
|
||||
const auto from = std::make_reverse_iterator(i);
|
||||
const auto after = std::find_if(from, _rows.rend(), [&](Row *row) {
|
||||
return row->entry()->chatListNameSortKey().compare(key) <= 0;
|
||||
}).base();
|
||||
if (after != i) {
|
||||
rotate(after, i, i + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void List::adjustByDate(not_null<Row*> row) {
|
||||
Expects(_sortMode == SortMode::Date);
|
||||
|
||||
const auto key = row->sortKey(_filterId);
|
||||
const auto index = row->index();
|
||||
const auto i = _rows.begin() + index;
|
||||
const auto before = std::find_if(i + 1, _rows.end(), [&](Row *row) {
|
||||
return (row->sortKey(_filterId) <= key);
|
||||
});
|
||||
if (before != i + 1) {
|
||||
rotate(i, i + 1, before);
|
||||
} else {
|
||||
const auto from = std::make_reverse_iterator(i);
|
||||
const auto after = std::find_if(from, _rows.rend(), [&](Row *row) {
|
||||
return (row->sortKey(_filterId) >= key);
|
||||
}).base();
|
||||
if (after != i) {
|
||||
rotate(after, i, i + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool List::updateHeight(Key key, float64 narrowRatio) {
|
||||
const auto i = _rowByKey.find(key);
|
||||
if (i == _rowByKey.cend()) {
|
||||
return false;
|
||||
}
|
||||
const auto row = i->second.get();
|
||||
const auto index = row->index();
|
||||
auto top = row->top();
|
||||
const auto was = row->height();
|
||||
row->recountHeight(narrowRatio, _filterId);
|
||||
if (row->height() == was) {
|
||||
return false;
|
||||
}
|
||||
for (auto i = _rows.begin() + index, e = _rows.end(); i != e; ++i) {
|
||||
(*i)->_top = top;
|
||||
top += (*i)->height();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool List::updateHeights(float64 narrowRatio) {
|
||||
_narrowRatio = narrowRatio;
|
||||
auto was = height();
|
||||
auto top = 0;
|
||||
for (const auto &row : _rows) {
|
||||
row->_top = top;
|
||||
row->recountHeight(narrowRatio, _filterId);
|
||||
top += row->height();
|
||||
}
|
||||
return (height() != was);
|
||||
}
|
||||
|
||||
bool List::moveToTop(Key key) {
|
||||
const auto i = _rowByKey.find(key);
|
||||
if (i == _rowByKey.cend()) {
|
||||
return false;
|
||||
}
|
||||
const auto index = i->second->index();
|
||||
const auto begin = _rows.begin();
|
||||
rotate(begin, begin + index, begin + index + 1);
|
||||
return true;
|
||||
}
|
||||
|
||||
void List::rotate(
|
||||
std::vector<not_null<Row*>>::iterator first,
|
||||
std::vector<not_null<Row*>>::iterator middle,
|
||||
std::vector<not_null<Row*>>::iterator last) {
|
||||
auto top = (*first)->top();
|
||||
std::rotate(first, middle, last);
|
||||
|
||||
auto count = (last - first);
|
||||
auto index = (first - _rows.begin());
|
||||
while (count--) {
|
||||
const auto row = *first++;
|
||||
row->_index = index++;
|
||||
row->_top = top;
|
||||
top += row->height();
|
||||
}
|
||||
}
|
||||
|
||||
bool List::remove(Key key, Row *replacedBy) {
|
||||
auto i = _rowByKey.find(key);
|
||||
if (i == _rowByKey.cend()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto row = i->second.get();
|
||||
row->entry()->owner().dialogsRowReplaced({ row, replacedBy });
|
||||
|
||||
auto top = row->top();
|
||||
const auto index = row->index();
|
||||
_rows.erase(_rows.begin() + index);
|
||||
for (auto i = index, count = int(_rows.size()); i != count; ++i) {
|
||||
const auto row = _rows[i];
|
||||
row->_index = i;
|
||||
row->_top = top;
|
||||
top += row->height();
|
||||
}
|
||||
_rowByKey.erase(i);
|
||||
return true;
|
||||
}
|
||||
|
||||
Row *List::rowAtY(int y) const {
|
||||
const auto i = findByY(y);
|
||||
if (i == cend()) {
|
||||
return nullptr;
|
||||
}
|
||||
const auto row = *i;
|
||||
const auto top = row->top();
|
||||
const auto bottom = top + row->height();
|
||||
return (top <= y && bottom > y) ? row.get() : nullptr;
|
||||
}
|
||||
|
||||
List::iterator List::findByY(int y) const {
|
||||
return ranges::lower_bound(_rows, y, ranges::less(), [](const Row *row) {
|
||||
return row->top() + row->height() - 1;
|
||||
});
|
||||
}
|
||||
|
||||
} // namespace Dialogs
|
||||
90
Telegram/SourceFiles/dialogs/dialogs_list.h
Normal file
90
Telegram/SourceFiles/dialogs/dialogs_list.h
Normal file
@@ -0,0 +1,90 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "dialogs/dialogs_row.h"
|
||||
|
||||
class PeerData;
|
||||
namespace Dialogs {
|
||||
|
||||
enum class SortMode;
|
||||
|
||||
class List final {
|
||||
public:
|
||||
List(SortMode sortMode, FilterId filterId = 0);
|
||||
List(const List &other) = delete;
|
||||
List &operator=(const List &other) = delete;
|
||||
List(List &&other) = default;
|
||||
List &operator=(List &&other) = default;
|
||||
~List() = default;
|
||||
|
||||
void clear() {
|
||||
_rows.clear();
|
||||
_rowByKey.clear();
|
||||
}
|
||||
[[nodiscard]] int size() const {
|
||||
return _rows.size();
|
||||
}
|
||||
[[nodiscard]] bool empty() const {
|
||||
return _rows.empty();
|
||||
}
|
||||
[[nodiscard]] int height() const {
|
||||
return _rows.empty()
|
||||
? 0
|
||||
: (_rows.back()->top() + _rows.back()->height());
|
||||
}
|
||||
[[nodiscard]] bool contains(Key key) const {
|
||||
return _rowByKey.find(key) != _rowByKey.end();
|
||||
}
|
||||
[[nodiscard]] Row *getRow(Key key) const {
|
||||
const auto i = _rowByKey.find(key);
|
||||
return (i != _rowByKey.end()) ? i->second.get() : nullptr;
|
||||
}
|
||||
[[nodiscard]] Row *rowAtY(int y) const;
|
||||
|
||||
not_null<Row*> addToEnd(Key key);
|
||||
Row *adjustByName(Key key);
|
||||
not_null<Row*> addByName(Key key);
|
||||
bool moveToTop(Key key);
|
||||
void adjustByDate(not_null<Row*> row);
|
||||
bool updateHeight(Key key, float64 narrowRatio);
|
||||
bool updateHeights(float64 narrowRatio);
|
||||
bool remove(Key key, Row *replacedBy = nullptr);
|
||||
|
||||
using const_iterator = std::vector<not_null<Row*>>::const_iterator;
|
||||
using iterator = const_iterator;
|
||||
|
||||
[[nodiscard]] const_iterator cbegin() const { return _rows.cbegin(); }
|
||||
[[nodiscard]] const_iterator cend() const { return _rows.cend(); }
|
||||
[[nodiscard]] const_iterator begin() const { return cbegin(); }
|
||||
[[nodiscard]] const_iterator end() const { return cend(); }
|
||||
[[nodiscard]] iterator begin() { return cbegin(); }
|
||||
[[nodiscard]] iterator end() { return cend(); }
|
||||
[[nodiscard]] const_iterator cfind(Row *value) const;
|
||||
[[nodiscard]] const_iterator find(Row *value) const {
|
||||
return cfind(value);
|
||||
}
|
||||
[[nodiscard]] iterator find(Row *value) { return cfind(value); }
|
||||
[[nodiscard]] iterator findByY(int y) const;
|
||||
|
||||
private:
|
||||
void adjustByName(not_null<Row*> row);
|
||||
void rotate(
|
||||
std::vector<not_null<Row*>>::iterator first,
|
||||
std::vector<not_null<Row*>>::iterator middle,
|
||||
std::vector<not_null<Row*>>::iterator last);
|
||||
|
||||
SortMode _sortMode = SortMode();
|
||||
FilterId _filterId = 0;
|
||||
float64 _narrowRatio = 0.;
|
||||
std::vector<not_null<Row*>> _rows;
|
||||
std::map<Key, std::unique_ptr<Row>> _rowByKey;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Dialogs
|
||||
236
Telegram/SourceFiles/dialogs/dialogs_main_list.cpp
Normal file
236
Telegram/SourceFiles/dialogs/dialogs_main_list.cpp
Normal file
@@ -0,0 +1,236 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#include "dialogs/dialogs_main_list.h"
|
||||
|
||||
#include "data/data_changes.h"
|
||||
#include "data/data_session.h"
|
||||
#include "data/data_chat_filters.h"
|
||||
#include "main/main_session.h"
|
||||
#include "history/history_unread_things.h"
|
||||
#include "history/history.h"
|
||||
|
||||
namespace Dialogs {
|
||||
|
||||
MainList::MainList(
|
||||
not_null<Main::Session*> session,
|
||||
FilterId filterId,
|
||||
rpl::producer<int> pinnedLimit)
|
||||
: _filterId(filterId)
|
||||
, _all(SortMode::Date, filterId)
|
||||
, _pinned(filterId, 1) {
|
||||
_unreadState.known = true;
|
||||
|
||||
std::move(
|
||||
pinnedLimit
|
||||
) | rpl::on_next([=](int limit) {
|
||||
_pinned.setLimit(std::max(limit, 1));
|
||||
}, _lifetime);
|
||||
|
||||
session->changes().realtimeNameUpdates(
|
||||
) | rpl::on_next([=](const Data::NameUpdate &update) {
|
||||
_all.peerNameChanged(_filterId, update.peer, update.oldFirstLetters);
|
||||
}, _lifetime);
|
||||
}
|
||||
|
||||
bool MainList::empty() const {
|
||||
return _all.empty();
|
||||
}
|
||||
|
||||
bool MainList::loaded() const {
|
||||
return _loaded;
|
||||
}
|
||||
|
||||
void MainList::setLoaded(bool loaded) {
|
||||
if (_loaded == loaded) {
|
||||
return;
|
||||
}
|
||||
const auto recomputer = gsl::finally([&] {
|
||||
recomputeFullListSize();
|
||||
});
|
||||
const auto notifier = unreadStateChangeNotifier(true);
|
||||
_loaded = loaded;
|
||||
}
|
||||
|
||||
void MainList::setAllAreMuted(bool allAreMuted) {
|
||||
if (_allAreMuted == allAreMuted) {
|
||||
return;
|
||||
}
|
||||
const auto notifier = unreadStateChangeNotifier(true);
|
||||
_allAreMuted = allAreMuted;
|
||||
}
|
||||
|
||||
void MainList::setCloudListSize(int size) {
|
||||
if (_cloudListSize == size) {
|
||||
return;
|
||||
}
|
||||
_cloudListSize = size;
|
||||
recomputeFullListSize();
|
||||
}
|
||||
|
||||
const rpl::variable<int> &MainList::fullSize() const {
|
||||
return _fullListSize;
|
||||
}
|
||||
|
||||
void MainList::clear() {
|
||||
const auto recomputer = gsl::finally([&] {
|
||||
recomputeFullListSize();
|
||||
});
|
||||
const auto notifier = unreadStateChangeNotifier(true);
|
||||
_pinned.clear();
|
||||
_all.clear();
|
||||
_unreadState = UnreadState();
|
||||
_cloudUnreadState = UnreadState();
|
||||
_unreadState.known = true;
|
||||
_cloudUnreadState.known = true;
|
||||
_cloudListSize = 0;
|
||||
}
|
||||
|
||||
RowsByLetter MainList::addEntry(Key key) {
|
||||
const auto result = _all.addToEnd(key);
|
||||
|
||||
const auto unread = key.entry()->chatListUnreadState();
|
||||
unreadEntryChanged(unread, true);
|
||||
recomputeFullListSize();
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
void MainList::removeEntry(Key key) {
|
||||
_all.remove(key);
|
||||
|
||||
const auto unread = key.entry()->chatListUnreadState();
|
||||
unreadEntryChanged(unread, false);
|
||||
recomputeFullListSize();
|
||||
}
|
||||
|
||||
void MainList::recomputeFullListSize() {
|
||||
_fullListSize = std::max(_all.size(), loaded() ? 0 : _cloudListSize);
|
||||
}
|
||||
|
||||
void MainList::unreadStateChanged(
|
||||
const UnreadState &wasState,
|
||||
const UnreadState &nowState) {
|
||||
const auto useClouded = _cloudUnreadState.known && !loaded();
|
||||
const auto updateCloudUnread = _cloudUnreadState.known && wasState.known;
|
||||
const auto notify = !useClouded || wasState.known;
|
||||
const auto notifier = unreadStateChangeNotifier(notify);
|
||||
_unreadState += nowState - wasState;
|
||||
if (_unreadState.chatsMuted > _unreadState.chats
|
||||
|| _unreadState.messagesMuted > _unreadState.messages) {
|
||||
[[maybe_unused]] int a = 0;
|
||||
}
|
||||
if (updateCloudUnread) {
|
||||
Assert(nowState.known);
|
||||
_cloudUnreadState += nowState - wasState;
|
||||
finalizeCloudUnread();
|
||||
}
|
||||
}
|
||||
|
||||
void MainList::unreadEntryChanged(
|
||||
const Dialogs::UnreadState &state,
|
||||
bool added) {
|
||||
if (!state.messages
|
||||
&& !state.chats
|
||||
&& !state.marks
|
||||
&& !state.mentions
|
||||
&& !state.reactions) {
|
||||
return;
|
||||
}
|
||||
const auto updateCloudUnread = _cloudUnreadState.known && state.known;
|
||||
const auto notify = !_cloudUnreadState.known || loaded() || state.known;
|
||||
const auto notifier = unreadStateChangeNotifier(notify);
|
||||
if (added) {
|
||||
_unreadState += state;
|
||||
} else {
|
||||
_unreadState -= state;
|
||||
}
|
||||
if (_unreadState.chatsMuted > _unreadState.chats
|
||||
|| _unreadState.messagesMuted > _unreadState.messages) {
|
||||
[[maybe_unused]] int a = 0;
|
||||
}
|
||||
if (updateCloudUnread) {
|
||||
if (added) {
|
||||
_cloudUnreadState += state;
|
||||
} else {
|
||||
_cloudUnreadState -= state;
|
||||
}
|
||||
finalizeCloudUnread();
|
||||
}
|
||||
}
|
||||
|
||||
void MainList::updateCloudUnread(const MTPDdialogFolder &data) {
|
||||
const auto notifier = unreadStateChangeNotifier(!loaded());
|
||||
|
||||
_cloudUnreadState.messages = data.vunread_muted_messages_count().v
|
||||
+ data.vunread_unmuted_messages_count().v;
|
||||
_cloudUnreadState.chats = data.vunread_muted_peers_count().v
|
||||
+ data.vunread_unmuted_peers_count().v;
|
||||
finalizeCloudUnread();
|
||||
|
||||
_cloudUnreadState.known = true;
|
||||
}
|
||||
|
||||
bool MainList::cloudUnreadKnown() const {
|
||||
return _cloudUnreadState.known;
|
||||
}
|
||||
|
||||
void MainList::finalizeCloudUnread() {
|
||||
// Cloud state for archive folder always counts everything as muted.
|
||||
_cloudUnreadState.messagesMuted = _cloudUnreadState.messages;
|
||||
_cloudUnreadState.chatsMuted = _cloudUnreadState.chats;
|
||||
|
||||
// We don't know the real value of marked chats counts in cloud unread.
|
||||
_cloudUnreadState.marksMuted = _cloudUnreadState.marks = 0;
|
||||
}
|
||||
|
||||
UnreadState MainList::unreadState() const {
|
||||
const auto useCloudState = _cloudUnreadState.known && !loaded();
|
||||
auto result = useCloudState ? _cloudUnreadState : _unreadState;
|
||||
|
||||
// We don't know the real value of marked chats counts in cloud unread.
|
||||
if (useCloudState) {
|
||||
result.marks = _unreadState.marks;
|
||||
result.marksMuted = _unreadState.marksMuted;
|
||||
}
|
||||
if (_allAreMuted) {
|
||||
result.messagesMuted = result.messages;
|
||||
result.chatsMuted = result.chats;
|
||||
result.marksMuted = result.marks;
|
||||
}
|
||||
#ifdef Q_OS_WIN
|
||||
[[maybe_unused]] volatile auto touch = 0
|
||||
+ _unreadState.marks + _unreadState.marksMuted
|
||||
+ _unreadState.messages + _unreadState.messagesMuted
|
||||
+ _unreadState.chats + _unreadState.chatsMuted
|
||||
+ _unreadState.reactions + _unreadState.reactionsMuted
|
||||
+ _unreadState.mentions;
|
||||
#endif // Q_OS_WIN
|
||||
return result;
|
||||
}
|
||||
|
||||
rpl::producer<UnreadState> MainList::unreadStateChanges() const {
|
||||
return _unreadStateChanges.events();
|
||||
}
|
||||
|
||||
not_null<IndexedList*> MainList::indexed() {
|
||||
return &_all;
|
||||
}
|
||||
|
||||
not_null<const IndexedList*> MainList::indexed() const {
|
||||
return &_all;
|
||||
}
|
||||
|
||||
not_null<PinnedList*> MainList::pinned() {
|
||||
return &_pinned;
|
||||
}
|
||||
|
||||
not_null<const PinnedList*> MainList::pinned() const {
|
||||
return &_pinned;
|
||||
}
|
||||
|
||||
} // namespace Dialogs
|
||||
88
Telegram/SourceFiles/dialogs/dialogs_main_list.h
Normal file
88
Telegram/SourceFiles/dialogs/dialogs_main_list.h
Normal file
@@ -0,0 +1,88 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "dialogs/dialogs_common.h"
|
||||
#include "dialogs/dialogs_indexed_list.h"
|
||||
#include "dialogs/dialogs_pinned_list.h"
|
||||
|
||||
namespace Main {
|
||||
class Session;
|
||||
} // namespace Main
|
||||
|
||||
namespace Data {
|
||||
class Thread;
|
||||
} // namespace Data
|
||||
|
||||
namespace Dialogs {
|
||||
|
||||
class MainList final {
|
||||
public:
|
||||
MainList(
|
||||
not_null<Main::Session*> session,
|
||||
FilterId filterId,
|
||||
rpl::producer<int> pinnedLimit);
|
||||
|
||||
bool empty() const;
|
||||
bool loaded() const;
|
||||
void setLoaded(bool loaded = true);
|
||||
void setAllAreMuted(bool allAreMuted = true);
|
||||
void clear();
|
||||
|
||||
RowsByLetter addEntry(Key key);
|
||||
void removeEntry(Key key);
|
||||
|
||||
void unreadStateChanged(
|
||||
const UnreadState &wasState,
|
||||
const UnreadState &nowState);
|
||||
void unreadEntryChanged(const UnreadState &state, bool added);
|
||||
void updateCloudUnread(const MTPDdialogFolder &data);
|
||||
[[nodiscard]] bool cloudUnreadKnown() const;
|
||||
[[nodiscard]] UnreadState unreadState() const;
|
||||
[[nodiscard]] rpl::producer<UnreadState> unreadStateChanges() const;
|
||||
|
||||
[[nodiscard]] not_null<IndexedList*> indexed();
|
||||
[[nodiscard]] not_null<const IndexedList*> indexed() const;
|
||||
[[nodiscard]] not_null<PinnedList*> pinned();
|
||||
[[nodiscard]] not_null<const PinnedList*> pinned() const;
|
||||
|
||||
void setCloudListSize(int size);
|
||||
[[nodiscard]] const rpl::variable<int> &fullSize() const;
|
||||
|
||||
private:
|
||||
void finalizeCloudUnread();
|
||||
void recomputeFullListSize();
|
||||
|
||||
inline auto unreadStateChangeNotifier(bool notify);
|
||||
|
||||
FilterId _filterId = 0;
|
||||
IndexedList _all;
|
||||
PinnedList _pinned;
|
||||
UnreadState _unreadState;
|
||||
UnreadState _cloudUnreadState;
|
||||
rpl::event_stream<UnreadState> _unreadStateChanges;
|
||||
rpl::variable<int> _fullListSize = 0;
|
||||
int _cloudListSize = 0;
|
||||
|
||||
bool _loaded = false;
|
||||
bool _allAreMuted = false;
|
||||
|
||||
rpl::lifetime _lifetime;
|
||||
|
||||
};
|
||||
|
||||
auto MainList::unreadStateChangeNotifier(bool notify) {
|
||||
const auto wasState = notify ? unreadState() : UnreadState();
|
||||
return gsl::finally([=] {
|
||||
if (notify) {
|
||||
_unreadStateChanges.fire_copy(wasState);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
} // namespace Dialogs
|
||||
164
Telegram/SourceFiles/dialogs/dialogs_pinned_list.cpp
Normal file
164
Telegram/SourceFiles/dialogs/dialogs_pinned_list.cpp
Normal file
@@ -0,0 +1,164 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#include "dialogs/dialogs_pinned_list.h"
|
||||
|
||||
#include "data/data_saved_messages.h"
|
||||
#include "dialogs/dialogs_key.h"
|
||||
#include "dialogs/dialogs_entry.h"
|
||||
#include "history/history.h"
|
||||
#include "data/data_session.h"
|
||||
#include "data/data_forum.h"
|
||||
|
||||
namespace Dialogs {
|
||||
|
||||
PinnedList::PinnedList(FilterId filterId, int limit)
|
||||
: _filterId(filterId)
|
||||
, _limit(limit) {
|
||||
Expects(limit > 0);
|
||||
}
|
||||
|
||||
void PinnedList::setLimit(int limit) {
|
||||
Expects(limit > 0);
|
||||
|
||||
if (_limit == limit) {
|
||||
return;
|
||||
}
|
||||
_limit = limit;
|
||||
applyLimit(_limit);
|
||||
}
|
||||
|
||||
void PinnedList::addPinned(Key key) {
|
||||
Expects(key.entry()->folderKnown());
|
||||
|
||||
addPinnedGetPosition(key);
|
||||
}
|
||||
|
||||
int PinnedList::addPinnedGetPosition(Key key) {
|
||||
const auto already = ranges::find(_data, key);
|
||||
if (already != end(_data)) {
|
||||
return already - begin(_data);
|
||||
}
|
||||
applyLimit(_limit - 1);
|
||||
const auto position = int(_data.size());
|
||||
_data.push_back(key);
|
||||
key.entry()->cachePinnedIndex(_filterId, position + 1);
|
||||
return position;
|
||||
}
|
||||
|
||||
void PinnedList::setPinned(Key key, bool pinned) {
|
||||
Expects(key.entry()->folderKnown() || _filterId != 0);
|
||||
|
||||
if (pinned) {
|
||||
const int position = addPinnedGetPosition(key);
|
||||
if (position) {
|
||||
const auto begin = _data.begin();
|
||||
std::rotate(begin, begin + position, begin + position + 1);
|
||||
for (auto i = 0; i != position + 1; ++i) {
|
||||
_data[i].entry()->cachePinnedIndex(_filterId, i + 1);
|
||||
}
|
||||
}
|
||||
} else if (const auto it = ranges::find(_data, key); it != end(_data)) {
|
||||
const auto index = int(it - begin(_data));
|
||||
_data.erase(it);
|
||||
key.entry()->cachePinnedIndex(_filterId, 0);
|
||||
for (auto i = index, count = int(size(_data)); i != count; ++i) {
|
||||
_data[i].entry()->cachePinnedIndex(_filterId, i + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void PinnedList::applyLimit(int limit) {
|
||||
Expects(limit >= 0);
|
||||
|
||||
while (_data.size() > limit) {
|
||||
setPinned(_data.back(), false);
|
||||
}
|
||||
}
|
||||
|
||||
void PinnedList::clear() {
|
||||
applyLimit(0);
|
||||
}
|
||||
|
||||
void PinnedList::applyList(
|
||||
not_null<Data::Session*> owner,
|
||||
const QVector<MTPDialogPeer> &list) {
|
||||
Expects(this != owner->savedMessages().chatsList()->pinned());
|
||||
|
||||
clear();
|
||||
for (const auto &peer : list) {
|
||||
peer.match([&](const MTPDdialogPeer &data) {
|
||||
if (const auto peerId = peerFromMTP(data.vpeer())) {
|
||||
addPinned(owner->history(peerId));
|
||||
}
|
||||
}, [&](const MTPDdialogPeerFolder &data) {
|
||||
addPinned(owner->folder(data.vfolder_id().v));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void PinnedList::applyList(
|
||||
not_null<Data::SavedMessages*> sublistsOwner,
|
||||
const QVector<MTPDialogPeer> &list) {
|
||||
Expects(this == sublistsOwner->chatsList()->pinned());
|
||||
|
||||
clear();
|
||||
for (const auto &peer : list) {
|
||||
peer.match([&](const MTPDdialogPeer &data) {
|
||||
if (const auto peerId = peerFromMTP(data.vpeer())) {
|
||||
const auto peer = sublistsOwner->owner().peer(peerId);
|
||||
addPinned(sublistsOwner->sublist(peer));
|
||||
}
|
||||
}, [](const MTPDdialogPeerFolder &data) {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void PinnedList::applyList(
|
||||
not_null<Data::Forum*> forum,
|
||||
const QVector<MTPint> &list) {
|
||||
Expects(this == forum->topicsList()->pinned());
|
||||
|
||||
clear();
|
||||
for (const auto &topicId : list) {
|
||||
addPinned(forum->topicFor(topicId.v));
|
||||
}
|
||||
}
|
||||
|
||||
void PinnedList::applyList(const std::vector<not_null<History*>> &list) {
|
||||
Expects(_filterId != 0);
|
||||
|
||||
const auto old = base::take(_data);
|
||||
|
||||
const auto count = int(list.size());
|
||||
_data.reserve(count);
|
||||
for (auto i = 0; i != count; ++i) {
|
||||
const auto history = list[i];
|
||||
_data.emplace_back(history);
|
||||
history->cachePinnedIndex(_filterId, i + 1);
|
||||
}
|
||||
|
||||
for (const auto &key : old) {
|
||||
const auto history = key.history();
|
||||
if (!history || !ranges::contains(_data, history, &Key::history)) {
|
||||
key.entry()->cachePinnedIndex(_filterId, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void PinnedList::reorder(Key key1, Key key2) {
|
||||
const auto index1 = ranges::find(_data, key1) - begin(_data);
|
||||
const auto index2 = ranges::find(_data, key2) - begin(_data);
|
||||
Assert(index1 >= 0 && index1 < _data.size());
|
||||
Assert(index2 >= 0 && index2 < _data.size());
|
||||
Assert(index1 != index2);
|
||||
std::swap(_data[index1], _data[index2]);
|
||||
key1.entry()->cachePinnedIndex(_filterId, index2 + 1);
|
||||
key2.entry()->cachePinnedIndex(_filterId, index1 + 1);
|
||||
}
|
||||
|
||||
} // namespace Dialogs
|
||||
63
Telegram/SourceFiles/dialogs/dialogs_pinned_list.h
Normal file
63
Telegram/SourceFiles/dialogs/dialogs_pinned_list.h
Normal file
@@ -0,0 +1,63 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
class History;
|
||||
|
||||
namespace Data {
|
||||
class SavedMessages;
|
||||
class Session;
|
||||
class Forum;
|
||||
} // namespace Data
|
||||
|
||||
namespace Dialogs {
|
||||
|
||||
class Key;
|
||||
|
||||
class PinnedList final {
|
||||
public:
|
||||
PinnedList(FilterId filterId, int limit);
|
||||
|
||||
void setLimit(int limit);
|
||||
|
||||
// Places on the last place in the list otherwise.
|
||||
// Does nothing if already pinned.
|
||||
void addPinned(Key key);
|
||||
|
||||
// if (pinned) places on the first place in the list.
|
||||
void setPinned(Key key, bool pinned);
|
||||
|
||||
void clear();
|
||||
|
||||
void applyList(
|
||||
not_null<Data::Session*> owner,
|
||||
const QVector<MTPDialogPeer> &list);
|
||||
void applyList(
|
||||
not_null<Data::SavedMessages*> sublistsOwner,
|
||||
const QVector<MTPDialogPeer> &list);
|
||||
void applyList(
|
||||
not_null<Data::Forum*> forum,
|
||||
const QVector<MTPint> &list);
|
||||
void applyList(const std::vector<not_null<History*>> &list);
|
||||
void reorder(Key key1, Key key2);
|
||||
|
||||
const std::vector<Key> &order() const {
|
||||
return _data;
|
||||
}
|
||||
|
||||
private:
|
||||
int addPinnedGetPosition(Key key);
|
||||
void applyLimit(int limit);
|
||||
|
||||
FilterId _filterId = 0;
|
||||
int _limit = 0;
|
||||
std::vector<Key> _data;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Dialogs
|
||||
267
Telegram/SourceFiles/dialogs/dialogs_quick_action.cpp
Normal file
267
Telegram/SourceFiles/dialogs/dialogs_quick_action.cpp
Normal file
@@ -0,0 +1,267 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#include "dialogs/dialogs_quick_action.h"
|
||||
|
||||
#include "dialogs/ui/dialogs_quick_action_context.h"
|
||||
#include "apiwrap.h"
|
||||
#include "data/data_histories.h"
|
||||
#include "data/data_peer.h"
|
||||
#include "data/data_session.h"
|
||||
#include "dialogs/dialogs_entry.h"
|
||||
#include "history/history.h"
|
||||
#include "lang/lang_instance.h"
|
||||
#include "lang/lang_keys.h"
|
||||
#include "lottie/lottie_icon.h"
|
||||
#include "main/main_session.h"
|
||||
#include "menu/menu_mute.h"
|
||||
#include "window/window_peer_menu.h"
|
||||
#include "window/window_session_controller.h"
|
||||
#include "styles/style_dialogs.h"
|
||||
|
||||
namespace Dialogs {
|
||||
namespace {
|
||||
|
||||
const style::font &SwipeActionFont(
|
||||
Dialogs::Ui::QuickDialogActionLabel action,
|
||||
int availableWidth) {
|
||||
struct Entry final {
|
||||
Dialogs::Ui::QuickDialogActionLabel action;
|
||||
QString langId;
|
||||
style::font font;
|
||||
};
|
||||
static auto Fonts = std::vector<Entry>();
|
||||
for (auto &entry : Fonts) {
|
||||
if (entry.action == action) {
|
||||
if (entry.langId == Lang::GetInstance().id()) {
|
||||
return entry.font;
|
||||
}
|
||||
}
|
||||
}
|
||||
constexpr auto kNormalFontSize = 13;
|
||||
constexpr auto kMinFontSize = 5;
|
||||
for (auto i = kNormalFontSize; i >= kMinFontSize; --i) {
|
||||
auto font = style::font(
|
||||
style::ConvertScale(i, style::Scale()),
|
||||
st::semiboldFont->flags(),
|
||||
st::semiboldFont->family());
|
||||
if (font->width(ResolveQuickDialogLabel(action)) <= availableWidth
|
||||
|| i == kMinFontSize) {
|
||||
Fonts.emplace_back(Entry{
|
||||
.action = action,
|
||||
.langId = Lang::GetInstance().id(),
|
||||
.font = std::move(font),
|
||||
});
|
||||
return Fonts.back().font;
|
||||
}
|
||||
}
|
||||
Unexpected("SwipeActionFont: can't find font.");
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void PerformQuickDialogAction(
|
||||
not_null<Window::SessionController*> controller,
|
||||
not_null<PeerData*> peer,
|
||||
Ui::QuickDialogAction action,
|
||||
FilterId filterId) {
|
||||
const auto history = peer->owner().history(peer);
|
||||
if (action == Dialogs::Ui::QuickDialogAction::Mute) {
|
||||
const auto isMuted = rpl::variable<bool>(
|
||||
MuteMenu::ThreadDescriptor(history).isMutedValue()).current();
|
||||
MuteMenu::ThreadDescriptor(history).updateMutePeriod(isMuted
|
||||
? 0
|
||||
: std::numeric_limits<TimeId>::max());
|
||||
controller->showToast(isMuted
|
||||
? tr::lng_quick_dialog_action_toast_unmute_success(tr::now)
|
||||
: tr::lng_quick_dialog_action_toast_mute_success(tr::now));
|
||||
} else if (action == Dialogs::Ui::QuickDialogAction::Pin) {
|
||||
const auto entry = (Dialogs::Entry*)(history);
|
||||
const auto isPinned = entry->isPinnedDialog(filterId);
|
||||
const auto onToggled = isPinned
|
||||
? Fn<void()>(nullptr)
|
||||
: [=] {
|
||||
controller->showToast(
|
||||
tr::lng_quick_dialog_action_toast_pin_success(tr::now));
|
||||
};
|
||||
Window::TogglePinnedThread(controller, entry, filterId, onToggled);
|
||||
if (isPinned) {
|
||||
controller->showToast(
|
||||
tr::lng_quick_dialog_action_toast_unpin_success(tr::now));
|
||||
}
|
||||
} else if (action == Dialogs::Ui::QuickDialogAction::Read) {
|
||||
if (Window::IsUnreadThread(history)) {
|
||||
Window::MarkAsReadThread(history);
|
||||
controller->showToast(
|
||||
tr::lng_quick_dialog_action_toast_read_success(tr::now));
|
||||
} else if (history) {
|
||||
peer->owner().histories().changeDialogUnreadMark(history, true);
|
||||
controller->showToast(
|
||||
tr::lng_quick_dialog_action_toast_unread_success(tr::now));
|
||||
}
|
||||
} else if (action == Dialogs::Ui::QuickDialogAction::Archive) {
|
||||
const auto isArchived = Window::IsArchived(history);
|
||||
controller->showToast(isArchived
|
||||
? tr::lng_quick_dialog_action_toast_unarchive_success(tr::now)
|
||||
: tr::lng_quick_dialog_action_toast_archive_success(tr::now));
|
||||
history->session().api().toggleHistoryArchived(
|
||||
history,
|
||||
!isArchived,
|
||||
[] {});
|
||||
} else if (action == Dialogs::Ui::QuickDialogAction::Delete) {
|
||||
Window::DeleteAndLeaveHandler(controller, peer)();
|
||||
}
|
||||
}
|
||||
|
||||
QString ResolveQuickDialogLottieIconName(Ui::QuickDialogActionLabel action) {
|
||||
switch (action) {
|
||||
case Ui::QuickDialogActionLabel::Mute:
|
||||
return u"swipe_mute"_q;
|
||||
case Ui::QuickDialogActionLabel::Unmute:
|
||||
return u"swipe_unmute"_q;
|
||||
case Ui::QuickDialogActionLabel::Pin:
|
||||
return u"swipe_pin"_q;
|
||||
case Ui::QuickDialogActionLabel::Unpin:
|
||||
return u"swipe_unpin"_q;
|
||||
case Ui::QuickDialogActionLabel::Read:
|
||||
return u"swipe_read"_q;
|
||||
case Ui::QuickDialogActionLabel::Unread:
|
||||
return u"swipe_unread"_q;
|
||||
case Ui::QuickDialogActionLabel::Archive:
|
||||
return u"swipe_archive"_q;
|
||||
case Ui::QuickDialogActionLabel::Unarchive:
|
||||
return u"swipe_unarchive"_q;
|
||||
case Ui::QuickDialogActionLabel::Delete:
|
||||
return u"swipe_delete"_q;
|
||||
default:
|
||||
return u"swipe_disabled"_q;
|
||||
}
|
||||
}
|
||||
|
||||
Ui::QuickDialogActionLabel ResolveQuickDialogLabel(
|
||||
not_null<History*> history,
|
||||
Ui::QuickDialogAction action,
|
||||
FilterId filterId) {
|
||||
if (action == Dialogs::Ui::QuickDialogAction::Mute) {
|
||||
if (history->peer->isSelf()) {
|
||||
return Ui::QuickDialogActionLabel::Disabled;
|
||||
}
|
||||
const auto isMuted = rpl::variable<bool>(
|
||||
MuteMenu::ThreadDescriptor(history).isMutedValue()).current();
|
||||
return isMuted
|
||||
? Ui::QuickDialogActionLabel::Unmute
|
||||
: Ui::QuickDialogActionLabel::Mute;
|
||||
} else if (action == Dialogs::Ui::QuickDialogAction::Pin) {
|
||||
const auto entry = (Dialogs::Entry*)(history);
|
||||
return entry->isPinnedDialog(filterId)
|
||||
? Ui::QuickDialogActionLabel::Unpin
|
||||
: Ui::QuickDialogActionLabel::Pin;
|
||||
} else if (action == Dialogs::Ui::QuickDialogAction::Read) {
|
||||
const auto unread = Window::IsUnreadThread(history);
|
||||
if (history->isForum() && !unread) {
|
||||
return Ui::QuickDialogActionLabel::Disabled;
|
||||
}
|
||||
return unread
|
||||
? Ui::QuickDialogActionLabel::Read
|
||||
: Ui::QuickDialogActionLabel::Unread;
|
||||
} else if (action == Dialogs::Ui::QuickDialogAction::Archive) {
|
||||
if (!Window::CanArchive(history, history->peer)) {
|
||||
return Ui::QuickDialogActionLabel::Disabled;
|
||||
}
|
||||
return Window::IsArchived(history)
|
||||
? Ui::QuickDialogActionLabel::Unarchive
|
||||
: Ui::QuickDialogActionLabel::Archive;
|
||||
} else if (action == Dialogs::Ui::QuickDialogAction::Delete) {
|
||||
return Ui::QuickDialogActionLabel::Delete;
|
||||
}
|
||||
return Ui::QuickDialogActionLabel::Disabled;
|
||||
}
|
||||
|
||||
QString ResolveQuickDialogLabel(Ui::QuickDialogActionLabel action) {
|
||||
switch (action) {
|
||||
case Ui::QuickDialogActionLabel::Mute:
|
||||
return tr::lng_settings_quick_dialog_action_mute(tr::now);
|
||||
case Ui::QuickDialogActionLabel::Unmute:
|
||||
return tr::lng_settings_quick_dialog_action_unmute(tr::now);
|
||||
case Ui::QuickDialogActionLabel::Pin:
|
||||
return tr::lng_settings_quick_dialog_action_pin(tr::now);
|
||||
case Ui::QuickDialogActionLabel::Unpin:
|
||||
return tr::lng_settings_quick_dialog_action_unpin(tr::now);
|
||||
case Ui::QuickDialogActionLabel::Read:
|
||||
return tr::lng_settings_quick_dialog_action_read(tr::now);
|
||||
case Ui::QuickDialogActionLabel::Unread:
|
||||
return tr::lng_settings_quick_dialog_action_unread(tr::now);
|
||||
case Ui::QuickDialogActionLabel::Archive:
|
||||
return tr::lng_settings_quick_dialog_action_archive(tr::now);
|
||||
case Ui::QuickDialogActionLabel::Unarchive:
|
||||
return tr::lng_settings_quick_dialog_action_unarchive(tr::now);
|
||||
case Ui::QuickDialogActionLabel::Delete:
|
||||
return tr::lng_settings_quick_dialog_action_delete(tr::now);
|
||||
default:
|
||||
return tr::lng_settings_quick_dialog_action_disabled(tr::now);
|
||||
};
|
||||
}
|
||||
|
||||
const style::color &ResolveQuickActionBg(
|
||||
Ui::QuickDialogActionLabel action) {
|
||||
switch (action) {
|
||||
case Ui::QuickDialogActionLabel::Delete:
|
||||
return st::attentionButtonFg;
|
||||
case Ui::QuickDialogActionLabel::Disabled:
|
||||
return st::windowSubTextFgOver;
|
||||
case Ui::QuickDialogActionLabel::Mute:
|
||||
case Ui::QuickDialogActionLabel::Unmute:
|
||||
case Ui::QuickDialogActionLabel::Pin:
|
||||
case Ui::QuickDialogActionLabel::Unpin:
|
||||
case Ui::QuickDialogActionLabel::Read:
|
||||
case Ui::QuickDialogActionLabel::Unread:
|
||||
case Ui::QuickDialogActionLabel::Archive:
|
||||
case Ui::QuickDialogActionLabel::Unarchive:
|
||||
default:
|
||||
return st::windowBgActive;
|
||||
};
|
||||
}
|
||||
|
||||
const style::color &ResolveQuickActionBgActive(
|
||||
Ui::QuickDialogActionLabel action) {
|
||||
return st::windowSubTextFgOver;
|
||||
}
|
||||
|
||||
void DrawQuickAction(
|
||||
QPainter &p,
|
||||
const QRect &rect,
|
||||
not_null<Lottie::Icon*> icon,
|
||||
Ui::QuickDialogActionLabel label,
|
||||
float64 iconRatio,
|
||||
bool twoLines) {
|
||||
const auto iconSize = st::dialogsQuickActionSize * iconRatio;
|
||||
const auto innerHeight = iconSize * 2;
|
||||
const auto top = (rect.height() - innerHeight) / 2;
|
||||
icon->paint(p, rect.x() + (rect.width() - iconSize) / 2, top);
|
||||
p.setPen(st::premiumButtonFg);
|
||||
p.setBrush(Qt::NoBrush);
|
||||
const auto availableWidth = rect.width();
|
||||
p.setFont(SwipeActionFont(label, availableWidth));
|
||||
if (twoLines) {
|
||||
auto text = ResolveQuickDialogLabel(label);
|
||||
const auto index = text.indexOf(' ');
|
||||
if (index != -1) {
|
||||
text = text.replace(index, 1, '\n');
|
||||
}
|
||||
p.drawText(
|
||||
QRect(rect.x(), top, availableWidth, innerHeight),
|
||||
std::move(text),
|
||||
style::al_bottom);
|
||||
} else {
|
||||
p.drawText(
|
||||
QRect(rect.x(), top, availableWidth, innerHeight),
|
||||
ResolveQuickDialogLabel(label),
|
||||
style::al_bottom);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace Dialogs
|
||||
57
Telegram/SourceFiles/dialogs/dialogs_quick_action.h
Normal file
57
Telegram/SourceFiles/dialogs/dialogs_quick_action.h
Normal file
@@ -0,0 +1,57 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
class History;
|
||||
class PeerData;
|
||||
|
||||
namespace Dialogs::Ui {
|
||||
enum class QuickDialogAction;
|
||||
enum class QuickDialogActionLabel;
|
||||
} // namespace Dialogs::Ui
|
||||
|
||||
namespace Lottie {
|
||||
class Icon;
|
||||
} // namespace Lottie
|
||||
|
||||
namespace Window {
|
||||
class SessionController;
|
||||
} // namespace Window
|
||||
|
||||
namespace Dialogs {
|
||||
|
||||
void PerformQuickDialogAction(
|
||||
not_null<Window::SessionController*> controller,
|
||||
not_null<PeerData*> peer,
|
||||
Ui::QuickDialogAction action,
|
||||
FilterId filterId);
|
||||
|
||||
[[nodiscard]] QString ResolveQuickDialogLottieIconName(
|
||||
Ui::QuickDialogActionLabel action);
|
||||
|
||||
[[nodiscard]] Ui::QuickDialogActionLabel ResolveQuickDialogLabel(
|
||||
not_null<History*> history,
|
||||
Ui::QuickDialogAction action,
|
||||
FilterId filterId);
|
||||
|
||||
[[nodiscard]] QString ResolveQuickDialogLabel(Ui::QuickDialogActionLabel);
|
||||
|
||||
[[nodiscard]] const style::color &ResolveQuickActionBg(
|
||||
Ui::QuickDialogActionLabel);
|
||||
[[nodiscard]] const style::color &ResolveQuickActionBgActive(
|
||||
Ui::QuickDialogActionLabel);
|
||||
|
||||
void DrawQuickAction(
|
||||
QPainter &p,
|
||||
const QRect &rect,
|
||||
not_null<Lottie::Icon*> icon,
|
||||
Ui::QuickDialogActionLabel label,
|
||||
float64 iconRatio = 1.,
|
||||
bool twoLines = false);
|
||||
|
||||
} // namespace Dialogs
|
||||
785
Telegram/SourceFiles/dialogs/dialogs_row.cpp
Normal file
785
Telegram/SourceFiles/dialogs/dialogs_row.cpp
Normal file
@@ -0,0 +1,785 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#include "dialogs/dialogs_row.h"
|
||||
|
||||
#include "ui/chat/chat_theme.h" // CountAverageColor.
|
||||
#include "ui/color_contrast.h"
|
||||
#include "ui/effects/credits_graphics.h"
|
||||
#include "ui/effects/outline_segments.h"
|
||||
#include "ui/effects/ripple_animation.h"
|
||||
#include "ui/effects/round_checkbox.h"
|
||||
#include "ui/image/image_prepare.h"
|
||||
#include "ui/text/custom_emoji_text_badge.h"
|
||||
#include "ui/text/format_values.h"
|
||||
#include "ui/text/text_options.h"
|
||||
#include "ui/text/text_utilities.h"
|
||||
#include "ui/painter.h"
|
||||
#include "dialogs/dialogs_entry.h"
|
||||
#include "dialogs/ui/dialogs_video_userpic.h"
|
||||
#include "dialogs/ui/dialogs_layout.h"
|
||||
#include "data/data_folder.h"
|
||||
#include "data/data_forum.h"
|
||||
#include "data/data_session.h"
|
||||
#include "data/data_stories.h"
|
||||
#include "data/data_peer_values.h"
|
||||
#include "data/data_user.h"
|
||||
#include "history/history.h"
|
||||
#include "history/history_item.h"
|
||||
#include "lang/lang_keys.h"
|
||||
#include "base/unixtime.h"
|
||||
#include "styles/style_calls.h"
|
||||
#include "styles/style_dialogs.h"
|
||||
|
||||
namespace Dialogs {
|
||||
namespace {
|
||||
|
||||
constexpr auto kTopLayer = 2;
|
||||
constexpr auto kBottomLayer = 1;
|
||||
constexpr auto kNoneLayer = 0;
|
||||
constexpr auto kBlurRadius = 24;
|
||||
|
||||
[[nodiscard]] const QPainterPath &SubscriptionOutlinePath() {
|
||||
static auto path = QPainterPath();
|
||||
if (!path.isEmpty()) {
|
||||
return path;
|
||||
}
|
||||
const auto scaledMoveTo = [&](float64 x, float64 y) {
|
||||
path.moveTo(style::ConvertFloatScale(x), style::ConvertFloatScale(y));
|
||||
};
|
||||
const auto scaledLineTo = [&](float64 x, float64 y) {
|
||||
path.lineTo(style::ConvertFloatScale(x), style::ConvertFloatScale(y));
|
||||
};
|
||||
const auto scaledCubicTo = [&](
|
||||
float64 x1,
|
||||
float64 y1,
|
||||
float64 x2,
|
||||
float64 y2,
|
||||
float64 x3,
|
||||
float64 y3) {
|
||||
path.cubicTo(
|
||||
style::ConvertFloatScale(x1),
|
||||
style::ConvertFloatScale(y1),
|
||||
style::ConvertFloatScale(x2),
|
||||
style::ConvertFloatScale(y2),
|
||||
style::ConvertFloatScale(x3),
|
||||
style::ConvertFloatScale(y3));
|
||||
};
|
||||
const auto scaledTranslate = [&](float64 x, float64 y) {
|
||||
path.translate(
|
||||
style::ConvertFloatScale(x),
|
||||
style::ConvertFloatScale(y));
|
||||
};
|
||||
|
||||
scaledMoveTo(42.3009, 18.3345);
|
||||
scaledLineTo(44.3285, 14.1203);
|
||||
scaledCubicTo(44.6152, 13.6549, 45.7858, 13.3542, 46.1909, 13.5523);
|
||||
scaledCubicTo(46.3355, 13.6044, 47.0064, 13.7541, 47.3833, 14.5053);
|
||||
scaledLineTo(49.3924 * 1.0071, 18.4206 * 0.9905);
|
||||
// 49.5459 * 1.007, 18.7336 * 0.9897.
|
||||
scaledCubicTo(49.8927213, 18.5406439, 52.5473, 18.8491, 53.3141, 18.8789);
|
||||
scaledCubicTo(53.6484, 18.8441, 55.8914, 20.0065, 54.3752, 20.7818);
|
||||
scaledCubicTo(54.1725, 20.8744, 41.3467, 31.3217, 41.3467, 31.3217);
|
||||
scaledCubicTo(40.7918, 31.5944, 41.2661, 31.4116, 40.8968, 30.9483);
|
||||
scaledCubicTo(39.9809, 30.3111, 40.0577, 25.4542, 40.1925, 25.5408);
|
||||
scaledCubicTo(39.9835, 25.6454, 38.4545, 22.9776, 37.8121, 22.3477);
|
||||
scaledLineTo(37.3236, 21.4448);
|
||||
scaledCubicTo(37.0943, 20.8845, 37.2524, 20.4742, 37.4164, 19.7765);
|
||||
scaledCubicTo(37.4703, 19.4582, 38.1756, 19.0759, 38.4504, 19.0422);
|
||||
scaledLineTo(41.6566, 18.6449);
|
||||
scaledCubicTo(41.5344, 18.6041, 42.2622, 18.6087, 42.3009, 18.3345);
|
||||
scaledTranslate(-42.3009, -18.3345);
|
||||
scaledTranslate(1.2, 0.4);
|
||||
|
||||
return path;
|
||||
}
|
||||
|
||||
[[nodiscard]] const QImage &SubscriptionIcon() {
|
||||
static auto starImage = QImage();
|
||||
if (!starImage.isNull()) {
|
||||
return starImage;
|
||||
}
|
||||
starImage = Ui::GenerateStars(st::dialogsSubscriptionBadgeSize, 1);
|
||||
return starImage;
|
||||
}
|
||||
|
||||
[[nodiscard]] QImage CornerBadgeTTL(
|
||||
not_null<PeerData*> peer,
|
||||
Ui::PeerUserpicView &view,
|
||||
int photoSize) {
|
||||
const auto ttl = peer->messagesTTL();
|
||||
if (!ttl) {
|
||||
return QImage();
|
||||
}
|
||||
const auto ratio = style::DevicePixelRatio();
|
||||
const auto fullSize = photoSize;
|
||||
const auto partRect = CornerBadgeTTLRect(fullSize);
|
||||
const auto &partSize = partRect.width();
|
||||
const auto partSkip = fullSize - partSize;
|
||||
auto result = Images::Circle(BlurredDarkenedPart(
|
||||
PeerData::GenerateUserpicImage(peer, view, fullSize * ratio, 0),
|
||||
QRect(
|
||||
QPoint(partSkip, partSkip) * ratio,
|
||||
QSize(partSize, partSize) * ratio)));
|
||||
result.setDevicePixelRatio(ratio);
|
||||
|
||||
auto q = QPainter(&result);
|
||||
PainterHighQualityEnabler hq(q);
|
||||
|
||||
const auto innerRect = QRect(QPoint(), partRect.size())
|
||||
- st::dialogsTTLBadgeInnerMargins;
|
||||
const auto ttlText = Ui::FormatTTLTiny(ttl);
|
||||
|
||||
q.setFont(st::dialogsScamFont);
|
||||
q.setPen(st::premiumButtonFg);
|
||||
q.drawText(
|
||||
innerRect,
|
||||
(ttlText.size() > 2) ? ttlText.mid(0, 2) : ttlText,
|
||||
style::al_center);
|
||||
|
||||
constexpr auto kPenWidth = 1.5;
|
||||
|
||||
const auto penWidth = style::ConvertScaleExact(kPenWidth);
|
||||
auto pen = QPen(st::premiumButtonFg);
|
||||
pen.setJoinStyle(Qt::RoundJoin);
|
||||
pen.setCapStyle(Qt::RoundCap);
|
||||
pen.setWidthF(penWidth);
|
||||
|
||||
q.setPen(pen);
|
||||
q.setBrush(Qt::NoBrush);
|
||||
q.drawArc(innerRect, arc::kQuarterLength, arc::kHalfLength);
|
||||
|
||||
q.setClipRect(innerRect
|
||||
- QMargins(innerRect.width() / 2, -penWidth, -penWidth, -penWidth));
|
||||
pen.setStyle(Qt::DotLine);
|
||||
q.setPen(pen);
|
||||
q.drawEllipse(innerRect);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
QRect CornerBadgeTTLRect(int photoSize) {
|
||||
const auto &partSize = st::dialogsTTLBadgeSize;
|
||||
return QRect(
|
||||
photoSize - partSize + st::dialogsTTLBadgeSkip.x(),
|
||||
photoSize - partSize + st::dialogsTTLBadgeSkip.y(),
|
||||
partSize,
|
||||
partSize);
|
||||
}
|
||||
|
||||
QImage BlurredDarkenedPart(QImage image, QRect part) {
|
||||
auto blurred = Images::BlurLargeImage(
|
||||
std::move(image),
|
||||
kBlurRadius).copy(part);
|
||||
|
||||
constexpr auto kMinAcceptableContrast = 4.5;
|
||||
const auto averageColor = Ui::CountAverageColor(blurred);
|
||||
const auto contrast = Ui::CountContrast(
|
||||
averageColor,
|
||||
st::premiumButtonFg->c);
|
||||
if (contrast < kMinAcceptableContrast) {
|
||||
constexpr auto kDarkerBy = 0.2;
|
||||
auto painterPart = QPainter(&blurred);
|
||||
painterPart.setOpacity(kDarkerBy);
|
||||
painterPart.fillRect(QRect(QPoint(), part.size()), Qt::black);
|
||||
}
|
||||
|
||||
blurred.setDevicePixelRatio(image.devicePixelRatio());
|
||||
return blurred;
|
||||
}
|
||||
|
||||
Row::CornerLayersManager::CornerLayersManager() = default;
|
||||
|
||||
bool Row::CornerLayersManager::isSameLayer(Layer layer) const {
|
||||
return isFinished() && (_nextLayer == layer);
|
||||
}
|
||||
|
||||
void Row::CornerLayersManager::setLayer(
|
||||
Layer layer,
|
||||
Fn<void()> updateCallback) {
|
||||
if (_nextLayer == layer) {
|
||||
return;
|
||||
}
|
||||
_lastFrameShown = false;
|
||||
_prevLayer = _nextLayer;
|
||||
_nextLayer = layer;
|
||||
if (_animation.animating()) {
|
||||
_animation.change(
|
||||
1.,
|
||||
st::dialogsOnlineBadgeDuration * (1. - _animation.value(1.)));
|
||||
} else if (updateCallback) {
|
||||
_animation.start(
|
||||
std::move(updateCallback),
|
||||
0.,
|
||||
1.,
|
||||
st::dialogsOnlineBadgeDuration);
|
||||
}
|
||||
}
|
||||
|
||||
float64 Row::CornerLayersManager::progressForLayer(Layer layer) const {
|
||||
return (_nextLayer == layer)
|
||||
? progress()
|
||||
: (_prevLayer == layer)
|
||||
? (1. - progress())
|
||||
: 0.;
|
||||
}
|
||||
|
||||
float64 Row::CornerLayersManager::progress() const {
|
||||
return _animation.value(1.);
|
||||
}
|
||||
|
||||
bool Row::CornerLayersManager::isFinished() const {
|
||||
return (progress() == 1.) && _lastFrameShown;
|
||||
}
|
||||
|
||||
void Row::CornerLayersManager::markFrameShown() {
|
||||
if (progress() == 1.) {
|
||||
_lastFrameShown = true;
|
||||
}
|
||||
}
|
||||
|
||||
bool Row::CornerLayersManager::isDisplayedNone() const {
|
||||
return (progress() == 1.) && (_nextLayer == 0);
|
||||
}
|
||||
|
||||
BasicRow::BasicRow() = default;
|
||||
BasicRow::~BasicRow() = default;
|
||||
|
||||
void BasicRow::addRipple(
|
||||
QPoint origin,
|
||||
QSize size,
|
||||
Fn<void()> updateCallback) {
|
||||
if (!_ripple) {
|
||||
addRippleWithMask(
|
||||
origin,
|
||||
Ui::RippleAnimation::RectMask(size),
|
||||
std::move(updateCallback));
|
||||
} else {
|
||||
_ripple->add(origin);
|
||||
}
|
||||
}
|
||||
|
||||
void BasicRow::addRippleWithMask(
|
||||
QPoint origin,
|
||||
QImage mask,
|
||||
Fn<void()> updateCallback) {
|
||||
_ripple = std::make_unique<Ui::RippleAnimation>(
|
||||
st::dialogsRipple,
|
||||
std::move(mask),
|
||||
std::move(updateCallback));
|
||||
_ripple->add(origin);
|
||||
}
|
||||
|
||||
void BasicRow::clearRipple() {
|
||||
_ripple = nullptr;
|
||||
}
|
||||
|
||||
void BasicRow::stopLastRipple() {
|
||||
if (_ripple) {
|
||||
_ripple->lastStop();
|
||||
}
|
||||
}
|
||||
|
||||
void BasicRow::paintRipple(
|
||||
QPainter &p,
|
||||
int x,
|
||||
int y,
|
||||
int outerWidth,
|
||||
const QColor *colorOverride) const {
|
||||
if (_ripple) {
|
||||
_ripple->paint(p, x, y, outerWidth, colorOverride);
|
||||
if (_ripple->empty()) {
|
||||
_ripple.reset();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void BasicRow::paintUserpic(
|
||||
Painter &p,
|
||||
not_null<Entry*> entry,
|
||||
PeerData *peer,
|
||||
Ui::VideoUserpic *videoUserpic,
|
||||
const Ui::PaintContext &context,
|
||||
bool hasUnreadBadgesAbove) const {
|
||||
PaintUserpic(p, entry, peer, videoUserpic, _userpic, context);
|
||||
}
|
||||
|
||||
Row::Row(Key key, int index, int top) : _id(key), _top(top), _index(index) {
|
||||
if (const auto history = key.history()) {
|
||||
updateCornerBadgeShown(history->peer);
|
||||
}
|
||||
}
|
||||
|
||||
Row::~Row() {
|
||||
clearTopicJumpRipple();
|
||||
}
|
||||
|
||||
const style::DialogRow &Row::ComputeSt(
|
||||
not_null<const Entry*> entry,
|
||||
FilterId filterId) {
|
||||
if (const auto history = entry->asHistory()) {
|
||||
const auto hasTags = entry->hasChatsFilterTags(filterId);
|
||||
const auto wideRow = history->isForum()
|
||||
|| history->amMonoforumAdmin();
|
||||
return wideRow
|
||||
? (hasTags ? st::taggedForumDialogRow : st::forumDialogRow)
|
||||
: hasTags
|
||||
? st::taggedDialogRow
|
||||
: st::defaultDialogRow;
|
||||
} else if (entry->asTopic()) {
|
||||
return st::forumTopicRow;
|
||||
}
|
||||
return st::defaultDialogRow;
|
||||
}
|
||||
|
||||
void Row::recountHeight(float64 narrowRatio, FilterId filterId) {
|
||||
const auto &st = ComputeSt(_id.entry(), filterId);
|
||||
_height = ((&st == &st::defaultDialogRow) || !_id.history())
|
||||
? st::defaultDialogRow.height
|
||||
: anim::interpolate(
|
||||
st.height,
|
||||
st::defaultDialogRow.height,
|
||||
narrowRatio);
|
||||
}
|
||||
|
||||
uint64 Row::sortKey(FilterId filterId) const {
|
||||
return _id.entry()->sortKeyInChatList(filterId);
|
||||
}
|
||||
|
||||
void Row::setCornerBadgeShown(
|
||||
CornerLayersManager::Layer nextLayer,
|
||||
Fn<void()> updateCallback) const {
|
||||
const auto cornerBadgeShown = (nextLayer ? 1 : 0);
|
||||
if (_cornerBadgeShown == cornerBadgeShown) {
|
||||
if (!cornerBadgeShown) {
|
||||
return;
|
||||
} else if (_cornerBadgeUserpic
|
||||
&& _cornerBadgeUserpic->layersManager.isSameLayer(nextLayer)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
const_cast<Row*>(this)->_cornerBadgeShown = cornerBadgeShown;
|
||||
ensureCornerBadgeUserpic();
|
||||
_cornerBadgeUserpic->layersManager.setLayer(
|
||||
nextLayer,
|
||||
std::move(updateCallback));
|
||||
if (!_cornerBadgeShown
|
||||
&& _cornerBadgeUserpic
|
||||
&& _cornerBadgeUserpic->layersManager.isDisplayedNone()) {
|
||||
_cornerBadgeUserpic = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
void Row::updateCornerBadgeShown(
|
||||
not_null<PeerData*> peer,
|
||||
Fn<void()> updateCallback,
|
||||
bool hasUnreadBadgesAbove) const {
|
||||
const auto user = peer->asUser();
|
||||
const auto now = user ? base::unixtime::now() : TimeId();
|
||||
const auto channel = user ? nullptr : peer->asChannel();
|
||||
const auto nextLayer = [&] {
|
||||
if (hasUnreadBadgesAbove) {
|
||||
return kNoneLayer;
|
||||
} else if (user && Data::IsUserOnline(user, now)) {
|
||||
return kTopLayer;
|
||||
} else if (channel
|
||||
&& (Data::ChannelHasActiveCall(channel)
|
||||
|| Data::ChannelHasSubscriptionUntilDate(channel))) {
|
||||
return kTopLayer;
|
||||
} else if (peer->messagesTTL()) {
|
||||
return kBottomLayer;
|
||||
}
|
||||
return kNoneLayer;
|
||||
}();
|
||||
setCornerBadgeShown(nextLayer, std::move(updateCallback));
|
||||
if ((nextLayer == kTopLayer) && user) {
|
||||
peer->owner().watchForOffline(user, now);
|
||||
}
|
||||
}
|
||||
|
||||
void Row::ensureCornerBadgeUserpic() const {
|
||||
if (_cornerBadgeUserpic) {
|
||||
return;
|
||||
}
|
||||
_cornerBadgeUserpic = std::make_unique<CornerBadgeUserpic>();
|
||||
}
|
||||
|
||||
void Row::PaintCornerBadgeFrame(
|
||||
not_null<CornerBadgeUserpic*> data,
|
||||
int framePadding,
|
||||
not_null<Entry*> entry,
|
||||
PeerData *peer,
|
||||
Ui::VideoUserpic *videoUserpic,
|
||||
Ui::PeerUserpicView &view,
|
||||
const Ui::PaintContext &context,
|
||||
bool subscribed) {
|
||||
data->frame.fill(Qt::transparent);
|
||||
|
||||
Painter q(&data->frame);
|
||||
q.translate(framePadding, framePadding);
|
||||
auto hq = std::optional<PainterHighQualityEnabler>();
|
||||
const auto photoSize = context.st->photoSize;
|
||||
const auto storiesCount = data->storiesCount;
|
||||
if (storiesCount) {
|
||||
hq.emplace(q);
|
||||
const auto line = st::dialogsStoriesFull.lineTwice / 2.;
|
||||
const auto skip = line * 3 / 2.;
|
||||
const auto scale = 1. - (2 * skip / photoSize);
|
||||
const auto center = photoSize / 2.;
|
||||
q.save();
|
||||
q.translate(center, center);
|
||||
q.scale(scale, scale);
|
||||
q.translate(-center, -center);
|
||||
}
|
||||
q.translate(-context.st->padding.left(), -context.st->padding.top());
|
||||
PaintUserpic(
|
||||
q,
|
||||
entry,
|
||||
peer,
|
||||
videoUserpic,
|
||||
view,
|
||||
context);
|
||||
q.translate(context.st->padding.left(), context.st->padding.top());
|
||||
if (storiesCount) {
|
||||
q.restore();
|
||||
|
||||
const auto outline = QRectF(0, 0, photoSize, photoSize);
|
||||
const auto storiesUnread = st::dialogsStoriesFull.lineTwice / 2.;
|
||||
const auto storiesLine = st::dialogsStoriesFull.lineReadTwice / 2.;
|
||||
auto segments = std::vector<Ui::OutlineSegment>();
|
||||
if (data->storiesHasVideoStream) {
|
||||
const auto storiesVideoStreamBrush = st::attentionButtonFg->b;
|
||||
segments.push_back({ storiesVideoStreamBrush, storiesUnread });
|
||||
} else {
|
||||
const auto storiesUnreadCount = data->storiesUnreadCount;
|
||||
const auto storiesUnreadBrush = [&] {
|
||||
if (context.active || !storiesUnreadCount) {
|
||||
return st::dialogsUnreadBgMutedActive->b;
|
||||
}
|
||||
auto gradient = Ui::UnreadStoryOutlineGradient(outline);
|
||||
return QBrush(gradient);
|
||||
}();
|
||||
const auto storiesBrush = context.active
|
||||
? st::dialogsUnreadBgMutedActive->b
|
||||
: st::dialogsUnreadBgMuted->b;
|
||||
segments.reserve(storiesCount);
|
||||
const auto storiesReadCount = storiesCount - storiesUnreadCount;
|
||||
for (auto i = 0; i != storiesReadCount; ++i) {
|
||||
segments.push_back({ storiesBrush, storiesLine });
|
||||
}
|
||||
for (auto i = 0; i != storiesUnreadCount; ++i) {
|
||||
segments.push_back({ storiesUnreadBrush, storiesUnread });
|
||||
}
|
||||
}
|
||||
if (peer && (peer->forum() || peer->monoforum())) {
|
||||
const auto radius = context.st->photoSize
|
||||
* Ui::ForumUserpicRadiusMultiplier();
|
||||
Ui::PaintOutlineSegments(q, outline, radius, segments);
|
||||
} else {
|
||||
Ui::PaintOutlineSegments(q, outline, segments);
|
||||
}
|
||||
|
||||
if (data->storiesHasVideoStream) {
|
||||
Ui::PaintLiveBadge(q, 0, 0, photoSize);
|
||||
}
|
||||
}
|
||||
|
||||
if (subscribed) {
|
||||
if (!hq) {
|
||||
hq.emplace(q);
|
||||
}
|
||||
// TODO: Unnecessarily repaints on activating peer.
|
||||
q.setCompositionMode(QPainter::CompositionMode_Source);
|
||||
const auto &s = st::dialogsSubscriptionBadgeSkip;
|
||||
auto path = SubscriptionOutlinePath();
|
||||
const auto x = photoSize - s.x() - st::dialogsSubscriptionBadgeSize;
|
||||
const auto y = photoSize - s.y() - st::dialogsSubscriptionBadgeSize;
|
||||
q.translate(x, y);
|
||||
q.fillPath(path, Qt::transparent);
|
||||
q.setCompositionMode(QPainter::CompositionMode_SourceOver);
|
||||
q.resetTransform();
|
||||
q.drawImage(x, y, SubscriptionIcon());
|
||||
return;
|
||||
}
|
||||
|
||||
const auto &manager = data->layersManager;
|
||||
if (const auto p = manager.progressForLayer(kBottomLayer); p > 0.) {
|
||||
const auto size = photoSize;
|
||||
if (data->cacheTTL.isNull() && peer && peer->messagesTTL()) {
|
||||
data->cacheTTL = CornerBadgeTTL(peer, view, size);
|
||||
}
|
||||
q.setOpacity(p);
|
||||
const auto point = CornerBadgeTTLRect(size).topLeft();
|
||||
q.drawImage(point, data->cacheTTL);
|
||||
q.setOpacity(1.);
|
||||
}
|
||||
const auto topLayerProgress = manager.progressForLayer(kTopLayer);
|
||||
if (!topLayerProgress) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!hq) {
|
||||
hq.emplace(q);
|
||||
}
|
||||
q.setCompositionMode(QPainter::CompositionMode_Source);
|
||||
|
||||
const auto online = peer && peer->isUser();
|
||||
const auto size = online
|
||||
? st::dialogsOnlineBadgeSize
|
||||
: st::dialogsCallBadgeSize;
|
||||
const auto stroke = st::dialogsOnlineBadgeStroke;
|
||||
const auto skip = online
|
||||
? st::dialogsOnlineBadgeSkip
|
||||
: st::dialogsCallBadgeSkip;
|
||||
const auto shrink = (size / 2) * (1. - topLayerProgress);
|
||||
|
||||
auto pen = QPen(Qt::transparent);
|
||||
pen.setWidthF(stroke * topLayerProgress);
|
||||
q.setPen(pen);
|
||||
q.setBrush(data->active
|
||||
? st::dialogsOnlineBadgeFgActive
|
||||
: st::dialogsOnlineBadgeFg);
|
||||
q.drawEllipse(QRectF(
|
||||
photoSize - skip.x() - size,
|
||||
photoSize - skip.y() - size,
|
||||
size,
|
||||
size
|
||||
).marginsRemoved({ shrink, shrink, shrink, shrink }));
|
||||
}
|
||||
|
||||
void Row::paintUserpic(
|
||||
Painter &p,
|
||||
not_null<Entry*> entry,
|
||||
PeerData *peer,
|
||||
Ui::VideoUserpic *videoUserpic,
|
||||
const Ui::PaintContext &context,
|
||||
bool hasUnreadBadgesAbove) const {
|
||||
if (peer) {
|
||||
updateCornerBadgeShown(peer, nullptr, hasUnreadBadgesAbove);
|
||||
}
|
||||
|
||||
const auto cornerBadgeShown = !_cornerBadgeUserpic
|
||||
? _cornerBadgeShown
|
||||
: !_cornerBadgeUserpic->layersManager.isDisplayedNone();
|
||||
const auto storiesPeer = peer
|
||||
? ((peer->isUser() || peer->isChannel()) ? peer : nullptr)
|
||||
: nullptr;
|
||||
const auto storiesFolder = peer ? nullptr : _id.folder();
|
||||
const auto storiesHas = storiesPeer
|
||||
? storiesPeer->hasActiveStories()
|
||||
: storiesFolder
|
||||
? (storiesFolder->storiesCount() > 0)
|
||||
: false;
|
||||
if (!cornerBadgeShown && !storiesHas) {
|
||||
BasicRow::paintUserpic(p, entry, peer, videoUserpic, context, false);
|
||||
if (!peer || !_cornerBadgeShown) {
|
||||
_cornerBadgeUserpic = nullptr;
|
||||
}
|
||||
return;
|
||||
}
|
||||
ensureCornerBadgeUserpic();
|
||||
const auto ratio = style::DevicePixelRatio();
|
||||
const auto framePadding = std::max({
|
||||
-st::dialogsCallBadgeSkip.x(),
|
||||
-st::dialogsCallBadgeSkip.y(),
|
||||
st::lineWidth * 2 });
|
||||
const auto frameSide = (2 * framePadding + context.st->photoSize)
|
||||
* ratio;
|
||||
const auto frameSize = QSize(frameSide, frameSide);
|
||||
const auto storiesSource = (storiesHas && storiesPeer)
|
||||
? storiesPeer->owner().stories().source(storiesPeer->id)
|
||||
: nullptr;
|
||||
const auto storiesCountReal = storiesSource
|
||||
? int(storiesSource->ids.size())
|
||||
: storiesFolder
|
||||
? storiesFolder->storiesCount()
|
||||
: storiesHas
|
||||
? 1
|
||||
: 0;
|
||||
const auto storiesUnreadCountReal = storiesSource
|
||||
? storiesSource->unreadCount()
|
||||
: storiesFolder
|
||||
? storiesFolder->storiesUnreadCount()
|
||||
: (storiesPeer && storiesPeer->hasUnreadStories())
|
||||
? 1
|
||||
: 0;
|
||||
const auto storiesHasVideoStream = storiesSource
|
||||
? storiesSource->hasVideoStream
|
||||
: (storiesPeer && storiesPeer->hasActiveVideoStream())
|
||||
? 1
|
||||
: 0;
|
||||
const auto limit = Ui::kOutlineSegmentsMax;
|
||||
const auto storiesCount = std::min(storiesCountReal, limit);
|
||||
const auto storiesUnreadCount = std::min(storiesUnreadCountReal, limit);
|
||||
if (_cornerBadgeUserpic->frame.size() != frameSize) {
|
||||
_cornerBadgeUserpic->frame = QImage(
|
||||
frameSize,
|
||||
QImage::Format_ARGB32_Premultiplied);
|
||||
_cornerBadgeUserpic->frame.setDevicePixelRatio(ratio);
|
||||
}
|
||||
auto key = peer ? peer->userpicUniqueKey(userpicView()) : InMemoryKey();
|
||||
key.first += peer ? peer->messagesTTL() : 0;
|
||||
const auto frameIndex = videoUserpic ? videoUserpic->frameIndex() : -1;
|
||||
const auto paletteVersionReal = style::PaletteVersion();
|
||||
const auto paletteVersion = (paletteVersionReal & ((1 << 17) - 1));
|
||||
const auto active = context.active ? 1 : 0;
|
||||
const auto keyChanged = (_cornerBadgeUserpic->key != key)
|
||||
|| (_cornerBadgeUserpic->paletteVersion != paletteVersion);
|
||||
if (keyChanged) {
|
||||
_cornerBadgeUserpic->cacheTTL = QImage();
|
||||
}
|
||||
const auto subscribed = Data::ChannelHasSubscriptionUntilDate(
|
||||
peer ? peer->asChannel() : nullptr);
|
||||
if (keyChanged
|
||||
|| !_cornerBadgeUserpic->layersManager.isFinished()
|
||||
|| _cornerBadgeUserpic->active != active
|
||||
|| _cornerBadgeUserpic->frameIndex != frameIndex
|
||||
|| _cornerBadgeUserpic->storiesCount != storiesCount
|
||||
|| _cornerBadgeUserpic->storiesUnreadCount != storiesUnreadCount
|
||||
|| _cornerBadgeUserpic->storiesHasVideoStream != storiesHasVideoStream
|
||||
|| videoUserpic) {
|
||||
_cornerBadgeUserpic->key = key;
|
||||
_cornerBadgeUserpic->paletteVersion = paletteVersion;
|
||||
_cornerBadgeUserpic->active = active;
|
||||
_cornerBadgeUserpic->storiesCount = storiesCount;
|
||||
_cornerBadgeUserpic->storiesUnreadCount = storiesUnreadCount;
|
||||
_cornerBadgeUserpic->storiesHasVideoStream = storiesHasVideoStream;
|
||||
_cornerBadgeUserpic->frameIndex = frameIndex;
|
||||
_cornerBadgeUserpic->layersManager.markFrameShown();
|
||||
PaintCornerBadgeFrame(
|
||||
_cornerBadgeUserpic.get(),
|
||||
framePadding,
|
||||
_id.entry(),
|
||||
peer,
|
||||
videoUserpic,
|
||||
userpicView(),
|
||||
context,
|
||||
subscribed);
|
||||
}
|
||||
p.drawImage(
|
||||
context.st->padding.left() - framePadding,
|
||||
context.st->padding.top() - framePadding,
|
||||
_cornerBadgeUserpic->frame);
|
||||
const auto history = _id.history();
|
||||
if (!history || history->peer->isUser() || subscribed) {
|
||||
return;
|
||||
}
|
||||
const auto actionPainter = history->sendActionPainter();
|
||||
const auto bg = context.active
|
||||
? st::dialogsBgActive
|
||||
: st::dialogsBg;
|
||||
const auto size = st::dialogsCallBadgeSize;
|
||||
const auto skip = st::dialogsCallBadgeSkip;
|
||||
p.setOpacity(
|
||||
_cornerBadgeUserpic->layersManager.progressForLayer(kTopLayer));
|
||||
p.translate(context.st->padding.left(), context.st->padding.top());
|
||||
actionPainter->paintSpeaking(
|
||||
p,
|
||||
context.st->photoSize - skip.x() - size,
|
||||
context.st->photoSize - skip.y() - size,
|
||||
context.width,
|
||||
bg,
|
||||
context.now);
|
||||
p.translate(-context.st->padding.left(), -context.st->padding.top());
|
||||
p.setOpacity(1.);
|
||||
}
|
||||
|
||||
bool Row::lookupIsInTopicJump(int x, int y) const {
|
||||
const auto history = this->history();
|
||||
return history && history->lastItemDialogsView().isInTopicJump(x, y);
|
||||
}
|
||||
|
||||
void Row::stopLastRipple() {
|
||||
BasicRow::stopLastRipple();
|
||||
const auto history = this->history();
|
||||
const auto view = history ? &history->lastItemDialogsView() : nullptr;
|
||||
if (view) {
|
||||
view->stopLastRipple();
|
||||
}
|
||||
}
|
||||
|
||||
void Row::clearRipple() {
|
||||
BasicRow::clearRipple();
|
||||
clearTopicJumpRipple();
|
||||
}
|
||||
|
||||
void Row::addTopicJumpRipple(
|
||||
QPoint origin,
|
||||
not_null<Ui::TopicJumpCache*> topicJumpCache,
|
||||
Fn<void()> updateCallback) {
|
||||
const auto history = this->history();
|
||||
const auto view = history ? &history->lastItemDialogsView() : nullptr;
|
||||
if (view) {
|
||||
view->addTopicJumpRipple(
|
||||
origin,
|
||||
topicJumpCache,
|
||||
std::move(updateCallback));
|
||||
_topicJumpRipple = 1;
|
||||
}
|
||||
}
|
||||
|
||||
void Row::clearTopicJumpRipple() {
|
||||
if (!_topicJumpRipple) {
|
||||
return;
|
||||
}
|
||||
const auto history = this->history();
|
||||
const auto view = history ? &history->lastItemDialogsView() : nullptr;
|
||||
if (view) {
|
||||
view->clearRipple();
|
||||
}
|
||||
_topicJumpRipple = 0;
|
||||
}
|
||||
|
||||
bool Row::topicJumpRipple() const {
|
||||
return _topicJumpRipple != 0;
|
||||
}
|
||||
|
||||
FakeRow::FakeRow(
|
||||
Key searchInChat,
|
||||
not_null<HistoryItem*> item,
|
||||
Fn<void()> repaint)
|
||||
: _searchInChat(searchInChat)
|
||||
, _item(item)
|
||||
, _repaint(std::move(repaint)) {
|
||||
invalidateTopic();
|
||||
}
|
||||
|
||||
void FakeRow::invalidateTopic() {
|
||||
_topic = _item->topic();
|
||||
if (_topic) {
|
||||
return;
|
||||
} else if (const auto rootId = _item->topicRootId()) {
|
||||
if (const auto forum = _item->history()->asForum()) {
|
||||
if (!forum->topicDeleted(rootId)) {
|
||||
forum->requestTopic(rootId, crl::guard(this, [=] {
|
||||
_topic = _item->topic();
|
||||
if (_topic) {
|
||||
_repaint();
|
||||
}
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const Ui::Text::String &FakeRow::name() const {
|
||||
if (_name.isEmpty()) {
|
||||
const auto from = _searchInChat
|
||||
? _item->displayFrom()
|
||||
: nullptr;
|
||||
const auto peer = from ? from : _item->history()->peer.get();
|
||||
_name.setText(
|
||||
st::semiboldTextStyle,
|
||||
peer->name(),
|
||||
Ui::NameTextOptions());
|
||||
}
|
||||
return _name;
|
||||
}
|
||||
|
||||
} // namespace Dialogs
|
||||
258
Telegram/SourceFiles/dialogs/dialogs_row.h
Normal file
258
Telegram/SourceFiles/dialogs/dialogs_row.h
Normal file
@@ -0,0 +1,258 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "ui/effects/animations.h"
|
||||
#include "ui/text/text.h"
|
||||
#include "ui/unread_badge.h"
|
||||
#include "ui/userpic_view.h"
|
||||
#include "dialogs/dialogs_key.h"
|
||||
#include "dialogs/ui/dialogs_message_view.h"
|
||||
|
||||
class History;
|
||||
class HistoryItem;
|
||||
|
||||
namespace style {
|
||||
struct DialogRow;
|
||||
} // namespace style
|
||||
|
||||
namespace Ui {
|
||||
class RippleAnimation;
|
||||
} // namespace Ui
|
||||
|
||||
namespace Dialogs::Ui {
|
||||
using namespace ::Ui;
|
||||
class RowPainter;
|
||||
class VideoUserpic;
|
||||
struct PaintContext;
|
||||
struct TopicJumpCache;
|
||||
} // namespace Dialogs::Ui
|
||||
|
||||
namespace Dialogs {
|
||||
|
||||
class Entry;
|
||||
enum class SortMode;
|
||||
|
||||
[[nodiscard]] QRect CornerBadgeTTLRect(int photoSize);
|
||||
[[nodiscard]] QImage BlurredDarkenedPart(QImage image, QRect part);
|
||||
|
||||
class BasicRow {
|
||||
public:
|
||||
BasicRow();
|
||||
virtual ~BasicRow();
|
||||
|
||||
virtual void paintUserpic(
|
||||
Painter &p,
|
||||
not_null<Entry*> entry,
|
||||
PeerData *peer,
|
||||
Ui::VideoUserpic *videoUserpic,
|
||||
const Ui::PaintContext &context,
|
||||
bool hasUnreadBadgesAbove) const;
|
||||
|
||||
void addRipple(QPoint origin, QSize size, Fn<void()> updateCallback);
|
||||
virtual void stopLastRipple();
|
||||
virtual void clearRipple();
|
||||
void addRippleWithMask(
|
||||
QPoint origin,
|
||||
QImage mask,
|
||||
Fn<void()> updateCallback);
|
||||
|
||||
void paintRipple(
|
||||
QPainter &p,
|
||||
int x,
|
||||
int y,
|
||||
int outerWidth,
|
||||
const QColor *colorOverride = nullptr) const;
|
||||
|
||||
[[nodiscard]] Ui::PeerUserpicView &userpicView() const {
|
||||
return _userpic;
|
||||
}
|
||||
|
||||
private:
|
||||
mutable Ui::PeerUserpicView _userpic;
|
||||
mutable std::unique_ptr<Ui::RippleAnimation> _ripple;
|
||||
|
||||
};
|
||||
|
||||
class List;
|
||||
class Row final : public BasicRow {
|
||||
public:
|
||||
explicit Row(std::nullptr_t) {
|
||||
}
|
||||
Row(Key key, int index, int top);
|
||||
~Row();
|
||||
|
||||
[[nodiscard]] static const style::DialogRow &ComputeSt(
|
||||
not_null<const Entry*> entry,
|
||||
FilterId filterId);
|
||||
|
||||
[[nodiscard]] int top() const {
|
||||
return _top;
|
||||
}
|
||||
[[nodiscard]] int height() const {
|
||||
Expects(_height != 0);
|
||||
|
||||
return _height;
|
||||
}
|
||||
void recountHeight(float64 narrowRatio, FilterId filterId);
|
||||
|
||||
void updateCornerBadgeShown(
|
||||
not_null<PeerData*> peer,
|
||||
Fn<void()> updateCallback = nullptr,
|
||||
bool hasUnreadBadgesAbove = false) const;
|
||||
void paintUserpic(
|
||||
Painter &p,
|
||||
not_null<Entry*> entry,
|
||||
PeerData *peer,
|
||||
Ui::VideoUserpic *videoUserpic,
|
||||
const Ui::PaintContext &context,
|
||||
bool hasUnreadBadgesAbove) const final override;
|
||||
|
||||
[[nodiscard]] bool lookupIsInTopicJump(int x, int y) const;
|
||||
void stopLastRipple() override;
|
||||
void clearRipple() override;
|
||||
void addTopicJumpRipple(
|
||||
QPoint origin,
|
||||
not_null<Ui::TopicJumpCache*> topicJumpCache,
|
||||
Fn<void()> updateCallback);
|
||||
void clearTopicJumpRipple();
|
||||
[[nodiscard]] bool topicJumpRipple() const;
|
||||
|
||||
[[nodiscard]] Key key() const {
|
||||
return _id;
|
||||
}
|
||||
[[nodiscard]] History *history() const {
|
||||
return _id.history();
|
||||
}
|
||||
[[nodiscard]] Data::Folder *folder() const {
|
||||
return _id.folder();
|
||||
}
|
||||
[[nodiscard]] Data::ForumTopic *topic() const {
|
||||
return _id.topic();
|
||||
}
|
||||
[[nodiscard]] Data::Thread *thread() const {
|
||||
return _id.thread();
|
||||
}
|
||||
[[nodiscard]] Data::SavedSublist *sublist() const {
|
||||
return _id.sublist();
|
||||
}
|
||||
[[nodiscard]] not_null<Entry*> entry() const {
|
||||
return _id.entry();
|
||||
}
|
||||
[[nodiscard]] int index() const {
|
||||
return _index;
|
||||
}
|
||||
[[nodiscard]] uint64 sortKey(FilterId filterId) const;
|
||||
|
||||
// for any attached data, for example View in contacts list
|
||||
void *attached = nullptr;
|
||||
|
||||
private:
|
||||
friend class List;
|
||||
|
||||
class CornerLayersManager {
|
||||
public:
|
||||
using Layer = int;
|
||||
CornerLayersManager();
|
||||
|
||||
[[nodiscard]] bool isSameLayer(Layer layer) const;
|
||||
[[nodiscard]] bool isDisplayedNone() const;
|
||||
[[nodiscard]] float64 progressForLayer(Layer layer) const;
|
||||
[[nodiscard]] float64 progress() const;
|
||||
[[nodiscard]] bool isFinished() const;
|
||||
void setLayer(Layer layer, Fn<void()> updateCallback);
|
||||
void markFrameShown();
|
||||
|
||||
private:
|
||||
bool _lastFrameShown = false;
|
||||
Layer _prevLayer = 0;
|
||||
Layer _nextLayer = 0;
|
||||
Ui::Animations::Simple _animation;
|
||||
|
||||
};
|
||||
|
||||
struct CornerBadgeUserpic {
|
||||
InMemoryKey key;
|
||||
CornerLayersManager layersManager;
|
||||
QImage frame;
|
||||
QImage cacheTTL;
|
||||
int frameIndex = -1;
|
||||
uint32 paletteVersion : 16 = 0;
|
||||
uint32 storiesCount : 7 = 0;
|
||||
uint32 storiesUnreadCount : 7 = 0;
|
||||
uint32 storiesHasVideoStream : 1 = 0;
|
||||
uint32 active : 1 = 0;
|
||||
};
|
||||
|
||||
void setCornerBadgeShown(
|
||||
CornerLayersManager::Layer nextLayer,
|
||||
Fn<void()> updateCallback) const;
|
||||
void ensureCornerBadgeUserpic() const;
|
||||
static void PaintCornerBadgeFrame(
|
||||
not_null<CornerBadgeUserpic*> data,
|
||||
int framePadding,
|
||||
not_null<Entry*> entry,
|
||||
PeerData *peer,
|
||||
Ui::VideoUserpic *videoUserpic,
|
||||
Ui::PeerUserpicView &view,
|
||||
const Ui::PaintContext &context,
|
||||
bool subscribed);
|
||||
|
||||
Key _id;
|
||||
mutable std::unique_ptr<CornerBadgeUserpic> _cornerBadgeUserpic;
|
||||
int _top = 0;
|
||||
int _height = 0;
|
||||
uint32 _index : 30 = 0;
|
||||
uint32 _cornerBadgeShown : 1 = 0;
|
||||
uint32 _topicJumpRipple : 1 = 0;
|
||||
|
||||
};
|
||||
|
||||
class FakeRow final : public BasicRow, public base::has_weak_ptr {
|
||||
public:
|
||||
FakeRow(
|
||||
Key searchInChat,
|
||||
not_null<HistoryItem*> item,
|
||||
Fn<void()> repaint);
|
||||
|
||||
[[nodiscard]] Key searchInChat() const {
|
||||
return _searchInChat;
|
||||
}
|
||||
[[nodiscard]] Data::ForumTopic *topic() const {
|
||||
return _topic;
|
||||
}
|
||||
[[nodiscard]] not_null<HistoryItem*> item() const {
|
||||
return _item;
|
||||
}
|
||||
[[nodiscard]] Ui::MessageView &itemView() const {
|
||||
return _itemView;
|
||||
}
|
||||
[[nodiscard]] Fn<void()> repaint() const {
|
||||
return _repaint;
|
||||
}
|
||||
[[nodiscard]] Ui::PeerBadge &badge() const {
|
||||
return _badge;
|
||||
}
|
||||
[[nodiscard]] const Ui::Text::String &name() const;
|
||||
|
||||
void invalidateTopic();
|
||||
|
||||
private:
|
||||
friend class Ui::RowPainter;
|
||||
|
||||
const Key _searchInChat;
|
||||
const not_null<HistoryItem*> _item;
|
||||
Data::ForumTopic *_topic = nullptr;
|
||||
const Fn<void()> _repaint;
|
||||
mutable Ui::MessageView _itemView;
|
||||
mutable Ui::PeerBadge _badge;
|
||||
mutable Ui::Text::String _name;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Dialogs
|
||||
@@ -0,0 +1,83 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#include "dialogs/dialogs_search_from_controllers.h"
|
||||
|
||||
#include "lang/lang_keys.h"
|
||||
#include "data/data_peer_values.h"
|
||||
#include "data/data_channel.h"
|
||||
#include "data/data_chat.h"
|
||||
#include "data/data_user.h"
|
||||
#include "main/main_session.h"
|
||||
#include "apiwrap.h"
|
||||
|
||||
namespace Dialogs {
|
||||
|
||||
object_ptr<Ui::BoxContent> SearchFromBox(
|
||||
not_null<PeerData*> peer,
|
||||
Fn<void(not_null<PeerData*>)> callback,
|
||||
Fn<void()> closedCallback) {
|
||||
auto createController = [
|
||||
peer,
|
||||
callback = std::move(callback)
|
||||
]() -> std::unique_ptr<PeerListController> {
|
||||
if (peer && (peer->isChat() || peer->isMegagroup())) {
|
||||
return std::make_unique<Dialogs::SearchFromController>(
|
||||
peer,
|
||||
std::move(callback));
|
||||
}
|
||||
return nullptr;
|
||||
};
|
||||
if (auto controller = createController()) {
|
||||
auto subscription = std::make_shared<rpl::lifetime>();
|
||||
auto box = Box<PeerListBox>(
|
||||
std::move(controller),
|
||||
[subscription](not_null<PeerListBox*> box) {
|
||||
box->addButton(tr::lng_cancel(), [box, subscription] {
|
||||
box->closeBox();
|
||||
});
|
||||
});
|
||||
box->boxClosing() | rpl::on_next(
|
||||
std::move(closedCallback),
|
||||
*subscription);
|
||||
return box;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
SearchFromController::SearchFromController(
|
||||
not_null<PeerData*> peer,
|
||||
Fn<void(not_null<PeerData*>)> callback)
|
||||
: AddSpecialBoxController(
|
||||
peer,
|
||||
ParticipantsBoxController::Role::Members,
|
||||
AdminDoneCallback(),
|
||||
BannedDoneCallback())
|
||||
, _callback(std::move(callback)) {
|
||||
_excludeSelf = false;
|
||||
}
|
||||
|
||||
void SearchFromController::prepare() {
|
||||
AddSpecialBoxController::prepare();
|
||||
delegate()->peerListSetTitle(tr::lng_search_messages_from());
|
||||
if (const auto megagroup = peer()->asMegagroup()) {
|
||||
if (!delegate()->peerListFindRow(megagroup->id.value)) {
|
||||
delegate()->peerListAppendRow(
|
||||
std::make_unique<PeerListRow>(megagroup));
|
||||
setDescriptionText({});
|
||||
delegate()->peerListRefreshRows();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void SearchFromController::rowClicked(not_null<PeerListRow*> row) {
|
||||
if (const auto onstack = base::duplicate(_callback)) {
|
||||
onstack(row->peer());
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace Dialogs
|
||||
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "boxes/peer_list_box.h"
|
||||
#include "boxes/peers/add_participants_box.h"
|
||||
|
||||
namespace Dialogs {
|
||||
|
||||
object_ptr<Ui::BoxContent> SearchFromBox(
|
||||
not_null<PeerData*> peer,
|
||||
Fn<void(not_null<PeerData*>)> callback,
|
||||
Fn<void()> closedCallback);
|
||||
|
||||
class SearchFromController : public AddSpecialBoxController {
|
||||
public:
|
||||
SearchFromController(
|
||||
not_null<PeerData*> peer,
|
||||
Fn<void(not_null<PeerData*>)> callback);
|
||||
|
||||
void prepare() override;
|
||||
void rowClicked(not_null<PeerListRow*> row) override;
|
||||
|
||||
private:
|
||||
Fn<void(not_null<PeerData*>)> _callback;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Dialogs
|
||||
347
Telegram/SourceFiles/dialogs/dialogs_search_posts.cpp
Normal file
347
Telegram/SourceFiles/dialogs/dialogs_search_posts.cpp
Normal file
@@ -0,0 +1,347 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#include "dialogs/dialogs_search_posts.h"
|
||||
|
||||
#include "apiwrap.h"
|
||||
#include "base/unixtime.h"
|
||||
#include "data/data_session.h"
|
||||
#include "data/data_peer_values.h"
|
||||
#include "history/history.h"
|
||||
#include "main/main_session.h"
|
||||
|
||||
namespace Dialogs {
|
||||
namespace {
|
||||
|
||||
constexpr auto kQueryDelay = crl::time(500);
|
||||
constexpr auto kPerPage = 50;
|
||||
|
||||
[[nodiscard]] const QRegularExpression &SearchSplitter() {
|
||||
static const auto result = QRegularExpression(QString::fromLatin1(""
|
||||
"[\\s\\-\\+\\(\\)\\[\\]\\{\\}\\<\\>\\,\\.\\!\\_\\;\\\"\\'\\x0]"));
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
PostsSearch::PostsSearch(not_null<Main::Session*> session)
|
||||
: _session(session)
|
||||
, _api(&_session->api().instance())
|
||||
, _timer([=] { applyQuery(); })
|
||||
, _recheckTimer([=] { recheck(); }) {
|
||||
Data::AmPremiumValue(_session) | rpl::on_next([=] {
|
||||
maybePushPremiumUpdate();
|
||||
}, _lifetime);
|
||||
}
|
||||
|
||||
rpl::producer<PostsSearchState> PostsSearch::stateUpdates() const {
|
||||
return _stateUpdates.events();
|
||||
}
|
||||
|
||||
rpl::producer<PostsSearchState> PostsSearch::pagesUpdates() const {
|
||||
return _pagesUpdates.events();
|
||||
}
|
||||
|
||||
void PostsSearch::requestMore() {
|
||||
if (!_query) {
|
||||
return;
|
||||
}
|
||||
auto &entry = _entries[*_query];
|
||||
if (_queryPushed != *_query || !entry.pagesPushed) {
|
||||
return;
|
||||
} else if (entry.pagesPushed < entry.pages.size()) {
|
||||
_pagesUpdates.fire(PostsSearchState{
|
||||
.page = entry.pages[entry.pagesPushed++],
|
||||
.totalCount = entry.totalCount,
|
||||
});
|
||||
} else {
|
||||
requestSearch(*_query);
|
||||
}
|
||||
}
|
||||
|
||||
void PostsSearch::setQuery(const QString &query) {
|
||||
const auto words = TextUtilities::PrepareSearchWords(
|
||||
query,
|
||||
&SearchSplitter());
|
||||
const auto prepared = words.isEmpty() ? QString() : words.join(' ');
|
||||
if (_queryExact == query) {
|
||||
return;
|
||||
}
|
||||
_queryExact = query;
|
||||
_query = prepared;
|
||||
const auto i = _entries.find(prepared);
|
||||
if (i != end(_entries)) {
|
||||
pushStateUpdate(i->second);
|
||||
} else if (prepared.isEmpty()) {
|
||||
applyQuery();
|
||||
} else {
|
||||
_timer.callOnce(kQueryDelay);
|
||||
}
|
||||
}
|
||||
|
||||
int PostsSearch::setAllowedStars(int stars) {
|
||||
if (!_query) {
|
||||
return 0;
|
||||
} else if (_floodState) {
|
||||
if (_floodState->freeSearchesLeft > 0) {
|
||||
stars = 0;
|
||||
} else if (_floodState->nextFreeSearchTime > 0
|
||||
&& _floodState->nextFreeSearchTime <= base::unixtime::now()) {
|
||||
stars = 0;
|
||||
} else {
|
||||
stars = std::min(int(_floodState->starsPerPaidSearch), stars);
|
||||
}
|
||||
}
|
||||
_entries[*_query].allowedStars = stars;
|
||||
requestSearch(*_query);
|
||||
return stars;
|
||||
}
|
||||
|
||||
void PostsSearch::pushStateUpdate(const Entry &entry) {
|
||||
Expects(_query.has_value());
|
||||
|
||||
const auto initial = (_queryPushed != *_query);
|
||||
if (initial) {
|
||||
_queryPushed = *_query;
|
||||
entry.pagesPushed = 0;
|
||||
} else if (entry.pagesPushed > 0) {
|
||||
if (entry.pagesPushed < entry.pages.size()) {
|
||||
_pagesUpdates.fire(PostsSearchState{
|
||||
.page = entry.pages[entry.pagesPushed++],
|
||||
.totalCount = entry.totalCount,
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
const auto empty = entry.pages.empty()
|
||||
|| (entry.pages.size() == 1 && entry.pages.front().empty());
|
||||
if (!empty || (entry.loaded && !_query->isEmpty())) {
|
||||
if (!entry.pages.empty()) {
|
||||
++entry.pagesPushed;
|
||||
}
|
||||
_stateUpdates.fire(PostsSearchState{
|
||||
.page = (entry.pages.empty()
|
||||
? std::vector<not_null<HistoryItem*>>()
|
||||
: entry.pages.front()),
|
||||
.totalCount = entry.totalCount,
|
||||
});
|
||||
} else if (entry.checkId || entry.searchId) {
|
||||
_stateUpdates.fire(PostsSearchState{
|
||||
.loading = true,
|
||||
});
|
||||
} else {
|
||||
Assert(_floodState.has_value());
|
||||
auto copy = _floodState;
|
||||
copy->query = *_queryExact;
|
||||
copy->needsPremium = !_session->premium();
|
||||
_stateUpdates.fire(PostsSearchState{
|
||||
.intro = std::move(copy),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void PostsSearch::maybePushPremiumUpdate() {
|
||||
if (!_floodState || !_query) {
|
||||
return;
|
||||
}
|
||||
auto &entry = _entries[*_query];
|
||||
if (!entry.pages.empty()
|
||||
|| entry.loaded
|
||||
|| entry.checkId
|
||||
|| entry.searchId) {
|
||||
return;
|
||||
}
|
||||
pushStateUpdate(entry);
|
||||
}
|
||||
|
||||
void PostsSearch::applyQuery() {
|
||||
Expects(_query.has_value());
|
||||
|
||||
_timer.cancel();
|
||||
if (_query->isEmpty()) {
|
||||
requestSearch(QString());
|
||||
} else {
|
||||
requestState(*_query);
|
||||
}
|
||||
}
|
||||
|
||||
void PostsSearch::requestSearch(const QString &query) {
|
||||
auto &entry = _entries[query];
|
||||
if (entry.searchId || entry.loaded) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto useStars = entry.allowedStars;
|
||||
entry.allowedStars = 0;
|
||||
|
||||
using Flag = MTPchannels_SearchPosts::Flag;
|
||||
entry.searchId = _api.request(MTPchannels_SearchPosts(
|
||||
MTP_flags(Flag::f_query
|
||||
| (useStars ? Flag::f_allow_paid_stars : Flag())),
|
||||
MTP_string(), // hashtag
|
||||
MTP_string(query),
|
||||
MTP_int(entry.offsetRate),
|
||||
(entry.offsetPeer ? entry.offsetPeer->input() : MTP_inputPeerEmpty()),
|
||||
MTP_int(entry.offsetId),
|
||||
MTP_int(kPerPage),
|
||||
MTP_long(useStars)
|
||||
)).done([=](const MTPmessages_Messages &result) {
|
||||
auto &entry = _entries[query];
|
||||
entry.searchId = 0;
|
||||
|
||||
const auto initial = !entry.offsetId;
|
||||
const auto owner = &_session->data();
|
||||
const auto processList = [&](const MTPVector<MTPMessage> &messages) {
|
||||
auto result = std::vector<not_null<HistoryItem*>>();
|
||||
for (const auto &message : messages.v) {
|
||||
const auto msgId = IdFromMessage(message);
|
||||
const auto peerId = PeerFromMessage(message);
|
||||
const auto lastDate = DateFromMessage(message);
|
||||
if (const auto peer = owner->peerLoaded(peerId)) {
|
||||
if (lastDate) {
|
||||
const auto item = owner->addNewMessage(
|
||||
message,
|
||||
MessageFlags(),
|
||||
NewMessageType::Existing);
|
||||
result.push_back(item);
|
||||
}
|
||||
entry.offsetPeer = peer;
|
||||
} else {
|
||||
LOG(("API Error: a search results with not loaded peer %1"
|
||||
).arg(peerId.value));
|
||||
}
|
||||
entry.offsetId = msgId;
|
||||
}
|
||||
return result;
|
||||
};
|
||||
auto totalCount = 0;
|
||||
auto messages = result.match([&](const MTPDmessages_messages &data) {
|
||||
owner->processUsers(data.vusers());
|
||||
owner->processChats(data.vchats());
|
||||
entry.loaded = true;
|
||||
auto list = processList(data.vmessages());
|
||||
totalCount = list.size();
|
||||
return list;
|
||||
}, [&](const MTPDmessages_messagesSlice &data) {
|
||||
owner->processUsers(data.vusers());
|
||||
owner->processChats(data.vchats());
|
||||
auto list = processList(data.vmessages());
|
||||
const auto nextRate = data.vnext_rate();
|
||||
const auto rateUpdated = nextRate
|
||||
&& (nextRate->v != entry.offsetRate);
|
||||
const auto finished = list.empty();
|
||||
if (rateUpdated) {
|
||||
entry.offsetRate = nextRate->v;
|
||||
}
|
||||
if (finished) {
|
||||
entry.loaded = true;
|
||||
}
|
||||
totalCount = data.vcount().v;
|
||||
if (const auto flood = data.vsearch_flood()) {
|
||||
setFloodStateFrom(flood->data());
|
||||
}
|
||||
return list;
|
||||
}, [&](const MTPDmessages_channelMessages &data) {
|
||||
LOG(("API Error: "
|
||||
"received messages.channelMessages when no channel "
|
||||
"was passed! (PostsSearch::performSearch)"));
|
||||
owner->processUsers(data.vusers());
|
||||
owner->processChats(data.vchats());
|
||||
auto list = processList(data.vmessages());
|
||||
if (list.empty()) {
|
||||
entry.loaded = true;
|
||||
}
|
||||
totalCount = data.vcount().v;
|
||||
return list;
|
||||
}, [&](const MTPDmessages_messagesNotModified &) {
|
||||
LOG(("API Error: received messages.messagesNotModified! "
|
||||
"(PostsSearch::performSearch)"));
|
||||
entry.loaded = true;
|
||||
return std::vector<not_null<HistoryItem*>>();
|
||||
});
|
||||
if (initial) {
|
||||
entry.pages.clear();
|
||||
}
|
||||
entry.pages.push_back(std::move(messages));
|
||||
const auto count = int(ranges::accumulate(
|
||||
entry.pages,
|
||||
size_type(),
|
||||
ranges::plus(),
|
||||
&std::vector<not_null<HistoryItem*>>::size));
|
||||
const auto full = entry.loaded ? count : std::max(count, totalCount);
|
||||
entry.totalCount = full;
|
||||
if (_query == query) {
|
||||
pushStateUpdate(entry);
|
||||
}
|
||||
}).fail([=](const MTP::Error &error) {
|
||||
auto &entry = _entries[query];
|
||||
entry.searchId = 0;
|
||||
|
||||
const auto initial = !entry.offsetId;
|
||||
const auto &type = error.type();
|
||||
if (initial && type.startsWith(u"FLOOD_WAIT_"_q)) {
|
||||
requestState(query);
|
||||
} else {
|
||||
entry.loaded = true;
|
||||
}
|
||||
}).handleFloodErrors().send();
|
||||
}
|
||||
|
||||
void PostsSearch::setFloodStateFrom(const MTPDsearchPostsFlood &data) {
|
||||
_recheckTimer.cancel();
|
||||
const auto left = std::max(data.vremains().v, 0);
|
||||
const auto next = data.vwait_till().value_or_empty();
|
||||
if (!left && next > 0) {
|
||||
const auto now = base::unixtime::now();
|
||||
const auto delay = std::clamp(next - now, 1, 86401);
|
||||
_recheckTimer.callOnce(delay * crl::time(1000));
|
||||
}
|
||||
_floodState = PostsSearchIntroState{
|
||||
.freeSearchesPerDay = data.vtotal_daily().v,
|
||||
.freeSearchesLeft = left,
|
||||
.nextFreeSearchTime = next,
|
||||
.starsPerPaidSearch = uint32(data.vstars_amount().v),
|
||||
};
|
||||
}
|
||||
|
||||
void PostsSearch::recheck() {
|
||||
requestState(*_query, true);
|
||||
}
|
||||
|
||||
void PostsSearch::requestState(const QString &query, bool force) {
|
||||
auto &entry = _entries[query];
|
||||
if (force) {
|
||||
_api.request(base::take(entry.checkId)).cancel();
|
||||
} else if (entry.checkId || entry.loaded) {
|
||||
return;
|
||||
}
|
||||
|
||||
using Flag = MTPchannels_CheckSearchPostsFlood::Flag;
|
||||
entry.checkId = _api.request(MTPchannels_CheckSearchPostsFlood(
|
||||
MTP_flags(Flag::f_query),
|
||||
MTP_string(query)
|
||||
)).done([=](const MTPSearchPostsFlood &result) {
|
||||
auto &entry = _entries[query];
|
||||
entry.checkId = 0;
|
||||
|
||||
const auto &data = result.data();
|
||||
setFloodStateFrom(data);
|
||||
if (data.is_query_is_free()) {
|
||||
if (!entry.loaded) {
|
||||
requestSearch(query);
|
||||
}
|
||||
} else if (_query == query) {
|
||||
pushStateUpdate(entry);
|
||||
}
|
||||
}).fail([=](const MTP::Error &error) {
|
||||
auto &entry = _entries[query];
|
||||
entry.checkId = 0;
|
||||
entry.loaded = true;
|
||||
}).handleFloodErrors().send();
|
||||
}
|
||||
|
||||
} // namespace Dialogs
|
||||
80
Telegram/SourceFiles/dialogs/dialogs_search_posts.h
Normal file
80
Telegram/SourceFiles/dialogs/dialogs_search_posts.h
Normal file
@@ -0,0 +1,80 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "base/timer.h"
|
||||
#include "dialogs/ui/posts_search_intro.h"
|
||||
#include "mtproto/sender.h"
|
||||
|
||||
namespace Main {
|
||||
class Session;
|
||||
} // namespace Main
|
||||
|
||||
namespace Dialogs {
|
||||
|
||||
struct PostsSearchState {
|
||||
std::optional<PostsSearchIntroState> intro;
|
||||
std::vector<not_null<HistoryItem*>> page;
|
||||
int totalCount = 0;
|
||||
bool loading = false;
|
||||
};
|
||||
|
||||
class PostsSearch final {
|
||||
public:
|
||||
explicit PostsSearch(not_null<Main::Session*> session);
|
||||
|
||||
[[nodiscard]] rpl::producer<PostsSearchState> stateUpdates() const;
|
||||
[[nodiscard]] rpl::producer<PostsSearchState> pagesUpdates() const;
|
||||
|
||||
void setQuery(const QString &query);
|
||||
int setAllowedStars(int stars);
|
||||
void requestMore();
|
||||
|
||||
private:
|
||||
struct Entry {
|
||||
std::vector<std::vector<not_null<HistoryItem*>>> pages;
|
||||
int totalCount = 0;
|
||||
mtpRequestId searchId = 0;
|
||||
mtpRequestId checkId = 0;
|
||||
PeerData *offsetPeer = nullptr;
|
||||
MsgId offsetId = 0;
|
||||
int offsetRate = 0;
|
||||
int allowedStars = 0;
|
||||
mutable int pagesPushed = 0;
|
||||
bool loaded = false;
|
||||
};
|
||||
|
||||
void recheck();
|
||||
void applyQuery();
|
||||
void requestSearch(const QString &query);
|
||||
void requestState(const QString &query, bool force = false);
|
||||
void setFloodStateFrom(const MTPDsearchPostsFlood &data);
|
||||
void pushStateUpdate(const Entry &entry);
|
||||
void maybePushPremiumUpdate();
|
||||
|
||||
const not_null<Main::Session*> _session;
|
||||
|
||||
MTP::Sender _api;
|
||||
|
||||
base::Timer _timer;
|
||||
base::Timer _recheckTimer;
|
||||
base::flat_map<QString, Entry> _entries;
|
||||
std::optional<QString> _queryExact;
|
||||
std::optional<QString> _query;
|
||||
QString _queryPushed;
|
||||
|
||||
std::optional<PostsSearchIntroState> _floodState;
|
||||
|
||||
rpl::event_stream<PostsSearchState> _stateUpdates;
|
||||
rpl::event_stream<PostsSearchState> _pagesUpdates;
|
||||
|
||||
rpl::lifetime _lifetime;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Dialogs
|
||||
451
Telegram/SourceFiles/dialogs/dialogs_search_tags.cpp
Normal file
451
Telegram/SourceFiles/dialogs/dialogs_search_tags.cpp
Normal file
@@ -0,0 +1,451 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#include "dialogs/dialogs_search_tags.h"
|
||||
|
||||
#include "base/qt/qt_key_modifiers.h"
|
||||
#include "boxes/premium_preview_box.h"
|
||||
#include "core/click_handler_types.h"
|
||||
#include "core/ui_integration.h"
|
||||
#include "data/stickers/data_custom_emoji.h"
|
||||
#include "data/data_document.h"
|
||||
#include "data/data_message_reactions.h"
|
||||
#include "data/data_peer_values.h"
|
||||
#include "data/data_session.h"
|
||||
#include "history/view/reactions/history_view_reactions.h"
|
||||
#include "main/main_session.h"
|
||||
#include "lang/lang_keys.h"
|
||||
#include "ui/effects/animation_value.h"
|
||||
#include "ui/text/text_utilities.h"
|
||||
#include "ui/painter.h"
|
||||
#include "ui/power_saving.h"
|
||||
#include "window/window_session_controller.h"
|
||||
#include "styles/style_chat.h"
|
||||
#include "styles/style_dialogs.h"
|
||||
|
||||
namespace Dialogs {
|
||||
namespace {
|
||||
|
||||
[[nodiscard]] QString ComposeText(const Data::Reaction &tag) {
|
||||
auto result = tag.title;
|
||||
if (!result.isEmpty() && tag.count > 0) {
|
||||
result.append(' ');
|
||||
}
|
||||
if (tag.count > 0) {
|
||||
result.append(QString::number(tag.count));
|
||||
}
|
||||
return TextUtilities::SingleLine(result);
|
||||
}
|
||||
|
||||
[[nodiscard]] ClickHandlerPtr MakePromoLink() {
|
||||
return std::make_shared<LambdaClickHandler>([=](ClickContext context) {
|
||||
const auto my = context.other.value<ClickHandlerContext>();
|
||||
if (const auto controller = my.sessionWindow.get()) {
|
||||
ShowPremiumPreviewBox(
|
||||
controller,
|
||||
PremiumFeature::TagsForMessages);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
[[nodiscard]] Ui::Text::String FillAdditionalText(int width) {
|
||||
auto emoji = Ui::Text::IconEmoji(&st::dialogsSearchTagArrow);
|
||||
auto result = Ui::Text::String();
|
||||
const auto attempt = [&](const auto &phrase) {
|
||||
result.setMarkedText(
|
||||
st::dialogsSearchTagPromo,
|
||||
phrase(tr::now, lt_arrow, emoji, tr::marked),
|
||||
kMarkupTextOptions);
|
||||
return result.maxWidth() < width;
|
||||
};
|
||||
if (attempt(tr::lng_add_tag_phrase_long)
|
||||
|| attempt(tr::lng_add_tag_phrase)) {
|
||||
return result;
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
struct SearchTags::Tag {
|
||||
Data::ReactionId id;
|
||||
std::unique_ptr<Ui::Text::CustomEmoji> custom;
|
||||
QString text;
|
||||
int textWidth = 0;
|
||||
mutable QImage image;
|
||||
QRect geometry;
|
||||
ClickHandlerPtr link;
|
||||
bool selected = false;
|
||||
bool promo = false;
|
||||
};
|
||||
|
||||
SearchTags::SearchTags(
|
||||
not_null<Data::Session*> owner,
|
||||
rpl::producer<std::vector<Data::Reaction>> tags,
|
||||
std::vector<Data::ReactionId> selected)
|
||||
: _owner(owner)
|
||||
, _added(selected) {
|
||||
rpl::combine(
|
||||
std::move(tags),
|
||||
Data::AmPremiumValue(&owner->session())
|
||||
) | rpl::on_next([=](
|
||||
const std::vector<Data::Reaction> &list,
|
||||
bool premium) {
|
||||
fill(list, premium);
|
||||
}, _lifetime);
|
||||
|
||||
// Mark the `selected` reactions as selected in `_tags`.
|
||||
for (const auto &id : selected) {
|
||||
const auto i = ranges::find(_tags, id, &Tag::id);
|
||||
if (i != end(_tags)) {
|
||||
i->selected = true;
|
||||
}
|
||||
}
|
||||
|
||||
style::PaletteChanged(
|
||||
) | rpl::on_next([=] {
|
||||
_normalBg = _selectedBg = QImage();
|
||||
}, _lifetime);
|
||||
}
|
||||
|
||||
SearchTags::~SearchTags() = default;
|
||||
|
||||
void SearchTags::fill(
|
||||
const std::vector<Data::Reaction> &list,
|
||||
bool premium) {
|
||||
const auto selected = collectSelected();
|
||||
_tags.clear();
|
||||
_tags.reserve(list.size());
|
||||
const auto link = [&](Data::ReactionId id) {
|
||||
return std::make_shared<GenericClickHandler>(crl::guard(this, [=](
|
||||
ClickContext context) {
|
||||
if (!premium) {
|
||||
MakePromoLink()->onClick(context);
|
||||
return;
|
||||
} else if (context.button == Qt::RightButton) {
|
||||
_menuRequests.fire_copy(id);
|
||||
return;
|
||||
}
|
||||
const auto i = ranges::find(_tags, id, &Tag::id);
|
||||
if (i != end(_tags)) {
|
||||
if (!i->selected && !base::IsShiftPressed()) {
|
||||
for (auto &tag : _tags) {
|
||||
tag.selected = false;
|
||||
}
|
||||
}
|
||||
i->selected = !i->selected;
|
||||
_selectedChanges.fire({});
|
||||
}
|
||||
}));
|
||||
};
|
||||
const auto push = [&](Data::ReactionId id, const QString &text) {
|
||||
const auto customId = id.custom();
|
||||
_tags.push_back({
|
||||
.id = id,
|
||||
.custom = (customId
|
||||
? _owner->customEmojiManager().create(
|
||||
customId,
|
||||
[=] { _repaintRequests.fire({}); })
|
||||
: nullptr),
|
||||
.text = text,
|
||||
.textWidth = st::reactionInlineTagFont->width(text),
|
||||
.link = link(id),
|
||||
.selected = ranges::contains(selected, id),
|
||||
});
|
||||
if (!customId) {
|
||||
_owner->reactions().preloadReactionImageFor(id);
|
||||
}
|
||||
};
|
||||
if (!premium) {
|
||||
const auto text = (list.empty() && _added.empty())
|
||||
? tr::lng_add_tag_button(tr::now)
|
||||
: tr::lng_unlock_tags(tr::now);
|
||||
_tags.push_back({
|
||||
.id = Data::ReactionId(),
|
||||
.text = text,
|
||||
.textWidth = st::reactionInlineTagFont->width(text),
|
||||
.link = MakePromoLink(),
|
||||
.promo = true,
|
||||
});
|
||||
}
|
||||
for (const auto &reaction : list) {
|
||||
if (reaction.count > 0
|
||||
|| ranges::contains(_added, reaction.id)
|
||||
|| ranges::contains(selected, reaction.id)) {
|
||||
push(reaction.id, ComposeText(reaction));
|
||||
}
|
||||
}
|
||||
for (const auto &reaction : _added) {
|
||||
if (!ranges::contains(_tags, reaction, &Tag::id)) {
|
||||
push(reaction, QString());
|
||||
}
|
||||
}
|
||||
if (_width > 0) {
|
||||
layout();
|
||||
_repaintRequests.fire({});
|
||||
}
|
||||
}
|
||||
|
||||
void SearchTags::layout() {
|
||||
Expects(_width > 0);
|
||||
|
||||
if (_tags.empty()) {
|
||||
_additionalText = {};
|
||||
_height = 0;
|
||||
return;
|
||||
}
|
||||
const auto &bg = validateBg(false, false);
|
||||
const auto skip = st::dialogsSearchTagSkip;
|
||||
const auto size = bg.size() / bg.devicePixelRatio();
|
||||
const auto xbase = size.width();
|
||||
const auto ybase = size.height();
|
||||
auto x = 0;
|
||||
auto y = 0;
|
||||
for (auto &tag : _tags) {
|
||||
const auto width = xbase + (tag.promo
|
||||
? std::max(0, tag.textWidth - st::dialogsSearchTagPromoLeft - st::dialogsSearchTagPromoRight)
|
||||
: tag.textWidth);
|
||||
if (x > 0 && x + width > _width) {
|
||||
x = 0;
|
||||
y += ybase + skip.y();
|
||||
}
|
||||
tag.geometry = QRect(x, y, width, ybase);
|
||||
x += width + skip.x();
|
||||
}
|
||||
_height = y + ybase + st::dialogsSearchTagBottom;
|
||||
if (_tags.size() == 1 && _tags.front().promo) {
|
||||
_additionalLeft = x - skip.x() + st::dialogsSearchTagPromoSkip;
|
||||
const auto additionalWidth = _width - _additionalLeft;
|
||||
_additionalText = FillAdditionalText(additionalWidth);
|
||||
} else {
|
||||
_additionalText = {};
|
||||
}
|
||||
}
|
||||
|
||||
void SearchTags::resizeToWidth(int width) {
|
||||
if (_width == width || width <= 0) {
|
||||
return;
|
||||
}
|
||||
_width = width;
|
||||
layout();
|
||||
}
|
||||
|
||||
int SearchTags::height() const {
|
||||
return _height.current();
|
||||
}
|
||||
|
||||
rpl::producer<int> SearchTags::heightValue() const {
|
||||
return _height.value();
|
||||
}
|
||||
|
||||
rpl::producer<> SearchTags::repaintRequests() const {
|
||||
return _repaintRequests.events();
|
||||
}
|
||||
|
||||
ClickHandlerPtr SearchTags::lookupHandler(QPoint point) const {
|
||||
for (const auto &tag : _tags) {
|
||||
if (tag.geometry.contains(point.x(), point.y())) {
|
||||
return tag.link;
|
||||
} else if (tag.promo
|
||||
&& !_additionalText.isEmpty()
|
||||
&& tag.geometry.united(QRect(
|
||||
_additionalLeft,
|
||||
tag.geometry.y(),
|
||||
_additionalText.maxWidth(),
|
||||
tag.geometry.height())).contains(point.x(), point.y())) {
|
||||
return tag.link;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
auto SearchTags::selectedChanges() const
|
||||
-> rpl::producer<std::vector<Data::ReactionId>> {
|
||||
return _selectedChanges.events() | rpl::map([=] {
|
||||
return collectSelected();
|
||||
});
|
||||
}
|
||||
|
||||
void SearchTags::paintCustomFrame(
|
||||
QPainter &p,
|
||||
not_null<Ui::Text::CustomEmoji*> emoji,
|
||||
QPoint innerTopLeft,
|
||||
crl::time now,
|
||||
bool paused,
|
||||
const QColor &textColor) const {
|
||||
if (_customCache.isNull()) {
|
||||
using namespace Ui::Text;
|
||||
const auto size = st::emojiSize;
|
||||
const auto factor = style::DevicePixelRatio();
|
||||
const auto adjusted = AdjustCustomEmojiSize(size);
|
||||
_customCache = QImage(
|
||||
QSize(adjusted, adjusted) * factor,
|
||||
QImage::Format_ARGB32_Premultiplied);
|
||||
_customCache.setDevicePixelRatio(factor);
|
||||
_customSkip = (size - adjusted) / 2;
|
||||
}
|
||||
_customCache.fill(Qt::transparent);
|
||||
auto q = QPainter(&_customCache);
|
||||
emoji->paint(q, {
|
||||
.textColor = textColor,
|
||||
.now = now,
|
||||
.paused = paused || On(PowerSaving::kEmojiChat),
|
||||
});
|
||||
q.end();
|
||||
_customCache = Images::Round(
|
||||
std::move(_customCache),
|
||||
(Images::Option::RoundLarge
|
||||
| Images::Option::RoundSkipTopRight
|
||||
| Images::Option::RoundSkipBottomRight));
|
||||
|
||||
p.drawImage(
|
||||
innerTopLeft + QPoint(_customSkip, _customSkip),
|
||||
_customCache);
|
||||
}
|
||||
|
||||
rpl::producer<Data::ReactionId> SearchTags::menuRequests() const {
|
||||
return _menuRequests.events();
|
||||
}
|
||||
|
||||
void SearchTags::paint(
|
||||
Painter &p,
|
||||
QPoint position,
|
||||
crl::time now,
|
||||
bool paused) const {
|
||||
const auto size = st::reactionInlineSize;
|
||||
const auto skip = (size - st::reactionInlineImage) / 2;
|
||||
const auto padding = st::reactionInlinePadding;
|
||||
for (const auto &tag : _tags) {
|
||||
const auto geometry = tag.geometry.translated(position);
|
||||
paintBackground(p, geometry, tag);
|
||||
paintText(p, geometry, tag);
|
||||
if (!tag.custom && !tag.promo && tag.image.isNull()) {
|
||||
tag.image = _owner->reactions().resolveReactionImageFor(tag.id);
|
||||
}
|
||||
const auto inner = geometry.marginsRemoved(padding);
|
||||
const auto image = QRect(
|
||||
inner.topLeft() + QPoint(skip, skip),
|
||||
QSize(st::reactionInlineImage, st::reactionInlineImage));
|
||||
if (tag.promo) {
|
||||
st::dialogsSearchTagLocked.paintInCenter(p, QRect(
|
||||
inner.x(),
|
||||
inner.y() + skip,
|
||||
size - st::dialogsSearchTagPromoLeft,
|
||||
st::reactionInlineImage));
|
||||
} else if (const auto custom = tag.custom.get()) {
|
||||
const auto textFg = tag.selected
|
||||
? st::dialogsNameFgActive->c
|
||||
: st::dialogsNameFgOver->c;
|
||||
paintCustomFrame(
|
||||
p,
|
||||
custom,
|
||||
inner.topLeft(),
|
||||
now,
|
||||
paused,
|
||||
textFg);
|
||||
} else if (!tag.image.isNull()) {
|
||||
p.drawImage(image.topLeft(), tag.image);
|
||||
}
|
||||
}
|
||||
paintAdditionalText(p, position);
|
||||
}
|
||||
|
||||
void SearchTags::paintAdditionalText(Painter &p, QPoint position) const {
|
||||
if (_additionalText.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
const auto x = position.x() + _additionalLeft;
|
||||
const auto tag = _tags.front().geometry;
|
||||
const auto height = st::dialogsSearchTagPromo.font->height;
|
||||
const auto y = position.y() + tag.y() + (tag.height() - height) / 2;
|
||||
p.setPen(st::windowSubTextFg);
|
||||
_additionalText.drawLeft(p, x, y, _width - _additionalLeft, _width);
|
||||
}
|
||||
|
||||
void SearchTags::paintBackground(
|
||||
QPainter &p,
|
||||
QRect geometry,
|
||||
const Tag &tag) const {
|
||||
const auto &image = validateBg(tag.selected, tag.promo);
|
||||
const auto ratio = int(image.devicePixelRatio());
|
||||
const auto size = image.size() / ratio;
|
||||
if (const auto fill = geometry.width() - size.width(); fill > 0) {
|
||||
const auto left = size.width() / 2;
|
||||
const auto right = size.width() - left;
|
||||
const auto x = geometry.x();
|
||||
const auto y = geometry.y();
|
||||
p.drawImage(
|
||||
QRect(x, y, left, size.height()),
|
||||
image,
|
||||
QRect(QPoint(), QSize(left, size.height()) * ratio));
|
||||
p.fillRect(
|
||||
QRect(x + left, y, fill, size.height()),
|
||||
bgColor(tag.selected, tag.promo));
|
||||
p.drawImage(
|
||||
QRect(x + left + fill, y, right, size.height()),
|
||||
image,
|
||||
QRect(left * ratio, 0, right * ratio, size.height() * ratio));
|
||||
} else {
|
||||
p.drawImage(geometry.topLeft(), image);
|
||||
}
|
||||
}
|
||||
|
||||
void SearchTags::paintText(
|
||||
QPainter &p,
|
||||
QRect geometry,
|
||||
const Tag &tag) const {
|
||||
using namespace HistoryView::Reactions;
|
||||
|
||||
if (tag.text.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
p.setPen(tag.promo
|
||||
? st::lightButtonFgOver
|
||||
: tag.selected
|
||||
? st::dialogsTextFgActive
|
||||
: st::windowSubTextFg);
|
||||
p.setFont(st::reactionInlineTagFont);
|
||||
const auto position = tag.promo
|
||||
? st::reactionInlineTagPromoPosition
|
||||
: st::reactionInlineTagNamePosition;
|
||||
const auto x = geometry.x() + position.x();
|
||||
const auto y = geometry.y() + position.y();
|
||||
p.drawText(x, y + st::reactionInlineTagFont->ascent, tag.text);
|
||||
}
|
||||
|
||||
QColor SearchTags::bgColor(bool selected, bool promo) const {
|
||||
return promo
|
||||
? st::lightButtonBgOver->c
|
||||
: selected
|
||||
? st::dialogsBgActive->c
|
||||
: st::dialogsBgOver->c;
|
||||
}
|
||||
|
||||
const QImage &SearchTags::validateBg(bool selected, bool promo) const {
|
||||
using namespace HistoryView::Reactions;
|
||||
auto &image = promo ? _promoBg : selected ? _selectedBg : _normalBg;
|
||||
if (image.isNull()) {
|
||||
const auto tagBg = bgColor(selected, promo);
|
||||
const auto dotBg = st::transparent->c;
|
||||
image = InlineList::PrepareTagBg(tagBg, dotBg);
|
||||
}
|
||||
return image;
|
||||
}
|
||||
|
||||
std::vector<Data::ReactionId> SearchTags::collectSelected() const {
|
||||
return _tags | ranges::views::filter(
|
||||
&Tag::selected
|
||||
) | ranges::views::transform(
|
||||
&Tag::id
|
||||
) | ranges::to_vector;
|
||||
}
|
||||
|
||||
rpl::lifetime &SearchTags::lifetime() {
|
||||
return _lifetime;
|
||||
}
|
||||
|
||||
} // namespace Dialogs
|
||||
92
Telegram/SourceFiles/dialogs/dialogs_search_tags.h
Normal file
92
Telegram/SourceFiles/dialogs/dialogs_search_tags.h
Normal file
@@ -0,0 +1,92 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "base/weak_ptr.h"
|
||||
|
||||
class Painter;
|
||||
|
||||
namespace Data {
|
||||
class Session;
|
||||
struct Reaction;
|
||||
struct ReactionId;
|
||||
} // namespace Data
|
||||
|
||||
namespace Ui::Text {
|
||||
class CustomEmoji;
|
||||
} // namespace Ui::Text
|
||||
|
||||
namespace Dialogs {
|
||||
|
||||
class SearchTags final : public base::has_weak_ptr {
|
||||
public:
|
||||
SearchTags(
|
||||
not_null<Data::Session*> owner,
|
||||
rpl::producer<std::vector<Data::Reaction>> tags,
|
||||
std::vector<Data::ReactionId> selected);
|
||||
~SearchTags();
|
||||
|
||||
void resizeToWidth(int width);
|
||||
[[nodiscard]] int height() const;
|
||||
[[nodiscard]] rpl::producer<int> heightValue() const;
|
||||
[[nodiscard]] rpl::producer<> repaintRequests() const;
|
||||
|
||||
[[nodiscard]] ClickHandlerPtr lookupHandler(QPoint point) const;
|
||||
[[nodiscard]] auto selectedChanges() const
|
||||
-> rpl::producer<std::vector<Data::ReactionId>>;
|
||||
|
||||
[[nodiscard]] rpl::producer<Data::ReactionId> menuRequests() const;
|
||||
|
||||
void paint(
|
||||
Painter &p,
|
||||
QPoint position,
|
||||
crl::time now,
|
||||
bool paused) const;
|
||||
|
||||
[[nodiscard]] rpl::lifetime &lifetime();
|
||||
|
||||
private:
|
||||
struct Tag;
|
||||
|
||||
void fill(const std::vector<Data::Reaction> &list, bool premium);
|
||||
void paintCustomFrame(
|
||||
QPainter &p,
|
||||
not_null<Ui::Text::CustomEmoji*> emoji,
|
||||
QPoint innerTopLeft,
|
||||
crl::time now,
|
||||
bool paused,
|
||||
const QColor &textColor) const;
|
||||
void layout();
|
||||
[[nodiscard]] std::vector<Data::ReactionId> collectSelected() const;
|
||||
[[nodiscard]] QColor bgColor(bool selected, bool promo) const;
|
||||
[[nodiscard]] const QImage &validateBg(bool selected, bool promo) const;
|
||||
void paintAdditionalText(Painter &p, QPoint position) const;
|
||||
void paintBackground(QPainter &p, QRect geometry, const Tag &tag) const;
|
||||
void paintText(QPainter &p, QRect geometry, const Tag &tag) const;
|
||||
|
||||
const not_null<Data::Session*> _owner;
|
||||
std::vector<Data::ReactionId> _added;
|
||||
std::vector<Tag> _tags;
|
||||
Ui::Text::String _additionalText;
|
||||
rpl::event_stream<> _selectedChanges;
|
||||
rpl::event_stream<> _repaintRequests;
|
||||
rpl::event_stream<Data::ReactionId> _menuRequests;
|
||||
mutable QImage _normalBg;
|
||||
mutable QImage _selectedBg;
|
||||
mutable QImage _promoBg;
|
||||
mutable QImage _customCache;
|
||||
mutable int _customSkip = 0;
|
||||
rpl::variable<int> _height;
|
||||
int _width = 0;
|
||||
int _additionalLeft = 0;
|
||||
|
||||
rpl::lifetime _lifetime;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Dialogs
|
||||
21
Telegram/SourceFiles/dialogs/dialogs_three_state_icon.h
Normal file
21
Telegram/SourceFiles/dialogs/dialogs_three_state_icon.h
Normal file
@@ -0,0 +1,21 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "styles/style_dialogs.h"
|
||||
|
||||
namespace Dialogs {
|
||||
|
||||
[[nodiscard]] inline const style::icon &ThreeStateIcon(
|
||||
const style::ThreeStateIcon &icons,
|
||||
bool active,
|
||||
bool over) {
|
||||
return active ? icons.active : over ? icons.over : icons.icon;
|
||||
}
|
||||
|
||||
} // namespace Dialogs
|
||||
787
Telegram/SourceFiles/dialogs/dialogs_top_bar_suggestion.cpp
Normal file
787
Telegram/SourceFiles/dialogs/dialogs_top_bar_suggestion.cpp
Normal file
@@ -0,0 +1,787 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#include "dialogs/dialogs_top_bar_suggestion.h"
|
||||
|
||||
#include "api/api_authorizations.h"
|
||||
#include "api/api_credits.h"
|
||||
#include "api/api_peer_photo.h"
|
||||
#include "api/api_premium.h"
|
||||
#include "apiwrap.h"
|
||||
#include "base/call_delayed.h"
|
||||
#include "boxes/star_gift_box.h" // ShowStarGiftBox.
|
||||
#include "boxes/star_gift_auction_box.h"
|
||||
#include "core/application.h"
|
||||
#include "core/click_handler_types.h"
|
||||
#include "core/ui_integration.h"
|
||||
#include "data/components/gift_auctions.h"
|
||||
#include "data/components/promo_suggestions.h"
|
||||
#include "data/data_birthday.h"
|
||||
#include "data/data_changes.h"
|
||||
#include "data/data_peer_values.h" // Data::AmPremiumValue.
|
||||
#include "data/data_session.h"
|
||||
#include "data/data_user.h"
|
||||
#include "dialogs/ui/dialogs_top_bar_suggestion_content.h"
|
||||
#include "history/view/history_view_group_call_bar.h"
|
||||
#include "info/profile/info_profile_values.h"
|
||||
#include "lang/lang_keys.h"
|
||||
#include "main/main_session.h"
|
||||
#include "settings/settings_active_sessions.h"
|
||||
#include "settings/settings_credits_graphics.h"
|
||||
#include "settings/settings_premium.h"
|
||||
#include "ui/boxes/confirm_box.h"
|
||||
#include "ui/controls/userpic_button.h"
|
||||
#include "ui/effects/credits_graphics.h"
|
||||
#include "ui/layers/generic_box.h"
|
||||
#include "ui/rect.h"
|
||||
#include "ui/text/format_values.h"
|
||||
#include "ui/text/text_utilities.h"
|
||||
#include "ui/toast/toast.h"
|
||||
#include "ui/ui_utility.h"
|
||||
#include "ui/vertical_list.h"
|
||||
#include "ui/wrap/fade_wrap.h"
|
||||
#include "ui/wrap/slide_wrap.h"
|
||||
#include "window/window_controller.h"
|
||||
#include "window/window_session_controller.h"
|
||||
#include "styles/style_boxes.h"
|
||||
#include "styles/style_chat.h"
|
||||
#include "styles/style_chat_helpers.h"
|
||||
#include "styles/style_dialogs.h"
|
||||
#include "styles/style_layers.h"
|
||||
|
||||
namespace Dialogs {
|
||||
namespace {
|
||||
|
||||
[[nodiscard]] not_null<Window::SessionController*> FindSessionController(
|
||||
not_null<Ui::RpWidget*> widget) {
|
||||
const auto window = Core::App().findWindow(widget);
|
||||
Assert(window != nullptr);
|
||||
return window->sessionController();
|
||||
}
|
||||
|
||||
[[nodiscard]] QString FormatAuthInfo(const Data::UnreviewedAuth &auth) {
|
||||
const auto location = auth.location.isEmpty()
|
||||
? QString()
|
||||
: "\U0001F30D " + auth.location;
|
||||
const auto device = auth.device.isEmpty()
|
||||
? QString()
|
||||
: "\U0001F4F1 " + auth.device;
|
||||
|
||||
if (!location.isEmpty() && !device.isEmpty()) {
|
||||
return location + " (" + device + ")";
|
||||
} else if (!location.isEmpty()) {
|
||||
return location;
|
||||
} else if (!device.isEmpty()) {
|
||||
return device;
|
||||
}
|
||||
return QString();
|
||||
}
|
||||
|
||||
void ShowAuthToast(
|
||||
not_null<Ui::RpWidget*> parent,
|
||||
not_null<Main::Session*> session,
|
||||
const std::vector<Data::UnreviewedAuth> &list,
|
||||
bool confirmed) {
|
||||
if (confirmed) {
|
||||
auto text = tr::lng_unconfirmed_auth_confirmed_message(
|
||||
tr::now,
|
||||
lt_link,
|
||||
tr::link(tr::lng_settings_sessions_title(tr::now)),
|
||||
tr::rich);
|
||||
auto filter = [=](
|
||||
ClickHandlerPtr handler,
|
||||
Qt::MouseButton button) {
|
||||
if (const auto controller = FindSessionController(parent)) {
|
||||
session->api().authorizations().reload();
|
||||
controller->showSettings(Settings::Sessions::Id());
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
Ui::Toast::Show(parent->window(), Ui::Toast::Config{
|
||||
.title = tr::lng_unconfirmed_auth_confirmed(tr::now),
|
||||
.text = std::move(text),
|
||||
.filter = std::move(filter),
|
||||
.duration = crl::time(5000),
|
||||
});
|
||||
} else {
|
||||
auto messageText = QString();
|
||||
if (list.size() == 1) {
|
||||
messageText = tr::lng_unconfirmed_auth_denied_single(
|
||||
tr::now,
|
||||
lt_country,
|
||||
FormatAuthInfo(list.front()));
|
||||
} else {
|
||||
auto authList = QString('\n');
|
||||
for (auto i = 0; i < std::min(int(list.size()), 10); ++i) {
|
||||
const auto info = FormatAuthInfo(list[i]);
|
||||
if (!info.isEmpty()) {
|
||||
authList += "• " + info + "\n";
|
||||
}
|
||||
}
|
||||
messageText = tr::lng_unconfirmed_auth_denied_multiple(
|
||||
tr::now,
|
||||
lt_country,
|
||||
authList);
|
||||
}
|
||||
if (const auto controller = FindSessionController(parent)) {
|
||||
const auto count = float64(list.size());
|
||||
controller->show(Box(ShowAuthDeniedBox, count, messageText));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
constexpr auto kSugSetBirthday = "BIRTHDAY_SETUP"_cs;
|
||||
constexpr auto kSugBirthdayContacts = "BIRTHDAY_CONTACTS_TODAY"_cs;
|
||||
constexpr auto kSugPremiumAnnual = "PREMIUM_ANNUAL"_cs;
|
||||
constexpr auto kSugPremiumUpgrade = "PREMIUM_UPGRADE"_cs;
|
||||
constexpr auto kSugPremiumRestore = "PREMIUM_RESTORE"_cs;
|
||||
constexpr auto kSugPremiumGrace = "PREMIUM_GRACE"_cs;
|
||||
constexpr auto kSugSetUserpic = "USERPIC_SETUP"_cs;
|
||||
constexpr auto kSugLowCreditsSubs = "STARS_SUBSCRIPTION_LOW_BALANCE"_cs;
|
||||
|
||||
|
||||
} // namespace
|
||||
|
||||
rpl::producer<Ui::SlideWrap<Ui::RpWidget>*> TopBarSuggestionValue(
|
||||
not_null<Ui::RpWidget*> parent,
|
||||
not_null<Main::Session*> session,
|
||||
rpl::producer<bool> outerWrapToggleValue) {
|
||||
return [=, outerWrapToggleValue = rpl::duplicate(outerWrapToggleValue)](
|
||||
auto consumer) {
|
||||
auto lifetime = rpl::lifetime();
|
||||
|
||||
struct Toggle {
|
||||
bool value = false;
|
||||
anim::type type;
|
||||
};
|
||||
|
||||
struct State {
|
||||
TopBarSuggestionContent *content = nullptr;
|
||||
Ui::SlideWrap<Ui::VerticalLayout> *unconfirmedWarning = nullptr;
|
||||
base::unique_qptr<Ui::SlideWrap<Ui::RpWidget>> wrap;
|
||||
rpl::variable<int> leftPadding;
|
||||
rpl::variable<Toggle> desiredWrapToggle;
|
||||
rpl::variable<bool> outerWrapToggle;
|
||||
rpl::lifetime birthdayLifetime;
|
||||
rpl::lifetime premiumLifetime;
|
||||
rpl::lifetime userpicLifetime;
|
||||
rpl::lifetime giftsLifetime;
|
||||
rpl::lifetime creditsLifetime;
|
||||
rpl::lifetime auctionsLifetime;
|
||||
std::unique_ptr<Api::CreditsHistory> creditsHistory;
|
||||
};
|
||||
|
||||
const auto state = lifetime.make_state<State>();
|
||||
state->outerWrapToggle = rpl::duplicate(outerWrapToggleValue);
|
||||
state->leftPadding = rpl::variable<int>(
|
||||
rpl::single(st::dialogsTopBarLeftPadding));
|
||||
const auto ensureContent = [=] {
|
||||
if (!state->content) {
|
||||
const auto window = FindSessionController(parent);
|
||||
state->content = Ui::CreateChild<TopBarSuggestionContent>(
|
||||
parent,
|
||||
[=] { return window->isGifPausedAtLeastFor(
|
||||
Window::GifPauseReason::Layer); });
|
||||
rpl::combine(
|
||||
parent->widthValue(),
|
||||
state->content->desiredHeightValue()
|
||||
) | rpl::on_next([=](int width, int height) {
|
||||
state->content->resize(width, height);
|
||||
}, state->content->lifetime());
|
||||
}
|
||||
};
|
||||
const auto ensureWrap = [=](not_null<Ui::RpWidget*> child) {
|
||||
if (!state->wrap) {
|
||||
state->wrap
|
||||
= base::make_unique_q<Ui::SlideWrap<Ui::RpWidget>>(
|
||||
parent,
|
||||
object_ptr<Ui::RpWidget>::fromRaw(child));
|
||||
state->desiredWrapToggle.force_assign(
|
||||
Toggle{ false, anim::type::instant });
|
||||
}
|
||||
};
|
||||
|
||||
const auto setLeftPaddingRelativeTo = [=](
|
||||
not_null<TopBarSuggestionContent*> content,
|
||||
not_null<Ui::RpWidget*> relativeTo) {
|
||||
content->setLeftPadding(state->leftPadding.value(
|
||||
) | rpl::map([w = relativeTo->width()](int padding) {
|
||||
return w + padding * 2;
|
||||
}));
|
||||
};
|
||||
|
||||
const auto processCurrentSuggestion = [=](auto repeat) -> void {
|
||||
state->birthdayLifetime.destroy();
|
||||
state->premiumLifetime.destroy();
|
||||
state->userpicLifetime.destroy();
|
||||
state->giftsLifetime.destroy();
|
||||
state->creditsLifetime.destroy();
|
||||
state->auctionsLifetime.destroy();
|
||||
|
||||
if (!session->api().authorizations().unreviewed().empty()) {
|
||||
state->content = nullptr;
|
||||
state->wrap = nullptr;
|
||||
const auto &list
|
||||
= session->api().authorizations().unreviewed();
|
||||
const auto hashes = ranges::views::all(
|
||||
list
|
||||
) | ranges::views::transform([](const auto &auth) {
|
||||
return auth.hash;
|
||||
}) | ranges::to_vector;
|
||||
|
||||
const auto content = CreateUnconfirmedAuthContent(
|
||||
parent,
|
||||
list,
|
||||
[=](bool confirmed) {
|
||||
ShowAuthToast(parent, session, list, confirmed);
|
||||
session->api().authorizations().review(
|
||||
hashes,
|
||||
confirmed);
|
||||
});
|
||||
ensureWrap(content);
|
||||
const auto wasUnconfirmedWarning = state->unconfirmedWarning;
|
||||
state->unconfirmedWarning = content;
|
||||
state->desiredWrapToggle.force_assign(Toggle{
|
||||
true,
|
||||
(state->unconfirmedWarning != wasUnconfirmedWarning)
|
||||
? anim::type::instant
|
||||
: anim::type::normal,
|
||||
});
|
||||
return;
|
||||
} else {
|
||||
if (state->unconfirmedWarning) {
|
||||
state->unconfirmedWarning = nullptr;
|
||||
state->wrap = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
ensureContent();
|
||||
ensureWrap(state->content);
|
||||
const auto content = state->content;
|
||||
const auto wrap = state->wrap.get();
|
||||
using RightIcon = TopBarSuggestionContent::RightIcon;
|
||||
const auto promo = &session->promoSuggestions();
|
||||
const auto auctions = &session->giftAuctions();
|
||||
if (auctions->hasActive()) {
|
||||
using namespace Data;
|
||||
struct Button {
|
||||
rpl::variable<TextWithEntities> text;
|
||||
Fn<void()> callback;
|
||||
base::has_weak_ptr guard;
|
||||
};
|
||||
auto &lifetime = state->auctionsLifetime;
|
||||
const auto button = lifetime.template make_state<Button>();
|
||||
const auto window = FindSessionController(parent);
|
||||
auctions->active(
|
||||
) | rpl::on_next([=](ActiveAuctions &&active) {
|
||||
const auto empty = active.list.empty();
|
||||
state->desiredWrapToggle.force_assign(
|
||||
Toggle{ !empty, anim::type::normal });
|
||||
if (empty) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto text = Ui::ActiveAuctionsState(active);
|
||||
const auto textColorOverride = text.someOutbid
|
||||
? st::attentionButtonFg->c
|
||||
: std::optional<QColor>();
|
||||
content->setContent(
|
||||
Ui::ActiveAuctionsTitle(active),
|
||||
std::move(text.text),
|
||||
Core::TextContext({ .session = session }),
|
||||
textColorOverride);
|
||||
button->text = Ui::ActiveAuctionsButton(active);
|
||||
button->callback = Ui::ActiveAuctionsCallback(
|
||||
window,
|
||||
active);
|
||||
}, state->auctionsLifetime);
|
||||
const auto callback = crl::guard(&button->guard, [=] {
|
||||
button->callback();
|
||||
});
|
||||
content->setRightButton(button->text.value(), callback);
|
||||
content->setClickedCallback(callback);
|
||||
content->setLeftPadding(state->leftPadding.value());
|
||||
state->desiredWrapToggle.force_assign(
|
||||
Toggle{ true, anim::type::normal });
|
||||
return;
|
||||
} else if (const auto custom = promo->custom()) {
|
||||
content->setRightIcon(RightIcon::Close);
|
||||
content->setLeftPadding(state->leftPadding.value());
|
||||
content->setClickedCallback([=] {
|
||||
const auto controller = FindSessionController(parent);
|
||||
UrlClickHandler::Open(
|
||||
custom->url,
|
||||
QVariant::fromValue(ClickHandlerContext{
|
||||
.sessionWindow = base::make_weak(controller),
|
||||
}));
|
||||
});
|
||||
content->setHideCallback([=] {
|
||||
promo->dismiss(custom->suggestion);
|
||||
repeat(repeat);
|
||||
});
|
||||
|
||||
content->setContent(
|
||||
custom->title,
|
||||
custom->description,
|
||||
Core::TextContext({ .session = session }));
|
||||
state->desiredWrapToggle.force_assign(
|
||||
Toggle{ true, anim::type::normal });
|
||||
return;
|
||||
} else if (session->premiumCanBuy()
|
||||
&& promo->current(kSugPremiumGrace.utf8())) {
|
||||
content->setRightIcon(RightIcon::Close);
|
||||
content->setLeftPadding(state->leftPadding.value());
|
||||
content->setClickedCallback([=] {
|
||||
const auto controller = FindSessionController(parent);
|
||||
UrlClickHandler::Open(
|
||||
u"https://t.me/premiumbot?start=status"_q,
|
||||
QVariant::fromValue(ClickHandlerContext{
|
||||
.sessionWindow = base::make_weak(controller),
|
||||
}));
|
||||
});
|
||||
content->setHideCallback([=] {
|
||||
promo->dismiss(kSugPremiumGrace.utf8());
|
||||
repeat(repeat);
|
||||
});
|
||||
content->setContent(
|
||||
tr::lng_dialogs_suggestions_premium_grace_title(
|
||||
tr::now,
|
||||
tr::bold),
|
||||
tr::lng_dialogs_suggestions_premium_grace_about(
|
||||
tr::now,
|
||||
TextWithEntities::Simple));
|
||||
state->desiredWrapToggle.force_assign(
|
||||
Toggle{ true, anim::type::normal });
|
||||
return;
|
||||
} else if (session->premiumCanBuy()
|
||||
&& promo->current(kSugLowCreditsSubs.utf8())) {
|
||||
state->creditsHistory = std::make_unique<Api::CreditsHistory>(
|
||||
session->user(),
|
||||
false,
|
||||
false);
|
||||
const auto show = [=](
|
||||
const QString &peers,
|
||||
uint64 needed,
|
||||
uint64 whole) {
|
||||
if (whole > needed) {
|
||||
return;
|
||||
}
|
||||
content->setRightIcon(RightIcon::Close);
|
||||
content->setLeftPadding(state->leftPadding.value());
|
||||
content->setClickedCallback([=] {
|
||||
const auto controller = FindSessionController(parent);
|
||||
controller->uiShow()->show(Box(
|
||||
Settings::SmallBalanceBox,
|
||||
controller->uiShow(),
|
||||
needed,
|
||||
Settings::SmallBalanceSubscription{ peers },
|
||||
[=] {
|
||||
promo->dismiss(kSugLowCreditsSubs.utf8());
|
||||
repeat(repeat);
|
||||
}));
|
||||
});
|
||||
content->setHideCallback([=] {
|
||||
promo->dismiss(kSugLowCreditsSubs.utf8());
|
||||
repeat(repeat);
|
||||
});
|
||||
|
||||
content->setContent(
|
||||
tr::lng_dialogs_suggestions_credits_sub_low_title(
|
||||
tr::now,
|
||||
lt_count,
|
||||
float64(needed - whole),
|
||||
lt_emoji,
|
||||
Ui::MakeCreditsIconEntity(),
|
||||
lt_channels,
|
||||
{ peers },
|
||||
tr::bold),
|
||||
tr::lng_dialogs_suggestions_credits_sub_low_about(
|
||||
tr::now,
|
||||
TextWithEntities::Simple),
|
||||
Ui::MakeCreditsIconContext(
|
||||
content->contentTitleSt().font->height,
|
||||
1));
|
||||
state->desiredWrapToggle.force_assign(
|
||||
Toggle{ true, anim::type::normal });
|
||||
};
|
||||
session->credits().load();
|
||||
state->creditsLifetime.destroy();
|
||||
session->credits().balanceValue() | rpl::on_next([=] {
|
||||
state->creditsLifetime.destroy();
|
||||
state->creditsHistory->requestSubscriptions(
|
||||
Data::CreditsStatusSlice::OffsetToken(),
|
||||
[=](Data::CreditsStatusSlice slice) {
|
||||
state->creditsHistory = nullptr;
|
||||
auto peers = QStringList();
|
||||
auto credits = uint64(0);
|
||||
for (const auto &entry : slice.subscriptions) {
|
||||
if (entry.barePeerId) {
|
||||
const auto peer = session->data().peer(
|
||||
PeerId(entry.barePeerId));
|
||||
peers.append(peer->name());
|
||||
credits += entry.subscription.credits;
|
||||
}
|
||||
}
|
||||
show(
|
||||
peers.join(", "),
|
||||
credits,
|
||||
session->credits().balance().whole());
|
||||
},
|
||||
true);
|
||||
}, state->creditsLifetime);
|
||||
|
||||
return;
|
||||
} else if (session->premiumCanBuy()
|
||||
&& promo->current(kSugBirthdayContacts.utf8())) {
|
||||
promo->requestContactBirthdays(crl::guard(content, [=] {
|
||||
const auto users = promo->knownBirthdaysToday().value_or(
|
||||
std::vector<UserId>());
|
||||
if (users.empty()) {
|
||||
repeat(repeat);
|
||||
return;
|
||||
}
|
||||
|
||||
const auto controller = FindSessionController(parent);
|
||||
const auto isSingle = users.size() == 1;
|
||||
const auto first = session->data().user(users.front());
|
||||
content->setRightIcon(RightIcon::Close);
|
||||
content->setClickedCallback([=] {
|
||||
if (isSingle) {
|
||||
Ui::ShowStarGiftBox(controller, first);
|
||||
} else {
|
||||
Ui::ChooseStarGiftRecipient(controller);
|
||||
}
|
||||
});
|
||||
content->setHideCallback([=] {
|
||||
promo->dismiss(kSugBirthdayContacts.utf8());
|
||||
controller->showToast(
|
||||
tr::lng_dialogs_suggestions_birthday_contact_dismiss(
|
||||
tr::now));
|
||||
repeat(repeat);
|
||||
});
|
||||
auto title = isSingle
|
||||
? tr::lng_dialogs_suggestions_birthday_contact_title(
|
||||
tr::now,
|
||||
lt_text,
|
||||
{ first->shortName() },
|
||||
tr::rich)
|
||||
: tr::lng_dialogs_suggestions_birthday_contacts_title(
|
||||
tr::now,
|
||||
lt_count,
|
||||
users.size(),
|
||||
tr::rich);
|
||||
auto text = isSingle
|
||||
? tr::lng_dialogs_suggestions_birthday_contact_about(
|
||||
tr::now,
|
||||
TextWithEntities::Simple)
|
||||
: tr::lng_dialogs_suggestions_birthday_contacts_about(
|
||||
tr::now,
|
||||
TextWithEntities::Simple);
|
||||
content->setContent(std::move(title), std::move(text));
|
||||
state->giftsLifetime.destroy();
|
||||
if (!isSingle) {
|
||||
struct UserViews {
|
||||
std::vector<HistoryView::UserpicInRow> inRow;
|
||||
QImage userpics;
|
||||
base::unique_qptr<Ui::RpWidget> widget;
|
||||
};
|
||||
const auto s
|
||||
= state->giftsLifetime.template make_state<
|
||||
UserViews>();
|
||||
s->widget = base::make_unique_q<Ui::RpWidget>(
|
||||
content);
|
||||
const auto widget = s->widget.get();
|
||||
widget->setAttribute(
|
||||
Qt::WA_TransparentForMouseEvents);
|
||||
content->sizeValue() | rpl::filter_size(
|
||||
) | rpl::on_next([=](const QSize &size) {
|
||||
widget->resize(size);
|
||||
widget->show();
|
||||
widget->raise();
|
||||
}, widget->lifetime());
|
||||
for (const auto &id : users) {
|
||||
if (const auto user = session->data().user(id)) {
|
||||
s->inRow.push_back({ .peer = user });
|
||||
}
|
||||
}
|
||||
widget->paintRequest() | rpl::on_next([=] {
|
||||
auto p = QPainter(widget);
|
||||
const auto regenerate = [&] {
|
||||
if (s->userpics.isNull()) {
|
||||
return true;
|
||||
}
|
||||
for (auto &entry : s->inRow) {
|
||||
if (entry.uniqueKey
|
||||
!= entry.peer->userpicUniqueKey(
|
||||
entry.view)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}();
|
||||
if (regenerate) {
|
||||
const auto &st = st::historyCommentsUserpics;
|
||||
HistoryView::GenerateUserpicsInRow(
|
||||
s->userpics,
|
||||
s->inRow,
|
||||
st,
|
||||
3);
|
||||
const auto v = int(users.size() * st.size
|
||||
- st.shift);
|
||||
content->setLeftPadding(
|
||||
state->leftPadding.value(
|
||||
) | rpl::map([v](int padding) {
|
||||
return padding * 2 + v;
|
||||
}));
|
||||
}
|
||||
p.drawImage(
|
||||
state->leftPadding.current(),
|
||||
(widget->height()
|
||||
- (s->userpics.height()
|
||||
/ style::DevicePixelRatio())) / 2,
|
||||
s->userpics);
|
||||
}, widget->lifetime());
|
||||
} else {
|
||||
using Ptr = base::unique_qptr<Ui::UserpicButton>;
|
||||
const auto ptr
|
||||
= state->giftsLifetime.template make_state<Ptr>(
|
||||
base::make_unique_q<Ui::UserpicButton>(
|
||||
content,
|
||||
first,
|
||||
st::uploadUserpicButton));
|
||||
const auto fake = ptr->get();
|
||||
fake->setAttribute(Qt::WA_TransparentForMouseEvents);
|
||||
rpl::combine(
|
||||
state->leftPadding.value(),
|
||||
content->sizeValue() | rpl::filter_size()
|
||||
) | rpl::on_next([=](int p, const QSize &s) {
|
||||
fake->raise();
|
||||
fake->show();
|
||||
fake->moveToLeft(
|
||||
p,
|
||||
(s.height() - fake->height()) / 2);
|
||||
}, fake->lifetime());
|
||||
setLeftPaddingRelativeTo(content, fake);
|
||||
}
|
||||
|
||||
state->desiredWrapToggle.force_assign(
|
||||
Toggle{ true, anim::type::normal });
|
||||
}));
|
||||
return;
|
||||
} else if (promo->current(kSugSetBirthday.utf8())
|
||||
&& !Data::IsBirthdayToday(session->user()->birthday())) {
|
||||
content->setRightIcon(RightIcon::Close);
|
||||
content->setLeftPadding(state->leftPadding.value());
|
||||
content->setClickedCallback([=] {
|
||||
const auto controller = FindSessionController(parent);
|
||||
Core::App().openInternalUrl(
|
||||
u"internal:edit_birthday:add_privacy"_q,
|
||||
QVariant::fromValue(ClickHandlerContext{
|
||||
.sessionWindow = base::make_weak(controller),
|
||||
}));
|
||||
|
||||
state->birthdayLifetime = Info::Profile::BirthdayValue(
|
||||
session->user()
|
||||
) | rpl::map(
|
||||
Data::IsBirthdayTodayValue
|
||||
) | rpl::flatten_latest(
|
||||
) | rpl::distinct_until_changed(
|
||||
) | rpl::on_next([=] {
|
||||
repeat(repeat);
|
||||
});
|
||||
});
|
||||
content->setHideCallback([=] {
|
||||
promo->dismiss(kSugSetBirthday.utf8());
|
||||
repeat(repeat);
|
||||
});
|
||||
content->setContent(
|
||||
tr::lng_dialogs_suggestions_birthday_title(
|
||||
tr::now,
|
||||
tr::bold),
|
||||
tr::lng_dialogs_suggestions_birthday_about(
|
||||
tr::now,
|
||||
TextWithEntities::Simple));
|
||||
state->desiredWrapToggle.force_assign(
|
||||
Toggle{ true, anim::type::normal });
|
||||
return;
|
||||
} else if (session->premiumPossible() && !session->premium()) {
|
||||
const auto isPremiumAnnual = promo->current(
|
||||
kSugPremiumAnnual.utf8());
|
||||
const auto isPremiumRestore = !isPremiumAnnual
|
||||
&& promo->current(kSugPremiumRestore.utf8());
|
||||
const auto isPremiumUpgrade = !isPremiumAnnual
|
||||
&& !isPremiumRestore
|
||||
&& promo->current(kSugPremiumUpgrade.utf8());
|
||||
const auto set = [=](QString discount) {
|
||||
constexpr auto kMinus = QChar(0x2212);
|
||||
const auto &title = isPremiumAnnual
|
||||
? tr::lng_dialogs_suggestions_premium_annual_title
|
||||
: isPremiumRestore
|
||||
? tr::lng_dialogs_suggestions_premium_restore_title
|
||||
: tr::lng_dialogs_suggestions_premium_upgrade_title;
|
||||
const auto &description = isPremiumAnnual
|
||||
? tr::lng_dialogs_suggestions_premium_annual_about
|
||||
: isPremiumRestore
|
||||
? tr::lng_dialogs_suggestions_premium_restore_about
|
||||
: tr::lng_dialogs_suggestions_premium_upgrade_about;
|
||||
content->setContent(
|
||||
title(
|
||||
tr::now,
|
||||
lt_text,
|
||||
{ discount.replace(kMinus, QChar()) },
|
||||
tr::bold),
|
||||
description(tr::now, TextWithEntities::Simple));
|
||||
content->setClickedCallback([=] {
|
||||
const auto controller = FindSessionController(parent);
|
||||
Settings::ShowPremium(controller, "dialogs_hint");
|
||||
promo->dismiss(isPremiumAnnual
|
||||
? kSugPremiumAnnual.utf8()
|
||||
: isPremiumRestore
|
||||
? kSugPremiumRestore.utf8()
|
||||
: kSugPremiumUpgrade.utf8());
|
||||
repeat(repeat);
|
||||
});
|
||||
state->desiredWrapToggle.force_assign(
|
||||
Toggle{ true, anim::type::normal });
|
||||
};
|
||||
if (isPremiumAnnual || isPremiumRestore || isPremiumUpgrade) {
|
||||
content->setRightIcon(RightIcon::Arrow);
|
||||
content->setLeftPadding(state->leftPadding.value());
|
||||
const auto api = &session->api().premium();
|
||||
api->statusTextValue() | rpl::on_next([=] {
|
||||
for (const auto &o : api->subscriptionOptions()) {
|
||||
if (o.months == 12) {
|
||||
set(o.discount);
|
||||
state->premiumLifetime.destroy();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}, state->premiumLifetime);
|
||||
api->reload();
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (promo->current(kSugSetUserpic.utf8())
|
||||
&& !session->user()->userpicPhotoId()) {
|
||||
const auto controller = FindSessionController(parent);
|
||||
content->setRightIcon(RightIcon::Close);
|
||||
const auto upload = Ui::CreateChild<Ui::UserpicButton>(
|
||||
content,
|
||||
&controller->window(),
|
||||
Ui::UserpicButton::Role::ChoosePhoto,
|
||||
st::uploadUserpicButton);
|
||||
rpl::combine(
|
||||
state->leftPadding.value(),
|
||||
content->sizeValue() | rpl::filter_size()
|
||||
) | rpl::on_next([=](int padding, const QSize &s) {
|
||||
upload->raise();
|
||||
upload->show();
|
||||
upload->moveToLeft(
|
||||
padding,
|
||||
(s.height() - upload->height()) / 2);
|
||||
}, content->lifetime());
|
||||
setLeftPaddingRelativeTo(content, upload);
|
||||
upload->chosenImages() | rpl::on_next([=](
|
||||
Ui::UserpicButton::ChosenImage &&chosen) {
|
||||
if (chosen.type == Ui::UserpicButton::ChosenType::Set) {
|
||||
session->api().peerPhoto().upload(
|
||||
session->user(),
|
||||
{
|
||||
std::move(chosen.image),
|
||||
chosen.markup.documentId,
|
||||
chosen.markup.colors,
|
||||
});
|
||||
}
|
||||
}, upload->lifetime());
|
||||
|
||||
state->userpicLifetime = session->changes().peerUpdates(
|
||||
session->user(),
|
||||
Data::PeerUpdate::Flag::Photo
|
||||
) | rpl::on_next([=] {
|
||||
if (session->user()->userpicPhotoId()) {
|
||||
repeat(repeat);
|
||||
}
|
||||
});
|
||||
|
||||
content->setHideCallback([=] {
|
||||
promo->dismiss(kSugSetUserpic.utf8());
|
||||
repeat(repeat);
|
||||
});
|
||||
|
||||
content->setClickedCallback([=] {
|
||||
const auto syntetic = [=](QEvent::Type type) {
|
||||
Ui::SendSynteticMouseEvent(
|
||||
upload,
|
||||
type,
|
||||
Qt::LeftButton,
|
||||
upload->mapToGlobal(QPoint(0, 0)));
|
||||
};
|
||||
syntetic(QEvent::MouseMove);
|
||||
syntetic(QEvent::MouseButtonPress);
|
||||
syntetic(QEvent::MouseButtonRelease);
|
||||
});
|
||||
content->setContent(
|
||||
tr::lng_dialogs_suggestions_userpics_title(
|
||||
tr::now,
|
||||
tr::bold),
|
||||
tr::lng_dialogs_suggestions_userpics_about(
|
||||
tr::now,
|
||||
TextWithEntities::Simple));
|
||||
state->desiredWrapToggle.force_assign(
|
||||
Toggle{ true, anim::type::normal });
|
||||
return;
|
||||
}
|
||||
state->desiredWrapToggle.force_assign(
|
||||
Toggle{ false, anim::type::normal });
|
||||
base::call_delayed(st::slideWrapDuration * 2, wrap, [=] {
|
||||
state->content = nullptr;
|
||||
state->wrap = nullptr;
|
||||
consumer.put_next(nullptr);
|
||||
});
|
||||
};
|
||||
|
||||
state->desiredWrapToggle.value() | rpl::combine_previous(
|
||||
) | rpl::filter([=] {
|
||||
return state->wrap != nullptr;
|
||||
}) | rpl::on_next([=](Toggle was, Toggle now) {
|
||||
state->wrap->toggle(
|
||||
state->outerWrapToggle.current() && now.value,
|
||||
(was.value == now.value)
|
||||
? anim::type::instant
|
||||
: now.type);
|
||||
}, lifetime);
|
||||
|
||||
state->outerWrapToggle.value() | rpl::combine_previous(
|
||||
) | rpl::filter([=] {
|
||||
return state->wrap != nullptr;
|
||||
}) | rpl::on_next([=](bool was, bool now) {
|
||||
const auto toggle = state->desiredWrapToggle.current();
|
||||
state->wrap->toggle(
|
||||
toggle.value && now,
|
||||
(was == now) ? toggle.type : anim::type::instant);
|
||||
}, lifetime);
|
||||
|
||||
rpl::merge(
|
||||
session->promoSuggestions().value(),
|
||||
session->api().authorizations().unreviewedChanges(),
|
||||
Data::AmPremiumValue(session) | rpl::skip(1) | rpl::to_empty,
|
||||
session->giftAuctions().hasActiveChanges() | rpl::to_empty
|
||||
) | rpl::on_next([=] {
|
||||
const auto was = state->wrap.get();
|
||||
const auto weak = base::make_weak(was);
|
||||
processCurrentSuggestion(processCurrentSuggestion);
|
||||
if (was != state->wrap || (was && !weak)) {
|
||||
consumer.put_next_copy(state->wrap.get());
|
||||
}
|
||||
}, lifetime);
|
||||
|
||||
return lifetime;
|
||||
};
|
||||
}
|
||||
|
||||
} // namespace Dialogs
|
||||
32
Telegram/SourceFiles/dialogs/dialogs_top_bar_suggestion.h
Normal file
32
Telegram/SourceFiles/dialogs/dialogs_top_bar_suggestion.h
Normal file
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
template <typename Object>
|
||||
class object_ptr;
|
||||
|
||||
namespace Main {
|
||||
class Session;
|
||||
} // namespace Main
|
||||
|
||||
namespace Ui {
|
||||
class RpWidget;
|
||||
template <typename Widget>
|
||||
class SlideWrap;
|
||||
} // namespace Ui
|
||||
|
||||
namespace Dialogs {
|
||||
|
||||
[[nodiscard]] auto TopBarSuggestionValue(
|
||||
not_null<Ui::RpWidget*> parent,
|
||||
not_null<Main::Session*>,
|
||||
rpl::producer<bool> outerWrapToggleValue)
|
||||
-> rpl::producer<Ui::SlideWrap<Ui::RpWidget>*>;
|
||||
|
||||
} // namespace Dialogs
|
||||
|
||||
4410
Telegram/SourceFiles/dialogs/dialogs_widget.cpp
Normal file
4410
Telegram/SourceFiles/dialogs/dialogs_widget.cpp
Normal file
File diff suppressed because it is too large
Load Diff
423
Telegram/SourceFiles/dialogs/dialogs_widget.h
Normal file
423
Telegram/SourceFiles/dialogs/dialogs_widget.h
Normal file
@@ -0,0 +1,423 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "api/api_peer_search.h"
|
||||
#include "base/timer.h"
|
||||
#include "dialogs/dialogs_key.h"
|
||||
#include "window/section_widget.h"
|
||||
#include "ui/controls/swipe_handler_data.h"
|
||||
#include "ui/effects/animations.h"
|
||||
#include "ui/userpic_view.h"
|
||||
#include "mtproto/sender.h"
|
||||
#include "api/api_single_message_search.h"
|
||||
|
||||
namespace MTP {
|
||||
class Error;
|
||||
} // namespace MTP
|
||||
|
||||
namespace Data {
|
||||
class Forum;
|
||||
enum class StorySourcesList : uchar;
|
||||
struct ReactionId;
|
||||
} // namespace Data
|
||||
|
||||
namespace Main {
|
||||
class Session;
|
||||
} // namespace Main
|
||||
|
||||
namespace HistoryView {
|
||||
class TopBarWidget;
|
||||
class ContactStatus;
|
||||
} // namespace HistoryView
|
||||
|
||||
namespace Ui {
|
||||
class AbstractButton;
|
||||
class IconButton;
|
||||
class PopupMenu;
|
||||
class DropdownMenu;
|
||||
class FlatButton;
|
||||
class InputField;
|
||||
class CrossButton;
|
||||
class PlainShadow;
|
||||
class DownloadBar;
|
||||
class GroupCallBar;
|
||||
class RequestsBar;
|
||||
class MoreChatsBar;
|
||||
class JumpDownButton;
|
||||
class ElasticScroll;
|
||||
template <typename Widget>
|
||||
class FadeWrapScaled;
|
||||
template <typename Widget>
|
||||
class SlideWrap;
|
||||
class VerticalLayout;
|
||||
} // namespace Ui
|
||||
|
||||
namespace Window {
|
||||
class SessionController;
|
||||
class ConnectionState;
|
||||
struct SectionShow;
|
||||
struct SeparateId;
|
||||
} // namespace Window
|
||||
|
||||
namespace Dialogs::Stories {
|
||||
class List;
|
||||
struct Content;
|
||||
} // namespace Dialogs::Stories
|
||||
|
||||
namespace Dialogs {
|
||||
|
||||
extern const char kOptionForumHideChatsList[];
|
||||
|
||||
struct RowDescriptor;
|
||||
class Row;
|
||||
class FakeRow;
|
||||
class Key;
|
||||
struct ChosenRow;
|
||||
class InnerWidget;
|
||||
struct SearchRequestType;
|
||||
enum class SearchRequestDelay : uchar;
|
||||
class Suggestions;
|
||||
class ChatSearchIn;
|
||||
enum class ChatSearchTab : uchar;
|
||||
enum class HashOrCashtag : uchar;
|
||||
|
||||
class Widget final : public Window::AbstractSectionWidget {
|
||||
public:
|
||||
enum class Layout {
|
||||
Main,
|
||||
Child,
|
||||
};
|
||||
Widget(
|
||||
QWidget *parent,
|
||||
not_null<Window::SessionController*> controller,
|
||||
Layout layout);
|
||||
|
||||
// When resizing the widget with top edge moved up or down and we
|
||||
// want to add this top movement to the scroll position, so inner
|
||||
// content will not move.
|
||||
void setGeometryWithTopMoved(const QRect &newGeometry, int topDelta);
|
||||
|
||||
void updateDragInScroll(bool inScroll);
|
||||
|
||||
void showForum(
|
||||
not_null<Data::Forum*> forum,
|
||||
const Window::SectionShow ¶ms);
|
||||
void setInnerFocus(bool unfocusSearch = false);
|
||||
[[nodiscard]] bool searchHasFocus() const;
|
||||
|
||||
[[nodiscard]] Data::Forum *openedForum() const;
|
||||
|
||||
void jumpToTop(bool belowPinned = false);
|
||||
void raiseWithTooltip();
|
||||
|
||||
[[nodiscard]] QPixmap grabNonNarrowScrollFrame();
|
||||
void startWidthAnimation();
|
||||
void stopWidthAnimation();
|
||||
|
||||
bool hasTopBarShadow() const {
|
||||
return true;
|
||||
}
|
||||
void showAnimated(
|
||||
Window::SlideDirection direction,
|
||||
const Window::SectionSlideParams ¶ms);
|
||||
void showFast();
|
||||
[[nodiscard]] rpl::producer<float64> shownProgressValue() const;
|
||||
|
||||
void scrollToEntry(const RowDescriptor &entry);
|
||||
|
||||
void searchMessages(SearchState state);
|
||||
|
||||
[[nodiscard]] RowDescriptor resolveChatNext(RowDescriptor from = {}) const;
|
||||
[[nodiscard]] RowDescriptor resolveChatPrevious(RowDescriptor from = {}) const;
|
||||
void updateHasFocus(not_null<QWidget*> focused);
|
||||
void toggleFiltersMenu(bool value);
|
||||
|
||||
// Float player interface.
|
||||
bool floatPlayerHandleWheelEvent(QEvent *e) override;
|
||||
QRect floatPlayerAvailableRect() override;
|
||||
|
||||
bool cancelSearchByMouseBack();
|
||||
|
||||
QVariant inputMethodQuery(Qt::InputMethodQuery query) const override;
|
||||
|
||||
~Widget();
|
||||
|
||||
protected:
|
||||
void dragEnterEvent(QDragEnterEvent *e) override;
|
||||
void dragMoveEvent(QDragMoveEvent *e) override;
|
||||
void dragLeaveEvent(QDragLeaveEvent *e) override;
|
||||
void dropEvent(QDropEvent *e) override;
|
||||
void resizeEvent(QResizeEvent *e) override;
|
||||
void keyPressEvent(QKeyEvent *e) override;
|
||||
void inputMethodEvent(QInputMethodEvent *e) override;
|
||||
void paintEvent(QPaintEvent *e) override;
|
||||
|
||||
private:
|
||||
struct SearchProcessState {
|
||||
base::flat_map<QString, MTPmessages_Messages> cache;
|
||||
base::flat_map<mtpRequestId, QString> queries;
|
||||
|
||||
PeerData *lastPeer = nullptr;
|
||||
MsgId lastId = 0;
|
||||
int32 nextRate = 0;
|
||||
mtpRequestId requestId = 0;
|
||||
bool full = false;
|
||||
};
|
||||
|
||||
void chosenRow(const ChosenRow &row);
|
||||
void listScrollUpdated();
|
||||
void searchCursorMoved();
|
||||
void completeHashtag(QString tag);
|
||||
void requestPublicPosts(bool fromStart);
|
||||
void requestMessages(bool fromStart);
|
||||
[[nodiscard]] not_null<SearchProcessState*> currentSearchProcess();
|
||||
|
||||
[[nodiscard]] bool computeSearchWithPostsPreview() const;
|
||||
|
||||
[[nodiscard]] QString currentSearchQuery() const;
|
||||
[[nodiscard]] int currentSearchQueryCursorPosition() const;
|
||||
void clearSearchField();
|
||||
void searchRequested(SearchRequestDelay delay);
|
||||
bool search(bool inCache = false, SearchRequestDelay after = {});
|
||||
void searchTopics();
|
||||
void searchMore();
|
||||
|
||||
void slideFinished();
|
||||
void searchReceived(
|
||||
SearchRequestType type,
|
||||
const MTPmessages_Messages &result,
|
||||
not_null<SearchProcessState*> process,
|
||||
bool cacheResults = false);
|
||||
void peerSearchReceived(Api::PeerSearchResult result);
|
||||
void escape();
|
||||
void submit();
|
||||
void cancelSearchRequest();
|
||||
[[nodiscard]] PeerData *searchInPeer() const;
|
||||
[[nodiscard]] Data::ForumTopic *searchInTopic() const;
|
||||
[[nodiscard]] PeerData *searchFromPeer() const;
|
||||
[[nodiscard]] const std::vector<Data::ReactionId> &searchInTags() const;
|
||||
|
||||
void setupSupportMode();
|
||||
void setupTouchChatPreview();
|
||||
void setupFrozenAccountBar();
|
||||
void setupConnectingWidget();
|
||||
void setupMainMenuToggle();
|
||||
void setupMoreChatsBar();
|
||||
void setupDownloadBar();
|
||||
void setupShortcuts();
|
||||
void setupStories();
|
||||
void setupSwipeBack();
|
||||
void setupTopBarSuggestions(not_null<Ui::VerticalLayout*> dialogs);
|
||||
void storiesExplicitCollapse();
|
||||
void collectStoriesUserpicsViews(Data::StorySourcesList list);
|
||||
void storiesToggleExplicitExpand(bool expand);
|
||||
void trackScroll(not_null<Ui::RpWidget*> widget);
|
||||
[[nodiscard]] bool peerSearchRequired() const;
|
||||
[[nodiscard]] bool searchForTopicsRequired(const QString &query) const;
|
||||
|
||||
// Child list may be unable to set specific search state.
|
||||
bool applySearchState(SearchState state);
|
||||
|
||||
void showCalendar();
|
||||
void showSearchFrom();
|
||||
void showMainMenu();
|
||||
void clearSearchCache(bool clearPosts);
|
||||
void setSearchQuery(const QString &query, int cursorPosition = -1);
|
||||
void updateTopBarSuggestions();
|
||||
void updateFrozenAccountBar();
|
||||
void updateControlsVisibility(bool fast = false);
|
||||
void updateLockUnlockVisibility(
|
||||
anim::type animated = anim::type::instant);
|
||||
void updateLoadMoreChatsVisibility();
|
||||
void updateStoriesVisibility();
|
||||
void updateJumpToDateVisibility(bool fast = false);
|
||||
void updateSearchFromVisibility(bool fast = false);
|
||||
void updateControlsGeometry();
|
||||
void refreshTopBars();
|
||||
void showSearchInTopBar(anim::type animated);
|
||||
void checkUpdateStatus();
|
||||
void openBotMainApp(not_null<UserData*> bot);
|
||||
void changeOpenedSubsection(
|
||||
FnMut<void()> change,
|
||||
bool fromRight,
|
||||
anim::type animated);
|
||||
void changeOpenedFolder(Data::Folder *folder, anim::type animated);
|
||||
void changeOpenedForum(Data::Forum *forum, anim::type animated);
|
||||
void hideChildList();
|
||||
void destroyChildListCanvas();
|
||||
[[nodiscard]] QPixmap grabForFolderSlideAnimation();
|
||||
void startSlideAnimation(
|
||||
QPixmap oldContentCache,
|
||||
QPixmap newContentCache,
|
||||
Window::SlideDirection direction);
|
||||
|
||||
void openChildList(
|
||||
not_null<Data::Forum*> forum,
|
||||
const Window::SectionShow ¶ms);
|
||||
void closeChildList(anim::type animated);
|
||||
|
||||
void fullSearchRefreshOn(rpl::producer<> events);
|
||||
void updateCancelSearch();
|
||||
[[nodiscard]] QString validateSearchQuery();
|
||||
void applySearchUpdate();
|
||||
void refreshLoadMoreButton(bool mayBlock, bool isBlocked);
|
||||
void loadMoreBlockedByDate();
|
||||
|
||||
void searchFailed(
|
||||
SearchRequestType type,
|
||||
const MTP::Error &error,
|
||||
not_null<SearchProcessState*> process);
|
||||
void searchApplyEmpty(
|
||||
SearchRequestType type,
|
||||
not_null<SearchProcessState*> process);
|
||||
|
||||
void updateForceDisplayWide();
|
||||
void scrollToDefault(bool verytop = false);
|
||||
void scrollToDefaultChecked(bool verytop = false);
|
||||
void setupScrollUpButton();
|
||||
void updateScrollUpVisibility();
|
||||
void startScrollUpButtonAnimation(bool shown);
|
||||
void updateScrollUpPosition();
|
||||
void updateLockUnlockPosition();
|
||||
void updateSuggestions(anim::type animated);
|
||||
void processSearchFocusChange();
|
||||
void closeSuggestions();
|
||||
|
||||
[[nodiscard]] bool redirectToSearchPossible() const;
|
||||
[[nodiscard]] bool redirectKeyToSearch(QKeyEvent *e) const;
|
||||
[[nodiscard]] bool redirectImeToSearch() const;
|
||||
|
||||
struct CancelSearchOptions {
|
||||
bool forceFullCancel = false;
|
||||
bool jumpBackToSearchedChat = false;
|
||||
};
|
||||
bool cancelSearch(CancelSearchOptions options);
|
||||
|
||||
MTP::Sender _api;
|
||||
|
||||
bool _dragInScroll = false;
|
||||
bool _dragForward = false;
|
||||
base::Timer _chooseByDragTimer;
|
||||
|
||||
const Layout _layout = Layout::Main;
|
||||
const int _narrowWidth = 0;
|
||||
|
||||
std::unique_ptr<Ui::AbstractButton> _frozenAccountBar;
|
||||
|
||||
object_ptr<Ui::RpWidget> _searchControls;
|
||||
object_ptr<HistoryView::TopBarWidget> _subsectionTopBar = { nullptr };
|
||||
struct {
|
||||
object_ptr<Ui::IconButton> toggle;
|
||||
object_ptr<Ui::AbstractButton> under;
|
||||
} _mainMenu;
|
||||
object_ptr<Ui::IconButton> _searchForNarrowLayout;
|
||||
object_ptr<Ui::InputField> _search;
|
||||
object_ptr<Ui::FadeWrapScaled<Ui::IconButton>> _chooseFromUser;
|
||||
object_ptr<Ui::FadeWrapScaled<Ui::IconButton>> _jumpToDate;
|
||||
object_ptr<Ui::CrossButton> _cancelSearch;
|
||||
object_ptr<Ui::FadeWrapScaled<Ui::IconButton>> _lockUnlock;
|
||||
|
||||
std::unique_ptr<Ui::MoreChatsBar> _moreChatsBar;
|
||||
|
||||
std::unique_ptr<Ui::PlainShadow> _forumTopShadow;
|
||||
std::unique_ptr<Ui::GroupCallBar> _forumGroupCallBar;
|
||||
std::unique_ptr<Ui::RequestsBar> _forumRequestsBar;
|
||||
std::unique_ptr<HistoryView::ContactStatus> _forumReportBar;
|
||||
|
||||
base::unique_qptr<Ui::RpWidget> _chatFilters;
|
||||
|
||||
QPointer<Ui::SlideWrap<Ui::RpWidget>> _topBarSuggestion;
|
||||
rpl::event_stream<int> _topBarSuggestionHeightChanged;
|
||||
rpl::event_stream<bool> _searchStateForTopBarSuggestion;
|
||||
rpl::event_stream<bool> _openedFolderOrForumChanges;
|
||||
|
||||
object_ptr<Ui::ElasticScroll> _scroll;
|
||||
QPointer<InnerWidget> _inner;
|
||||
std::unique_ptr<Suggestions> _suggestions;
|
||||
std::vector<std::unique_ptr<Suggestions>> _hidingSuggestions;
|
||||
class BottomButton;
|
||||
object_ptr<BottomButton> _updateTelegram = { nullptr };
|
||||
object_ptr<BottomButton> _loadMoreChats = { nullptr };
|
||||
std::unique_ptr<Ui::DownloadBar> _downloadBar;
|
||||
std::unique_ptr<Window::ConnectionState> _connecting;
|
||||
|
||||
Ui::Animations::Simple _scrollToAnimation;
|
||||
int _scrollAnimationTo = 0;
|
||||
std::unique_ptr<Window::SlideAnimation> _showAnimation;
|
||||
rpl::variable<float64> _shownProgressValue;
|
||||
|
||||
Ui::Animations::Simple _scrollToTopShown;
|
||||
object_ptr<Ui::JumpDownButton> _scrollToTop;
|
||||
bool _scrollToTopIsShown = false;
|
||||
bool _forumSearchRequested = false;
|
||||
HashOrCashtag _searchHashOrCashtag = {};
|
||||
bool _searchWithPostsPreview = false;
|
||||
|
||||
Data::Folder *_openedFolder = nullptr;
|
||||
Data::Forum *_openedForum = nullptr;
|
||||
SearchState _searchState;
|
||||
History *_searchInMigrated = nullptr;
|
||||
rpl::lifetime _searchTagsLifetime;
|
||||
QString _lastSearchText;
|
||||
bool _searchSuggestionsLocked = false;
|
||||
bool _searchHasFocus = false;
|
||||
bool _processingSearch = false;
|
||||
|
||||
rpl::event_stream<rpl::producer<Stories::Content>> _storiesContents;
|
||||
base::flat_map<PeerId, Ui::PeerUserpicView> _storiesUserpicsViewsHidden;
|
||||
base::flat_map<PeerId, Ui::PeerUserpicView> _storiesUserpicsViewsShown;
|
||||
Fn<void()> _updateScrollGeometryCached;
|
||||
std::unique_ptr<Stories::List> _stories;
|
||||
Ui::Animations::Simple _storiesExplicitExpandAnimation;
|
||||
rpl::variable<int> _storiesExplicitExpandValue = 0;
|
||||
int _storiesExplicitExpandScrollTop = 0;
|
||||
int _aboveScrollAdded = 0;
|
||||
bool _storiesExplicitExpand = false;
|
||||
bool _postponeProcessSearchFocusChange = false;
|
||||
|
||||
base::Timer _searchTimer;
|
||||
|
||||
QString _topicSearchQuery;
|
||||
TimeId _topicSearchOffsetDate = 0;
|
||||
MsgId _topicSearchOffsetId = 0;
|
||||
MsgId _topicSearchOffsetTopicId = 0;
|
||||
bool _topicSearchFull = false;
|
||||
mtpRequestId _topicSearchRequest = 0;
|
||||
|
||||
QString _searchQuery;
|
||||
PeerData *_searchQueryFrom = nullptr;
|
||||
std::vector<Data::ReactionId> _searchQueryTags;
|
||||
ChatSearchTab _searchQueryTab = {};
|
||||
ChatTypeFilter _searchQueryFilter = {};
|
||||
|
||||
Ui::Controls::SwipeBackResult _swipeBackData;
|
||||
bool _swipeBackMirrored = false;
|
||||
bool _swipeBackIconMirrored = false;
|
||||
|
||||
SearchProcessState _searchProcess;
|
||||
SearchProcessState _migratedProcess;
|
||||
SearchProcessState _postsProcess;
|
||||
int _historiesRequest = 0; // Not real mtpRequestId.
|
||||
|
||||
Api::PeerSearch _peerSearch;
|
||||
Api::SingleMessageSearch _singleMessageSearch;
|
||||
|
||||
QPixmap _widthAnimationCache;
|
||||
|
||||
int _topDelta = 0;
|
||||
|
||||
std::unique_ptr<Widget> _childList;
|
||||
std::unique_ptr<Ui::RpWidget> _childListShadow;
|
||||
rpl::variable<float64> _childListShown;
|
||||
rpl::variable<PeerId> _childListPeerId;
|
||||
std::unique_ptr<Ui::RpWidget> _hideChildListCanvas;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Dialogs
|
||||
83
Telegram/SourceFiles/dialogs/ui/chat_search_empty.cpp
Normal file
83
Telegram/SourceFiles/dialogs/ui/chat_search_empty.cpp
Normal file
@@ -0,0 +1,83 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#include "dialogs/ui/chat_search_empty.h"
|
||||
|
||||
#include "base/object_ptr.h"
|
||||
#include "lottie/lottie_icon.h"
|
||||
#include "settings/settings_common.h"
|
||||
#include "ui/widgets/labels.h"
|
||||
#include "styles/style_dialogs.h"
|
||||
|
||||
namespace Dialogs {
|
||||
|
||||
SearchEmpty::SearchEmpty(
|
||||
QWidget *parent,
|
||||
Icon icon,
|
||||
rpl::producer<TextWithEntities> text)
|
||||
: RpWidget(parent) {
|
||||
setup(icon, std::move(text));
|
||||
}
|
||||
|
||||
void SearchEmpty::setMinimalHeight(int minimalHeight) {
|
||||
const auto minimal = st::recentPeersEmptyHeightMin;
|
||||
resize(width(), std::max(minimalHeight, minimal));
|
||||
}
|
||||
|
||||
void SearchEmpty::setup(Icon icon, rpl::producer<TextWithEntities> text) {
|
||||
const auto label = Ui::CreateChild<Ui::FlatLabel>(
|
||||
this,
|
||||
std::move(text),
|
||||
st::defaultPeerListAbout);
|
||||
const auto size = st::recentPeersEmptySize;
|
||||
const auto animation = [&] {
|
||||
switch (icon) {
|
||||
case Icon::Search: return u"search"_q;
|
||||
case Icon::NoResults: return u"noresults"_q;
|
||||
}
|
||||
Unexpected("Icon in SearchEmpty::setup.");
|
||||
}();
|
||||
const auto [widget, animate] = Settings::CreateLottieIcon(
|
||||
this,
|
||||
{
|
||||
.name = animation,
|
||||
.sizeOverride = { size, size },
|
||||
},
|
||||
st::recentPeersEmptyMargin);
|
||||
const auto animated = widget.data();
|
||||
|
||||
sizeValue() | rpl::on_next([=](QSize size) {
|
||||
const auto padding = st::recentPeersEmptyMargin;
|
||||
const auto paddings = padding.left() + padding.right();
|
||||
label->resizeToWidth(size.width() - paddings);
|
||||
const auto x = (size.width() - animated->width()) / 2;
|
||||
const auto y = (size.height() - animated->height()) / 3;
|
||||
const auto top = y + animated->height() + st::recentPeersEmptySkip;
|
||||
const auto sub = std::max(top + label->height() - size.height(), 0);
|
||||
animated->move(x, y - sub);
|
||||
label->move((size.width() - label->width()) / 2, top - sub);
|
||||
}, lifetime());
|
||||
|
||||
label->setClickHandlerFilter([=](
|
||||
const ClickHandlerPtr &handler,
|
||||
Qt::MouseButton) {
|
||||
_handlerActivated.fire_copy(handler);
|
||||
return false;
|
||||
});
|
||||
|
||||
_animate = [animate] {
|
||||
animate(anim::repeat::once);
|
||||
};
|
||||
}
|
||||
|
||||
void SearchEmpty::animate() {
|
||||
if (const auto onstack = _animate) {
|
||||
onstack();
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace Dialogs
|
||||
44
Telegram/SourceFiles/dialogs/ui/chat_search_empty.h
Normal file
44
Telegram/SourceFiles/dialogs/ui/chat_search_empty.h
Normal file
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "ui/rp_widget.h"
|
||||
|
||||
namespace Dialogs {
|
||||
|
||||
enum class SearchEmptyIcon {
|
||||
Search,
|
||||
NoResults,
|
||||
};
|
||||
|
||||
class SearchEmpty final : public Ui::RpWidget {
|
||||
public:
|
||||
using Icon = SearchEmptyIcon;
|
||||
|
||||
SearchEmpty(
|
||||
QWidget *parent,
|
||||
Icon icon,
|
||||
rpl::producer<TextWithEntities> text);
|
||||
|
||||
void setMinimalHeight(int minimalHeight);
|
||||
|
||||
void animate();
|
||||
|
||||
[[nodiscard]] rpl::producer<ClickHandlerPtr> handlerActivated() const {
|
||||
return _handlerActivated.events();
|
||||
}
|
||||
|
||||
private:
|
||||
void setup(Icon icon, rpl::producer<TextWithEntities> text);
|
||||
|
||||
Fn<void()> _animate;
|
||||
rpl::event_stream<ClickHandlerPtr> _handlerActivated;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Dialogs
|
||||
474
Telegram/SourceFiles/dialogs/ui/chat_search_in.cpp
Normal file
474
Telegram/SourceFiles/dialogs/ui/chat_search_in.cpp
Normal file
@@ -0,0 +1,474 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#include "dialogs/ui/chat_search_in.h"
|
||||
|
||||
#include "lang/lang_keys.h"
|
||||
#include "ui/effects/ripple_animation.h"
|
||||
#include "ui/text/text_utilities.h"
|
||||
#include "ui/widgets/buttons.h"
|
||||
#include "ui/widgets/popup_menu.h"
|
||||
#include "ui/widgets/shadow.h"
|
||||
#include "ui/widgets/menu/menu_item_base.h"
|
||||
#include "ui/dynamic_image.h"
|
||||
#include "ui/painter.h"
|
||||
#include "styles/style_dialogs.h"
|
||||
#include "styles/style_window.h"
|
||||
|
||||
namespace Dialogs {
|
||||
namespace {
|
||||
|
||||
class Action final : public Ui::Menu::ItemBase {
|
||||
public:
|
||||
Action(
|
||||
not_null<Ui::PopupMenu*> parentMenu,
|
||||
std::shared_ptr<Ui::DynamicImage> icon,
|
||||
const QString &label,
|
||||
bool chosen);
|
||||
~Action();
|
||||
|
||||
bool isEnabled() const override;
|
||||
not_null<QAction*> action() const override;
|
||||
|
||||
void handleKeyPress(not_null<QKeyEvent*> e) override;
|
||||
|
||||
protected:
|
||||
QPoint prepareRippleStartPosition() const override;
|
||||
QImage prepareRippleMask() const override;
|
||||
|
||||
int contentHeight() const override;
|
||||
|
||||
private:
|
||||
void paint(Painter &p);
|
||||
|
||||
void resolveMinWidth();
|
||||
void refreshDimensions();
|
||||
|
||||
const not_null<Ui::PopupMenu*> _parentMenu;
|
||||
const not_null<QAction*> _dummyAction;
|
||||
const style::Menu &_st;
|
||||
const int _height = 0;
|
||||
|
||||
std::shared_ptr<Ui::DynamicImage> _icon;
|
||||
Ui::Text::String _text;
|
||||
bool _checked = false;
|
||||
|
||||
};
|
||||
|
||||
[[nodiscard]] QString TabLabel(
|
||||
ChatSearchTab tab,
|
||||
ChatSearchPeerTabType type = {}) {
|
||||
switch (tab) {
|
||||
case ChatSearchTab::MyMessages:
|
||||
return tr::lng_search_tab_my_messages(tr::now);
|
||||
case ChatSearchTab::ThisTopic:
|
||||
return tr::lng_search_tab_this_topic(tr::now);
|
||||
case ChatSearchTab::ThisPeer:
|
||||
switch (type) {
|
||||
case ChatSearchPeerTabType::Chat:
|
||||
return tr::lng_search_tab_this_chat(tr::now);
|
||||
case ChatSearchPeerTabType::Channel:
|
||||
return tr::lng_search_tab_this_channel(tr::now);
|
||||
case ChatSearchPeerTabType::Group:
|
||||
return tr::lng_search_tab_this_group(tr::now);
|
||||
}
|
||||
Unexpected("Type in Dialogs::TabLabel.");
|
||||
case ChatSearchTab::PublicPosts:
|
||||
return tr::lng_search_tab_public_posts(tr::now);
|
||||
}
|
||||
Unexpected("Tab in Dialogs::TabLabel.");
|
||||
}
|
||||
|
||||
Action::Action(
|
||||
not_null<Ui::PopupMenu*> parentMenu,
|
||||
std::shared_ptr<Ui::DynamicImage> icon,
|
||||
const QString &label,
|
||||
bool chosen)
|
||||
: ItemBase(parentMenu->menu(), parentMenu->menu()->st())
|
||||
, _parentMenu(parentMenu)
|
||||
, _dummyAction(CreateChild<QAction>(parentMenu->menu().get()))
|
||||
, _st(parentMenu->menu()->st())
|
||||
, _height(st::dialogsSearchInHeight)
|
||||
, _icon(std::move(icon))
|
||||
, _checked(chosen) {
|
||||
const auto parent = parentMenu->menu();
|
||||
|
||||
_text.setText(st::semiboldTextStyle, label);
|
||||
_icon->subscribeToUpdates([=] { update(); });
|
||||
|
||||
initResizeHook(parent->sizeValue());
|
||||
resolveMinWidth();
|
||||
|
||||
paintRequest(
|
||||
) | rpl::on_next([=] {
|
||||
Painter p(this);
|
||||
paint(p);
|
||||
}, lifetime());
|
||||
|
||||
enableMouseSelecting();
|
||||
}
|
||||
|
||||
Action::~Action() {
|
||||
_icon->subscribeToUpdates(nullptr);
|
||||
}
|
||||
|
||||
void Action::resolveMinWidth() {
|
||||
const auto maxWidth = st::dialogsSearchInPhotoPadding
|
||||
+ st::dialogsSearchInPhotoSize
|
||||
+ st::dialogsSearchInSkip
|
||||
+ _text.maxWidth()
|
||||
+ st::dialogsSearchInCheckSkip
|
||||
+ st::dialogsSearchInCheck.width()
|
||||
+ st::dialogsSearchInCheckSkip;
|
||||
setMinWidth(maxWidth);
|
||||
}
|
||||
|
||||
void Action::paint(Painter &p) {
|
||||
const auto enabled = isEnabled();
|
||||
const auto selected = isSelected();
|
||||
if (selected && _st.itemBgOver->c.alpha() < 255) {
|
||||
p.fillRect(0, 0, width(), _height, _st.itemBg);
|
||||
}
|
||||
const auto &bg = selected ? _st.itemBgOver : _st.itemBg;
|
||||
p.fillRect(0, 0, width(), _height, bg);
|
||||
if (enabled) {
|
||||
paintRipple(p, 0, 0);
|
||||
}
|
||||
|
||||
auto x = st::dialogsSearchInPhotoPadding;
|
||||
const auto photos = st::dialogsSearchInPhotoSize;
|
||||
const auto photoy = (height() - photos) / 2;
|
||||
p.drawImage(QRect{ x, photoy, photos, photos }, _icon->image(photos));
|
||||
x += photos + st::dialogsSearchInSkip;
|
||||
const auto available = width()
|
||||
- x
|
||||
- st::dialogsSearchInCheckSkip
|
||||
- st::dialogsSearchInCheck.width()
|
||||
- st::dialogsSearchInCheckSkip;
|
||||
|
||||
p.setPen(!enabled
|
||||
? _st.itemFgDisabled
|
||||
: selected
|
||||
? _st.itemFgOver
|
||||
: _st.itemFg);
|
||||
_text.drawLeftElided(
|
||||
p,
|
||||
x,
|
||||
st::dialogsSearchInNameTop,
|
||||
available,
|
||||
width());
|
||||
x += available;
|
||||
if (_checked) {
|
||||
x += st::dialogsSearchInCheckSkip;
|
||||
const auto &icon = st::dialogsSearchInCheck;
|
||||
const auto icony = (height() - icon.height()) / 2;
|
||||
icon.paint(p, x, icony, width());
|
||||
}
|
||||
}
|
||||
|
||||
bool Action::isEnabled() const {
|
||||
return true;
|
||||
}
|
||||
|
||||
not_null<QAction*> Action::action() const {
|
||||
return _dummyAction;
|
||||
}
|
||||
|
||||
QPoint Action::prepareRippleStartPosition() const {
|
||||
return mapFromGlobal(QCursor::pos());
|
||||
}
|
||||
|
||||
QImage Action::prepareRippleMask() const {
|
||||
return Ui::RippleAnimation::RectMask(size());
|
||||
}
|
||||
|
||||
int Action::contentHeight() const {
|
||||
return _height;
|
||||
}
|
||||
|
||||
void Action::handleKeyPress(not_null<QKeyEvent*> e) {
|
||||
if (!isSelected()) {
|
||||
return;
|
||||
}
|
||||
const auto key = e->key();
|
||||
if (key == Qt::Key_Enter || key == Qt::Key_Return) {
|
||||
setClicked(Ui::Menu::TriggeredSource::Keyboard);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
FixedHashtagSearchQuery FixHashtagSearchQuery(
|
||||
const QString &query,
|
||||
int cursorPosition,
|
||||
HashOrCashtag tag) {
|
||||
const auto trimmed = query.trimmed();
|
||||
const auto hash = int(trimmed.isEmpty()
|
||||
? query.size()
|
||||
: query.indexOf(trimmed));
|
||||
const auto start = std::min(cursorPosition, hash);
|
||||
const auto first = QChar(tag == HashOrCashtag::Cashtag ? '$' : '#');
|
||||
auto result = query.mid(0, start);
|
||||
for (const auto &ch : query.mid(start)) {
|
||||
if (ch.isSpace()) {
|
||||
if (cursorPosition > result.size()) {
|
||||
--cursorPosition;
|
||||
}
|
||||
continue;
|
||||
} else if (result.size() == start) {
|
||||
result += first;
|
||||
if (ch != first) {
|
||||
++cursorPosition;
|
||||
}
|
||||
}
|
||||
if (ch != first) {
|
||||
result += ch;
|
||||
}
|
||||
}
|
||||
if (result.size() == start) {
|
||||
result += first;
|
||||
++cursorPosition;
|
||||
}
|
||||
return { result, cursorPosition };
|
||||
}
|
||||
|
||||
HashOrCashtag IsHashOrCashtagSearchQuery(const QString &query) {
|
||||
const auto trimmed = query.trimmed();
|
||||
const auto first = trimmed.isEmpty() ? QChar() : trimmed[0];
|
||||
if (first == '#') {
|
||||
for (const auto &ch : trimmed) {
|
||||
if (ch.isSpace()) {
|
||||
return HashOrCashtag::None;
|
||||
}
|
||||
}
|
||||
return HashOrCashtag::Hashtag;
|
||||
} else if (first == '$') {
|
||||
for (auto it = trimmed.begin() + 1; it != trimmed.end(); ++it) {
|
||||
if ((*it) < 'A' || (*it) > 'Z') {
|
||||
return HashOrCashtag::None;
|
||||
}
|
||||
}
|
||||
return HashOrCashtag::Cashtag;
|
||||
}
|
||||
return HashOrCashtag::None;
|
||||
}
|
||||
|
||||
void ChatSearchIn::Section::update() {
|
||||
outer->update();
|
||||
}
|
||||
|
||||
ChatSearchIn::ChatSearchIn(QWidget *parent)
|
||||
: RpWidget(parent) {
|
||||
_in.clicks.events() | rpl::on_next([=] {
|
||||
showMenu();
|
||||
}, lifetime());
|
||||
}
|
||||
|
||||
ChatSearchIn::~ChatSearchIn() = default;
|
||||
|
||||
void ChatSearchIn::apply(
|
||||
std::vector<PossibleTab> tabs,
|
||||
ChatSearchTab active,
|
||||
ChatSearchPeerTabType peerTabType,
|
||||
std::shared_ptr<Ui::DynamicImage> fromUserpic,
|
||||
QString fromName) {
|
||||
_tabs = std::move(tabs);
|
||||
_peerTabType = peerTabType;
|
||||
_active = active;
|
||||
const auto i = ranges::find(_tabs, active, &PossibleTab::tab);
|
||||
Assert(i != end(_tabs));
|
||||
Assert(i->icon != nullptr);
|
||||
updateSection(
|
||||
&_in,
|
||||
i->icon->clone(),
|
||||
tr::semibold(TabLabel(active, peerTabType)));
|
||||
|
||||
auto text = tr::lng_dlg_search_from(
|
||||
tr::now,
|
||||
lt_user,
|
||||
tr::semibold(fromName),
|
||||
tr::marked);
|
||||
updateSection(&_from, std::move(fromUserpic), std::move(text));
|
||||
|
||||
resizeToWidth(width());
|
||||
}
|
||||
|
||||
rpl::producer<> ChatSearchIn::cancelInRequests() const {
|
||||
return _in.cancelRequests.events();
|
||||
}
|
||||
|
||||
rpl::producer<> ChatSearchIn::cancelFromRequests() const {
|
||||
return _from.cancelRequests.events();
|
||||
}
|
||||
|
||||
rpl::producer<> ChatSearchIn::changeFromRequests() const {
|
||||
return _from.clicks.events();
|
||||
}
|
||||
|
||||
rpl::producer<ChatSearchTab> ChatSearchIn::tabChanges() const {
|
||||
return _active.changes();
|
||||
}
|
||||
|
||||
void ChatSearchIn::showMenu() {
|
||||
_menu = base::make_unique_q<Ui::PopupMenu>(
|
||||
this,
|
||||
st::dialogsSearchInMenu);
|
||||
const auto active = _active.current();
|
||||
auto activeIndex = 0;
|
||||
for (const auto &tab : _tabs) {
|
||||
if (!tab.icon) {
|
||||
continue;
|
||||
}
|
||||
const auto value = tab.tab;
|
||||
if (value == active) {
|
||||
activeIndex = _menu->actions().size();
|
||||
}
|
||||
auto action = base::make_unique_q<Action>(
|
||||
_menu.get(),
|
||||
tab.icon,
|
||||
TabLabel(value, _peerTabType),
|
||||
(value == active));
|
||||
action->setClickedCallback([=] {
|
||||
_active = value;
|
||||
});
|
||||
_menu->addAction(std::move(action));
|
||||
}
|
||||
const auto count = int(_menu->actions().size());
|
||||
const auto bottomLeft = (activeIndex * 2 >= count);
|
||||
const auto single = st::dialogsSearchInHeight;
|
||||
const auto in = mapToGlobal(_in.outer->pos()
|
||||
+ QPoint(0, bottomLeft ? count * single : 0));
|
||||
_menu->setForcedOrigin(bottomLeft
|
||||
? Ui::PanelAnimation::Origin::BottomLeft
|
||||
: Ui::PanelAnimation::Origin::TopLeft);
|
||||
if (_menu->prepareGeometryFor(in)) {
|
||||
_menu->move(_menu->pos() - QPoint(_menu->inner().x(), activeIndex * single));
|
||||
_menu->popupPrepared();
|
||||
}
|
||||
}
|
||||
|
||||
void ChatSearchIn::paintEvent(QPaintEvent *e) {
|
||||
auto p = Painter(this);
|
||||
const auto top = QRect(0, 0, width(), st::searchedBarHeight);
|
||||
p.fillRect(top, st::searchedBarBg);
|
||||
p.fillRect(rect().translated(0, st::searchedBarHeight), st::dialogsBg);
|
||||
|
||||
p.setFont(st::searchedBarFont);
|
||||
p.setPen(st::searchedBarFg);
|
||||
p.drawTextLeft(
|
||||
st::searchedBarPosition.x(),
|
||||
st::searchedBarPosition.y(),
|
||||
width(),
|
||||
tr::lng_dlg_search_in(tr::now));
|
||||
}
|
||||
|
||||
int ChatSearchIn::resizeGetHeight(int newWidth) {
|
||||
auto result = st::searchedBarHeight;
|
||||
if (const auto raw = _in.outer.get()) {
|
||||
raw->resizeToWidth(newWidth);
|
||||
raw->move(0, result);
|
||||
result += raw->height();
|
||||
_in.shadow->setGeometry(0, result, newWidth, st::lineWidth);
|
||||
result += st::lineWidth;
|
||||
}
|
||||
if (const auto raw = _from.outer.get()) {
|
||||
raw->resizeToWidth(newWidth);
|
||||
raw->move(0, result);
|
||||
result += raw->height();
|
||||
_from.shadow->setGeometry(0, result, newWidth, st::lineWidth);
|
||||
result += st::lineWidth;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
void ChatSearchIn::updateSection(
|
||||
not_null<Section*> section,
|
||||
std::shared_ptr<Ui::DynamicImage> image,
|
||||
TextWithEntities text) {
|
||||
if (section->subscribed) {
|
||||
section->image->subscribeToUpdates(nullptr);
|
||||
section->subscribed = false;
|
||||
}
|
||||
if (!image) {
|
||||
if (section->outer) {
|
||||
section->cancel = nullptr;
|
||||
section->shadow = nullptr;
|
||||
section->outer = nullptr;
|
||||
section->subscribed = false;
|
||||
}
|
||||
return;
|
||||
} else if (!section->outer) {
|
||||
auto button = std::make_unique<Ui::AbstractButton>(this);
|
||||
const auto raw = button.get();
|
||||
section->outer = std::move(button);
|
||||
|
||||
raw->resize(
|
||||
st::columnMinimalWidthLeft,
|
||||
st::dialogsSearchInHeight);
|
||||
|
||||
raw->paintRequest() | rpl::on_next([=] {
|
||||
auto p = QPainter(raw);
|
||||
if (!section->subscribed) {
|
||||
section->subscribed = true;
|
||||
section->image->subscribeToUpdates([=] {
|
||||
raw->update();
|
||||
});
|
||||
}
|
||||
const auto outer = raw->width();
|
||||
const auto size = st::dialogsSearchInPhotoSize;
|
||||
const auto left = st::dialogsSearchInPhotoPadding;
|
||||
const auto top = (st::dialogsSearchInHeight - size) / 2;
|
||||
p.drawImage(
|
||||
QRect{ left, top, size, size },
|
||||
section->image->image(size));
|
||||
|
||||
const auto x = left + size + st::dialogsSearchInSkip;
|
||||
const auto available = outer
|
||||
- st::dialogsSearchInSkip
|
||||
- section->cancel->width()
|
||||
- 2 * st::dialogsSearchInDownSkip
|
||||
- st::dialogsSearchInDown.width()
|
||||
- x;
|
||||
const auto use = std::min(section->text.maxWidth(), available);
|
||||
const auto iconx = x + use + st::dialogsSearchInDownSkip;
|
||||
const auto icony = st::dialogsSearchInDownTop;
|
||||
st::dialogsSearchInDown.paint(p, iconx, icony, outer);
|
||||
p.setPen(st::windowBoldFg);
|
||||
section->text.draw(p, {
|
||||
.position = QPoint(x, st::dialogsSearchInNameTop),
|
||||
.outerWidth = outer,
|
||||
.availableWidth = available,
|
||||
.elisionLines = 1,
|
||||
});
|
||||
}, raw->lifetime());
|
||||
|
||||
section->shadow = std::make_unique<Ui::PlainShadow>(this);
|
||||
section->shadow->show();
|
||||
|
||||
const auto st = &st::dialogsCancelSearchInPeer;
|
||||
section->cancel = std::make_unique<Ui::IconButton>(raw, *st);
|
||||
section->cancel->show();
|
||||
raw->sizeValue() | rpl::on_next([=](QSize size) {
|
||||
const auto left = size.width() - section->cancel->width();
|
||||
const auto top = (size.height() - st->height) / 2;
|
||||
section->cancel->moveToLeft(left, top);
|
||||
}, section->cancel->lifetime());
|
||||
section->cancel->clicks() | rpl::to_empty | rpl::start_to_stream(
|
||||
section->cancelRequests,
|
||||
section->cancel->lifetime());
|
||||
|
||||
raw->clicks() | rpl::to_empty | rpl::start_to_stream(
|
||||
section->clicks,
|
||||
raw->lifetime());
|
||||
|
||||
raw->show();
|
||||
}
|
||||
section->image = std::move(image);
|
||||
section->text.setMarkedText(st::dialogsSearchFromStyle, std::move(text));
|
||||
}
|
||||
|
||||
} // namespace Dialogs
|
||||
107
Telegram/SourceFiles/dialogs/ui/chat_search_in.h
Normal file
107
Telegram/SourceFiles/dialogs/ui/chat_search_in.h
Normal file
@@ -0,0 +1,107 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "base/unique_qptr.h"
|
||||
#include "ui/rp_widget.h"
|
||||
|
||||
namespace Ui {
|
||||
class PlainShadow;
|
||||
class DynamicImage;
|
||||
class IconButton;
|
||||
class PopupMenu;
|
||||
} // namespace Ui
|
||||
|
||||
namespace Dialogs {
|
||||
|
||||
enum class ChatSearchTab : uchar {
|
||||
MyMessages,
|
||||
ThisTopic,
|
||||
ThisPeer,
|
||||
PublicPosts,
|
||||
};
|
||||
|
||||
enum class ChatSearchPeerTabType : uchar {
|
||||
Chat,
|
||||
Channel,
|
||||
Group,
|
||||
};
|
||||
|
||||
class ChatSearchIn final : public Ui::RpWidget {
|
||||
public:
|
||||
explicit ChatSearchIn(QWidget *parent);
|
||||
~ChatSearchIn();
|
||||
|
||||
struct PossibleTab {
|
||||
ChatSearchTab tab = {};
|
||||
std::shared_ptr<Ui::DynamicImage> icon;
|
||||
};
|
||||
void apply(
|
||||
std::vector<PossibleTab> tabs,
|
||||
ChatSearchTab active,
|
||||
ChatSearchPeerTabType peerTabType,
|
||||
std::shared_ptr<Ui::DynamicImage> fromUserpic,
|
||||
QString fromName);
|
||||
|
||||
[[nodiscard]] rpl::producer<> cancelInRequests() const;
|
||||
[[nodiscard]] rpl::producer<> cancelFromRequests() const;
|
||||
[[nodiscard]] rpl::producer<> changeFromRequests() const;
|
||||
[[nodiscard]] rpl::producer<ChatSearchTab> tabChanges() const;
|
||||
|
||||
private:
|
||||
struct Section {
|
||||
std::unique_ptr<Ui::RpWidget> outer;
|
||||
std::unique_ptr<Ui::IconButton> cancel;
|
||||
std::unique_ptr<Ui::PlainShadow> shadow;
|
||||
std::shared_ptr<Ui::DynamicImage> image;
|
||||
Ui::Text::String text;
|
||||
rpl::event_stream<> clicks;
|
||||
rpl::event_stream<> cancelRequests;
|
||||
bool subscribed = false;
|
||||
|
||||
void update();
|
||||
};
|
||||
|
||||
int resizeGetHeight(int newWidth) override;
|
||||
void paintEvent(QPaintEvent *e) override;
|
||||
void showMenu();
|
||||
|
||||
void updateSection(
|
||||
not_null<Section*> section,
|
||||
std::shared_ptr<Ui::DynamicImage> image,
|
||||
TextWithEntities text);
|
||||
|
||||
Section _in;
|
||||
Section _from;
|
||||
rpl::variable<ChatSearchTab> _active;
|
||||
|
||||
base::unique_qptr<Ui::PopupMenu> _menu;
|
||||
|
||||
std::vector<PossibleTab> _tabs;
|
||||
ChatSearchPeerTabType _peerTabType = ChatSearchPeerTabType::Chat;
|
||||
|
||||
};
|
||||
|
||||
enum class HashOrCashtag : uchar {
|
||||
None,
|
||||
Hashtag,
|
||||
Cashtag,
|
||||
};
|
||||
|
||||
struct FixedHashtagSearchQuery {
|
||||
QString text;
|
||||
int cursorPosition = 0;
|
||||
};
|
||||
[[nodiscard]] FixedHashtagSearchQuery FixHashtagSearchQuery(
|
||||
const QString &query,
|
||||
int cursorPosition,
|
||||
HashOrCashtag tag);
|
||||
|
||||
[[nodiscard]] HashOrCashtag IsHashOrCashtagSearchQuery(const QString &query);
|
||||
|
||||
} // namespace Dialogs
|
||||
1268
Telegram/SourceFiles/dialogs/ui/dialogs_layout.cpp
Normal file
1268
Telegram/SourceFiles/dialogs/ui/dialogs_layout.cpp
Normal file
File diff suppressed because it is too large
Load Diff
116
Telegram/SourceFiles/dialogs/ui/dialogs_layout.h
Normal file
116
Telegram/SourceFiles/dialogs/ui/dialogs_layout.h
Normal file
@@ -0,0 +1,116 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "dialogs/ui/dialogs_quick_action_context.h"
|
||||
#include "ui/cached_round_corners.h"
|
||||
|
||||
namespace style {
|
||||
struct DialogRow;
|
||||
struct VerifiedBadge;
|
||||
} // namespace style
|
||||
|
||||
namespace st {
|
||||
extern const style::DialogRow &defaultDialogRow;
|
||||
} // namespace st
|
||||
|
||||
namespace Data {
|
||||
class Forum;
|
||||
class Folder;
|
||||
class Thread;
|
||||
} // namespace Data
|
||||
|
||||
namespace Dialogs {
|
||||
class Row;
|
||||
class FakeRow;
|
||||
class BasicRow;
|
||||
struct RightButton;
|
||||
} // namespace Dialogs
|
||||
|
||||
namespace Dialogs::Ui {
|
||||
|
||||
using namespace ::Ui;
|
||||
|
||||
class VideoUserpic;
|
||||
|
||||
struct TopicJumpCorners {
|
||||
CornersPixmaps normal;
|
||||
CornersPixmaps inverted;
|
||||
QPixmap small;
|
||||
int invertedRadius = 0;
|
||||
int smallKey = 0; // = `-radius` if top right else `radius`.
|
||||
};
|
||||
|
||||
struct TopicJumpCache {
|
||||
TopicJumpCorners corners;
|
||||
TopicJumpCorners over;
|
||||
TopicJumpCorners selected;
|
||||
TopicJumpCorners rippleMask;
|
||||
};
|
||||
|
||||
struct PaintContext {
|
||||
RightButton *rightButton = nullptr;
|
||||
std::vector<QImage*> *chatsFilterTags = nullptr;
|
||||
QuickActionContext *quickActionContext = nullptr;
|
||||
not_null<const style::DialogRow*> st;
|
||||
TopicJumpCache *topicJumpCache = nullptr;
|
||||
Data::Folder *folder = nullptr;
|
||||
Data::Forum *forum = nullptr;
|
||||
required<QBrush> currentBg;
|
||||
FilterId filter = 0;
|
||||
float64 topicsExpanded = 0.;
|
||||
crl::time now = 0;
|
||||
QStringView searchLowerText;
|
||||
int width = 0;
|
||||
bool active = false;
|
||||
bool selected = false;
|
||||
bool topicJumpSelected = false;
|
||||
bool paused = false;
|
||||
bool search = false;
|
||||
bool narrow = false;
|
||||
bool displayUnreadInfo = false;
|
||||
};
|
||||
|
||||
[[nodiscard]] const style::icon *ChatTypeIcon(
|
||||
not_null<PeerData*> peer,
|
||||
const PaintContext &context);
|
||||
[[nodiscard]] const style::icon *ChatTypeIcon(not_null<PeerData*> peer);
|
||||
|
||||
[[nodiscard]] const style::VerifiedBadge &VerifiedStyle(
|
||||
const PaintContext &context);
|
||||
|
||||
class RowPainter {
|
||||
public:
|
||||
static void Paint(
|
||||
Painter &p,
|
||||
not_null<const Row*> row,
|
||||
VideoUserpic *videoUserpic,
|
||||
const PaintContext &context);
|
||||
static void Paint(
|
||||
Painter &p,
|
||||
not_null<const FakeRow*> row,
|
||||
const PaintContext &context);
|
||||
static QRect SendActionAnimationRect(
|
||||
not_null<const Data::Thread*> thread,
|
||||
FilterId filterId,
|
||||
QRect rect,
|
||||
int fullWidth,
|
||||
bool textUpdated);
|
||||
};
|
||||
|
||||
void PaintCollapsedRow(
|
||||
Painter &p,
|
||||
const BasicRow &row,
|
||||
Data::Folder *folder,
|
||||
const QString &text,
|
||||
int unread,
|
||||
const PaintContext &context);
|
||||
|
||||
int PaintRightButton(QPainter &p, const PaintContext &context);
|
||||
|
||||
} // namespace Dialogs::Ui
|
||||
612
Telegram/SourceFiles/dialogs/ui/dialogs_message_view.cpp
Normal file
612
Telegram/SourceFiles/dialogs/ui/dialogs_message_view.cpp
Normal file
@@ -0,0 +1,612 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#include "dialogs/ui/dialogs_message_view.h"
|
||||
|
||||
#include "history/history.h"
|
||||
#include "history/history_item.h"
|
||||
#include "history/view/history_view_element.h"
|
||||
#include "history/view/history_view_item_preview.h"
|
||||
#include "main/main_session.h"
|
||||
#include "dialogs/dialogs_three_state_icon.h"
|
||||
#include "dialogs/ui/dialogs_layout.h"
|
||||
#include "dialogs/ui/dialogs_topics_view.h"
|
||||
#include "ui/effects/spoiler_mess.h"
|
||||
#include "ui/text/custom_emoji_helper.h"
|
||||
#include "ui/text/text_options.h"
|
||||
#include "ui/text/text_utilities.h"
|
||||
#include "ui/painter.h"
|
||||
#include "ui/rect.h"
|
||||
#include "ui/power_saving.h"
|
||||
#include "core/ui_integration.h"
|
||||
#include "lang/lang_keys.h"
|
||||
#include "lang/lang_text_entity.h"
|
||||
#include "styles/style_dialogs.h"
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr auto kEmojiLoopCount = 2;
|
||||
|
||||
template <ushort kTag>
|
||||
struct TextWithTagOffset {
|
||||
TextWithTagOffset(TextWithEntities text) : text(std::move(text)) {
|
||||
}
|
||||
TextWithTagOffset(QString text) : text({ std::move(text) }) {
|
||||
}
|
||||
static TextWithTagOffset FromString(const QString &text) {
|
||||
return { { text } };
|
||||
}
|
||||
|
||||
TextWithEntities text;
|
||||
int offset = -1;
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
namespace Lang {
|
||||
|
||||
template <ushort kTag>
|
||||
struct ReplaceTag<TextWithTagOffset<kTag>> {
|
||||
static TextWithTagOffset<kTag> Call(
|
||||
TextWithTagOffset<kTag> &&original,
|
||||
ushort tag,
|
||||
const TextWithTagOffset<kTag> &replacement);
|
||||
};
|
||||
|
||||
template <ushort kTag>
|
||||
TextWithTagOffset<kTag> ReplaceTag<TextWithTagOffset<kTag>>::Call(
|
||||
TextWithTagOffset<kTag> &&original,
|
||||
ushort tag,
|
||||
const TextWithTagOffset<kTag> &replacement) {
|
||||
const auto replacementPosition = FindTagReplacementPosition(
|
||||
original.text.text,
|
||||
tag);
|
||||
if (replacementPosition < 0) {
|
||||
return std::move(original);
|
||||
}
|
||||
original.text = ReplaceTag<TextWithEntities>::Replace(
|
||||
std::move(original.text),
|
||||
replacement.text,
|
||||
replacementPosition);
|
||||
if (tag == kTag) {
|
||||
original.offset = replacementPosition;
|
||||
} else if (original.offset > replacementPosition) {
|
||||
constexpr auto kReplaceCommandLength = 4;
|
||||
const auto replacementSize = replacement.text.text.size();
|
||||
original.offset += replacementSize - kReplaceCommandLength;
|
||||
}
|
||||
return std::move(original);
|
||||
}
|
||||
|
||||
} // namespace Lang
|
||||
|
||||
namespace Dialogs::Ui {
|
||||
|
||||
TextWithEntities DialogsPreviewText(TextWithEntities text) {
|
||||
auto result = Ui::Text::Filtered(
|
||||
std::move(text),
|
||||
{
|
||||
EntityType::Pre,
|
||||
EntityType::Code,
|
||||
EntityType::Spoiler,
|
||||
EntityType::StrikeOut,
|
||||
EntityType::Underline,
|
||||
EntityType::Italic,
|
||||
EntityType::CustomEmoji,
|
||||
EntityType::Colorized,
|
||||
});
|
||||
for (auto &entity : result.entities) {
|
||||
if (entity.type() == EntityType::Pre) {
|
||||
entity = EntityInText(
|
||||
EntityType::Code,
|
||||
entity.offset(),
|
||||
entity.length());
|
||||
} else if (entity.type() == EntityType::Colorized
|
||||
&& !entity.data().isEmpty()) {
|
||||
// Drop 'data' so that only link-color colorization takes place.
|
||||
entity = EntityInText(
|
||||
EntityType::Colorized,
|
||||
entity.offset(),
|
||||
entity.length());
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
struct MessageView::LoadingContext {
|
||||
std::any context;
|
||||
rpl::lifetime lifetime;
|
||||
};
|
||||
|
||||
MessageView::MessageView()
|
||||
: _senderCache(st::dialogsTextWidthMin)
|
||||
, _textCache(st::dialogsTextWidthMin) {
|
||||
}
|
||||
|
||||
MessageView::~MessageView() = default;
|
||||
|
||||
void MessageView::itemInvalidated(not_null<const HistoryItem*> item) {
|
||||
if (_textCachedFor == item.get()) {
|
||||
_textCachedFor = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
bool MessageView::dependsOn(not_null<const HistoryItem*> item) const {
|
||||
return (_textCachedFor == item.get());
|
||||
}
|
||||
|
||||
bool MessageView::prepared(
|
||||
not_null<const HistoryItem*> item,
|
||||
Data::Forum *forum,
|
||||
Data::SavedMessages *monoforum) const {
|
||||
return (_textCachedFor == item.get())
|
||||
&& ((!forum && !monoforum)
|
||||
|| (_topics
|
||||
&& _topics->forum() == forum
|
||||
&& _topics->monoforum() == monoforum
|
||||
&& _topics->prepared()));
|
||||
}
|
||||
|
||||
void MessageView::prepare(
|
||||
not_null<const HistoryItem*> item,
|
||||
Data::Forum *forum,
|
||||
Data::SavedMessages *monoforum,
|
||||
Fn<void()> customEmojiRepaint,
|
||||
ToPreviewOptions options) {
|
||||
if (!forum && !monoforum) {
|
||||
_topics = nullptr;
|
||||
} else if (!_topics
|
||||
|| _topics->forum() != forum
|
||||
|| _topics->monoforum() != monoforum) {
|
||||
_topics = std::make_unique<TopicsView>(forum, monoforum);
|
||||
if (forum) {
|
||||
_topics->prepare(item->topicRootId(), customEmojiRepaint);
|
||||
} else {
|
||||
_topics->prepare(item->sublistPeerId(), customEmojiRepaint);
|
||||
}
|
||||
} else if (!_topics->prepared()) {
|
||||
if (forum) {
|
||||
_topics->prepare(item->topicRootId(), customEmojiRepaint);
|
||||
} else {
|
||||
_topics->prepare(item->sublistPeerId(), customEmojiRepaint);
|
||||
}
|
||||
}
|
||||
if (_textCachedFor == item.get()) {
|
||||
return;
|
||||
}
|
||||
options.existing = &_imagesCache;
|
||||
options.ignoreTopic = true;
|
||||
options.spoilerLoginCode = true;
|
||||
auto preview = item->toPreview(options);
|
||||
_leftIcon = (preview.icon == ItemPreview::Icon::ForwardedMessage)
|
||||
? &st::dialogsMiniForward
|
||||
: (preview.icon == ItemPreview::Icon::ReplyToStory)
|
||||
? &st::dialogsMiniReplyStory
|
||||
: nullptr;
|
||||
const auto hasImages = !preview.images.empty();
|
||||
const auto history = item->history();
|
||||
auto context = Core::TextContext({
|
||||
.session = &history->session(),
|
||||
.repaint = customEmojiRepaint,
|
||||
.customEmojiLoopLimit = kEmojiLoopCount,
|
||||
});
|
||||
const auto senderTill = (preview.arrowInTextPosition > 0)
|
||||
? preview.arrowInTextPosition
|
||||
: preview.imagesInTextPosition;
|
||||
if ((hasImages || _leftIcon) && senderTill > 0) {
|
||||
auto sender = Text::Mid(preview.text, 0, senderTill);
|
||||
TextUtilities::Trim(sender);
|
||||
_senderCache.setMarkedText(
|
||||
st::dialogsTextStyle,
|
||||
std::move(sender),
|
||||
DialogTextOptions());
|
||||
preview.text = Text::Mid(preview.text, senderTill);
|
||||
} else {
|
||||
_senderCache = { st::dialogsTextWidthMin };
|
||||
}
|
||||
TextUtilities::Trim(preview.text);
|
||||
auto textToCache = DialogsPreviewText(std::move(preview.text));
|
||||
|
||||
if (!options.searchLowerText.isEmpty()) {
|
||||
static constexpr auto kLeftShift = 15;
|
||||
auto minFrom = std::numeric_limits<uint16>::max();
|
||||
|
||||
const auto words = Ui::Text::Words(options.searchLowerText);
|
||||
textToCache.entities.reserve(textToCache.entities.size()
|
||||
+ words.size());
|
||||
|
||||
for (const auto &word : words) {
|
||||
const auto selection = HistoryView::FindSearchQueryHighlight(
|
||||
textToCache.text,
|
||||
word);
|
||||
if (!selection.empty()) {
|
||||
minFrom = std::min(minFrom, selection.from);
|
||||
textToCache.entities.push_back(EntityInText{
|
||||
EntityType::Colorized,
|
||||
selection.from,
|
||||
selection.to - selection.from
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (minFrom == std::numeric_limits<uint16>::max()
|
||||
&& !item->replyTo().quote.empty()) {
|
||||
auto textQuote = TextWithEntities();
|
||||
for (const auto &word : words) {
|
||||
const auto selection = HistoryView::FindSearchQueryHighlight(
|
||||
item->replyTo().quote.text,
|
||||
word);
|
||||
if (!selection.empty()) {
|
||||
minFrom = 0;
|
||||
if (textQuote.empty()) {
|
||||
textQuote = item->replyTo().quote;
|
||||
}
|
||||
textQuote.entities.push_back(EntityInText{
|
||||
EntityType::Colorized,
|
||||
selection.from,
|
||||
selection.to - selection.from
|
||||
});
|
||||
}
|
||||
}
|
||||
if (!textQuote.empty()) {
|
||||
auto helper = Ui::Text::CustomEmojiHelper(context);
|
||||
const auto factory = Ui::Text::PaletteDependentEmoji{
|
||||
.factory = [=] {
|
||||
const auto &icon = st::dialogsMiniQuoteIcon;
|
||||
auto image = QImage(
|
||||
icon.size() * style::DevicePixelRatio(),
|
||||
QImage::Format_ARGB32_Premultiplied);
|
||||
image.setDevicePixelRatio(style::DevicePixelRatio());
|
||||
image.fill(Qt::transparent);
|
||||
{
|
||||
auto p = Painter(&image);
|
||||
icon.paintInCenter(
|
||||
p,
|
||||
Rect(icon.size()),
|
||||
st::dialogsTextFg->c);
|
||||
}
|
||||
return image;
|
||||
},
|
||||
.margin = QMargins(
|
||||
st::lineWidth * 2,
|
||||
0,
|
||||
st::lineWidth * 2,
|
||||
0),
|
||||
};
|
||||
textToCache = textQuote
|
||||
.append(helper.paletteDependent(factory))
|
||||
.append(std::move(textToCache));
|
||||
context = helper.context(customEmojiRepaint);
|
||||
}
|
||||
}
|
||||
|
||||
if (!words.empty() && minFrom != std::numeric_limits<uint16>::max()) {
|
||||
std::sort(
|
||||
textToCache.entities.begin(),
|
||||
textToCache.entities.end(),
|
||||
[](const auto &a, const auto &b) {
|
||||
return a.offset() < b.offset();
|
||||
});
|
||||
|
||||
const auto textSize = textToCache.text.size();
|
||||
minFrom = (minFrom > textSize || minFrom < kLeftShift)
|
||||
? 0
|
||||
: minFrom - kLeftShift;
|
||||
|
||||
textToCache = (TextWithEntities{
|
||||
minFrom > 0 ? kQEllipsis : QString()
|
||||
}).append(Text::Mid(std::move(textToCache), minFrom));
|
||||
}
|
||||
}
|
||||
_hasPlainLinkAtBegin = !textToCache.entities.empty()
|
||||
&& (textToCache.entities.front().type() == EntityType::Colorized);
|
||||
_textCache.setMarkedText(
|
||||
st::dialogsTextStyle,
|
||||
std::move(textToCache),
|
||||
DialogTextOptions(),
|
||||
std::move(context));
|
||||
_textCachedFor = item;
|
||||
_imagesCache = std::move(preview.images);
|
||||
if (!ranges::any_of(_imagesCache, &ItemPreviewImage::hasSpoiler)) {
|
||||
_spoiler = nullptr;
|
||||
} else if (!_spoiler) {
|
||||
_spoiler = std::make_unique<SpoilerAnimation>(customEmojiRepaint);
|
||||
}
|
||||
if (preview.loadingContext.has_value()) {
|
||||
if (!_loadingContext) {
|
||||
_loadingContext = std::make_unique<LoadingContext>();
|
||||
item->history()->session().downloaderTaskFinished(
|
||||
) | rpl::on_next([=] {
|
||||
_textCachedFor = nullptr;
|
||||
}, _loadingContext->lifetime);
|
||||
}
|
||||
_loadingContext->context = std::move(preview.loadingContext);
|
||||
} else {
|
||||
_loadingContext = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
bool MessageView::isInTopicJump(int x, int y) const {
|
||||
return _topics && _topics->isInTopicJumpArea(x, y);
|
||||
}
|
||||
|
||||
void MessageView::addTopicJumpRipple(
|
||||
QPoint origin,
|
||||
not_null<TopicJumpCache*> topicJumpCache,
|
||||
Fn<void()> updateCallback) {
|
||||
if (_topics) {
|
||||
_topics->addTopicJumpRipple(
|
||||
origin,
|
||||
topicJumpCache,
|
||||
std::move(updateCallback));
|
||||
}
|
||||
}
|
||||
|
||||
void MessageView::stopLastRipple() {
|
||||
if (_topics) {
|
||||
_topics->stopLastRipple();
|
||||
}
|
||||
}
|
||||
|
||||
void MessageView::clearRipple() {
|
||||
if (_topics) {
|
||||
_topics->clearRipple();
|
||||
}
|
||||
}
|
||||
|
||||
int MessageView::countWidth() const {
|
||||
auto result = 0;
|
||||
if (!_senderCache.isEmpty()) {
|
||||
result += _senderCache.maxWidth();
|
||||
if (!_imagesCache.empty() && !_leftIcon) {
|
||||
result += st::dialogsMiniPreviewSkip
|
||||
+ st::dialogsMiniPreviewRight;
|
||||
}
|
||||
}
|
||||
if (_leftIcon) {
|
||||
const auto w = _leftIcon->icon.icon.width();
|
||||
result += w
|
||||
+ (_imagesCache.empty()
|
||||
? _leftIcon->skipText
|
||||
: _leftIcon->skipMedia);
|
||||
}
|
||||
if (!_imagesCache.empty()) {
|
||||
result += (_imagesCache.size()
|
||||
* (st::dialogsMiniPreview + st::dialogsMiniPreviewSkip))
|
||||
+ st::dialogsMiniPreviewRight;
|
||||
}
|
||||
return result + _textCache.maxWidth();
|
||||
}
|
||||
|
||||
void MessageView::paint(
|
||||
Painter &p,
|
||||
const QRect &geometry,
|
||||
const PaintContext &context) const {
|
||||
if (geometry.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
p.setFont(st::dialogsTextFont);
|
||||
p.setPen(context.active
|
||||
? st::dialogsTextFgActive
|
||||
: context.selected
|
||||
? st::dialogsTextFgOver
|
||||
: st::dialogsTextFg);
|
||||
const auto withTopic = _topics && context.st->topicsHeight;
|
||||
const auto palette = &(withTopic
|
||||
? (context.active
|
||||
? st::dialogsTextPaletteInTopicActive
|
||||
: context.selected
|
||||
? st::dialogsTextPaletteInTopicOver
|
||||
: st::dialogsTextPaletteInTopic)
|
||||
: (context.active
|
||||
? st::dialogsTextPaletteActive
|
||||
: context.selected
|
||||
? st::dialogsTextPaletteOver
|
||||
: st::dialogsTextPalette));
|
||||
|
||||
auto rect = geometry;
|
||||
const auto checkJump = withTopic && !context.active;
|
||||
const auto jump1 = checkJump ? _topics->jumpToTopicWidth() : 0;
|
||||
if (jump1) {
|
||||
paintJumpToLast(p, rect, context, jump1);
|
||||
} else if (_topics) {
|
||||
_topics->clearTopicJumpGeometry();
|
||||
}
|
||||
|
||||
if (withTopic) {
|
||||
_topics->paint(p, rect, context);
|
||||
rect.setTop(rect.top() + context.st->topicsHeight);
|
||||
}
|
||||
|
||||
auto finalRight = rect.x() + rect.width();
|
||||
if (jump1) {
|
||||
rect.setWidth(rect.width() - st::forumDialogJumpArrowSkip);
|
||||
finalRight -= st::forumDialogJumpArrowSkip;
|
||||
}
|
||||
const auto pausedSpoiler = context.paused
|
||||
|| On(PowerSaving::kChatSpoiler);
|
||||
if (!_senderCache.isEmpty()) {
|
||||
_senderCache.draw(p, {
|
||||
.position = rect.topLeft(),
|
||||
.availableWidth = rect.width(),
|
||||
.palette = palette,
|
||||
.elisionHeight = rect.height(),
|
||||
});
|
||||
rect.setLeft(rect.x() + _senderCache.maxWidth());
|
||||
if (!_imagesCache.empty() && !_leftIcon) {
|
||||
const auto skip = st::dialogsMiniPreviewSkip
|
||||
+ st::dialogsMiniPreviewRight;
|
||||
rect.setLeft(rect.x() + skip);
|
||||
}
|
||||
}
|
||||
|
||||
if (_leftIcon) {
|
||||
const auto &icon = ThreeStateIcon(
|
||||
_leftIcon->icon,
|
||||
context.active,
|
||||
context.selected);
|
||||
const auto w = (icon.width());
|
||||
if (rect.width() > w) {
|
||||
if (_hasPlainLinkAtBegin && !context.active) {
|
||||
icon.paint(
|
||||
p,
|
||||
rect.topLeft(),
|
||||
rect.width(),
|
||||
palette->linkFg->c);
|
||||
} else {
|
||||
icon.paint(p, rect.topLeft(), rect.width());
|
||||
}
|
||||
rect.setLeft(rect.x()
|
||||
+ w
|
||||
+ (_imagesCache.empty()
|
||||
? _leftIcon->skipText
|
||||
: _leftIcon->skipMedia));
|
||||
}
|
||||
}
|
||||
for (const auto &image : _imagesCache) {
|
||||
const auto w = st::dialogsMiniPreview + st::dialogsMiniPreviewSkip;
|
||||
if (rect.width() < w) {
|
||||
break;
|
||||
}
|
||||
const auto mini = QRect(
|
||||
rect.x(),
|
||||
rect.y() + st::dialogsMiniPreviewTop,
|
||||
st::dialogsMiniPreview,
|
||||
st::dialogsMiniPreview);
|
||||
if (!image.data.isNull()) {
|
||||
p.drawImage(mini, image.data);
|
||||
if (image.hasSpoiler()) {
|
||||
const auto frame = DefaultImageSpoiler().frame(
|
||||
_spoiler->index(context.now, pausedSpoiler));
|
||||
if (image.isEllipse()) {
|
||||
const auto radius = st::dialogsMiniPreview / 2;
|
||||
static auto mask = Images::CornersMask(radius);
|
||||
FillSpoilerRect(
|
||||
p,
|
||||
mini,
|
||||
Images::CornersMaskRef(mask),
|
||||
frame,
|
||||
_cornersCache);
|
||||
} else {
|
||||
FillSpoilerRect(p, mini, frame);
|
||||
}
|
||||
}
|
||||
}
|
||||
rect.setLeft(rect.x() + w);
|
||||
}
|
||||
if (!_imagesCache.empty()) {
|
||||
rect.setLeft(rect.x() + st::dialogsMiniPreviewRight);
|
||||
}
|
||||
// Style of _textCache.
|
||||
static const auto ellipsisWidth = st::dialogsTextStyle.font->width(
|
||||
kQEllipsis);
|
||||
if (rect.width() > ellipsisWidth) {
|
||||
_textCache.draw(p, {
|
||||
.position = rect.topLeft(),
|
||||
.availableWidth = rect.width(),
|
||||
.palette = palette,
|
||||
.spoiler = Text::DefaultSpoilerCache(),
|
||||
.now = context.now,
|
||||
.pausedEmoji = context.paused || On(PowerSaving::kEmojiChat),
|
||||
.pausedSpoiler = pausedSpoiler,
|
||||
.elisionHeight = rect.height(),
|
||||
});
|
||||
rect.setLeft(rect.x() + _textCache.maxWidth());
|
||||
}
|
||||
if (jump1) {
|
||||
const auto position = st::forumDialogJumpArrowPosition
|
||||
+ QPoint((rect.width() > 0) ? rect.x() : finalRight, rect.y());
|
||||
(context.selected
|
||||
? st::forumDialogJumpArrowOver
|
||||
: st::forumDialogJumpArrow).paint(p, position, context.width);
|
||||
}
|
||||
}
|
||||
|
||||
void MessageView::paintJumpToLast(
|
||||
Painter &p,
|
||||
const QRect &rect,
|
||||
const PaintContext &context,
|
||||
int width1) const {
|
||||
if (!context.topicJumpCache) {
|
||||
_topics->clearTopicJumpGeometry();
|
||||
return;
|
||||
}
|
||||
const auto width2 = countWidth() + st::forumDialogJumpArrowSkip;
|
||||
const auto geometry = FillJumpToLastBg(p, {
|
||||
.st = context.st,
|
||||
.corners = (context.selected
|
||||
? &context.topicJumpCache->over
|
||||
: &context.topicJumpCache->corners),
|
||||
.geometry = rect,
|
||||
.bg = (context.selected
|
||||
? st::dialogsRippleBg
|
||||
: st::dialogsBgOver),
|
||||
.width1 = width1,
|
||||
.width2 = width2,
|
||||
});
|
||||
if (context.topicJumpSelected) {
|
||||
p.setOpacity(0.1);
|
||||
FillJumpToLastPrepared(p, {
|
||||
.st = context.st,
|
||||
.corners = &context.topicJumpCache->selected,
|
||||
.bg = st::dialogsTextFg,
|
||||
.prepared = geometry,
|
||||
});
|
||||
p.setOpacity(1.);
|
||||
}
|
||||
if (!_topics->changeTopicJumpGeometry(geometry)) {
|
||||
auto color = st::dialogsTextFg->c;
|
||||
color.setAlpha(color.alpha() / 10);
|
||||
if (color.alpha() > 0) {
|
||||
_topics->paintRipple(p, 0, 0, context.width, &color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
HistoryView::ItemPreview PreviewWithSender(
|
||||
HistoryView::ItemPreview &&preview,
|
||||
const QString &sender,
|
||||
TextWithEntities topic) {
|
||||
const auto wrappedSender = st::wrap_rtl(sender);
|
||||
auto senderWithOffset = topic.empty()
|
||||
? TextWithTagOffset<lt_from>::FromString(wrappedSender)
|
||||
: tr::lng_dialogs_text_from_in_topic(
|
||||
tr::now,
|
||||
lt_from,
|
||||
{ wrappedSender },
|
||||
lt_topic,
|
||||
std::move(topic),
|
||||
TextWithTagOffset<lt_from>::FromString);
|
||||
auto wrappedWithOffset = tr::lng_dialogs_text_from_wrapped(
|
||||
tr::now,
|
||||
lt_from,
|
||||
std::move(senderWithOffset.text),
|
||||
TextWithTagOffset<lt_from>::FromString);
|
||||
const auto wrappedSize = wrappedWithOffset.text.text.size();
|
||||
auto fullWithOffset = tr::lng_dialogs_text_with_from(
|
||||
tr::now,
|
||||
lt_from_part,
|
||||
Ui::Text::Colorized(std::move(wrappedWithOffset.text)),
|
||||
lt_message,
|
||||
std::move(preview.text),
|
||||
TextWithTagOffset<lt_from_part>::FromString);
|
||||
preview.text = std::move(fullWithOffset.text);
|
||||
preview.arrowInTextPosition = (fullWithOffset.offset < 0
|
||||
|| wrappedWithOffset.offset < 0
|
||||
|| senderWithOffset.offset < 0)
|
||||
? -1
|
||||
: (fullWithOffset.offset
|
||||
+ wrappedWithOffset.offset
|
||||
+ senderWithOffset.offset
|
||||
+ sender.size());
|
||||
preview.imagesInTextPosition = (fullWithOffset.offset < 0)
|
||||
? 0
|
||||
: (fullWithOffset.offset + wrappedSize);
|
||||
return std::move(preview);
|
||||
}
|
||||
|
||||
} // namespace Dialogs::Ui
|
||||
110
Telegram/SourceFiles/dialogs/ui/dialogs_message_view.h
Normal file
110
Telegram/SourceFiles/dialogs/ui/dialogs_message_view.h
Normal file
@@ -0,0 +1,110 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include <any>
|
||||
|
||||
class Image;
|
||||
class HistoryItem;
|
||||
enum class ImageRoundRadius;
|
||||
|
||||
namespace style {
|
||||
struct DialogRow;
|
||||
struct DialogsMiniIcon;
|
||||
} // namespace style
|
||||
|
||||
namespace Ui {
|
||||
class SpoilerAnimation;
|
||||
} // namespace Ui
|
||||
|
||||
namespace Data {
|
||||
class Forum;
|
||||
class SavedMessages;
|
||||
} // namespace Data
|
||||
|
||||
namespace HistoryView {
|
||||
struct ToPreviewOptions;
|
||||
struct ItemPreviewImage;
|
||||
struct ItemPreview;
|
||||
} // namespace HistoryView
|
||||
|
||||
namespace Dialogs::Ui {
|
||||
|
||||
using namespace ::Ui;
|
||||
|
||||
struct PaintContext;
|
||||
struct TopicJumpCache;
|
||||
class TopicsView;
|
||||
|
||||
[[nodiscard]] TextWithEntities DialogsPreviewText(TextWithEntities text);
|
||||
|
||||
class MessageView final {
|
||||
public:
|
||||
MessageView();
|
||||
~MessageView();
|
||||
|
||||
using ToPreviewOptions = HistoryView::ToPreviewOptions;
|
||||
using ItemPreviewImage = HistoryView::ItemPreviewImage;
|
||||
using ItemPreview = HistoryView::ItemPreview;
|
||||
|
||||
void itemInvalidated(not_null<const HistoryItem*> item);
|
||||
[[nodiscard]] bool dependsOn(not_null<const HistoryItem*> item) const;
|
||||
|
||||
[[nodiscard]] bool prepared(
|
||||
not_null<const HistoryItem*> item,
|
||||
Data::Forum *forum,
|
||||
Data::SavedMessages *monoforum) const;
|
||||
void prepare(
|
||||
not_null<const HistoryItem*> item,
|
||||
Data::Forum *forum,
|
||||
Data::SavedMessages *monoforum,
|
||||
Fn<void()> customEmojiRepaint,
|
||||
ToPreviewOptions options);
|
||||
|
||||
void paint(
|
||||
Painter &p,
|
||||
const QRect &geometry,
|
||||
const PaintContext &context) const;
|
||||
|
||||
[[nodiscard]] bool isInTopicJump(int x, int y) const;
|
||||
void addTopicJumpRipple(
|
||||
QPoint origin,
|
||||
not_null<TopicJumpCache*> topicJumpCache,
|
||||
Fn<void()> updateCallback);
|
||||
void stopLastRipple();
|
||||
void clearRipple();
|
||||
|
||||
private:
|
||||
struct LoadingContext;
|
||||
|
||||
[[nodiscard]] int countWidth() const;
|
||||
void paintJumpToLast(
|
||||
Painter &p,
|
||||
const QRect &rect,
|
||||
const PaintContext &context,
|
||||
int width1) const;
|
||||
|
||||
mutable const HistoryItem *_textCachedFor = nullptr;
|
||||
mutable Text::String _senderCache;
|
||||
mutable std::unique_ptr<TopicsView> _topics;
|
||||
mutable Text::String _textCache;
|
||||
mutable std::vector<ItemPreviewImage> _imagesCache;
|
||||
mutable std::unique_ptr<SpoilerAnimation> _spoiler;
|
||||
mutable std::unique_ptr<LoadingContext> _loadingContext;
|
||||
mutable const style::DialogsMiniIcon *_leftIcon = nullptr;
|
||||
mutable QImage _cornersCache;
|
||||
mutable bool _hasPlainLinkAtBegin = false;
|
||||
|
||||
};
|
||||
|
||||
[[nodiscard]] HistoryView::ItemPreview PreviewWithSender(
|
||||
HistoryView::ItemPreview &&preview,
|
||||
const QString &sender,
|
||||
TextWithEntities topic);
|
||||
|
||||
} // namespace Dialogs::Ui
|
||||
23
Telegram/SourceFiles/dialogs/ui/dialogs_quick_action.h
Normal file
23
Telegram/SourceFiles/dialogs/ui/dialogs_quick_action.h
Normal file
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
namespace Dialogs::Ui {
|
||||
|
||||
using namespace ::Ui;
|
||||
|
||||
enum class QuickDialogAction {
|
||||
Mute,
|
||||
Pin,
|
||||
Read,
|
||||
Archive,
|
||||
Delete,
|
||||
Disabled,
|
||||
};
|
||||
|
||||
} // namespace Dialogs::Ui
|
||||
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "dialogs/ui/dialogs_quick_action.h"
|
||||
#include "ui/controls/swipe_handler_data.h"
|
||||
|
||||
namespace Lottie {
|
||||
class Icon;
|
||||
} // namespace Lottie
|
||||
|
||||
namespace Ui {
|
||||
class RippleAnimation;
|
||||
} // namespace Ui
|
||||
|
||||
namespace Dialogs::Ui {
|
||||
|
||||
using namespace ::Ui;
|
||||
|
||||
enum class QuickDialogActionLabel {
|
||||
Mute,
|
||||
Unmute,
|
||||
Pin,
|
||||
Unpin,
|
||||
Read,
|
||||
Unread,
|
||||
Archive,
|
||||
Unarchive,
|
||||
Delete,
|
||||
Disabled,
|
||||
};
|
||||
|
||||
struct QuickActionContext {
|
||||
::Ui::Controls::SwipeContextData data;
|
||||
std::unique_ptr<Lottie::Icon> icon;
|
||||
std::unique_ptr<Ui::RippleAnimation> ripple;
|
||||
std::unique_ptr<Ui::RippleAnimation> rippleFg;
|
||||
QuickDialogAction action;
|
||||
crl::time finishedAt = 0;
|
||||
};
|
||||
|
||||
} // namespace Dialogs::Ui
|
||||
278
Telegram/SourceFiles/dialogs/ui/dialogs_stories_content.cpp
Normal file
278
Telegram/SourceFiles/dialogs/ui/dialogs_stories_content.cpp
Normal file
@@ -0,0 +1,278 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#include "dialogs/ui/dialogs_stories_content.h"
|
||||
|
||||
#include "base/unixtime.h"
|
||||
#include "data/data_changes.h"
|
||||
#include "data/data_document.h"
|
||||
#include "data/data_document_media.h"
|
||||
#include "data/data_file_origin.h"
|
||||
#include "data/data_photo.h"
|
||||
#include "data/data_photo_media.h"
|
||||
#include "data/data_session.h"
|
||||
#include "data/data_stories.h"
|
||||
#include "data/data_user.h"
|
||||
#include "dialogs/ui/dialogs_stories_list.h"
|
||||
#include "info/stories/info_stories_widget.h"
|
||||
#include "info/info_controller.h"
|
||||
#include "info/info_memento.h"
|
||||
#include "main/main_session.h"
|
||||
#include "media/stories/media_stories_stealth.h"
|
||||
#include "lang/lang_keys.h"
|
||||
#include "ui/dynamic_image.h"
|
||||
#include "ui/dynamic_thumbnails.h"
|
||||
#include "ui/painter.h"
|
||||
#include "window/window_session_controller.h"
|
||||
#include "styles/style_layers.h"
|
||||
#include "styles/style_menu_icons.h"
|
||||
#include "styles/style_media_stories.h"
|
||||
|
||||
namespace Dialogs::Stories {
|
||||
namespace {
|
||||
|
||||
constexpr auto kShownLastCount = 3;
|
||||
|
||||
class State final {
|
||||
public:
|
||||
State(not_null<Data::Stories*> data, Data::StorySourcesList list);
|
||||
|
||||
[[nodiscard]] Content next();
|
||||
|
||||
private:
|
||||
const not_null<Data::Stories*> _data;
|
||||
const Data::StorySourcesList _list;
|
||||
base::flat_map<
|
||||
not_null<PeerData*>,
|
||||
std::shared_ptr<Ui::DynamicImage>> _userpics;
|
||||
|
||||
};
|
||||
|
||||
State::State(not_null<Data::Stories*> data, Data::StorySourcesList list)
|
||||
: _data(data)
|
||||
, _list(list) {
|
||||
}
|
||||
|
||||
Content State::next() {
|
||||
const auto &sources = _data->sources(_list);
|
||||
auto result = Content{ .total = int(sources.size()) };
|
||||
result.elements.reserve(sources.size());
|
||||
for (const auto &info : sources) {
|
||||
const auto source = _data->source(info.id);
|
||||
Assert(source != nullptr);
|
||||
|
||||
auto userpic = std::shared_ptr<Ui::DynamicImage>();
|
||||
const auto peer = source->peer;
|
||||
if (const auto i = _userpics.find(peer); i != end(_userpics)) {
|
||||
userpic = i->second;
|
||||
} else {
|
||||
userpic = Ui::MakeUserpicThumbnail(peer, true);
|
||||
_userpics.emplace(peer, userpic);
|
||||
}
|
||||
result.elements.push_back({
|
||||
.id = uint64(peer->id.value),
|
||||
.name = peer->shortName(),
|
||||
.thumbnail = std::move(userpic),
|
||||
.count = info.count,
|
||||
.unreadCount = info.unreadCount,
|
||||
.hasVideoStream = info.hasVideoStream ? 1U : 0U,
|
||||
.skipSmall = peer->isSelf() ? 1U : 0U,
|
||||
});
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
rpl::producer<Content> ContentForSession(
|
||||
not_null<Main::Session*> session,
|
||||
Data::StorySourcesList list) {
|
||||
return [=](auto consumer) {
|
||||
auto result = rpl::lifetime();
|
||||
const auto stories = &session->data().stories();
|
||||
const auto state = result.make_state<State>(stories, list);
|
||||
rpl::single(
|
||||
rpl::empty
|
||||
) | rpl::then(
|
||||
stories->sourcesChanged(list)
|
||||
) | rpl::on_next([=] {
|
||||
consumer.put_next(state->next());
|
||||
}, result);
|
||||
return result;
|
||||
};
|
||||
}
|
||||
|
||||
rpl::producer<Content> LastForPeer(not_null<PeerData*> peer) {
|
||||
using namespace rpl::mappers;
|
||||
|
||||
const auto stories = &peer->owner().stories();
|
||||
const auto peerId = peer->id;
|
||||
|
||||
return rpl::single(
|
||||
peerId
|
||||
) | rpl::then(
|
||||
stories->sourceChanged() | rpl::filter(_1 == peerId)
|
||||
) | rpl::map([=] {
|
||||
auto ids = std::vector<StoryId>();
|
||||
auto readTill = StoryId();
|
||||
auto total = 0;
|
||||
if (const auto source = stories->source(peerId)) {
|
||||
readTill = source->readTill;
|
||||
total = int(source->ids.size());
|
||||
ids = ranges::views::all(source->ids)
|
||||
| ranges::views::reverse
|
||||
| ranges::views::take(kShownLastCount)
|
||||
| ranges::views::transform(&Data::StoryIdDates::id)
|
||||
| ranges::to_vector;
|
||||
}
|
||||
return rpl::make_producer<Content>([=](auto consumer) {
|
||||
auto lifetime = rpl::lifetime();
|
||||
if (ids.empty()) {
|
||||
consumer.put_next(Content());
|
||||
consumer.put_done();
|
||||
return lifetime;
|
||||
}
|
||||
|
||||
struct State {
|
||||
Fn<void()> check;
|
||||
base::has_weak_ptr guard;
|
||||
int readTill = StoryId();
|
||||
bool pushed = false;
|
||||
};
|
||||
const auto state = lifetime.make_state<State>();
|
||||
state->readTill = readTill;
|
||||
state->check = [=] {
|
||||
if (state->pushed) {
|
||||
return;
|
||||
}
|
||||
auto done = true;
|
||||
auto resolving = false;
|
||||
auto result = Content{ .total = total };
|
||||
for (const auto id : ids) {
|
||||
const auto storyId = FullStoryId{ peerId, id };
|
||||
const auto maybe = stories->lookup(storyId);
|
||||
if (maybe) {
|
||||
if (!resolving) {
|
||||
const auto stream = (*maybe)->call();
|
||||
const auto unread = stream
|
||||
|| (id > state->readTill);
|
||||
result.elements.reserve(ids.size());
|
||||
result.elements.push_back({
|
||||
.id = uint64(id),
|
||||
.thumbnail = Ui::MakeStoryThumbnail(*maybe),
|
||||
.count = 1U,
|
||||
.unreadCount = unread ? 1U : 0U,
|
||||
.hasVideoStream = stream ? 1U : 0U,
|
||||
});
|
||||
if (unread) {
|
||||
done = false;
|
||||
}
|
||||
}
|
||||
} else if (maybe.error() == Data::NoStory::Unknown) {
|
||||
resolving = true;
|
||||
stories->resolve(
|
||||
storyId,
|
||||
crl::guard(&state->guard, state->check));
|
||||
}
|
||||
}
|
||||
if (resolving) {
|
||||
return;
|
||||
}
|
||||
state->pushed = true;
|
||||
consumer.put_next(std::move(result));
|
||||
if (done) {
|
||||
consumer.put_done();
|
||||
}
|
||||
};
|
||||
|
||||
rpl::single(peerId) | rpl::then(
|
||||
stories->itemsChanged() | rpl::filter(_1 == peerId)
|
||||
) | rpl::on_next(state->check, lifetime);
|
||||
|
||||
stories->session().changes().storyUpdates(
|
||||
Data::StoryUpdate::Flag::MarkRead
|
||||
) | rpl::on_next([=](const Data::StoryUpdate &update) {
|
||||
if (update.story->peer()->id == peerId) {
|
||||
if (update.story->id() > state->readTill) {
|
||||
state->readTill = update.story->id();
|
||||
if (ranges::contains(ids, state->readTill)
|
||||
|| state->readTill > ids.front()) {
|
||||
state->pushed = false;
|
||||
state->check();
|
||||
}
|
||||
}
|
||||
}
|
||||
}, lifetime);
|
||||
|
||||
return lifetime;
|
||||
});
|
||||
}) | rpl::flatten_latest();
|
||||
}
|
||||
|
||||
void FillSourceMenu(
|
||||
not_null<Window::SessionController*> controller,
|
||||
const ShowMenuRequest &request) {
|
||||
const auto owner = &controller->session().data();
|
||||
const auto peer = owner->peer(PeerId(request.id));
|
||||
const auto &add = request.callback;
|
||||
if (peer->isSelf()) {
|
||||
add(tr::lng_stories_archive_button(tr::now), [=] {
|
||||
controller->showSection(Info::Stories::Make(
|
||||
peer,
|
||||
Info::Stories::ArchiveId()));
|
||||
}, &st::menuIconStoriesArchiveSection);
|
||||
add(tr::lng_stories_my_title(tr::now), [=] {
|
||||
controller->showSection(Info::Stories::Make(peer));
|
||||
}, &st::menuIconStoriesSavedSection);
|
||||
} else {
|
||||
const auto group = peer->isMegagroup();
|
||||
const auto channel = peer->isChannel();
|
||||
const auto showHistoryText = group
|
||||
? tr::lng_context_open_group(tr::now)
|
||||
: channel
|
||||
? tr::lng_context_open_channel(tr::now)
|
||||
: tr::lng_profile_send_message(tr::now);
|
||||
add(showHistoryText, [=] {
|
||||
controller->showPeerHistory(peer);
|
||||
}, channel ? &st::menuIconChannel : &st::menuIconChatBubble);
|
||||
const auto viewProfileText = group
|
||||
? tr::lng_context_view_group(tr::now)
|
||||
: channel
|
||||
? tr::lng_context_view_channel(tr::now)
|
||||
: tr::lng_context_view_profile(tr::now);
|
||||
add(viewProfileText, [=] {
|
||||
controller->showPeerInfo(peer);
|
||||
}, channel ? &st::menuIconInfo : &st::menuIconProfile);
|
||||
if (!peer->hasActiveVideoStream() && peer->hasUnreadStories()) {
|
||||
Media::Stories::AddStealthModeMenu(add, peer, controller);
|
||||
}
|
||||
const auto in = [&](Data::StorySourcesList list) {
|
||||
return ranges::contains(
|
||||
owner->stories().sources(list),
|
||||
peer->id,
|
||||
&Data::StoriesSourceInfo::id);
|
||||
};
|
||||
const auto toggle = [=](bool shown) {
|
||||
owner->stories().toggleHidden(
|
||||
peer->id,
|
||||
!shown,
|
||||
controller->uiShow());
|
||||
};
|
||||
if (in(Data::StorySourcesList::NotHidden)) {
|
||||
add(tr::lng_stories_archive(tr::now), [=] {
|
||||
toggle(false);
|
||||
}, &st::menuIconArchive);
|
||||
}
|
||||
if (in(Data::StorySourcesList::Hidden)) {
|
||||
add(tr::lng_stories_unarchive(tr::now), [=] {
|
||||
toggle(true);
|
||||
}, &st::menuIconUnarchive);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace Dialogs::Stories
|
||||
38
Telegram/SourceFiles/dialogs/ui/dialogs_stories_content.h
Normal file
38
Telegram/SourceFiles/dialogs/ui/dialogs_stories_content.h
Normal file
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
namespace Data {
|
||||
enum class StorySourcesList : uchar;
|
||||
class Story;
|
||||
} // namespace Data
|
||||
|
||||
namespace Main {
|
||||
class Session;
|
||||
} // namespace Main
|
||||
|
||||
namespace Window {
|
||||
class SessionController;
|
||||
} // namespace Window
|
||||
|
||||
namespace Dialogs::Stories {
|
||||
|
||||
struct Content;
|
||||
struct ShowMenuRequest;
|
||||
|
||||
[[nodiscard]] rpl::producer<Content> ContentForSession(
|
||||
not_null<Main::Session*> session,
|
||||
Data::StorySourcesList list);
|
||||
|
||||
[[nodiscard]] rpl::producer<Content> LastForPeer(not_null<PeerData*> peer);
|
||||
|
||||
void FillSourceMenu(
|
||||
not_null<Window::SessionController*> controller,
|
||||
const ShowMenuRequest &request);
|
||||
|
||||
} // namespace Dialogs::Stories
|
||||
1249
Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.cpp
Normal file
1249
Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.cpp
Normal file
File diff suppressed because it is too large
Load Diff
223
Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.h
Normal file
223
Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.h
Normal file
@@ -0,0 +1,223 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "base/qt/qt_compare.h"
|
||||
#include "base/timer.h"
|
||||
#include "base/weak_ptr.h"
|
||||
#include "ui/effects/animations.h"
|
||||
#include "ui/text/text_custom_emoji.h"
|
||||
#include "ui/widgets/menu/menu_add_action_callback.h"
|
||||
#include "ui/rp_widget.h"
|
||||
|
||||
class QPainter;
|
||||
|
||||
namespace style {
|
||||
struct DialogsStories;
|
||||
struct DialogsStoriesList;
|
||||
} // namespace style
|
||||
|
||||
namespace Ui {
|
||||
class PopupMenu;
|
||||
class DynamicImage;
|
||||
struct OutlineSegment;
|
||||
class ImportantTooltip;
|
||||
} // namespace Ui
|
||||
|
||||
namespace Dialogs::Stories {
|
||||
|
||||
struct Element {
|
||||
uint64 id = 0;
|
||||
QString name;
|
||||
std::shared_ptr<Ui::DynamicImage> thumbnail;
|
||||
uint32 count : 15 = 0;
|
||||
uint32 unreadCount : 15 = 0;
|
||||
uint32 hasVideoStream : 1 = 0;
|
||||
uint32 skipSmall : 1 = 0;
|
||||
|
||||
friend inline bool operator==(
|
||||
const Element &a,
|
||||
const Element &b) = default;
|
||||
};
|
||||
|
||||
struct Content {
|
||||
std::vector<Element> elements;
|
||||
int total = 0;
|
||||
|
||||
friend inline bool operator==(
|
||||
const Content &a,
|
||||
const Content &b) = default;
|
||||
};
|
||||
|
||||
struct ShowMenuRequest {
|
||||
uint64 id = 0;
|
||||
Ui::Menu::MenuCallback callback;
|
||||
};
|
||||
|
||||
class List final : public Ui::RpWidget {
|
||||
public:
|
||||
List(
|
||||
not_null<QWidget*> parent,
|
||||
const style::DialogsStoriesList &st,
|
||||
rpl::producer<Content> content);
|
||||
~List();
|
||||
|
||||
void setExpandedHeight(int height, bool momentum = false);
|
||||
void setLayoutConstraints(
|
||||
QPoint positionSmall,
|
||||
style::align alignSmall,
|
||||
QRect geometryFull = QRect());
|
||||
void setShowTooltip(
|
||||
not_null<Ui::RpWidget*> tooltipParent,
|
||||
rpl::producer<bool> shown,
|
||||
Fn<void()> hide);
|
||||
void raiseTooltip();
|
||||
|
||||
struct CollapsedGeometry {
|
||||
QRect geometry;
|
||||
float64 expanded = 0.;
|
||||
float64 singleWidth = 0.;
|
||||
};
|
||||
[[nodiscard]] CollapsedGeometry collapsedGeometryCurrent() const;
|
||||
[[nodiscard]] rpl::producer<> collapsedGeometryChanged() const;
|
||||
|
||||
[[nodiscard]] bool empty() const {
|
||||
return _empty.current();
|
||||
}
|
||||
[[nodiscard]] rpl::producer<bool> emptyValue() const {
|
||||
return _empty.value();
|
||||
}
|
||||
[[nodiscard]] rpl::producer<uint64> clicks() const;
|
||||
[[nodiscard]] rpl::producer<ShowMenuRequest> showMenuRequests() const;
|
||||
[[nodiscard]] rpl::producer<bool> toggleExpandedRequests() const;
|
||||
//[[nodiscard]] rpl::producer<> entered() const;
|
||||
[[nodiscard]] rpl::producer<> loadMoreRequests() const;
|
||||
|
||||
[[nodiscard]] auto verticalScrollEvents() const
|
||||
-> rpl::producer<not_null<QWheelEvent*>>;
|
||||
|
||||
private:
|
||||
struct Layout;
|
||||
enum class State {
|
||||
Small,
|
||||
Changing,
|
||||
Full,
|
||||
};
|
||||
struct Item {
|
||||
Element element;
|
||||
QImage nameCache;
|
||||
QColor nameCacheColor;
|
||||
std::vector<Ui::OutlineSegment> segments;
|
||||
bool subscribed = false;
|
||||
};
|
||||
struct Data {
|
||||
std::vector<Item> items;
|
||||
|
||||
[[nodiscard]] bool empty() const {
|
||||
return items.empty();
|
||||
}
|
||||
};
|
||||
|
||||
void showContent(Content &&content);
|
||||
//void enterEventHook(QEnterEvent *e) override;
|
||||
void resizeEvent(QResizeEvent *e) override;
|
||||
void paintEvent(QPaintEvent *e) override;
|
||||
void wheelEvent(QWheelEvent *e) override;
|
||||
void mousePressEvent(QMouseEvent *e) override;
|
||||
void mouseMoveEvent(QMouseEvent *e) override;
|
||||
void mouseReleaseEvent(QMouseEvent *e) override;
|
||||
void contextMenuEvent(QContextMenuEvent *e) override;
|
||||
|
||||
void paint(
|
||||
QPainter &p,
|
||||
const Layout &layout,
|
||||
float64 photo,
|
||||
float64 line,
|
||||
bool layered);
|
||||
void ensureLayer();
|
||||
void validateThumbnail(not_null<Item*> item);
|
||||
void validateName(not_null<Item*> item);
|
||||
void updateScrollMax();
|
||||
void updateSelected();
|
||||
void checkDragging();
|
||||
bool finishDragging();
|
||||
void checkLoadMore();
|
||||
void requestExpanded(bool expanded);
|
||||
|
||||
void updateTooltipGeometry();
|
||||
[[nodiscard]] TextWithEntities computeTooltipText() const;
|
||||
void toggleTooltip(bool fast);
|
||||
|
||||
bool checkForFullState();
|
||||
void setState(State state);
|
||||
void updateGeometry();
|
||||
[[nodiscard]] QRect countSmallGeometry() const;
|
||||
void updateExpanding();
|
||||
void updateExpanding(int expandingHeight, int expandedHeight);
|
||||
void validateSegments(
|
||||
not_null<Item*> item,
|
||||
const QBrush &brush,
|
||||
float64 line,
|
||||
bool forUnread);
|
||||
|
||||
[[nodiscard]] Layout computeLayout();
|
||||
[[nodiscard]] Layout computeLayout(float64 expanded) const;
|
||||
|
||||
const style::DialogsStoriesList &_st;
|
||||
Content _content;
|
||||
Data _data;
|
||||
rpl::event_stream<uint64> _clicks;
|
||||
rpl::event_stream<ShowMenuRequest> _showMenuRequests;
|
||||
rpl::event_stream<bool> _toggleExpandedRequests;
|
||||
//rpl::event_stream<> _entered;
|
||||
rpl::event_stream<> _loadMoreRequests;
|
||||
rpl::event_stream<> _collapsedGeometryChanged;
|
||||
|
||||
QImage _layer;
|
||||
QPoint _positionSmall;
|
||||
style::align _alignSmall = {};
|
||||
QRect _geometryFull;
|
||||
QRect _changingGeometryFrom;
|
||||
State _state = State::Small;
|
||||
rpl::variable<bool> _empty = true;
|
||||
|
||||
QPoint _lastMousePosition;
|
||||
std::optional<QPoint> _mouseDownPosition;
|
||||
int _startDraggingLeft = 0;
|
||||
int _scrollLeft = 0;
|
||||
int _scrollLeftMax = 0;
|
||||
bool _dragging = false;
|
||||
Qt::Orientation _scrollingLock = {};
|
||||
|
||||
Ui::Animations::Simple _expandedAnimation;
|
||||
Ui::Animations::Simple _expandCatchUpAnimation;
|
||||
float64 _lastRatio = 0.;
|
||||
int _lastExpandedHeight = 0;
|
||||
bool _expandIgnored : 1 = false;
|
||||
bool _expanded : 1 = false;
|
||||
|
||||
mutable CollapsedGeometry _lastCollapsedGeometry;
|
||||
mutable float64 _lastCollapsedRatio = 0.;
|
||||
|
||||
int _selected = -1;
|
||||
int _pressed = -1;
|
||||
|
||||
rpl::event_stream<not_null<QWheelEvent*>> _verticalScrollEvents;
|
||||
|
||||
rpl::variable<TextWithEntities> _tooltipText;
|
||||
rpl::variable<bool> _tooltipNotHidden;
|
||||
Fn<void()> _tooltipHide;
|
||||
std::unique_ptr<Ui::ImportantTooltip> _tooltip;
|
||||
bool _tooltipWindowActive = false;
|
||||
|
||||
base::unique_qptr<Ui::PopupMenu> _menu;
|
||||
base::has_weak_ptr _menuGuard;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Dialogs::Stories
|
||||
2973
Telegram/SourceFiles/dialogs/ui/dialogs_suggestions.cpp
Normal file
2973
Telegram/SourceFiles/dialogs/ui/dialogs_suggestions.cpp
Normal file
File diff suppressed because it is too large
Load Diff
305
Telegram/SourceFiles/dialogs/ui/dialogs_suggestions.h
Normal file
305
Telegram/SourceFiles/dialogs/ui/dialogs_suggestions.h
Normal file
@@ -0,0 +1,305 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "base/object_ptr.h"
|
||||
#include "base/timer.h"
|
||||
#include "dialogs/ui/top_peers_strip.h"
|
||||
#include "ui/controls/swipe_handler_data.h"
|
||||
#include "ui/effects/animations.h"
|
||||
#include "ui/rp_widget.h"
|
||||
|
||||
class PeerListContent;
|
||||
|
||||
namespace Data {
|
||||
class Thread;
|
||||
} // namespace Data
|
||||
|
||||
namespace Info {
|
||||
class WrapWidget;
|
||||
} // namespace Info
|
||||
|
||||
namespace Main {
|
||||
class Session;
|
||||
} // namespace Main
|
||||
|
||||
namespace Storage {
|
||||
enum class SharedMediaType : signed char;
|
||||
} // namespace Storage
|
||||
|
||||
namespace Ui::Controls {
|
||||
struct SwipeHandlerArgs;
|
||||
} // namespace Ui::Controls
|
||||
|
||||
namespace Ui {
|
||||
class BoxContent;
|
||||
class ScrollArea;
|
||||
class ElasticScroll;
|
||||
class SettingsSlider;
|
||||
class VerticalLayout;
|
||||
template <typename Widget>
|
||||
class SlideWrap;
|
||||
} // namespace Ui
|
||||
|
||||
namespace Window {
|
||||
class SessionController;
|
||||
} // namespace Window
|
||||
|
||||
namespace Dialogs {
|
||||
|
||||
class InnerWidget;
|
||||
class PostsSearch;
|
||||
class PostsSearchIntro;
|
||||
struct PostsSearchIntroState;
|
||||
enum class SearchEmptyIcon;
|
||||
|
||||
struct RecentPeersList {
|
||||
std::vector<not_null<PeerData*>> list;
|
||||
};
|
||||
|
||||
class Suggestions final : public Ui::RpWidget {
|
||||
public:
|
||||
Suggestions(
|
||||
not_null<QWidget*> parent,
|
||||
not_null<Window::SessionController*> controller,
|
||||
rpl::producer<TopPeersList> topPeers,
|
||||
RecentPeersList recentPeers);
|
||||
~Suggestions();
|
||||
|
||||
void selectJump(Qt::Key direction, int pageSize = 0);
|
||||
void chooseRow();
|
||||
|
||||
bool consumeSearchQuery(const QString &query);
|
||||
[[nodiscard]] rpl::producer<> clearSearchQueryRequests() const;
|
||||
|
||||
[[nodiscard]] Data::Thread *updateFromParentDrag(QPoint globalPosition);
|
||||
void dragLeft();
|
||||
|
||||
void show(anim::type animated, Fn<void()> finish);
|
||||
void hide(anim::type animated, Fn<void()> finish);
|
||||
[[nodiscard]] float64 shownOpacity() const;
|
||||
|
||||
[[nodiscard]] bool persist() const;
|
||||
void clearPersistance();
|
||||
|
||||
[[nodiscard]] rpl::producer<not_null<PeerData*>> topPeerChosen() const {
|
||||
return _topPeerChosen.events();
|
||||
}
|
||||
[[nodiscard]] auto recentPeerChosen() const
|
||||
-> rpl::producer<not_null<PeerData*>> {
|
||||
return _recent->chosen.events();
|
||||
}
|
||||
[[nodiscard]] auto myChannelChosen() const
|
||||
-> rpl::producer<not_null<PeerData*>> {
|
||||
return _myChannels->chosen.events();
|
||||
}
|
||||
[[nodiscard]] auto recommendationChosen() const
|
||||
-> rpl::producer<not_null<PeerData*>> {
|
||||
return _recommendations->chosen.events();
|
||||
}
|
||||
[[nodiscard]] auto recentAppChosen() const
|
||||
-> rpl::producer<not_null<PeerData*>> {
|
||||
return _recentApps->chosen.events();
|
||||
}
|
||||
[[nodiscard]] auto popularAppChosen() const
|
||||
-> rpl::producer<not_null<PeerData*>> {
|
||||
return _popularApps->chosen.events();
|
||||
}
|
||||
[[nodiscard]] auto openBotMainAppRequests() const
|
||||
-> rpl::producer<not_null<PeerData*>> {
|
||||
return _openBotMainAppRequests.events();
|
||||
}
|
||||
[[nodiscard]] rpl::producer<> closeRequests() const {
|
||||
return _closeRequests.events();
|
||||
}
|
||||
|
||||
class ObjectListController;
|
||||
|
||||
private:
|
||||
using MediaType = Storage::SharedMediaType;
|
||||
enum class Tab : uchar {
|
||||
Chats,
|
||||
Channels,
|
||||
Apps,
|
||||
Posts,
|
||||
Media,
|
||||
Downloads,
|
||||
};
|
||||
enum class JumpResult : uchar {
|
||||
NotApplied,
|
||||
Applied,
|
||||
AppliedAndOut,
|
||||
};
|
||||
|
||||
struct Key {
|
||||
Tab tab = Tab::Chats;
|
||||
MediaType mediaType = {};
|
||||
|
||||
friend inline auto operator<=>(Key, Key) = default;
|
||||
friend inline bool operator==(Key, Key) = default;
|
||||
};
|
||||
|
||||
struct ObjectList {
|
||||
not_null<Ui::SlideWrap<PeerListContent>*> wrap;
|
||||
rpl::variable<int> count;
|
||||
Fn<bool()> choose;
|
||||
Fn<JumpResult(Qt::Key, int)> selectJump;
|
||||
Fn<uint64(QPoint)> updateFromParentDrag;
|
||||
Fn<void()> dragLeft;
|
||||
Fn<bool(not_null<QTouchEvent*>)> processTouch;
|
||||
rpl::event_stream<not_null<PeerData*>> chosen;
|
||||
};
|
||||
|
||||
struct MediaList {
|
||||
Info::WrapWidget *wrap = nullptr;
|
||||
rpl::variable<int> count;
|
||||
};
|
||||
|
||||
[[nodiscard]] static std::vector<Key> TabKeysFor(
|
||||
not_null<Window::SessionController*> controller);
|
||||
|
||||
void paintEvent(QPaintEvent *e) override;
|
||||
void resizeEvent(QResizeEvent *e) override;
|
||||
|
||||
void setupTabs();
|
||||
void setupChats();
|
||||
void setupChannels();
|
||||
void setupApps();
|
||||
void reinstallSwipe(not_null<Ui::ElasticScroll*>);
|
||||
[[nodiscard]] auto generateIncompleteSwipeArgs()
|
||||
-> Ui::Controls::SwipeHandlerArgs;
|
||||
|
||||
void selectJumpChats(Qt::Key direction, int pageSize);
|
||||
void selectJumpChannels(Qt::Key direction, int pageSize);
|
||||
void selectJumpApps(Qt::Key direction, int pageSize);
|
||||
|
||||
[[nodiscard]] Data::Thread *updateFromChatsDrag(QPoint globalPosition);
|
||||
[[nodiscard]] Data::Thread *updateFromChannelsDrag(
|
||||
QPoint globalPosition);
|
||||
[[nodiscard]] Data::Thread *updateFromAppsDrag(QPoint globalPosition);
|
||||
[[nodiscard]] Data::Thread *fromListId(uint64 peerListRowId);
|
||||
|
||||
[[nodiscard]] std::unique_ptr<ObjectList> setupRecentPeers(
|
||||
RecentPeersList recentPeers);
|
||||
[[nodiscard]] auto setupEmptyRecent()
|
||||
-> object_ptr<Ui::SlideWrap<Ui::RpWidget>>;
|
||||
|
||||
[[nodiscard]] std::unique_ptr<ObjectList> setupMyChannels();
|
||||
[[nodiscard]] std::unique_ptr<ObjectList> setupRecommendations();
|
||||
[[nodiscard]] auto setupEmptyChannels()
|
||||
-> object_ptr<Ui::SlideWrap<Ui::RpWidget>>;
|
||||
|
||||
[[nodiscard]] std::unique_ptr<ObjectList> setupRecentApps();
|
||||
[[nodiscard]] std::unique_ptr<ObjectList> setupPopularApps();
|
||||
|
||||
[[nodiscard]] std::unique_ptr<ObjectList> setupObjectList(
|
||||
not_null<Ui::ElasticScroll*> scroll,
|
||||
not_null<Ui::VerticalLayout*> parent,
|
||||
not_null<ObjectListController*> controller,
|
||||
Fn<int()> addToScroll = nullptr);
|
||||
|
||||
[[nodiscard]] object_ptr<Ui::SlideWrap<Ui::RpWidget>> setupEmpty(
|
||||
not_null<QWidget*> parent,
|
||||
SearchEmptyIcon icon,
|
||||
rpl::producer<QString> text);
|
||||
|
||||
void switchTab(Key key);
|
||||
void startShownAnimation(bool shown, Fn<void()> finish);
|
||||
void startSlideAnimation(Key was, Key now);
|
||||
void ensureContent(Key key);
|
||||
void finishShow();
|
||||
|
||||
void handlePressForChatPreview(PeerId id, Fn<void(bool)> callback);
|
||||
void updateControlsGeometry();
|
||||
void applySearchQuery();
|
||||
|
||||
void setupPostsSearch();
|
||||
void setPostsSearchQuery(const QString &query);
|
||||
void setupPostsResults();
|
||||
void setupPostsIntro(const PostsSearchIntroState &intro);
|
||||
void updatePostsSearchVisibleRange();
|
||||
|
||||
const not_null<Window::SessionController*> _controller;
|
||||
|
||||
const std::unique_ptr<Ui::ScrollArea> _tabsScroll;
|
||||
const not_null<Ui::SettingsSlider*> _tabs;
|
||||
Ui::Animations::Simple _tabsScrollAnimation;
|
||||
const std::vector<Key> _tabKeys;
|
||||
rpl::variable<Key> _key;
|
||||
|
||||
const std::unique_ptr<Ui::ElasticScroll> _chatsScroll;
|
||||
const not_null<Ui::VerticalLayout*> _chatsContent;
|
||||
|
||||
const not_null<Ui::SlideWrap<TopPeersStrip>*> _topPeersWrap;
|
||||
const not_null<TopPeersStrip*> _topPeers;
|
||||
rpl::event_stream<not_null<PeerData*>> _topPeerChosen;
|
||||
rpl::event_stream<not_null<PeerData*>> _openBotMainAppRequests;
|
||||
rpl::event_stream<> _closeRequests;
|
||||
|
||||
const std::unique_ptr<ObjectList> _recent;
|
||||
|
||||
const not_null<Ui::SlideWrap<Ui::RpWidget>*> _emptyRecent;
|
||||
|
||||
const std::unique_ptr<Ui::ElasticScroll> _channelsScroll;
|
||||
const not_null<Ui::VerticalLayout*> _channelsContent;
|
||||
|
||||
const std::unique_ptr<ObjectList> _myChannels;
|
||||
const std::unique_ptr<ObjectList> _recommendations;
|
||||
|
||||
const not_null<Ui::SlideWrap<Ui::RpWidget>*> _emptyChannels;
|
||||
|
||||
const std::unique_ptr<Ui::ElasticScroll> _appsScroll;
|
||||
const not_null<Ui::VerticalLayout*> _appsContent;
|
||||
|
||||
std::unique_ptr<PostsSearch> _postsSearch;
|
||||
const std::unique_ptr<Ui::ElasticScroll> _postsScroll;
|
||||
const not_null<Ui::RpWidget*> _postsWrap;
|
||||
PostsSearchIntro *_postsSearchIntro = nullptr;
|
||||
InnerWidget *_postsContent = nullptr;
|
||||
|
||||
rpl::producer<> _recentAppsRefreshed;
|
||||
Fn<bool(not_null<PeerData*>)> _recentAppsShows;
|
||||
const std::unique_ptr<ObjectList> _recentApps;
|
||||
const std::unique_ptr<ObjectList> _popularApps;
|
||||
|
||||
base::flat_map<Key, MediaList> _mediaLists;
|
||||
rpl::event_stream<> _clearSearchQueryRequests;
|
||||
QString _searchQuery;
|
||||
base::Timer _searchQueryTimer;
|
||||
|
||||
Ui::Animations::Simple _shownAnimation;
|
||||
Fn<void()> _showFinished;
|
||||
bool _hidden = false;
|
||||
bool _persist = false;
|
||||
QPixmap _cache;
|
||||
|
||||
Ui::Animations::Simple _slideAnimation;
|
||||
QPixmap _slideLeft;
|
||||
QPixmap _slideRight;
|
||||
|
||||
Ui::Controls::SwipeBackResult _swipeBackData;
|
||||
rpl::lifetime _swipeLifetime;
|
||||
|
||||
int _slideLeftTop = 0;
|
||||
int _slideRightTop = 0;
|
||||
|
||||
};
|
||||
|
||||
[[nodiscard]] rpl::producer<TopPeersList> TopPeersContent(
|
||||
not_null<Main::Session*> session);
|
||||
|
||||
[[nodiscard]] RecentPeersList RecentPeersContent(
|
||||
not_null<Main::Session*> session);
|
||||
|
||||
[[nodiscard]] object_ptr<Ui::BoxContent> StarsExamplesBox(
|
||||
not_null<Window::SessionController*> window);
|
||||
|
||||
[[nodiscard]] object_ptr<Ui::BoxContent> PopularAppsAboutBox(
|
||||
not_null<Window::SessionController*> window);
|
||||
|
||||
} // namespace Dialogs
|
||||
@@ -0,0 +1,527 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#include "dialogs/ui/dialogs_top_bar_suggestion_content.h"
|
||||
|
||||
#include "base/call_delayed.h"
|
||||
#include "data/data_authorization.h"
|
||||
#include "lang/lang_keys.h"
|
||||
#include "lottie/lottie_icon.h"
|
||||
#include "settings/settings_common.h"
|
||||
#include "ui/effects/animation_value.h"
|
||||
#include "ui/layers/generic_box.h"
|
||||
#include "ui/painter.h"
|
||||
#include "ui/power_saving.h"
|
||||
#include "ui/rect.h"
|
||||
#include "ui/text/format_values.h"
|
||||
#include "ui/text/text_custom_emoji.h"
|
||||
#include "ui/ui_rpl_filter.h"
|
||||
#include "ui/vertical_list.h"
|
||||
#include "ui/vertical_list.h"
|
||||
#include "ui/widgets/buttons.h"
|
||||
#include "ui/widgets/labels.h"
|
||||
#include "ui/widgets/shadow.h"
|
||||
#include "ui/wrap/fade_wrap.h"
|
||||
#include "ui/wrap/padding_wrap.h"
|
||||
#include "ui/wrap/slide_wrap.h"
|
||||
#include "ui/wrap/vertical_layout.h"
|
||||
#include "styles/style_boxes.h"
|
||||
#include "styles/style_chat_helpers.h"
|
||||
#include "styles/style_chat.h"
|
||||
#include "styles/style_dialogs.h"
|
||||
#include "styles/style_layers.h"
|
||||
#include "styles/style_premium.h"
|
||||
#include "styles/style_settings.h"
|
||||
|
||||
namespace Dialogs {
|
||||
|
||||
class UnconfirmedAuthWrap : public Ui::SlideWrap<Ui::VerticalLayout> {
|
||||
public:
|
||||
UnconfirmedAuthWrap(
|
||||
not_null<Ui::RpWidget*> parent,
|
||||
object_ptr<Ui::VerticalLayout> &&child)
|
||||
: Ui::SlideWrap<Ui::VerticalLayout>(parent, std::move(child)) {
|
||||
}
|
||||
|
||||
rpl::producer<int> desiredHeightValue() const override {
|
||||
return entity()->heightValue();
|
||||
}
|
||||
};
|
||||
|
||||
not_null<Ui::SlideWrap<Ui::VerticalLayout>*> CreateUnconfirmedAuthContent(
|
||||
not_null<Ui::RpWidget*> parent,
|
||||
const std::vector<Data::UnreviewedAuth> &list,
|
||||
Fn<void(bool)> callback) {
|
||||
const auto wrap = Ui::CreateChild<UnconfirmedAuthWrap>(
|
||||
parent,
|
||||
object_ptr<Ui::VerticalLayout>(parent));
|
||||
const auto content = wrap->entity();
|
||||
content->paintRequest() | rpl::on_next([=] {
|
||||
auto p = QPainter(content);
|
||||
p.fillRect(content->rect(), st::dialogsBg);
|
||||
}, content->lifetime());
|
||||
|
||||
const auto padding = st::dialogsUnconfirmedAuthPadding;
|
||||
|
||||
Ui::AddSkip(content);
|
||||
|
||||
content->add(
|
||||
object_ptr<Ui::FlatLabel>(
|
||||
content,
|
||||
tr::lng_unconfirmed_auth_title(),
|
||||
st::dialogsUnconfirmedAuthTitle),
|
||||
padding,
|
||||
style::al_top);
|
||||
|
||||
Ui::AddSkip(content);
|
||||
|
||||
auto messageText = QString();
|
||||
if (list.size() == 1) {
|
||||
const auto &auth = list.at(0);
|
||||
messageText = tr::lng_unconfirmed_auth_single(
|
||||
tr::now,
|
||||
lt_from,
|
||||
auth.device,
|
||||
lt_country,
|
||||
auth.location);
|
||||
} else {
|
||||
auto commonLocation = list.at(0).location;
|
||||
for (auto i = 1; i < list.size(); ++i) {
|
||||
if (commonLocation != list.at(i).location) {
|
||||
commonLocation.clear();
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (commonLocation.isEmpty()) {
|
||||
messageText = tr::lng_unconfirmed_auth_multiple(
|
||||
tr::now,
|
||||
lt_count,
|
||||
list.size());
|
||||
} else {
|
||||
messageText = tr::lng_unconfirmed_auth_multiple_from(
|
||||
tr::now,
|
||||
lt_count,
|
||||
list.size(),
|
||||
lt_country,
|
||||
commonLocation);
|
||||
}
|
||||
}
|
||||
|
||||
content->add(
|
||||
object_ptr<Ui::FlatLabel>(
|
||||
content,
|
||||
rpl::single(messageText),
|
||||
st::dialogsUnconfirmedAuthAbout),
|
||||
padding,
|
||||
style::al_top)->setTryMakeSimilarLines(true);
|
||||
|
||||
Ui::AddSkip(content);
|
||||
const auto buttons = content->add(object_ptr<Ui::FixedHeightWidget>(
|
||||
content,
|
||||
st::dialogsUnconfirmedAuthButton.height));
|
||||
const auto yes = Ui::CreateChild<Ui::RoundButton>(
|
||||
buttons,
|
||||
tr::lng_unconfirmed_auth_confirm(),
|
||||
st::dialogsUnconfirmedAuthButton);
|
||||
const auto no = Ui::CreateChild<Ui::RoundButton>(
|
||||
buttons,
|
||||
tr::lng_unconfirmed_auth_deny(),
|
||||
st::dialogsUnconfirmedAuthButtonNo);
|
||||
yes->setTextTransform(Ui::RoundButton::TextTransform::NoTransform);
|
||||
no->setTextTransform(Ui::RoundButton::TextTransform::NoTransform);
|
||||
yes->setClickedCallback([=] {
|
||||
wrap->toggle(false, anim::type::normal);
|
||||
base::call_delayed(st::universalDuration, wrap, [=] {
|
||||
callback(true);
|
||||
});
|
||||
});
|
||||
no->setClickedCallback([=] {
|
||||
wrap->toggle(false, anim::type::normal);
|
||||
base::call_delayed(st::universalDuration, wrap, [=] {
|
||||
callback(false);
|
||||
});
|
||||
});
|
||||
buttons->sizeValue(
|
||||
) | rpl::filter_size(
|
||||
) | rpl::on_next([=](const QSize &s) {
|
||||
const auto halfWidth = (s.width() - rect::m::sum::h(padding)) / 2;
|
||||
yes->moveToLeft(
|
||||
padding.left() + (halfWidth - yes->width()) / 2,
|
||||
0);
|
||||
no->moveToLeft(
|
||||
padding.left() + halfWidth + (halfWidth - no->width()) / 2,
|
||||
0);
|
||||
}, buttons->lifetime());
|
||||
Ui::AddSkip(content);
|
||||
content->add(object_ptr<Ui::FadeShadow>(content));
|
||||
|
||||
return wrap;
|
||||
}
|
||||
|
||||
void ShowAuthDeniedBox(
|
||||
not_null<Ui::GenericBox*> box,
|
||||
float64 count,
|
||||
const QString &messageText) {
|
||||
box->setStyle(st::showOrBox);
|
||||
box->setWidth(st::boxWideWidth);
|
||||
const auto buttonPadding = QMargins(
|
||||
st::showOrBox.buttonPadding.left(),
|
||||
0,
|
||||
st::showOrBox.buttonPadding.right(),
|
||||
0);
|
||||
auto icon = Settings::CreateLottieIcon(
|
||||
box,
|
||||
{
|
||||
.name = u"ban"_q,
|
||||
.sizeOverride = st::dialogsSuggestionDeniedAuthLottie,
|
||||
},
|
||||
st::dialogsSuggestionDeniedAuthLottieMargins);
|
||||
Settings::AddLottieIconWithCircle(
|
||||
box->verticalLayout(),
|
||||
std::move(icon.widget),
|
||||
st::settingsBlockedListIconPadding,
|
||||
st::dialogsSuggestionDeniedAuthLottieCircle);
|
||||
box->setShowFinishedCallback([=, animate = std::move(icon.animate)] {
|
||||
animate(anim::repeat::once);
|
||||
});
|
||||
Ui::AddSkip(box->verticalLayout());
|
||||
box->addRow(
|
||||
object_ptr<Ui::FlatLabel>(
|
||||
box,
|
||||
tr::lng_unconfirmed_auth_denied_title(
|
||||
lt_count,
|
||||
rpl::single(count)),
|
||||
st::boostCenteredTitle),
|
||||
st::showOrTitlePadding + buttonPadding,
|
||||
style::al_top);
|
||||
Ui::AddSkip(box->verticalLayout());
|
||||
box->addRow(
|
||||
object_ptr<Ui::FlatLabel>(
|
||||
box,
|
||||
messageText,
|
||||
st::boostText),
|
||||
st::showOrAboutPadding + buttonPadding,
|
||||
style::al_top);
|
||||
Ui::AddSkip(box->verticalLayout());
|
||||
const auto warning = box->addRow(
|
||||
object_ptr<Ui::FlatLabel>(
|
||||
box,
|
||||
tr::lng_unconfirmed_auth_denied_warning(tr::bold),
|
||||
st::boostText),
|
||||
st::showOrAboutPadding + buttonPadding
|
||||
+ QMargins(st::boostTextSkip, 0, st::boostTextSkip, 0),
|
||||
style::al_top);
|
||||
warning->setTextColorOverride(st::attentionButtonFg->c);
|
||||
const auto warningBg = Ui::CreateChild<Ui::RpWidget>(
|
||||
box->verticalLayout());
|
||||
warning->geometryValue() | rpl::on_next([=](QRect r) {
|
||||
warningBg->setGeometry(r + Margins(st::boostTextSkip));
|
||||
}, warningBg->lifetime());
|
||||
warningBg->paintOn([=](QPainter &p) {
|
||||
auto hq = PainterHighQualityEnabler(p);
|
||||
p.setPen(Qt::NoPen);
|
||||
p.setBrush(st::attentionButtonBgOver);
|
||||
p.drawRoundedRect(
|
||||
warningBg->rect(),
|
||||
st::buttonRadius,
|
||||
st::buttonRadius);
|
||||
});
|
||||
warningBg->show();
|
||||
warning->raise();
|
||||
warningBg->stackUnder(warning);
|
||||
const auto confirm = box->addButton(
|
||||
object_ptr<Ui::RoundButton>(
|
||||
box,
|
||||
rpl::single(QString()),
|
||||
st::defaultActiveButton));
|
||||
confirm->setClickedCallback([=] {
|
||||
box->closeBox();
|
||||
});
|
||||
confirm->resize(
|
||||
st::showOrShowButton.width,
|
||||
st::showOrShowButton.height);
|
||||
|
||||
const auto textLabel = Ui::CreateChild<Ui::FlatLabel>(
|
||||
confirm,
|
||||
tr::lng_archive_hint_button(),
|
||||
st::defaultSubsectionTitle);
|
||||
textLabel->setTextColorOverride(st::defaultActiveButton.textFg->c);
|
||||
textLabel->setAttribute(Qt::WA_TransparentForMouseEvents);
|
||||
|
||||
const auto timerLabel = Ui::CreateChild<Ui::FlatLabel>(
|
||||
confirm,
|
||||
rpl::single(QString()),
|
||||
st::defaultSubsectionTitle);
|
||||
timerLabel->setTextColorOverride(
|
||||
anim::with_alpha(st::defaultActiveButton.textFg->c, 0.75));
|
||||
timerLabel->setAttribute(Qt::WA_TransparentForMouseEvents);
|
||||
|
||||
constexpr auto kTimer = 5;
|
||||
const auto remaining = confirm->lifetime().make_state<int>(kTimer);
|
||||
const auto timerLifetime
|
||||
= confirm->lifetime().make_state<rpl::lifetime>();
|
||||
const auto timer = timerLifetime->make_state<base::Timer>([=] {
|
||||
if ((*remaining) > 0) {
|
||||
timerLabel->setText(QString::number((*remaining)--));
|
||||
} else {
|
||||
timerLabel->hide();
|
||||
confirm->setAttribute(Qt::WA_TransparentForMouseEvents, false);
|
||||
box->setCloseByEscape(true);
|
||||
box->setCloseByOutsideClick(true);
|
||||
timerLifetime->destroy();
|
||||
}
|
||||
});
|
||||
box->setCloseByEscape(false);
|
||||
box->setCloseByOutsideClick(false);
|
||||
confirm->setAttribute(Qt::WA_TransparentForMouseEvents, true);
|
||||
timerLabel->setText(QString::number((*remaining)));
|
||||
timer->callEach(1000);
|
||||
|
||||
rpl::combine(
|
||||
confirm->sizeValue(),
|
||||
textLabel->sizeValue(),
|
||||
timerLabel->sizeValue(),
|
||||
timerLabel->shownValue()
|
||||
) | rpl::on_next([=](QSize btn, QSize text, QSize timer, bool shown) {
|
||||
const auto skip = st::normalFont->spacew;
|
||||
const auto totalWidth = shown
|
||||
? (text.width() + skip + timer.width())
|
||||
: text.width();
|
||||
const auto left = (btn.width() - totalWidth) / 2;
|
||||
textLabel->moveToLeft(left, (btn.height() - text.height()) / 2);
|
||||
timerLabel->moveToLeft(
|
||||
left + text.width() + skip,
|
||||
(btn.height() - timer.height()) / 2);
|
||||
}, confirm->lifetime());
|
||||
}
|
||||
|
||||
TopBarSuggestionContent::TopBarSuggestionContent(
|
||||
not_null<Ui::RpWidget*> parent,
|
||||
Fn<bool()> emojiPaused)
|
||||
: Ui::RippleButton(parent, st::defaultRippleAnimationBgOver)
|
||||
, _titleSt(st::semiboldTextStyle)
|
||||
, _contentTitleSt(st::dialogsTopBarSuggestionTitleStyle)
|
||||
, _contentTextSt(st::dialogsTopBarSuggestionAboutStyle)
|
||||
, _emojiPaused(std::move(emojiPaused)) {
|
||||
setRightIcon(RightIcon::Close);
|
||||
}
|
||||
|
||||
void TopBarSuggestionContent::setRightIcon(RightIcon icon) {
|
||||
_rightButton = nullptr;
|
||||
if (icon == _rightIcon) {
|
||||
return;
|
||||
}
|
||||
_rightHide = nullptr;
|
||||
_rightArrow = nullptr;
|
||||
_rightIcon = icon;
|
||||
if (icon == RightIcon::Close) {
|
||||
_rightHide = base::make_unique_q<Ui::IconButton>(
|
||||
this,
|
||||
st::dialogsCancelSearchInPeer);
|
||||
const auto rightHide = _rightHide.get();
|
||||
sizeValue() | rpl::filter_size(
|
||||
) | rpl::on_next([=](const QSize &s) {
|
||||
rightHide->moveToRight(st::buttonRadius, st::lineWidth);
|
||||
}, rightHide->lifetime());
|
||||
rightHide->show();
|
||||
} else if (icon == RightIcon::Arrow) {
|
||||
_rightArrow = base::make_unique_q<Ui::IconButton>(
|
||||
this,
|
||||
st::backButton);
|
||||
const auto arrow = _rightArrow.get();
|
||||
arrow->setIconOverride(
|
||||
&st::settingsPremiumArrow,
|
||||
&st::settingsPremiumArrowOver);
|
||||
arrow->setAttribute(Qt::WA_TransparentForMouseEvents);
|
||||
sizeValue() | rpl::filter_size(
|
||||
) | rpl::on_next([=](const QSize &s) {
|
||||
const auto &point = st::settingsPremiumArrowShift;
|
||||
arrow->moveToLeft(
|
||||
s.width() - arrow->width(),
|
||||
point.y() + (s.height() - arrow->height()) / 2);
|
||||
}, arrow->lifetime());
|
||||
arrow->show();
|
||||
}
|
||||
}
|
||||
|
||||
void TopBarSuggestionContent::setRightButton(
|
||||
rpl::producer<TextWithEntities> text,
|
||||
Fn<void()> callback) {
|
||||
_rightHide = nullptr;
|
||||
_rightArrow = nullptr;
|
||||
_rightIcon = RightIcon::None;
|
||||
if (!text) {
|
||||
_rightButton = nullptr;
|
||||
return;
|
||||
}
|
||||
using namespace Ui;
|
||||
_rightButton = base::make_unique_q<RoundButton>(
|
||||
this,
|
||||
rpl::single(QString()),
|
||||
st::dialogsTopBarRightButton);
|
||||
_rightButton->setText(std::move(text));
|
||||
rpl::combine(
|
||||
sizeValue(),
|
||||
_rightButton->sizeValue()
|
||||
) | rpl::on_next([=](QSize outer, QSize inner) {
|
||||
const auto top = (outer.height() - inner.height()) / 2;
|
||||
_rightButton->moveToRight(top, top, outer.width());
|
||||
}, _rightButton->lifetime());
|
||||
_rightButton->setFullRadius(true);
|
||||
_rightButton->setTextTransform(RoundButton::TextTransform::NoTransform);
|
||||
_rightButton->setClickedCallback(std::move(callback));
|
||||
_rightButton->show();
|
||||
}
|
||||
|
||||
void TopBarSuggestionContent::draw(QPainter &p) {
|
||||
const auto kLinesForPhoto = 3;
|
||||
|
||||
const auto r = Ui::RpWidget::rect();
|
||||
p.fillRect(r, st::historyPinnedBg);
|
||||
p.fillRect(
|
||||
r.x(),
|
||||
r.y() + r.height() - st::lineWidth,
|
||||
r.width(),
|
||||
st::lineWidth,
|
||||
st::shadowFg);
|
||||
Ui::RippleButton::paintRipple(p, 0, 0);
|
||||
const auto leftPadding = _leftPadding;
|
||||
const auto rightPadding = 0;
|
||||
const auto topPadding = st::msgReplyPadding.top();
|
||||
const auto availableWidthNoPhoto = r.width()
|
||||
- (_rightArrow
|
||||
? (_rightArrow->width() / 4 * 3) // Takes full height.
|
||||
: 0)
|
||||
- leftPadding
|
||||
- rightPadding;
|
||||
const auto availableWidth = availableWidthNoPhoto
|
||||
- (_rightHide ? _rightHide->width() : 0);
|
||||
const auto titleRight = leftPadding;
|
||||
const auto hasSecondLineTitle = availableWidth < _contentTitle.maxWidth();
|
||||
const auto paused = On(PowerSaving::kEmojiChat)
|
||||
|| (_emojiPaused && _emojiPaused());
|
||||
p.setPen(st::windowActiveTextFg);
|
||||
p.setPen(st::windowFg);
|
||||
{
|
||||
const auto left = leftPadding;
|
||||
const auto top = topPadding;
|
||||
_contentTitle.draw(p, {
|
||||
.position = QPoint(left, top),
|
||||
.outerWidth = hasSecondLineTitle
|
||||
? availableWidth
|
||||
: (availableWidth - titleRight),
|
||||
.availableWidth = availableWidth,
|
||||
.pausedEmoji = paused,
|
||||
.elisionLines = hasSecondLineTitle ? 2 : 1,
|
||||
});
|
||||
}
|
||||
{
|
||||
const auto left = leftPadding;
|
||||
const auto top = hasSecondLineTitle
|
||||
? (topPadding
|
||||
+ _titleSt.font->height
|
||||
+ _contentTitleSt.font->height)
|
||||
: topPadding + _titleSt.font->height;
|
||||
auto lastContentLineAmount = 0;
|
||||
const auto lineHeight = _contentTextSt.font->height;
|
||||
const auto lineLayout = [&](int line) -> Ui::Text::LineGeometry {
|
||||
line++;
|
||||
lastContentLineAmount = line;
|
||||
const auto diff = (st::sponsoredMessageBarMaxHeight)
|
||||
- line * lineHeight;
|
||||
if (diff < 3 * lineHeight) {
|
||||
return {
|
||||
.width = availableWidthNoPhoto,
|
||||
.elided = true,
|
||||
};
|
||||
} else if (diff < 2 * lineHeight) {
|
||||
return {};
|
||||
}
|
||||
line += (hasSecondLineTitle ? 2 : 1) + 1;
|
||||
return {
|
||||
.width = (line > kLinesForPhoto)
|
||||
? availableWidthNoPhoto
|
||||
: availableWidth,
|
||||
};
|
||||
};
|
||||
p.setPen(_descriptionColorOverride.value_or(st::windowSubTextFg->c));
|
||||
_contentText.draw(p, {
|
||||
.position = QPoint(left, top),
|
||||
.outerWidth = availableWidth,
|
||||
.availableWidth = availableWidth,
|
||||
.geometry = Ui::Text::GeometryDescriptor{
|
||||
.layout = std::move(lineLayout),
|
||||
},
|
||||
.pausedEmoji = paused,
|
||||
});
|
||||
_lastPaintedContentTop = top;
|
||||
_lastPaintedContentLineAmount = lastContentLineAmount;
|
||||
}
|
||||
}
|
||||
|
||||
void TopBarSuggestionContent::setContent(
|
||||
TextWithEntities title,
|
||||
TextWithEntities description,
|
||||
std::optional<Ui::Text::MarkedContext> context,
|
||||
std::optional<QColor> descriptionColorOverride) {
|
||||
_descriptionColorOverride = descriptionColorOverride;
|
||||
if (context) {
|
||||
context->repaint = [=] { update(); };
|
||||
_contentTitle.setMarkedText(
|
||||
_contentTitleSt,
|
||||
std::move(title),
|
||||
kMarkupTextOptions,
|
||||
*context);
|
||||
_contentText.setMarkedText(
|
||||
_contentTextSt,
|
||||
std::move(description),
|
||||
kMarkupTextOptions,
|
||||
base::take(*context));
|
||||
} else {
|
||||
_contentTitle.setMarkedText(_contentTitleSt, std::move(title));
|
||||
_contentText.setMarkedText(_contentTextSt, std::move(description));
|
||||
}
|
||||
update();
|
||||
}
|
||||
|
||||
void TopBarSuggestionContent::paintEvent(QPaintEvent *) {
|
||||
auto p = QPainter(this);
|
||||
draw(p);
|
||||
}
|
||||
|
||||
rpl::producer<int> TopBarSuggestionContent::desiredHeightValue() const {
|
||||
return rpl::combine(
|
||||
_lastPaintedContentTop.value(),
|
||||
_lastPaintedContentLineAmount.value()
|
||||
) | rpl::distinct_until_changed() | rpl::map([=](
|
||||
int lastTop,
|
||||
int lastLines) {
|
||||
const auto bottomPadding = st::msgReplyPadding.top();
|
||||
const auto desiredHeight = lastTop
|
||||
+ (lastLines * _contentTextSt.font->height)
|
||||
+ bottomPadding;
|
||||
return std::min(desiredHeight, st::sponsoredMessageBarMaxHeight);
|
||||
});
|
||||
}
|
||||
|
||||
void TopBarSuggestionContent::setHideCallback(Fn<void()> hideCallback) {
|
||||
Expects(_rightHide != nullptr);
|
||||
_rightHide->setClickedCallback(std::move(hideCallback));
|
||||
}
|
||||
|
||||
void TopBarSuggestionContent::setLeftPadding(rpl::producer<int> value) {
|
||||
std::move(value) | rpl::on_next([=](int padding) {
|
||||
_leftPadding = padding;
|
||||
update();
|
||||
}, lifetime());
|
||||
}
|
||||
|
||||
const style::TextStyle & TopBarSuggestionContent::contentTitleSt() const {
|
||||
return _contentTitleSt;
|
||||
}
|
||||
|
||||
} // namespace Dialogs
|
||||
@@ -0,0 +1,101 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "ui/widgets/buttons.h"
|
||||
|
||||
namespace Ui {
|
||||
class DynamicImage;
|
||||
class GenericBox;
|
||||
class IconButton;
|
||||
class VerticalLayout;
|
||||
template<typename Widget>
|
||||
class SlideWrap;
|
||||
} // namespace Ui
|
||||
|
||||
namespace Ui::Text {
|
||||
struct MarkedContext;
|
||||
} // namespace Ui::Text
|
||||
|
||||
namespace Data {
|
||||
struct UnreviewedAuth;
|
||||
} // namespace Data
|
||||
|
||||
namespace Dialogs {
|
||||
|
||||
not_null<Ui::SlideWrap<Ui::VerticalLayout>*> CreateUnconfirmedAuthContent(
|
||||
not_null<Ui::RpWidget*> parent,
|
||||
const std::vector<Data::UnreviewedAuth> &list,
|
||||
Fn<void(bool)> callback);
|
||||
|
||||
void ShowAuthDeniedBox(
|
||||
not_null<Ui::GenericBox*> box,
|
||||
float64 count,
|
||||
const QString &messageText);
|
||||
|
||||
class TopBarSuggestionContent : public Ui::RippleButton {
|
||||
public:
|
||||
enum class RightIcon {
|
||||
None,
|
||||
Close,
|
||||
Arrow,
|
||||
};
|
||||
|
||||
TopBarSuggestionContent(
|
||||
not_null<Ui::RpWidget*> parent,
|
||||
Fn<bool()> emojiPaused = nullptr);
|
||||
|
||||
void setContent(
|
||||
TextWithEntities title,
|
||||
TextWithEntities description,
|
||||
std::optional<Ui::Text::MarkedContext> context = std::nullopt,
|
||||
std::optional<QColor> descriptionColorOverride = std::nullopt);
|
||||
|
||||
[[nodiscard]] rpl::producer<int> desiredHeightValue() const override;
|
||||
|
||||
void setHideCallback(Fn<void()>);
|
||||
void setRightIcon(RightIcon);
|
||||
void setRightButton(
|
||||
rpl::producer<TextWithEntities> text,
|
||||
Fn<void()> callback);
|
||||
void setLeftPadding(rpl::producer<int>);
|
||||
|
||||
[[nodiscard]] const style::TextStyle &contentTitleSt() const;
|
||||
|
||||
protected:
|
||||
void paintEvent(QPaintEvent *) override;
|
||||
|
||||
private:
|
||||
void draw(QPainter &p);
|
||||
|
||||
const style::TextStyle &_titleSt;
|
||||
const style::TextStyle &_contentTitleSt;
|
||||
const style::TextStyle &_contentTextSt;
|
||||
|
||||
Ui::Text::String _contentTitle;
|
||||
Ui::Text::String _contentText;
|
||||
rpl::variable<int> _lastPaintedContentLineAmount = 0;
|
||||
rpl::variable<int> _lastPaintedContentTop = 0;
|
||||
std::optional<QColor> _descriptionColorOverride;
|
||||
|
||||
base::unique_qptr<Ui::IconButton> _rightHide;
|
||||
base::unique_qptr<Ui::IconButton> _rightArrow;
|
||||
base::unique_qptr<Ui::RoundButton> _rightButton;
|
||||
Fn<void()> _hideCallback;
|
||||
Fn<bool()> _emojiPaused;
|
||||
|
||||
int _leftPadding = 0;
|
||||
|
||||
RightIcon _rightIcon = RightIcon::None;
|
||||
|
||||
std::shared_ptr<Ui::DynamicImage> _rightPhoto;
|
||||
QImage _rightPhotoImage;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Dialogs
|
||||
442
Telegram/SourceFiles/dialogs/ui/dialogs_topics_view.cpp
Normal file
442
Telegram/SourceFiles/dialogs/ui/dialogs_topics_view.cpp
Normal file
@@ -0,0 +1,442 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#include "dialogs/ui/dialogs_topics_view.h"
|
||||
|
||||
#include "dialogs/ui/dialogs_layout.h"
|
||||
#include "data/stickers/data_custom_emoji.h"
|
||||
#include "data/data_forum.h"
|
||||
#include "data/data_forum_topic.h"
|
||||
#include "data/data_peer.h"
|
||||
#include "data/data_saved_messages.h"
|
||||
#include "data/data_saved_sublist.h"
|
||||
#include "data/data_session.h"
|
||||
#include "core/ui_integration.h"
|
||||
#include "lang/lang_keys.h"
|
||||
#include "main/main_session.h"
|
||||
#include "ui/painter.h"
|
||||
#include "ui/power_saving.h"
|
||||
#include "ui/text/text_options.h"
|
||||
#include "ui/text/text_utilities.h"
|
||||
#include "ui/effects/ripple_animation.h"
|
||||
#include "styles/style_dialogs.h"
|
||||
|
||||
namespace Dialogs::Ui {
|
||||
namespace {
|
||||
|
||||
constexpr auto kIconLoopCount = 1;
|
||||
|
||||
} // namespace
|
||||
|
||||
TopicsView::TopicsView(Data::Forum *forum, Data::SavedMessages *monoforum)
|
||||
: _forum(forum)
|
||||
, _monoforum(monoforum) {
|
||||
}
|
||||
|
||||
TopicsView::~TopicsView() = default;
|
||||
|
||||
bool TopicsView::prepared() const {
|
||||
const auto version = _forum
|
||||
? _forum->recentTopicsListVersion()
|
||||
: _monoforum->recentSublistsListVersion();
|
||||
return (_version == version);
|
||||
}
|
||||
|
||||
void TopicsView::prepare(MsgId frontRootId, Fn<void()> customEmojiRepaint) {
|
||||
Expects(_forum != nullptr);
|
||||
|
||||
const auto &list = _forum->recentTopics();
|
||||
_version = _forum->recentTopicsListVersion();
|
||||
_titles.reserve(list.size());
|
||||
auto index = 0;
|
||||
for (const auto &topic : list) {
|
||||
const auto from = begin(_titles) + index;
|
||||
const auto key = topic->rootId().bare;
|
||||
const auto i = ranges::find(
|
||||
from,
|
||||
end(_titles),
|
||||
key,
|
||||
&Title::key);
|
||||
if (i != end(_titles)) {
|
||||
if (i != from) {
|
||||
ranges::rotate(from, i, i + 1);
|
||||
}
|
||||
} else if (index >= _titles.size()) {
|
||||
_titles.emplace_back();
|
||||
}
|
||||
auto &title = _titles[index++];
|
||||
const auto unread = topic->chatListBadgesState().unread;
|
||||
if (title.key == key
|
||||
&& title.unread == unread
|
||||
&& title.version == topic->titleVersion()) {
|
||||
continue;
|
||||
}
|
||||
const auto context = Core::TextContext({
|
||||
.session = &topic->session(),
|
||||
.repaint = customEmojiRepaint,
|
||||
.customEmojiLoopLimit = kIconLoopCount,
|
||||
});
|
||||
auto topicTitle = topic->titleWithIcon();
|
||||
title.key = key;
|
||||
title.version = topic->titleVersion();
|
||||
title.unread = unread;
|
||||
title.title.setMarkedText(
|
||||
st::dialogsTextStyle,
|
||||
(unread
|
||||
? Ui::Text::Colorized(
|
||||
Ui::Text::Wrapped(
|
||||
std::move(topicTitle),
|
||||
EntityType::Bold))
|
||||
: std::move(topicTitle)),
|
||||
DialogTextOptions(),
|
||||
context);
|
||||
}
|
||||
while (_titles.size() > index) {
|
||||
_titles.pop_back();
|
||||
}
|
||||
const auto i = frontRootId
|
||||
? ranges::find(_titles, frontRootId.bare, &Title::key)
|
||||
: end(_titles);
|
||||
_jumpToTopic = (i != end(_titles));
|
||||
if (_jumpToTopic) {
|
||||
if (i != begin(_titles)) {
|
||||
ranges::rotate(begin(_titles), i, i + 1);
|
||||
}
|
||||
if (!_titles.front().unread) {
|
||||
_jumpToTopic = false;
|
||||
}
|
||||
}
|
||||
_allLoaded = _forum->topicsList()->loaded();
|
||||
}
|
||||
|
||||
void TopicsView::prepare(PeerId frontPeerId, Fn<void()> customEmojiRepaint) {
|
||||
Expects(_monoforum != nullptr);
|
||||
|
||||
const auto &list = _monoforum->recentSublists();
|
||||
const auto manager = &_monoforum->session().data().customEmojiManager();
|
||||
_version = _monoforum->recentSublistsListVersion();
|
||||
_titles.reserve(list.size());
|
||||
auto index = 0;
|
||||
for (const auto &sublist : list) {
|
||||
const auto from = begin(_titles) + index;
|
||||
const auto peer = sublist->sublistPeer();
|
||||
const auto key = peer->id.value;
|
||||
const auto i = ranges::find(
|
||||
from,
|
||||
end(_titles),
|
||||
key,
|
||||
&Title::key);
|
||||
if (i != end(_titles)) {
|
||||
if (i != from) {
|
||||
ranges::rotate(from, i, i + 1);
|
||||
}
|
||||
} else if (index >= _titles.size()) {
|
||||
_titles.emplace_back();
|
||||
}
|
||||
auto &title = _titles[index++];
|
||||
const auto unread = sublist->chatListBadgesState().unread;
|
||||
if (title.key == key
|
||||
&& title.unread == unread
|
||||
&& title.version == peer->nameVersion()) {
|
||||
continue;
|
||||
}
|
||||
const auto context = Core::TextContext({
|
||||
.session = &sublist->session(),
|
||||
.repaint = customEmojiRepaint,
|
||||
.customEmojiLoopLimit = kIconLoopCount,
|
||||
});
|
||||
auto topicTitle = TextWithEntities().append(
|
||||
Ui::Text::SingleCustomEmoji(
|
||||
manager->peerUserpicEmojiData(peer),
|
||||
u"@"_q)
|
||||
).append(' ').append(peer->shortName());
|
||||
title.key = key;
|
||||
title.version = peer->nameVersion();
|
||||
title.unread = unread;
|
||||
title.title.setMarkedText(
|
||||
st::dialogsTextStyle,
|
||||
(unread
|
||||
? Ui::Text::Colorized(
|
||||
Ui::Text::Wrapped(
|
||||
std::move(topicTitle),
|
||||
EntityType::Bold))
|
||||
: std::move(topicTitle)),
|
||||
DialogTextOptions(),
|
||||
context);
|
||||
}
|
||||
while (_titles.size() > index) {
|
||||
_titles.pop_back();
|
||||
}
|
||||
const auto i = frontPeerId
|
||||
? ranges::find(_titles, frontPeerId.value, &Title::key)
|
||||
: end(_titles);
|
||||
_jumpToTopic = (i != end(_titles));
|
||||
if (_jumpToTopic) {
|
||||
if (i != begin(_titles)) {
|
||||
ranges::rotate(begin(_titles), i, i + 1);
|
||||
}
|
||||
if (!_titles.front().unread) {
|
||||
_jumpToTopic = false;
|
||||
}
|
||||
}
|
||||
_allLoaded = _monoforum->chatsList()->loaded();
|
||||
}
|
||||
|
||||
int TopicsView::jumpToTopicWidth() const {
|
||||
return _jumpToTopic ? _titles.front().title.maxWidth() : 0;
|
||||
}
|
||||
|
||||
void TopicsView::paint(
|
||||
Painter &p,
|
||||
const QRect &geometry,
|
||||
const PaintContext &context) const {
|
||||
p.setFont(st::dialogsTextFont);
|
||||
p.setPen(context.active
|
||||
? st::dialogsTextFgActive
|
||||
: context.selected
|
||||
? st::dialogsTextFgOver
|
||||
: st::dialogsTextFg);
|
||||
const auto palette = &(context.active
|
||||
? st::dialogsTextPaletteArchiveActive
|
||||
: context.selected
|
||||
? st::dialogsTextPaletteArchiveOver
|
||||
: st::dialogsTextPaletteArchive);
|
||||
auto rect = geometry;
|
||||
rect.setWidth(rect.width() - _lastTopicJumpGeometry.rightCut);
|
||||
auto skipBig = _jumpToTopic && !context.active;
|
||||
if (_titles.empty()) {
|
||||
const auto text = (_monoforum && _allLoaded)
|
||||
? tr::lng_filters_no_chats(tr::now)
|
||||
: tr::lng_contacts_loading(tr::now);
|
||||
p.drawText(
|
||||
rect.x(),
|
||||
rect.y() + st::normalFont->ascent,
|
||||
text);
|
||||
return;
|
||||
}
|
||||
for (const auto &title : _titles) {
|
||||
if (rect.width() < title.title.style()->font->elidew) {
|
||||
break;
|
||||
}
|
||||
title.title.draw(p, {
|
||||
.position = rect.topLeft(),
|
||||
.availableWidth = rect.width(),
|
||||
.palette = palette,
|
||||
.spoiler = Text::DefaultSpoilerCache(),
|
||||
.now = context.now,
|
||||
.pausedEmoji = context.paused || On(PowerSaving::kEmojiChat),
|
||||
.pausedSpoiler = context.paused || On(PowerSaving::kChatSpoiler),
|
||||
.elisionLines = 1,
|
||||
});
|
||||
const auto skip = skipBig
|
||||
? context.st->topicsSkipBig
|
||||
: context.st->topicsSkip;
|
||||
rect.setLeft(rect.left() + title.title.maxWidth() + skip);
|
||||
skipBig = false;
|
||||
}
|
||||
}
|
||||
|
||||
bool TopicsView::changeTopicJumpGeometry(JumpToLastGeometry geometry) {
|
||||
if (_lastTopicJumpGeometry != geometry) {
|
||||
_lastTopicJumpGeometry = geometry;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void TopicsView::clearTopicJumpGeometry() {
|
||||
changeTopicJumpGeometry({});
|
||||
}
|
||||
|
||||
bool TopicsView::isInTopicJumpArea(int x, int y) const {
|
||||
return _lastTopicJumpGeometry.area1.contains(x, y)
|
||||
|| _lastTopicJumpGeometry.area2.contains(x, y);
|
||||
}
|
||||
|
||||
void TopicsView::addTopicJumpRipple(
|
||||
QPoint origin,
|
||||
not_null<TopicJumpCache*> topicJumpCache,
|
||||
Fn<void()> updateCallback) {
|
||||
auto mask = topicJumpRippleMask(topicJumpCache);
|
||||
if (mask.isNull()) {
|
||||
return;
|
||||
}
|
||||
_ripple = std::make_unique<Ui::RippleAnimation>(
|
||||
st::dialogsRipple,
|
||||
std::move(mask),
|
||||
std::move(updateCallback));
|
||||
_ripple->add(origin);
|
||||
}
|
||||
|
||||
void TopicsView::stopLastRipple() {
|
||||
if (_ripple) {
|
||||
_ripple->lastStop();
|
||||
}
|
||||
}
|
||||
|
||||
void TopicsView::clearRipple() {
|
||||
_ripple = nullptr;
|
||||
}
|
||||
|
||||
void TopicsView::paintRipple(
|
||||
QPainter &p,
|
||||
int x,
|
||||
int y,
|
||||
int outerWidth,
|
||||
const QColor *colorOverride) const {
|
||||
if (_ripple) {
|
||||
_ripple->paint(p, x, y, outerWidth, colorOverride);
|
||||
if (_ripple->empty()) {
|
||||
_ripple.reset();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
QImage TopicsView::topicJumpRippleMask(
|
||||
not_null<TopicJumpCache*> topicJumpCache) const {
|
||||
const auto &st = st::forumDialogRow;
|
||||
const auto area1 = _lastTopicJumpGeometry.area1;
|
||||
if (area1.isEmpty()) {
|
||||
return QImage();
|
||||
}
|
||||
const auto area2 = _lastTopicJumpGeometry.area2;
|
||||
const auto drawer = [&](QPainter &p) {
|
||||
const auto white = style::complex_color([] { return Qt::white; });
|
||||
// p.setOpacity(.1);
|
||||
FillJumpToLastPrepared(p, {
|
||||
.st = &st,
|
||||
.corners = &topicJumpCache->rippleMask,
|
||||
.bg = white.color(),
|
||||
.prepared = _lastTopicJumpGeometry,
|
||||
});
|
||||
};
|
||||
return Ui::RippleAnimation::MaskByDrawer(
|
||||
QRect(0, 0, 1, 1).united(area1).united(area2).size(),
|
||||
false,
|
||||
drawer);
|
||||
}
|
||||
|
||||
JumpToLastGeometry FillJumpToLastBg(QPainter &p, JumpToLastBg context) {
|
||||
const auto padding = st::forumDialogJumpPadding;
|
||||
const auto availableWidth = context.geometry.width();
|
||||
const auto want1 = std::min(context.width1, availableWidth);
|
||||
const auto use1 = std::min(want1, availableWidth - padding.right());
|
||||
const auto use2 = std::min(context.width2, availableWidth);
|
||||
const auto rightCut = want1 - use1;
|
||||
const auto origin = context.geometry.topLeft();
|
||||
const auto delta = std::abs(use1 - use2);
|
||||
if (delta <= context.st->topicsSkip / 2) {
|
||||
const auto w = std::max(use1, use2);
|
||||
const auto h = context.st->topicsHeight + st::normalFont->height;
|
||||
const auto fill = QRect(origin, QSize(w, h));
|
||||
const auto full = fill.marginsAdded(padding);
|
||||
auto result = JumpToLastGeometry{ rightCut, full };
|
||||
FillJumpToLastPrepared(p, {
|
||||
.st = context.st,
|
||||
.corners = context.corners,
|
||||
.bg = context.bg,
|
||||
.prepared = result,
|
||||
});
|
||||
return result;
|
||||
}
|
||||
const auto h1 = context.st->topicsHeight;
|
||||
const auto h2 = st::normalFont->height;
|
||||
const auto rect1 = QRect(origin, QSize(use1, h1));
|
||||
const auto fill1 = rect1.marginsAdded({
|
||||
padding.left(),
|
||||
padding.top(),
|
||||
padding.right(),
|
||||
(use1 < use2 ? -padding.top() : padding.bottom()),
|
||||
});
|
||||
const auto add = QPoint(0, h1);
|
||||
const auto rect2 = QRect(origin + add, QSize(use2, h2));
|
||||
const auto fill2 = rect2.marginsAdded({
|
||||
padding.left(),
|
||||
(use2 < use1 ? -padding.bottom() : padding.top()),
|
||||
padding.right(),
|
||||
padding.bottom(),
|
||||
});
|
||||
auto result = JumpToLastGeometry{ rightCut, fill1, fill2 };
|
||||
FillJumpToLastPrepared(p, {
|
||||
.st = context.st,
|
||||
.corners = context.corners,
|
||||
.bg = context.bg,
|
||||
.prepared = result,
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
void FillJumpToLastPrepared(QPainter &p, JumpToLastPrepared context) {
|
||||
auto &normal = context.corners->normal;
|
||||
auto &inverted = context.corners->inverted;
|
||||
auto &small = context.corners->small;
|
||||
const auto radius = st::forumDialogJumpRadius;
|
||||
const auto &bg = context.bg;
|
||||
const auto area1 = context.prepared.area1;
|
||||
const auto area2 = context.prepared.area2;
|
||||
if (area2.isNull()) {
|
||||
if (normal.p[0].isNull()) {
|
||||
normal = Ui::PrepareCornerPixmaps(radius, bg);
|
||||
}
|
||||
Ui::FillRoundRect(p, area1, bg, normal);
|
||||
return;
|
||||
}
|
||||
const auto width1 = area1.width();
|
||||
const auto width2 = area2.width();
|
||||
const auto delta = std::abs(width1 - width2);
|
||||
const auto h1 = context.st->topicsHeight;
|
||||
const auto h2 = st::normalFont->height;
|
||||
const auto hmin = std::min(h1, h2);
|
||||
const auto wantedInvertedRadius = hmin - radius;
|
||||
const auto invertedr = std::min(wantedInvertedRadius, delta / 2);
|
||||
const auto smallr = std::min(radius, delta - invertedr);
|
||||
const auto smallkey = (width1 < width2) ? smallr : (-smallr);
|
||||
if (normal.p[0].isNull()) {
|
||||
normal = Ui::PrepareCornerPixmaps(radius, bg);
|
||||
}
|
||||
if (inverted.p[0].isNull()
|
||||
|| context.corners->invertedRadius != invertedr) {
|
||||
context.corners->invertedRadius = invertedr;
|
||||
inverted = Ui::PrepareInvertedCornerPixmaps(invertedr, bg);
|
||||
}
|
||||
if (smallr != radius
|
||||
&& (small.isNull() || context.corners->smallKey != smallkey)) {
|
||||
context.corners->smallKey = smallr;
|
||||
auto pixmaps = Ui::PrepareCornerPixmaps(smallr, bg);
|
||||
small = pixmaps.p[(width1 < width2) ? 1 : 3];
|
||||
}
|
||||
auto no1 = normal;
|
||||
no1.p[2] = QPixmap();
|
||||
if (width1 < width2) {
|
||||
no1.p[3] = QPixmap();
|
||||
} else if (smallr != radius) {
|
||||
no1.p[3] = small;
|
||||
}
|
||||
Ui::FillRoundRect(p, area1, bg, no1);
|
||||
if (width1 < width2) {
|
||||
p.drawPixmap(
|
||||
area1.x() + width1,
|
||||
area1.y() + area1.height() - invertedr,
|
||||
inverted.p[3]);
|
||||
}
|
||||
auto no2 = normal;
|
||||
no2.p[0] = QPixmap();
|
||||
if (width2 < width1) {
|
||||
no2.p[1] = QPixmap();
|
||||
} else if (smallr != radius) {
|
||||
no2.p[1] = small;
|
||||
}
|
||||
Ui::FillRoundRect(p, area2, bg, no2);
|
||||
if (width2 < width1) {
|
||||
p.drawPixmap(
|
||||
area2.x() + width2,
|
||||
area2.y(),
|
||||
inverted.p[0]);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace Dialogs::Ui
|
||||
130
Telegram/SourceFiles/dialogs/ui/dialogs_topics_view.h
Normal file
130
Telegram/SourceFiles/dialogs/ui/dialogs_topics_view.h
Normal file
@@ -0,0 +1,130 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
class Painter;
|
||||
|
||||
namespace style {
|
||||
struct DialogRow;
|
||||
} // namespace style
|
||||
|
||||
namespace Data {
|
||||
class Forum;
|
||||
class ForumTopic;
|
||||
class SavedMessages;
|
||||
class SavedSublist;
|
||||
} // namespace Data
|
||||
|
||||
namespace Ui {
|
||||
class RippleAnimation;
|
||||
} // namespace Ui
|
||||
|
||||
namespace Dialogs::Ui {
|
||||
|
||||
using namespace ::Ui;
|
||||
|
||||
struct PaintContext;
|
||||
struct TopicJumpCache;
|
||||
struct TopicJumpCorners;
|
||||
|
||||
struct JumpToLastBg {
|
||||
not_null<const style::DialogRow*> st;
|
||||
not_null<TopicJumpCorners*> corners;
|
||||
QRect geometry;
|
||||
const style::color &bg;
|
||||
int width1 = 0;
|
||||
int width2 = 0;
|
||||
};
|
||||
struct JumpToLastGeometry {
|
||||
int rightCut = 0;
|
||||
QRect area1;
|
||||
QRect area2;
|
||||
|
||||
friend inline bool operator==(
|
||||
const JumpToLastGeometry&,
|
||||
const JumpToLastGeometry&) = default;
|
||||
};
|
||||
JumpToLastGeometry FillJumpToLastBg(QPainter &p, JumpToLastBg context);
|
||||
|
||||
struct JumpToLastPrepared {
|
||||
not_null<const style::DialogRow*> st;
|
||||
not_null<TopicJumpCorners*> corners;
|
||||
const style::color &bg;
|
||||
const JumpToLastGeometry &prepared;
|
||||
};
|
||||
void FillJumpToLastPrepared(QPainter &p, JumpToLastPrepared context);
|
||||
|
||||
class TopicsView final {
|
||||
public:
|
||||
TopicsView(Data::Forum *forum, Data::SavedMessages *monoforum);
|
||||
~TopicsView();
|
||||
|
||||
[[nodiscard]] Data::Forum *forum() const {
|
||||
return _forum;
|
||||
}
|
||||
[[nodiscard]] Data::SavedMessages *monoforum() const {
|
||||
return _monoforum;
|
||||
}
|
||||
|
||||
[[nodiscard]] bool prepared() const;
|
||||
void prepare(MsgId frontRootId, Fn<void()> customEmojiRepaint);
|
||||
void prepare(PeerId frontPeerId, Fn<void()> customEmojiRepaint);
|
||||
|
||||
[[nodiscard]] int jumpToTopicWidth() const;
|
||||
|
||||
void paint(
|
||||
Painter &p,
|
||||
const QRect &geometry,
|
||||
const PaintContext &context) const;
|
||||
|
||||
bool changeTopicJumpGeometry(JumpToLastGeometry geometry);
|
||||
void clearTopicJumpGeometry();
|
||||
[[nodiscard]] bool isInTopicJumpArea(int x, int y) const;
|
||||
void addTopicJumpRipple(
|
||||
QPoint origin,
|
||||
not_null<TopicJumpCache*> topicJumpCache,
|
||||
Fn<void()> updateCallback);
|
||||
void paintRipple(
|
||||
QPainter &p,
|
||||
int x,
|
||||
int y,
|
||||
int outerWidth,
|
||||
const QColor *colorOverride) const;
|
||||
void stopLastRipple();
|
||||
void clearRipple();
|
||||
|
||||
[[nodiscard]] rpl::lifetime &lifetime() {
|
||||
return _lifetime;
|
||||
}
|
||||
|
||||
private:
|
||||
struct Title {
|
||||
Text::String title;
|
||||
uint64 key = 0;
|
||||
int version = -1;
|
||||
bool unread = false;
|
||||
};
|
||||
|
||||
[[nodiscard]] QImage topicJumpRippleMask(
|
||||
not_null<TopicJumpCache*> topicJumpCache) const;
|
||||
|
||||
Data::Forum * const _forum = nullptr;
|
||||
Data::SavedMessages * const _monoforum = nullptr;
|
||||
|
||||
mutable std::vector<Title> _titles;
|
||||
mutable std::unique_ptr<RippleAnimation> _ripple;
|
||||
JumpToLastGeometry _lastTopicJumpGeometry;
|
||||
int _version = -1;
|
||||
bool _jumpToTopic = false;
|
||||
bool _allLoaded = false;
|
||||
|
||||
rpl::lifetime _lifetime;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Dialogs::Ui
|
||||
175
Telegram/SourceFiles/dialogs/ui/dialogs_video_userpic.cpp
Normal file
175
Telegram/SourceFiles/dialogs/ui/dialogs_video_userpic.cpp
Normal file
@@ -0,0 +1,175 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#include "dialogs/ui/dialogs_video_userpic.h"
|
||||
|
||||
#include "core/file_location.h"
|
||||
#include "data/data_peer.h"
|
||||
#include "data/data_photo.h"
|
||||
#include "data/data_photo_media.h"
|
||||
#include "data/data_file_origin.h"
|
||||
#include "data/data_session.h"
|
||||
#include "dialogs/dialogs_entry.h"
|
||||
#include "dialogs/ui/dialogs_layout.h"
|
||||
#include "ui/painter.h"
|
||||
#include "styles/style_dialogs.h"
|
||||
|
||||
namespace Dialogs::Ui {
|
||||
|
||||
VideoUserpic::VideoUserpic(not_null<PeerData*> peer, Fn<void()> repaint)
|
||||
: _peer(peer)
|
||||
, _repaint(std::move(repaint)) {
|
||||
}
|
||||
|
||||
VideoUserpic::~VideoUserpic() = default;
|
||||
|
||||
int VideoUserpic::frameIndex() const {
|
||||
return -1;
|
||||
}
|
||||
|
||||
void VideoUserpic::paintLeft(
|
||||
Painter &p,
|
||||
Ui::PeerUserpicView &view,
|
||||
int x,
|
||||
int y,
|
||||
int w,
|
||||
int size,
|
||||
bool paused) {
|
||||
_lastSize = size;
|
||||
|
||||
const auto photoId = _peer->userpicPhotoId();
|
||||
if (_videoPhotoId != photoId) {
|
||||
_videoPhotoId = photoId;
|
||||
_video = nullptr;
|
||||
_videoPhotoMedia = nullptr;
|
||||
const auto photo = _peer->owner().photo(photoId);
|
||||
if (photo->isNull()) {
|
||||
_peer->updateFullForced();
|
||||
} else {
|
||||
_videoPhotoMedia = photo->createMediaView();
|
||||
_videoPhotoMedia->videoWanted(
|
||||
Data::PhotoSize::Small,
|
||||
_peer->userpicPhotoOrigin());
|
||||
}
|
||||
}
|
||||
if (!_video) {
|
||||
if (!_videoPhotoMedia) {
|
||||
const auto photo = _peer->owner().photo(photoId);
|
||||
if (!photo->isNull()) {
|
||||
_videoPhotoMedia = photo->createMediaView();
|
||||
_videoPhotoMedia->videoWanted(
|
||||
Data::PhotoSize::Small,
|
||||
_peer->userpicPhotoOrigin());
|
||||
}
|
||||
}
|
||||
if (_videoPhotoMedia) {
|
||||
auto small = _videoPhotoMedia->videoContent(
|
||||
Data::PhotoSize::Small);
|
||||
auto bytes = small.isEmpty()
|
||||
? _videoPhotoMedia->videoContent(Data::PhotoSize::Large)
|
||||
: small;
|
||||
if (!bytes.isEmpty()) {
|
||||
auto callback = [=](Media::Clip::Notification notification) {
|
||||
clipCallback(notification);
|
||||
};
|
||||
_video = Media::Clip::MakeReader(
|
||||
Core::FileLocation(),
|
||||
std::move(bytes),
|
||||
std::move(callback));
|
||||
}
|
||||
}
|
||||
}
|
||||
if (rtl()) {
|
||||
x = w - x - size;
|
||||
}
|
||||
if (_video && _video->ready()) {
|
||||
startReady();
|
||||
|
||||
const auto now = paused ? crl::time(0) : crl::now();
|
||||
p.drawImage(x, y, _video->current(request(size), now));
|
||||
} else {
|
||||
_peer->paintUserpicLeft(p, view, x, y, w, size);
|
||||
}
|
||||
}
|
||||
|
||||
Media::Clip::FrameRequest VideoUserpic::request(int size) const {
|
||||
return {
|
||||
.frame = { size, size },
|
||||
.outer = { size, size },
|
||||
.factor = style::DevicePixelRatio(),
|
||||
.radius = ImageRoundRadius::Ellipse,
|
||||
};
|
||||
}
|
||||
|
||||
bool VideoUserpic::startReady(int size) {
|
||||
if (!_video->ready() || _video->started()) {
|
||||
return false;
|
||||
} else if (!_lastSize) {
|
||||
_lastSize = size ? size : _video->width();
|
||||
}
|
||||
_video->start(request(_lastSize));
|
||||
_repaint();
|
||||
return true;
|
||||
}
|
||||
|
||||
void VideoUserpic::clipCallback(Media::Clip::Notification notification) {
|
||||
using namespace Media::Clip;
|
||||
|
||||
switch (notification) {
|
||||
case Notification::Reinit: {
|
||||
if (_video->state() == State::Error) {
|
||||
_video.setBad();
|
||||
} else if (startReady()) {
|
||||
_repaint();
|
||||
}
|
||||
} break;
|
||||
|
||||
case Notification::Repaint: _repaint(); break;
|
||||
}
|
||||
}
|
||||
|
||||
void PaintUserpic(
|
||||
Painter &p,
|
||||
not_null<Entry*> entry,
|
||||
PeerData *peer,
|
||||
VideoUserpic *videoUserpic,
|
||||
PeerUserpicView &view,
|
||||
const Ui::PaintContext &context) {
|
||||
if (peer) {
|
||||
PaintUserpic(
|
||||
p,
|
||||
peer,
|
||||
videoUserpic,
|
||||
view,
|
||||
context.st->padding.left(),
|
||||
context.st->padding.top(),
|
||||
context.width,
|
||||
context.st->photoSize,
|
||||
context.paused);
|
||||
} else {
|
||||
entry->paintUserpic(p, view, context);
|
||||
}
|
||||
}
|
||||
|
||||
void PaintUserpic(
|
||||
Painter &p,
|
||||
not_null<PeerData*> peer,
|
||||
Ui::VideoUserpic *videoUserpic,
|
||||
Ui::PeerUserpicView &view,
|
||||
int x,
|
||||
int y,
|
||||
int outerWidth,
|
||||
int size,
|
||||
bool paused) {
|
||||
if (videoUserpic) {
|
||||
videoUserpic->paintLeft(p, view, x, y, outerWidth, size, paused);
|
||||
} else {
|
||||
peer->paintUserpicLeft(p, view, x, y, outerWidth, size);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace Dialogs::Ui
|
||||
82
Telegram/SourceFiles/dialogs/ui/dialogs_video_userpic.h
Normal file
82
Telegram/SourceFiles/dialogs/ui/dialogs_video_userpic.h
Normal file
@@ -0,0 +1,82 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "media/clip/media_clip_reader.h"
|
||||
|
||||
class Painter;
|
||||
|
||||
namespace Data {
|
||||
class PhotoMedia;
|
||||
} // namespace Data
|
||||
|
||||
namespace Ui {
|
||||
struct PeerUserpicView;
|
||||
} // namespace Ui
|
||||
|
||||
namespace Dialogs {
|
||||
class Entry;
|
||||
} // namespace Dialogs
|
||||
|
||||
namespace Dialogs::Ui {
|
||||
|
||||
using namespace ::Ui;
|
||||
|
||||
struct PaintContext;
|
||||
|
||||
class VideoUserpic final {
|
||||
public:
|
||||
VideoUserpic(not_null<PeerData*> peer, Fn<void()> repaint);
|
||||
~VideoUserpic();
|
||||
|
||||
[[nodiscard]] int frameIndex() const;
|
||||
|
||||
void paintLeft(
|
||||
Painter &p,
|
||||
PeerUserpicView &view,
|
||||
int x,
|
||||
int y,
|
||||
int w,
|
||||
int size,
|
||||
bool paused);
|
||||
|
||||
private:
|
||||
void clipCallback(Media::Clip::Notification notification);
|
||||
[[nodiscard]] Media::Clip::FrameRequest request(int size) const;
|
||||
bool startReady(int size = 0);
|
||||
|
||||
const not_null<PeerData*> _peer;
|
||||
const Fn<void()> _repaint;
|
||||
|
||||
Media::Clip::ReaderPointer _video;
|
||||
int _lastSize = 0;
|
||||
std::shared_ptr<Data::PhotoMedia> _videoPhotoMedia;
|
||||
PhotoId _videoPhotoId = 0;
|
||||
|
||||
};
|
||||
|
||||
void PaintUserpic(
|
||||
Painter &p,
|
||||
not_null<Entry*> entry,
|
||||
PeerData *peer,
|
||||
VideoUserpic *videoUserpic,
|
||||
PeerUserpicView &view,
|
||||
const Ui::PaintContext &context);
|
||||
|
||||
void PaintUserpic(
|
||||
Painter &p,
|
||||
not_null<PeerData*> peer,
|
||||
VideoUserpic *videoUserpic,
|
||||
PeerUserpicView &view,
|
||||
int x,
|
||||
int y,
|
||||
int outerWidth,
|
||||
int size,
|
||||
bool paused);
|
||||
|
||||
} // namespace Dialogs::Ui
|
||||
253
Telegram/SourceFiles/dialogs/ui/posts_search_intro.cpp
Normal file
253
Telegram/SourceFiles/dialogs/ui/posts_search_intro.cpp
Normal file
@@ -0,0 +1,253 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#include "dialogs/ui/posts_search_intro.h"
|
||||
|
||||
#include "base/timer_rpl.h"
|
||||
#include "base/unixtime.h"
|
||||
#include "lang/lang_keys.h"
|
||||
#include "ui/controls/button_labels.h"
|
||||
#include "ui/text/text_utilities.h"
|
||||
#include "ui/widgets/buttons.h"
|
||||
#include "ui/widgets/labels.h"
|
||||
#include "ui/wrap/vertical_layout.h"
|
||||
#include "styles/style_credits.h"
|
||||
#include "styles/style_dialogs.h"
|
||||
|
||||
namespace Dialogs {
|
||||
namespace {
|
||||
|
||||
[[nodiscard]] rpl::producer<QString> FormatCountdownTill(TimeId when) {
|
||||
return rpl::single(rpl::empty) | rpl::then(
|
||||
base::timer_each(1000)
|
||||
) | rpl::map([=] {
|
||||
const auto now = base::unixtime::now();
|
||||
const auto delta = std::max(when - now, 0);
|
||||
const auto hours = delta / 3600;
|
||||
const auto minutes = (delta % 3600) / 60;
|
||||
const auto seconds = delta % 60;
|
||||
constexpr auto kZero = QChar('0');
|
||||
return (hours > 0)
|
||||
? u"%1:%2:%3"_q
|
||||
.arg(hours)
|
||||
.arg(minutes, 2, 10, kZero)
|
||||
.arg(seconds, 2, 10, kZero)
|
||||
: u"%1:%2"_q
|
||||
.arg(minutes)
|
||||
.arg(seconds, 2, 10, kZero);
|
||||
});
|
||||
}
|
||||
|
||||
void SetSearchButtonLabel(
|
||||
not_null<Ui::RpWidget*> button,
|
||||
rpl::producer<TextWithEntities> text) {
|
||||
const auto left = &st::postsSearchIcon;
|
||||
const auto leftPadding = st::postsSearchIconPadding;
|
||||
const auto right = &st::postsSearchArrow;
|
||||
const auto rightPadding = st::postsSearchArrowPadding;
|
||||
const auto leftSkip = left->size().grownBy(leftPadding).width();
|
||||
const auto rightSkip = right->size().grownBy(rightPadding).width();
|
||||
|
||||
struct State {
|
||||
State() : linkFg([] {
|
||||
auto copy = st::windowFgActive->c;
|
||||
copy.setAlphaF(0.6);
|
||||
return copy;
|
||||
}), st(st::resaleButtonTitle) {
|
||||
}
|
||||
|
||||
style::complex_color linkFg;
|
||||
style::FlatLabel st;
|
||||
};
|
||||
auto lifetime = rpl::lifetime();
|
||||
const auto state = lifetime.make_state<State>();
|
||||
state->st.palette.linkFg = state->linkFg.color();
|
||||
|
||||
const auto label = Ui::CreateChild<Ui::FlatLabel>(
|
||||
button,
|
||||
rpl::duplicate(text),
|
||||
state->st);
|
||||
label->lifetime().add(std::move(lifetime));
|
||||
label->show();
|
||||
|
||||
const auto icons = Ui::CreateChild<Ui::RpWidget>(button);
|
||||
icons->show();
|
||||
rpl::combine(
|
||||
button->sizeValue(),
|
||||
std::move(text)
|
||||
) | rpl::on_next([=](QSize size, const auto &) {
|
||||
icons->setGeometry(QRect(QPoint(), size));
|
||||
const auto available = size.width() - leftSkip - rightSkip;
|
||||
if (available <= 0) {
|
||||
return;
|
||||
}
|
||||
const auto width = std::min(available, label->textMaxWidth());
|
||||
label->resizeToWidth(width);
|
||||
const auto full = leftSkip + width + rightSkip;
|
||||
const auto x = (size.width() - full) / 2;
|
||||
const auto y = (size.height() - label->height()) / 2;
|
||||
label->moveToLeft(x + leftSkip, y, size.width());
|
||||
}, icons->lifetime());
|
||||
|
||||
icons->paintRequest() | rpl::on_next([=] {
|
||||
auto p = QPainter(icons);
|
||||
left->paint(
|
||||
p,
|
||||
label->x() - leftSkip + leftPadding.left(),
|
||||
label->y() + leftPadding.top(),
|
||||
icons->width());
|
||||
right->paint(
|
||||
p,
|
||||
label->x() + label->width() + rightPadding.left(),
|
||||
label->y() + rightPadding.top(),
|
||||
icons->width());
|
||||
}, icons->lifetime());
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
PostsSearchIntro::PostsSearchIntro(
|
||||
not_null<Ui::RpWidget*> parent,
|
||||
PostsSearchIntroState state)
|
||||
: RpWidget(parent)
|
||||
, _state(std::move(state))
|
||||
, _content(std::make_unique<Ui::VerticalLayout>(this)) {
|
||||
setup();
|
||||
}
|
||||
|
||||
PostsSearchIntro::~PostsSearchIntro() = default;
|
||||
|
||||
void PostsSearchIntro::update(PostsSearchIntroState state) {
|
||||
_state = std::move(state);
|
||||
}
|
||||
|
||||
rpl::producer<int> PostsSearchIntro::searchWithStars() const {
|
||||
return _button->clicks() | rpl::map([=] {
|
||||
const auto &now = _state.current();
|
||||
return (now.needsPremium || now.freeSearchesLeft)
|
||||
? 0
|
||||
: int(now.starsPerPaidSearch);
|
||||
});
|
||||
}
|
||||
|
||||
void PostsSearchIntro::setup() {
|
||||
auto title = _state.value(
|
||||
) | rpl::map([](const PostsSearchIntroState &state) {
|
||||
return (state.needsPremium || state.freeSearchesLeft > 0)
|
||||
? tr::lng_posts_title()
|
||||
: tr::lng_posts_limit_reached();
|
||||
}) | rpl::flatten_latest();
|
||||
|
||||
auto subtitle = _state.value(
|
||||
) | rpl::map([](const PostsSearchIntroState &state) {
|
||||
return (state.needsPremium || state.freeSearchesLeft > 0)
|
||||
? tr::lng_posts_start()
|
||||
: tr::lng_posts_limit_about(
|
||||
lt_count,
|
||||
rpl::single(state.freeSearchesPerDay * 1.));
|
||||
}) | rpl::flatten_latest();
|
||||
|
||||
auto footer = _state.value(
|
||||
) | rpl::map([](const PostsSearchIntroState &state)
|
||||
-> rpl::producer<QString> {
|
||||
if (state.needsPremium) {
|
||||
return tr::lng_posts_need_subscribe();
|
||||
} else if (state.freeSearchesLeft > 0) {
|
||||
return tr::lng_posts_remaining(
|
||||
lt_count,
|
||||
rpl::single(state.freeSearchesLeft * 1.));
|
||||
} else {
|
||||
return rpl::single(QString());
|
||||
}
|
||||
}) | rpl::flatten_latest();
|
||||
|
||||
_title = _content->add(
|
||||
object_ptr<Ui::FlatLabel>(
|
||||
_content.get(),
|
||||
std::move(title),
|
||||
st::postsSearchIntroTitle),
|
||||
st::postsSearchIntroTitleMargin,
|
||||
style::al_top);
|
||||
_title->setTryMakeSimilarLines(true);
|
||||
_subtitle = _content->add(
|
||||
object_ptr<Ui::FlatLabel>(
|
||||
_content.get(),
|
||||
std::move(subtitle),
|
||||
st::postsSearchIntroSubtitle),
|
||||
st::postsSearchIntroSubtitleMargin,
|
||||
style::al_top);
|
||||
_subtitle->setTryMakeSimilarLines(true);
|
||||
_button = _content->add(
|
||||
object_ptr<Ui::RoundButton>(
|
||||
_content.get(),
|
||||
rpl::single(QString()),
|
||||
st::postsSearchIntroButton),
|
||||
style::al_top);
|
||||
_button->setTextTransform(Ui::RoundButton::TextTransform::NoTransform);
|
||||
_footer = _content->add(
|
||||
object_ptr<Ui::FlatLabel>(
|
||||
_content.get(),
|
||||
std::move(footer),
|
||||
st::postsSearchIntroFooter),
|
||||
st::postsSearchIntroFooterMargin,
|
||||
style::al_top);
|
||||
_footer->setTryMakeSimilarLines(true);
|
||||
|
||||
_state.value(
|
||||
) | rpl::on_next([=](const PostsSearchIntroState &state) {
|
||||
if (state.query.trimmed().isEmpty() && !state.needsPremium) {
|
||||
_button->resize(_button->width(), 0);
|
||||
_content->resizeToWidth(width());
|
||||
return;
|
||||
}
|
||||
|
||||
auto copy = _button->children();
|
||||
for (const auto child : copy) {
|
||||
delete child;
|
||||
}
|
||||
if (state.needsPremium) {
|
||||
_button->setText(tr::lng_posts_subscribe());
|
||||
} else if (state.freeSearchesLeft > 0) {
|
||||
_button->setText(rpl::single(QString()));
|
||||
|
||||
SetSearchButtonLabel(_button, tr::lng_posts_search_button(
|
||||
lt_query,
|
||||
rpl::single(Ui::Text::Colorized(state.query.trimmed())),
|
||||
tr::marked));
|
||||
} else {
|
||||
_button->setText(rpl::single(QString()));
|
||||
|
||||
Ui::SetButtonTwoLabels(
|
||||
_button,
|
||||
tr::lng_posts_limit_search_paid(
|
||||
lt_cost,
|
||||
rpl::single(Ui::Text::IconEmoji(
|
||||
&st::starIconEmoji
|
||||
).append(
|
||||
Lang::FormatCountDecimal(state.starsPerPaidSearch))),
|
||||
tr::marked),
|
||||
tr::lng_posts_limit_unlocks(
|
||||
lt_duration,
|
||||
FormatCountdownTill(
|
||||
state.nextFreeSearchTime
|
||||
) | rpl::map(tr::marked),
|
||||
tr::marked),
|
||||
st::resaleButtonTitle,
|
||||
st::resaleButtonSubtitle);
|
||||
}
|
||||
_button->resize(_button->width(), st::postsSearchIntroButton.height);
|
||||
_content->resizeToWidth(width());
|
||||
}, _button->lifetime());
|
||||
}
|
||||
|
||||
void PostsSearchIntro::resizeEvent(QResizeEvent *e) {
|
||||
_content->resizeToWidth(width());
|
||||
const auto top = std::max(0, (height() - _content->height()) / 3);
|
||||
_content->move(0, top);
|
||||
}
|
||||
|
||||
} // namespace Dialogs
|
||||
59
Telegram/SourceFiles/dialogs/ui/posts_search_intro.h
Normal file
59
Telegram/SourceFiles/dialogs/ui/posts_search_intro.h
Normal file
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "ui/rp_widget.h"
|
||||
|
||||
namespace Ui {
|
||||
class FlatLabel;
|
||||
class RoundButton;
|
||||
class VerticalLayout;
|
||||
} // namespace Ui
|
||||
|
||||
namespace Dialogs {
|
||||
|
||||
struct PostsSearchIntroState {
|
||||
QString query;
|
||||
int freeSearchesPerDay = 0;
|
||||
int freeSearchesLeft = 0;
|
||||
TimeId nextFreeSearchTime = 0;
|
||||
uint32 starsPerPaidSearch : 31 = 0;
|
||||
uint32 needsPremium : 1 = 0;
|
||||
|
||||
friend inline bool operator==(
|
||||
PostsSearchIntroState,
|
||||
PostsSearchIntroState) = default;
|
||||
};
|
||||
|
||||
class PostsSearchIntro final : public Ui::RpWidget {
|
||||
public:
|
||||
PostsSearchIntro(
|
||||
not_null<Ui::RpWidget*> parent,
|
||||
PostsSearchIntroState state);
|
||||
~PostsSearchIntro();
|
||||
|
||||
void update(PostsSearchIntroState state);
|
||||
|
||||
[[nodiscard]] rpl::producer<int> searchWithStars() const;
|
||||
|
||||
private:
|
||||
void resizeEvent(QResizeEvent *e) override;
|
||||
|
||||
void setup();
|
||||
|
||||
rpl::variable<PostsSearchIntroState> _state;
|
||||
|
||||
std::unique_ptr<Ui::VerticalLayout> _content;
|
||||
Ui::FlatLabel *_title = nullptr;
|
||||
Ui::FlatLabel *_subtitle = nullptr;
|
||||
Ui::RoundButton *_button = nullptr;
|
||||
Ui::FlatLabel *_footer = nullptr;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Dialogs
|
||||
995
Telegram/SourceFiles/dialogs/ui/top_peers_strip.cpp
Normal file
995
Telegram/SourceFiles/dialogs/ui/top_peers_strip.cpp
Normal file
@@ -0,0 +1,995 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#include "dialogs/ui/top_peers_strip.h"
|
||||
|
||||
#include "base/event_filter.h"
|
||||
#include "lang/lang_keys.h"
|
||||
#include "ui/effects/ripple_animation.h"
|
||||
#include "ui/text/text.h"
|
||||
#include "ui/widgets/buttons.h"
|
||||
#include "ui/widgets/labels.h"
|
||||
#include "ui/widgets/menu/menu_add_action_callback_factory.h"
|
||||
#include "ui/widgets/popup_menu.h"
|
||||
#include "ui/widgets/scroll_area.h"
|
||||
#include "ui/dynamic_image.h"
|
||||
#include "ui/painter.h"
|
||||
#include "ui/unread_badge_paint.h"
|
||||
#include "styles/style_dialogs.h"
|
||||
#include "styles/style_widgets.h"
|
||||
|
||||
#include <QtWidgets/QApplication>
|
||||
|
||||
namespace Dialogs {
|
||||
|
||||
struct TopPeersStrip::Entry {
|
||||
uint64 id = 0;
|
||||
Ui::Text::String name;
|
||||
std::shared_ptr<Ui::DynamicImage> userpic;
|
||||
std::unique_ptr<Ui::RippleAnimation> ripple;
|
||||
Ui::Animations::Simple onlineShown;
|
||||
QImage userpicFrame;
|
||||
float64 userpicFrameOnline = 0.;
|
||||
QString badgeString;
|
||||
uint32 badge : 27 = 0;
|
||||
uint32 userpicFrameDirty : 1 = 0;
|
||||
uint32 subscribed : 1 = 0;
|
||||
uint32 unread : 1 = 0;
|
||||
uint32 online : 1 = 0;
|
||||
uint32 muted : 1 = 0;
|
||||
};
|
||||
|
||||
struct TopPeersStrip::Layout {
|
||||
int single = 0;
|
||||
int inrow = 0;
|
||||
float64 fsingle = 0.;
|
||||
float64 added = 0.;
|
||||
};
|
||||
|
||||
TopPeersStrip::TopPeersStrip(
|
||||
not_null<QWidget*> parent,
|
||||
rpl::producer<TopPeersList> content)
|
||||
: RpWidget(parent)
|
||||
, _header(this)
|
||||
, _strip(this)
|
||||
, _selection(st::topPeersRadius, st::windowBgOver) {
|
||||
setupHeader();
|
||||
setupStrip();
|
||||
|
||||
std::move(content) | rpl::on_next([=](const TopPeersList &list) {
|
||||
apply(list);
|
||||
}, lifetime());
|
||||
|
||||
rpl::combine(
|
||||
_count.value(),
|
||||
_expanded.value()
|
||||
) | rpl::on_next([=] {
|
||||
resizeToWidth(width());
|
||||
}, _strip.lifetime());
|
||||
|
||||
resize(0, _header.height() + _strip.height());
|
||||
}
|
||||
|
||||
void TopPeersStrip::setupHeader() {
|
||||
_header.resize(0, st::searchedBarHeight);
|
||||
|
||||
const auto label = Ui::CreateChild<Ui::FlatLabel>(
|
||||
&_header,
|
||||
tr::lng_recent_frequent(),
|
||||
st::searchedBarLabel);
|
||||
const auto single = outer().width();
|
||||
|
||||
rpl::combine(
|
||||
_count.value(),
|
||||
widthValue()
|
||||
) | rpl::map(
|
||||
(rpl::mappers::_1 * single) > (rpl::mappers::_2 + (single * 2) / 3)
|
||||
) | rpl::distinct_until_changed() | rpl::on_next([=](bool more) {
|
||||
setExpanded(false);
|
||||
if (!more) {
|
||||
const auto toggle = _toggleExpanded.current();
|
||||
_toggleExpanded = nullptr;
|
||||
delete toggle;
|
||||
return;
|
||||
} else if (_toggleExpanded.current()) {
|
||||
return;
|
||||
}
|
||||
const auto toggle = Ui::CreateChild<Ui::LinkButton>(
|
||||
&_header,
|
||||
tr::lng_channels_your_more(tr::now),
|
||||
st::searchedBarLink);
|
||||
toggle->show();
|
||||
toggle->setClickedCallback([=] {
|
||||
const auto expand = !_expanded.current();
|
||||
toggle->setText(expand
|
||||
? tr::lng_recent_frequent_collapse(tr::now)
|
||||
: tr::lng_recent_frequent_all(tr::now));
|
||||
setExpanded(expand);
|
||||
});
|
||||
rpl::combine(
|
||||
_header.sizeValue(),
|
||||
toggle->widthValue()
|
||||
) | rpl::on_next([=](QSize size, int width) {
|
||||
const auto x = st::searchedBarPosition.x();
|
||||
const auto y = st::searchedBarPosition.y();
|
||||
toggle->moveToRight(0, 0, size.width());
|
||||
label->resizeToWidth(size.width() - x - width);
|
||||
label->moveToLeft(x, y, size.width());
|
||||
}, toggle->lifetime());
|
||||
_toggleExpanded = toggle;
|
||||
}, _header.lifetime());
|
||||
|
||||
rpl::combine(
|
||||
_header.sizeValue(),
|
||||
_toggleExpanded.value()
|
||||
) | rpl::filter(
|
||||
rpl::mappers::_2 == nullptr
|
||||
) | rpl::on_next([=](QSize size, const auto) {
|
||||
const auto x = st::searchedBarPosition.x();
|
||||
const auto y = st::searchedBarPosition.y();
|
||||
label->resizeToWidth(size.width() - x * 2);
|
||||
label->moveToLeft(x, y, size.width());
|
||||
}, _header.lifetime());
|
||||
|
||||
_header.paintRequest() | rpl::on_next([=](QRect clip) {
|
||||
QPainter(&_header).fillRect(clip, st::searchedBarBg);
|
||||
}, _header.lifetime());
|
||||
}
|
||||
|
||||
void TopPeersStrip::setExpanded(bool expanded) {
|
||||
if (_expanded.current() == expanded) {
|
||||
return;
|
||||
}
|
||||
const auto from = expanded ? 0. : 1.;
|
||||
const auto to = expanded ? 1. : 0.;
|
||||
_expandAnimation.start([=] {
|
||||
if (!_expandAnimation.animating()) {
|
||||
updateScrollMax();
|
||||
}
|
||||
resizeToWidth(width());
|
||||
update();
|
||||
}, from, to, st::slideDuration, anim::easeOutQuint);
|
||||
_expanded = expanded;
|
||||
}
|
||||
|
||||
void TopPeersStrip::setupStrip() {
|
||||
_strip.resize(0, st::topPeers.height);
|
||||
|
||||
_strip.setMouseTracking(true);
|
||||
|
||||
base::install_event_filter(&_strip, [=](not_null<QEvent*> e) {
|
||||
const auto type = e->type();
|
||||
if (type == QEvent::Wheel) {
|
||||
stripWheelEvent(static_cast<QWheelEvent*>(e.get()));
|
||||
} else if (type == QEvent::MouseButtonPress) {
|
||||
stripMousePressEvent(static_cast<QMouseEvent*>(e.get()));
|
||||
} else if (type == QEvent::MouseMove) {
|
||||
stripMouseMoveEvent(static_cast<QMouseEvent*>(e.get()));
|
||||
} else if (type == QEvent::MouseButtonRelease) {
|
||||
stripMouseReleaseEvent(static_cast<QMouseEvent*>(e.get()));
|
||||
} else if (type == QEvent::ContextMenu) {
|
||||
stripContextMenuEvent(static_cast<QContextMenuEvent*>(e.get()));
|
||||
} else if (type == QEvent::Leave) {
|
||||
stripLeaveEvent(e.get());
|
||||
} else {
|
||||
return base::EventFilterResult::Continue;
|
||||
}
|
||||
return base::EventFilterResult::Cancel;
|
||||
});
|
||||
|
||||
_strip.paintRequest() | rpl::on_next([=](QRect clip) {
|
||||
paintStrip(clip);
|
||||
}, _strip.lifetime());
|
||||
}
|
||||
|
||||
TopPeersStrip::~TopPeersStrip() {
|
||||
unsubscribeUserpics(true);
|
||||
}
|
||||
|
||||
int TopPeersStrip::resizeGetHeight(int newWidth) {
|
||||
_header.resize(newWidth, _header.height());
|
||||
const auto single = QSize(outer().width(), st::topPeers.height);
|
||||
const auto inRow = newWidth / single.width();
|
||||
const auto rows = (inRow > 0)
|
||||
? ((std::max(_count.current(), 1) + inRow - 1) / inRow)
|
||||
: 1;
|
||||
const auto height = single.height() * rows;
|
||||
const auto value = _expandAnimation.value(_expanded.current() ? 1. : 0.);
|
||||
const auto result = anim::interpolate(single.height(), height, value);
|
||||
_strip.setGeometry(0, _header.height(), newWidth, result);
|
||||
updateScrollMax(newWidth);
|
||||
return _strip.y() + _strip.height();
|
||||
}
|
||||
|
||||
rpl::producer<not_null<QWheelEvent*>> TopPeersStrip::verticalScrollEvents() const {
|
||||
return _verticalScrollEvents.events();
|
||||
}
|
||||
|
||||
void TopPeersStrip::stripWheelEvent(QWheelEvent *e) {
|
||||
const auto phase = e->phase();
|
||||
const auto fullDelta = e->pixelDelta().isNull()
|
||||
? e->angleDelta()
|
||||
: e->pixelDelta();
|
||||
if (phase == Qt::ScrollBegin || phase == Qt::ScrollEnd) {
|
||||
_scrollingLock = Qt::Orientation();
|
||||
if (fullDelta.isNull()) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
const auto vertical = qAbs(fullDelta.x()) < qAbs(fullDelta.y());
|
||||
if (_scrollingLock == Qt::Orientation() && phase != Qt::NoScrollPhase) {
|
||||
_scrollingLock = vertical ? Qt::Vertical : Qt::Horizontal;
|
||||
}
|
||||
if (_scrollingLock == Qt::Vertical || (vertical && !_scrollLeftMax)) {
|
||||
_verticalScrollEvents.fire(e);
|
||||
return;
|
||||
} else if (_expandAnimation.animating()) {
|
||||
return;
|
||||
}
|
||||
const auto delta = vertical
|
||||
? fullDelta.y()
|
||||
: ((style::RightToLeft() ? -1 : 1) * fullDelta.x());
|
||||
|
||||
const auto now = _scrollLeft;
|
||||
const auto used = now - delta;
|
||||
const auto next = std::clamp(used, 0, _scrollLeftMax);
|
||||
if (next != now) {
|
||||
_scrollLeft = next;
|
||||
unsubscribeUserpics();
|
||||
updateSelected();
|
||||
update();
|
||||
}
|
||||
e->accept();
|
||||
}
|
||||
|
||||
void TopPeersStrip::stripLeaveEvent(QEvent *e) {
|
||||
if (!_selectionByKeyboard) {
|
||||
clearSelection();
|
||||
}
|
||||
if (!_dragging) {
|
||||
_lastMousePosition = std::nullopt;
|
||||
}
|
||||
}
|
||||
|
||||
void TopPeersStrip::stripMousePressEvent(QMouseEvent *e) {
|
||||
if (e->button() != Qt::LeftButton) {
|
||||
return;
|
||||
}
|
||||
_lastMousePosition = e->globalPos();
|
||||
_selectionByKeyboard = false;
|
||||
updateSelected();
|
||||
|
||||
_mouseDownPosition = _lastMousePosition;
|
||||
_pressed = _selected;
|
||||
|
||||
if (_selected >= 0) {
|
||||
Assert(_selected < _entries.size());
|
||||
auto &entry = _entries[_selected];
|
||||
if (!entry.ripple) {
|
||||
entry.ripple = std::make_unique<Ui::RippleAnimation>(
|
||||
st::defaultRippleAnimation,
|
||||
Ui::RippleAnimation::RoundRectMask(
|
||||
innerRounded().size(),
|
||||
st::topPeersRadius),
|
||||
[=] { update(); });
|
||||
}
|
||||
const auto layout = currentLayout();
|
||||
const auto expanded = _expanded.current();
|
||||
const auto row = expanded ? (_selected / layout.inrow) : 0;
|
||||
const auto column = (_selected - (row * layout.inrow));
|
||||
const auto x = layout.added + column * layout.fsingle - scrollLeft();
|
||||
const auto y = row * st::topPeers.height;
|
||||
entry.ripple->add(e->pos() - QPoint(
|
||||
x + st::topPeersMargin.left(),
|
||||
y + st::topPeersMargin.top()));
|
||||
|
||||
_presses.fire_copy(entry.id);
|
||||
}
|
||||
}
|
||||
|
||||
void TopPeersStrip::stripMouseMoveEvent(QMouseEvent *e) {
|
||||
if (!_lastMousePosition) {
|
||||
_lastMousePosition = e->globalPos();
|
||||
if (_selectionByKeyboard) {
|
||||
return;
|
||||
}
|
||||
} else if (_selectionByKeyboard
|
||||
&& (_lastMousePosition == e->globalPos())) {
|
||||
return;
|
||||
}
|
||||
selectByMouse(e->globalPos());
|
||||
|
||||
if (!_dragging && _mouseDownPosition) {
|
||||
if ((*_lastMousePosition - *_mouseDownPosition).manhattanLength()
|
||||
>= QApplication::startDragDistance()) {
|
||||
_pressCancelled.fire({});
|
||||
if (!_expandAnimation.animating()) {
|
||||
_dragging = true;
|
||||
_startDraggingLeft = _scrollLeft;
|
||||
}
|
||||
}
|
||||
}
|
||||
checkDragging();
|
||||
}
|
||||
|
||||
void TopPeersStrip::selectByMouse(QPoint globalPosition) {
|
||||
_lastMousePosition = globalPosition;
|
||||
_selectionByKeyboard = false;
|
||||
updateSelected();
|
||||
}
|
||||
|
||||
void TopPeersStrip::checkDragging() {
|
||||
if (_dragging && !_expandAnimation.animating()) {
|
||||
const auto sign = (style::RightToLeft() ? -1 : 1);
|
||||
const auto newLeft = std::clamp(
|
||||
(sign * (_mouseDownPosition->x() - _lastMousePosition->x())
|
||||
+ _startDraggingLeft),
|
||||
0,
|
||||
_scrollLeftMax);
|
||||
if (newLeft != _scrollLeft) {
|
||||
_scrollLeft = newLeft;
|
||||
unsubscribeUserpics();
|
||||
update();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void TopPeersStrip::unsubscribeUserpics(bool all) {
|
||||
if (!all && (_expandAnimation.animating() || _expanded.current())) {
|
||||
return;
|
||||
}
|
||||
const auto single = outer().width();
|
||||
auto x = -_scrollLeft;
|
||||
for (auto &entry : _entries) {
|
||||
if (all || x + single <= 0 || x >= width()) {
|
||||
if (entry.subscribed) {
|
||||
entry.userpic->subscribeToUpdates(nullptr);
|
||||
entry.subscribed = false;
|
||||
}
|
||||
entry.userpicFrame = QImage();
|
||||
entry.onlineShown.stop();
|
||||
entry.ripple = nullptr;
|
||||
}
|
||||
x += single;
|
||||
}
|
||||
}
|
||||
|
||||
void TopPeersStrip::subscribeUserpic(Entry &entry) {
|
||||
const auto raw = entry.userpic.get();
|
||||
entry.userpic->subscribeToUpdates([=] {
|
||||
const auto i = ranges::find(
|
||||
_entries,
|
||||
raw,
|
||||
[&](const Entry &entry) { return entry.userpic.get(); });
|
||||
if (i != end(_entries)) {
|
||||
i->userpicFrameDirty = 1;
|
||||
}
|
||||
update();
|
||||
});
|
||||
entry.subscribed = true;
|
||||
}
|
||||
|
||||
void TopPeersStrip::stripMouseReleaseEvent(QMouseEvent *e) {
|
||||
_pressCancelled.fire({});
|
||||
|
||||
_lastMousePosition = e->globalPos();
|
||||
const auto guard = gsl::finally([&] {
|
||||
_mouseDownPosition = std::nullopt;
|
||||
});
|
||||
|
||||
const auto pressed = clearPressed();
|
||||
if (finishDragging()) {
|
||||
return;
|
||||
}
|
||||
_selectionByKeyboard = false;
|
||||
updateSelected();
|
||||
if (_selected >= 0 && _selected == pressed) {
|
||||
Assert(_selected < _entries.size());
|
||||
_clicks.fire_copy(_entries[_selected].id);
|
||||
}
|
||||
}
|
||||
|
||||
int TopPeersStrip::clearPressed() {
|
||||
const auto pressed = std::exchange(_pressed, -1);
|
||||
if (pressed >= 0) {
|
||||
Assert(pressed < _entries.size());
|
||||
auto &entry = _entries[pressed];
|
||||
if (entry.ripple) {
|
||||
entry.ripple->lastStop();
|
||||
}
|
||||
}
|
||||
return pressed;
|
||||
}
|
||||
|
||||
void TopPeersStrip::updateScrollMax(int newWidth) {
|
||||
if (_expandAnimation.animating()) {
|
||||
return;
|
||||
} else if (!newWidth) {
|
||||
newWidth = width();
|
||||
}
|
||||
if (_expanded.current()) {
|
||||
_scrollLeft = 0;
|
||||
_scrollLeftMax = 0;
|
||||
} else {
|
||||
const auto single = outer().width();
|
||||
const auto widthFull = int(_entries.size()) * single;
|
||||
_scrollLeftMax = std::max(widthFull - newWidth, 0);
|
||||
_scrollLeft = std::clamp(_scrollLeft, 0, _scrollLeftMax);
|
||||
}
|
||||
unsubscribeUserpics();
|
||||
update();
|
||||
}
|
||||
|
||||
bool TopPeersStrip::empty() const {
|
||||
return !_count.current();
|
||||
}
|
||||
|
||||
rpl::producer<bool> TopPeersStrip::emptyValue() const {
|
||||
return _count.value()
|
||||
| rpl::map(!rpl::mappers::_1)
|
||||
| rpl::distinct_until_changed();
|
||||
}
|
||||
|
||||
rpl::producer<uint64> TopPeersStrip::clicks() const {
|
||||
return _clicks.events();
|
||||
}
|
||||
|
||||
rpl::producer<uint64> TopPeersStrip::pressed() const {
|
||||
return _presses.events();
|
||||
}
|
||||
|
||||
rpl::producer<> TopPeersStrip::pressCancelled() const {
|
||||
return _pressCancelled.events();
|
||||
}
|
||||
|
||||
void TopPeersStrip::pressLeftToContextMenu(bool shown) {
|
||||
if (!shown) {
|
||||
_contexted = -1;
|
||||
update();
|
||||
return;
|
||||
}
|
||||
_contexted = clearPressed();
|
||||
if (finishDragging()) {
|
||||
return;
|
||||
}
|
||||
_mouseDownPosition = std::nullopt;
|
||||
}
|
||||
|
||||
auto TopPeersStrip::showMenuRequests() const
|
||||
-> rpl::producer<ShowTopPeerMenuRequest> {
|
||||
return _showMenuRequests.events();
|
||||
}
|
||||
|
||||
auto TopPeersStrip::scrollToRequests() const
|
||||
-> rpl::producer<Ui::ScrollToRequest> {
|
||||
return _scrollToRequests.events();
|
||||
}
|
||||
|
||||
void TopPeersStrip::removeLocally(uint64 id) {
|
||||
if (!id) {
|
||||
unsubscribeUserpics(true);
|
||||
setSelected(-1);
|
||||
_pressed = -1;
|
||||
_entries.clear();
|
||||
_hiddenLocally = true;
|
||||
_count = 0;
|
||||
return;
|
||||
}
|
||||
_removed.emplace(id);
|
||||
const auto i = ranges::find(_entries, id, &Entry::id);
|
||||
if (i == end(_entries)) {
|
||||
return;
|
||||
} else if (i->subscribed) {
|
||||
i->userpic->subscribeToUpdates(nullptr);
|
||||
}
|
||||
const auto index = int(i - begin(_entries));
|
||||
_entries.erase(i);
|
||||
if (_selected > index) {
|
||||
--_selected;
|
||||
}
|
||||
if (_pressed > index) {
|
||||
--_pressed;
|
||||
}
|
||||
if (_contexted > index) {
|
||||
--_contexted;
|
||||
}
|
||||
updateScrollMax();
|
||||
_count = int(_entries.size());
|
||||
update();
|
||||
}
|
||||
|
||||
bool TopPeersStrip::selectedByKeyboard() const {
|
||||
return _selectionByKeyboard && _selected >= 0;
|
||||
}
|
||||
|
||||
bool TopPeersStrip::selectByKeyboard(Qt::Key direction) {
|
||||
if (_entries.empty()) {
|
||||
return false;
|
||||
} else if (direction == Qt::Key()) {
|
||||
_selectionByKeyboard = true;
|
||||
if (_selected < 0) {
|
||||
setSelected(0);
|
||||
scrollToSelected();
|
||||
return true;
|
||||
}
|
||||
} else if (direction == Qt::Key_Left) {
|
||||
if (_selected > 0) {
|
||||
_selectionByKeyboard = true;
|
||||
setSelected(_selected - 1);
|
||||
scrollToSelected();
|
||||
return true;
|
||||
}
|
||||
} else if (direction == Qt::Key_Right) {
|
||||
if (_selected + 1 < _entries.size()) {
|
||||
_selectionByKeyboard = true;
|
||||
setSelected(_selected + 1);
|
||||
scrollToSelected();
|
||||
return true;
|
||||
}
|
||||
} else if (direction == Qt::Key_Up) {
|
||||
const auto layout = currentLayout();
|
||||
if (_selected < 0) {
|
||||
_selectionByKeyboard = true;
|
||||
const auto rows = _expanded.current()
|
||||
? ((int(_entries.size()) + layout.inrow - 1) / layout.inrow)
|
||||
: 1;
|
||||
setSelected((rows - 1) * layout.inrow);
|
||||
scrollToSelected();
|
||||
return true;
|
||||
} else if (!_expanded.current()) {
|
||||
deselectByKeyboard();
|
||||
} else if (_selected >= 0) {
|
||||
const auto row = _selected / layout.inrow;
|
||||
if (row > 0) {
|
||||
_selectionByKeyboard = true;
|
||||
setSelected(_selected - layout.inrow);
|
||||
scrollToSelected();
|
||||
return true;
|
||||
} else {
|
||||
deselectByKeyboard();
|
||||
}
|
||||
}
|
||||
} else if (direction == Qt::Key_Down) {
|
||||
if (_selected >= 0 && _expanded.current()) {
|
||||
const auto layout = currentLayout();
|
||||
const auto row = _selected / layout.inrow;
|
||||
const auto rows = (int(_entries.size()) + layout.inrow - 1)
|
||||
/ layout.inrow;
|
||||
if (row + 1 < rows) {
|
||||
_selectionByKeyboard = true;
|
||||
setSelected(std::min(
|
||||
_selected + layout.inrow,
|
||||
int(_entries.size()) - 1));
|
||||
scrollToSelected();
|
||||
return true;
|
||||
} else {
|
||||
deselectByKeyboard();
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void TopPeersStrip::deselectByKeyboard() {
|
||||
if (_selectionByKeyboard) {
|
||||
setSelected(-1);
|
||||
}
|
||||
}
|
||||
|
||||
bool TopPeersStrip::chooseRow() {
|
||||
if (_selected >= 0) {
|
||||
Assert(_selected < _entries.size());
|
||||
_clicks.fire_copy(_entries[_selected].id);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
uint64 TopPeersStrip::updateFromParentDrag(QPoint globalPosition) {
|
||||
if (!rect().contains(mapFromGlobal(globalPosition))) {
|
||||
dragLeft();
|
||||
return 0;
|
||||
}
|
||||
selectByMouse(globalPosition);
|
||||
|
||||
return (_selected >= 0) ? _entries[_selected].id : 0;
|
||||
}
|
||||
|
||||
void TopPeersStrip::dragLeft() {
|
||||
clearSelection();
|
||||
}
|
||||
|
||||
void TopPeersStrip::apply(const TopPeersList &list) {
|
||||
if (_hiddenLocally) {
|
||||
return;
|
||||
}
|
||||
auto now = std::vector<Entry>();
|
||||
|
||||
const auto selectedId = (_selected >= 0) ? _entries[_selected].id : 0;
|
||||
const auto pressedId = (_pressed >= 0) ? _entries[_pressed].id : 0;
|
||||
const auto contextedId = (_contexted >= 0) ? _entries[_contexted].id : 0;
|
||||
const auto restoreIndex = [&](uint64 id) {
|
||||
if (!id) {
|
||||
return -1;
|
||||
}
|
||||
const auto i = ranges::find(_entries, id, &Entry::id);
|
||||
return (i != end(_entries)) ? int(i - begin(_entries)) : -1;
|
||||
};
|
||||
|
||||
for (const auto &entry : list.entries) {
|
||||
if (_removed.contains(entry.id)) {
|
||||
continue;
|
||||
}
|
||||
const auto i = ranges::find(_entries, entry.id, &Entry::id);
|
||||
if (i != end(_entries)) {
|
||||
now.push_back(base::take(*i));
|
||||
} else {
|
||||
now.push_back({ .id = entry.id });
|
||||
}
|
||||
apply(now.back(), entry);
|
||||
}
|
||||
if (now.empty()) {
|
||||
_count = 0;
|
||||
}
|
||||
for (auto &entry : _entries) {
|
||||
if (entry.subscribed) {
|
||||
entry.userpic->subscribeToUpdates(nullptr);
|
||||
entry.subscribed = false;
|
||||
}
|
||||
}
|
||||
_entries = std::move(now);
|
||||
_selected = restoreIndex(selectedId);
|
||||
_pressed = restoreIndex(pressedId);
|
||||
_contexted = restoreIndex(contextedId);
|
||||
updateScrollMax();
|
||||
unsubscribeUserpics();
|
||||
_count = int(_entries.size());
|
||||
update();
|
||||
}
|
||||
|
||||
void TopPeersStrip::apply(Entry &entry, const TopPeersEntry &data) {
|
||||
Expects(entry.id == data.id);
|
||||
Expects(data.userpic != nullptr);
|
||||
|
||||
if (entry.name.toString() != data.name) {
|
||||
entry.name.setText(st::topPeers.nameStyle, data.name);
|
||||
}
|
||||
if (entry.userpic.get() != data.userpic.get()) {
|
||||
if (entry.subscribed) {
|
||||
entry.userpic->subscribeToUpdates(nullptr);
|
||||
}
|
||||
entry.userpic = data.userpic;
|
||||
if (entry.subscribed) {
|
||||
subscribeUserpic(entry);
|
||||
}
|
||||
}
|
||||
if (entry.online != data.online) {
|
||||
entry.online = data.online;
|
||||
if (!entry.subscribed) {
|
||||
entry.onlineShown.stop();
|
||||
} else {
|
||||
entry.onlineShown.start(
|
||||
[=] { update(); },
|
||||
entry.online ? 0. : 1.,
|
||||
entry.online ? 1. : 0.,
|
||||
st::dialogsOnlineBadgeDuration);
|
||||
}
|
||||
}
|
||||
if (entry.badge != data.badge) {
|
||||
entry.badge = data.badge;
|
||||
entry.badgeString = QString();
|
||||
entry.userpicFrameDirty = 1;
|
||||
}
|
||||
if (entry.unread != data.unread) {
|
||||
entry.unread = data.unread;
|
||||
if (!entry.badge) {
|
||||
entry.userpicFrameDirty = 1;
|
||||
}
|
||||
}
|
||||
if (entry.muted != data.muted) {
|
||||
entry.muted = data.muted;
|
||||
if (entry.badge || entry.unread) {
|
||||
entry.userpicFrameDirty = 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
QRect TopPeersStrip::outer() const {
|
||||
const auto &st = st::topPeers;
|
||||
const auto single = st.photoLeft * 2 + st.photo;
|
||||
return QRect(0, 0, single, st::topPeers.height);
|
||||
}
|
||||
|
||||
QRect TopPeersStrip::innerRounded() const {
|
||||
return outer().marginsRemoved(st::topPeersMargin);
|
||||
}
|
||||
|
||||
int TopPeersStrip::scrollLeft() const {
|
||||
const auto value = _expandAnimation.value(_expanded.current() ? 1. : 0.);
|
||||
return anim::interpolate(_scrollLeft, 0, value);
|
||||
}
|
||||
|
||||
void TopPeersStrip::paintStrip(QRect clip) {
|
||||
auto p = Painter(&_strip);
|
||||
|
||||
const auto &st = st::topPeers;
|
||||
const auto scroll = scrollLeft();
|
||||
|
||||
const auto rows = (height() + st.height - 1) / st.height;
|
||||
const auto fromrow = std::min(clip.y() / st.height, rows);
|
||||
const auto tillrow = std::min(
|
||||
(clip.y() + clip.height() + st.height - 1) / st.height,
|
||||
rows);
|
||||
const auto layout = currentLayout();
|
||||
const auto fsingle = layout.fsingle;
|
||||
const auto added = layout.added;
|
||||
|
||||
for (auto row = fromrow; row != tillrow; ++row) {
|
||||
const auto shift = scroll + row * layout.inrow * fsingle;
|
||||
const auto from = std::min(
|
||||
int(std::floor((shift + clip.x()) / fsingle)),
|
||||
int(_entries.size()));
|
||||
const auto till = std::clamp(
|
||||
int(std::ceil(
|
||||
(shift + clip.x() + clip.width() + fsingle - 1) / fsingle + 1
|
||||
)),
|
||||
from,
|
||||
int(_entries.size()));
|
||||
|
||||
auto x = int(base::SafeRound(-shift + from * fsingle + added));
|
||||
auto y = row * st.height;
|
||||
const auto highlighted = (_contexted >= 0)
|
||||
? _contexted
|
||||
: (_pressed >= 0)
|
||||
? _pressed
|
||||
: _selected;
|
||||
for (auto i = from; i != till; ++i) {
|
||||
auto &entry = _entries[i];
|
||||
const auto selected = (i == highlighted);
|
||||
if (selected) {
|
||||
_selection.paint(p, innerRounded().translated(x, y));
|
||||
}
|
||||
if (entry.ripple) {
|
||||
entry.ripple->paint(
|
||||
p,
|
||||
x + st::topPeersMargin.left(),
|
||||
y + st::topPeersMargin.top(),
|
||||
width());
|
||||
if (entry.ripple->empty()) {
|
||||
entry.ripple = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
if (!entry.subscribed) {
|
||||
subscribeUserpic(entry);
|
||||
}
|
||||
paintUserpic(p, x, y, i, selected);
|
||||
|
||||
p.setPen(st::dialogsNameFg);
|
||||
entry.name.drawElided(
|
||||
p,
|
||||
x + st.nameLeft,
|
||||
y + st.nameTop,
|
||||
layout.single - 2 * st.nameLeft,
|
||||
1,
|
||||
style::al_top);
|
||||
x += fsingle;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void TopPeersStrip::paintUserpic(
|
||||
Painter &p,
|
||||
int x,
|
||||
int y,
|
||||
int index,
|
||||
bool selected) {
|
||||
Expects(index >= 0 && index < _entries.size());
|
||||
|
||||
auto &entry = _entries[index];
|
||||
const auto &st = st::topPeers;
|
||||
const auto size = st.photo;
|
||||
const auto rect = QRect(x + st.photoLeft, y + st.photoTop, size, size);
|
||||
|
||||
const auto online = entry.onlineShown.value(entry.online ? 1. : 0.);
|
||||
const auto useFrame = !entry.userpicFrame.isNull()
|
||||
&& !entry.userpicFrameDirty
|
||||
&& (entry.userpicFrameOnline == online);
|
||||
if (useFrame) {
|
||||
p.drawImage(rect, entry.userpicFrame);
|
||||
return;
|
||||
}
|
||||
const auto simple = entry.userpic->image(size);
|
||||
const auto ratio = style::DevicePixelRatio();
|
||||
const auto renderFrame = (online > 0) || entry.badge || entry.unread;
|
||||
if (!renderFrame) {
|
||||
entry.userpicFrame = QImage();
|
||||
p.drawImage(rect, simple);
|
||||
return;
|
||||
} else if (entry.userpicFrame.size() != QSize(size, size) * ratio) {
|
||||
entry.userpicFrame = QImage(
|
||||
QSize(size, size) * ratio,
|
||||
QImage::Format_ARGB32_Premultiplied);
|
||||
entry.userpicFrame.setDevicePixelRatio(ratio);
|
||||
}
|
||||
entry.userpicFrame.fill(Qt::transparent);
|
||||
entry.userpicFrameDirty = 0;
|
||||
entry.userpicFrameOnline = online;
|
||||
|
||||
auto q = QPainter(&entry.userpicFrame);
|
||||
const auto inner = QRect(0, 0, size, size);
|
||||
q.drawImage(inner, simple);
|
||||
|
||||
auto hq = PainterHighQualityEnabler(q);
|
||||
|
||||
if (online > 0) {
|
||||
q.setCompositionMode(QPainter::CompositionMode_Source);
|
||||
const auto onlineSize = st::dialogsOnlineBadgeSize;
|
||||
const auto stroke = st::dialogsOnlineBadgeStroke;
|
||||
const auto skip = st::dialogsOnlineBadgeSkip;
|
||||
const auto shrink = (onlineSize / 2) * (1. - online);
|
||||
|
||||
auto pen = QPen(Qt::transparent);
|
||||
pen.setWidthF(stroke * online);
|
||||
q.setPen(pen);
|
||||
q.setBrush(st::dialogsOnlineBadgeFg);
|
||||
q.drawEllipse(QRectF(
|
||||
size - skip.x() - onlineSize,
|
||||
size - skip.y() - onlineSize,
|
||||
onlineSize,
|
||||
onlineSize
|
||||
).marginsRemoved({ shrink, shrink, shrink, shrink }));
|
||||
q.setCompositionMode(QPainter::CompositionMode_SourceOver);
|
||||
}
|
||||
|
||||
if (entry.badge || entry.unread) {
|
||||
if (entry.badgeString.isEmpty()) {
|
||||
entry.badgeString = !entry.badge
|
||||
? u" "_q
|
||||
: (entry.badge < 1000)
|
||||
? QString::number(entry.badge)
|
||||
: (QString::number(entry.badge / 1000) + 'K');
|
||||
}
|
||||
auto st = Ui::UnreadBadgeStyle();
|
||||
st.selected = selected;
|
||||
st.muted = entry.muted;
|
||||
const auto &counter = entry.badgeString;
|
||||
const auto badge = PaintUnreadBadge(q, counter, size, 0, st);
|
||||
|
||||
const auto width = style::ConvertScaleExact(2.);
|
||||
const auto add = (width - style::ConvertScaleExact(1.)) / 2.;
|
||||
auto pen = QPen(Qt::transparent);
|
||||
pen.setWidthF(width);
|
||||
q.setCompositionMode(QPainter::CompositionMode_Source);
|
||||
q.setPen(pen);
|
||||
q.setBrush(Qt::NoBrush);
|
||||
q.drawEllipse(QRectF(badge).marginsAdded({ add, add, add, add }));
|
||||
}
|
||||
|
||||
q.end();
|
||||
|
||||
p.drawImage(rect, entry.userpicFrame);
|
||||
}
|
||||
|
||||
void TopPeersStrip::stripContextMenuEvent(QContextMenuEvent *e) {
|
||||
_menu = nullptr;
|
||||
|
||||
if (e->reason() == QContextMenuEvent::Mouse) {
|
||||
_lastMousePosition = e->globalPos();
|
||||
_selectionByKeyboard = false;
|
||||
updateSelected();
|
||||
}
|
||||
if (_selected < 0 || _entries.empty()) {
|
||||
return;
|
||||
}
|
||||
Assert(_selected < _entries.size());
|
||||
_menu = base::make_unique_q<Ui::PopupMenu>(
|
||||
this,
|
||||
st::popupMenuWithIcons);
|
||||
_showMenuRequests.fire({
|
||||
_entries[_selected].id,
|
||||
Ui::Menu::CreateAddActionCallback(_menu),
|
||||
});
|
||||
if (_menu->empty()) {
|
||||
_menu = nullptr;
|
||||
return;
|
||||
}
|
||||
const auto updateAfterMenuDestroyed = [=] {
|
||||
const auto globalPosition = QCursor::pos();
|
||||
if (rect().contains(mapFromGlobal(globalPosition))) {
|
||||
_lastMousePosition = globalPosition;
|
||||
_selectionByKeyboard = false;
|
||||
updateSelected();
|
||||
}
|
||||
};
|
||||
QObject::connect(
|
||||
_menu.get(),
|
||||
&QObject::destroyed,
|
||||
crl::guard(&_menuGuard, updateAfterMenuDestroyed));
|
||||
_menu->popup(e->globalPos());
|
||||
e->accept();
|
||||
}
|
||||
|
||||
bool TopPeersStrip::finishDragging() {
|
||||
if (!_dragging) {
|
||||
return false;
|
||||
}
|
||||
checkDragging();
|
||||
_dragging = false;
|
||||
_selectionByKeyboard = false;
|
||||
updateSelected();
|
||||
return true;
|
||||
}
|
||||
|
||||
TopPeersStrip::Layout TopPeersStrip::currentLayout() const {
|
||||
const auto single = outer().width();
|
||||
const auto inrow = std::max(width() / single, 1);
|
||||
const auto value = _expandAnimation.value(_expanded.current() ? 1. : 0.);
|
||||
const auto esingle = (width() / float64(inrow));
|
||||
const auto fsingle = single + (esingle - single) * value;
|
||||
|
||||
return {
|
||||
.single = single,
|
||||
.inrow = inrow,
|
||||
.fsingle = fsingle,
|
||||
.added = (fsingle - single) / 2.,
|
||||
};
|
||||
}
|
||||
|
||||
void TopPeersStrip::updateSelected() {
|
||||
if (_pressed >= 0 || !_lastMousePosition || _selectionByKeyboard) {
|
||||
return;
|
||||
}
|
||||
const auto p = _strip.mapFromGlobal(*_lastMousePosition);
|
||||
const auto expanded = _expanded.current();
|
||||
const auto row = expanded ? (p.y() / st::topPeers.height) : 0;
|
||||
const auto layout = currentLayout();
|
||||
const auto column = (_scrollLeft + p.x()) / layout.fsingle;
|
||||
const auto index = row * layout.inrow + int(std::floor(column));
|
||||
setSelected((index < 0 || index >= _entries.size()) ? -1 : index);
|
||||
}
|
||||
|
||||
void TopPeersStrip::setSelected(int selected) {
|
||||
if (_selected != selected) {
|
||||
const auto over = (selected >= 0);
|
||||
if (over != (_selected >= 0)) {
|
||||
setCursor(over ? style::cur_pointer : style::cur_default);
|
||||
}
|
||||
_selected = selected;
|
||||
update();
|
||||
}
|
||||
}
|
||||
|
||||
void TopPeersStrip::clearSelection() {
|
||||
setSelected(-1);
|
||||
}
|
||||
|
||||
void TopPeersStrip::scrollToSelected() {
|
||||
if (_selected < 0) {
|
||||
return;
|
||||
} else if (_expanded.current()) {
|
||||
const auto layout = currentLayout();
|
||||
const auto row = _selected / layout.inrow;
|
||||
const auto header = _header.height();
|
||||
const auto top = header + row * st::topPeers.height;
|
||||
const auto bottom = top + st::topPeers.height;
|
||||
_scrollToRequests.fire({ top - (row ? 0 : header), bottom});
|
||||
} else {
|
||||
const auto single = outer().width();
|
||||
const auto left = _selected * single;
|
||||
const auto right = left + single;
|
||||
if (_scrollLeft > left) {
|
||||
_scrollLeft = std::clamp(left, 0, _scrollLeftMax);
|
||||
} else if (_scrollLeft + width() < right) {
|
||||
_scrollLeft = std::clamp(right - width(), 0, _scrollLeftMax);
|
||||
}
|
||||
const auto height = _header.height() + st::topPeers.height;
|
||||
_scrollToRequests.fire({ 0, height });
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace Dialogs
|
||||
151
Telegram/SourceFiles/dialogs/ui/top_peers_strip.h
Normal file
151
Telegram/SourceFiles/dialogs/ui/top_peers_strip.h
Normal file
@@ -0,0 +1,151 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "base/weak_ptr.h"
|
||||
#include "ui/effects/animations.h"
|
||||
#include "ui/widgets/menu/menu_add_action_callback.h"
|
||||
#include "ui/round_rect.h"
|
||||
#include "ui/rp_widget.h"
|
||||
|
||||
namespace Ui {
|
||||
class DynamicImage;
|
||||
class LinkButton;
|
||||
struct ScrollToRequest;
|
||||
} // namespace Ui
|
||||
|
||||
namespace Dialogs {
|
||||
|
||||
struct TopPeersEntry {
|
||||
uint64 id = 0;
|
||||
QString name;
|
||||
std::shared_ptr<Ui::DynamicImage> userpic;
|
||||
uint32 badge : 28 = 0;
|
||||
uint32 unread : 1 = 0;
|
||||
uint32 muted : 1 = 0;
|
||||
uint32 online : 1 = 0;
|
||||
};
|
||||
|
||||
struct TopPeersList {
|
||||
std::vector<TopPeersEntry> entries;
|
||||
};
|
||||
|
||||
struct ShowTopPeerMenuRequest {
|
||||
uint64 id = 0;
|
||||
Ui::Menu::MenuCallback callback;
|
||||
};
|
||||
|
||||
class TopPeersStrip final : public Ui::RpWidget {
|
||||
public:
|
||||
TopPeersStrip(
|
||||
not_null<QWidget*> parent,
|
||||
rpl::producer<TopPeersList> content);
|
||||
~TopPeersStrip();
|
||||
|
||||
[[nodiscard]] bool empty() const;
|
||||
[[nodiscard]] rpl::producer<bool> emptyValue() const;
|
||||
[[nodiscard]] rpl::producer<uint64> clicks() const;
|
||||
[[nodiscard]] rpl::producer<uint64> pressed() const;
|
||||
[[nodiscard]] rpl::producer<> pressCancelled() const;
|
||||
[[nodiscard]] auto showMenuRequests() const
|
||||
-> rpl::producer<ShowTopPeerMenuRequest>;
|
||||
[[nodiscard]] auto scrollToRequests() const
|
||||
-> rpl::producer<Ui::ScrollToRequest>;
|
||||
|
||||
void removeLocally(uint64 id = 0);
|
||||
|
||||
[[nodiscard]] bool selectedByKeyboard() const;
|
||||
bool selectByKeyboard(Qt::Key direction);
|
||||
void deselectByKeyboard();
|
||||
bool chooseRow();
|
||||
void pressLeftToContextMenu(bool shown);
|
||||
|
||||
uint64 updateFromParentDrag(QPoint globalPosition);
|
||||
void dragLeft();
|
||||
|
||||
[[nodiscard]] auto verticalScrollEvents() const
|
||||
-> rpl::producer<not_null<QWheelEvent*>>;
|
||||
|
||||
private:
|
||||
struct Entry;
|
||||
struct Layout;
|
||||
|
||||
int resizeGetHeight(int newWidth) override;
|
||||
|
||||
void setupHeader();
|
||||
void setupStrip();
|
||||
|
||||
void paintStrip(QRect clip);
|
||||
void stripWheelEvent(QWheelEvent *e);
|
||||
void stripMousePressEvent(QMouseEvent *e);
|
||||
void stripMouseMoveEvent(QMouseEvent *e);
|
||||
void stripMouseReleaseEvent(QMouseEvent *e);
|
||||
void stripContextMenuEvent(QContextMenuEvent *e);
|
||||
void stripLeaveEvent(QEvent *e);
|
||||
|
||||
void updateScrollMax(int newWidth = 0);
|
||||
void updateSelected();
|
||||
void setSelected(int selected);
|
||||
void setExpanded(bool expanded);
|
||||
void scrollToSelected();
|
||||
void checkDragging();
|
||||
bool finishDragging();
|
||||
void subscribeUserpic(Entry &entry);
|
||||
void unsubscribeUserpics(bool all = false);
|
||||
void paintUserpic(Painter &p, int x, int y, int index, bool selected);
|
||||
void clearSelection();
|
||||
void selectByMouse(QPoint globalPosition);
|
||||
|
||||
[[nodiscard]] QRect outer() const;
|
||||
[[nodiscard]] QRect innerRounded() const;
|
||||
[[nodiscard]] int scrollLeft() const;
|
||||
[[nodiscard]] Layout currentLayout() const;
|
||||
int clearPressed();
|
||||
void apply(const TopPeersList &list);
|
||||
void apply(Entry &entry, const TopPeersEntry &data);
|
||||
|
||||
Ui::RpWidget _header;
|
||||
Ui::RpWidget _strip;
|
||||
|
||||
std::vector<Entry> _entries;
|
||||
rpl::variable<int> _count = 0;
|
||||
base::flat_set<uint64> _removed;
|
||||
rpl::variable<Ui::LinkButton*> _toggleExpanded = nullptr;
|
||||
|
||||
rpl::event_stream<uint64> _clicks;
|
||||
rpl::event_stream<uint64> _presses;
|
||||
rpl::event_stream<> _pressCancelled;
|
||||
rpl::event_stream<ShowTopPeerMenuRequest> _showMenuRequests;
|
||||
rpl::event_stream<not_null<QWheelEvent*>> _verticalScrollEvents;
|
||||
|
||||
std::optional<QPoint> _lastMousePosition;
|
||||
std::optional<QPoint> _mouseDownPosition;
|
||||
int _startDraggingLeft = 0;
|
||||
int _scrollLeft = 0;
|
||||
int _scrollLeftMax = 0;
|
||||
bool _dragging = false;
|
||||
Qt::Orientation _scrollingLock = {};
|
||||
|
||||
int _selected = -1;
|
||||
int _pressed = -1;
|
||||
int _contexted = -1;
|
||||
bool _selectionByKeyboard = false;
|
||||
bool _hiddenLocally = false;
|
||||
|
||||
Ui::Animations::Simple _expandAnimation;
|
||||
rpl::variable<bool> _expanded = false;
|
||||
|
||||
rpl::event_stream<Ui::ScrollToRequest> _scrollToRequests;
|
||||
|
||||
Ui::RoundRect _selection;
|
||||
base::unique_qptr<Ui::PopupMenu> _menu;
|
||||
base::has_weak_ptr _menuGuard;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Dialogs
|
||||
Reference in New Issue
Block a user