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

This commit is contained in:
allhaileris
2026-02-16 15:50:16 +03:00
commit afb81b8278
13816 changed files with 3689732 additions and 0 deletions

View 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;
}
}

View 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

View 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

View 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

View 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

View 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

File diff suppressed because it is too large Load Diff

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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/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

View File

@@ -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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

File diff suppressed because it is too large Load Diff

View 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 &params);
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 &params);
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 &params);
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

View 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

View 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

View 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

View 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

File diff suppressed because it is too large Load Diff

View 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

View 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

View 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

View 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

View File

@@ -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

View 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

View 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

File diff suppressed because it is too large Load Diff

View 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

File diff suppressed because it is too large Load Diff

View 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

View File

@@ -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

View File

@@ -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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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