diff --git a/Telegram/PrepareWin.bat b/Telegram/PrepareWin.bat index d7b546d08..262c7a1fa 100644 --- a/Telegram/PrepareWin.bat +++ b/Telegram/PrepareWin.bat @@ -1,10 +1,10 @@ @echo OFF -set "AppVersion=8024" -set "AppVersionStrSmall=0.8.24" -set "AppVersionStr=0.8.24" -set "AppVersionStrFull=0.8.24.0" -set "DevChannel=0" +set "AppVersion=8025" +set "AppVersionStrSmall=0.8.25" +set "AppVersionStr=0.8.25" +set "AppVersionStrFull=0.8.25.0" +set "DevChannel=1" if %DevChannel% neq 0 goto preparedev diff --git a/Telegram/Resources/lang.strings b/Telegram/Resources/lang.strings index 7ecf842e1..f0fb249ad 100644 --- a/Telegram/Resources/lang.strings +++ b/Telegram/Resources/lang.strings @@ -70,6 +70,8 @@ Copyright (c) 2014 John Preston, https://desktop.telegram.org "lng_status_service_notifications" = "service notifications"; "lng_status_bot" = "bot"; +"lng_status_bot_reads_all" = "sees all messages"; +"lng_status_bot_not_reads_all" = "only sees messages starting with /"; "lng_status_offline" = "last seen a long time ago"; "lng_status_recently" = "last seen recently"; "lng_status_last_week" = "last seen within a week"; @@ -334,7 +336,10 @@ Copyright (c) 2014 John Preston, https://desktop.telegram.org "lng_profile_chat_unaccessible" = "Group is unaccessible"; "lng_topbar_info" = "Info"; +"lng_profile_about_section" = "About"; "lng_profile_settings_section" = "Settings"; +"lng_profile_bot_settings" = "Settings"; +"lng_profile_bot_help" = "Help"; "lng_profile_participants_section" = "Members"; "lng_profile_info" = "Contact info"; "lng_profile_group_info" = "Group info"; @@ -344,6 +349,7 @@ Copyright (c) 2014 John Preston, https://desktop.telegram.org "lng_profile_clear_history" = "Clear history"; "lng_profile_send_message" = "Send Message"; "lng_profile_share_contact" = "Share Contact"; +"lng_profile_invite_to_group" = "Add to Group"; "lng_profile_delete_contact" = "Delete"; "lng_profile_set_group_photo" = "Set Photo"; "lng_profile_add_participant" = "Add Member"; @@ -458,6 +464,12 @@ Copyright (c) 2014 John Preston, https://desktop.telegram.org "lng_from_you" = "You"; "lng_bot_description" = "What can this bot do?"; +"lng_bot_choose_group" = "Choose Group"; +"lng_bot_no_groups" = "You have no groups"; +"lng_bot_groups_not_found" = "No groups found"; +"lng_bot_sure_invite" = "Add the bot to «{group}»?"; +"lng_bot_already_in_group" = "The bot is already a member of the group."; + "lng_typing" = "typing"; "lng_user_typing" = "{user} is typing"; "lng_users_typing" = "{user} and {second_user} are typing"; @@ -587,7 +599,7 @@ Copyright (c) 2014 John Preston, https://desktop.telegram.org "lng_new_version_wrap" = "Telegram Desktop was updated to version {version}\n\n{changes}\n\nFull version history is available here:\n{link}"; "lng_new_version_minor" = "— Bug fixes and other minor improvements"; -"lng_new_version_text" = "— Improved sticker panel\n— Bug fixes and minor stuff"; +"lng_new_version_text" = "This new version includes support for bots using the new bot API. If you're an engineer, create your own bots like @quiz_bot or @hot_or_bot using the @botfather.\n\nLearn more at {blog_link}"; "lng_menu_insert_unicode" = "Insert Unicode control character"; diff --git a/Telegram/Resources/style.txt b/Telegram/Resources/style.txt index 038a267b1..7b34bbfb9 100644 --- a/Telegram/Resources/style.txt +++ b/Telegram/Resources/style.txt @@ -992,6 +992,18 @@ btnAttachEmoji: iconedButton(btnAttachDocument) { width: 33px; } +btnBotKbShow: iconedButton(btnAttachEmoji) { + icon: sprite(375px, 74px, 21px, 16px); + iconPos: point(6px, 16px); + downIcon: sprite(375px, 74px, 21px, 16px); + downIconPos: point(6px, 16px); +} +btnBotKbHide: iconedButton(btnAttachEmoji) { + icon: sprite(352px, 74px, 23px, 14px); + iconPos: point(5px, 17px); + downIcon: sprite(352px, 74px, 23px, 14px); + downIconPos: point(5px, 17px); +} btnRecordAudio: sprite(363px, 366px, 16px, 24px); btnRecordAudioActive: sprite(379px, 366px, 16px, 24px); recordSignalColor: #f17077; @@ -1041,7 +1053,7 @@ textRectMargins: margins(-2px, -1px, -2px, -1px); taMsgField: flatTextarea(taDefFlat) { font: msgFont; } -maxFieldHeight: 250px; +maxFieldHeight: 265px; newMsgSound: ':/gui/art/newmsg.wav'; @@ -1658,6 +1670,31 @@ stickerIconLeft: sprite(342px, 72px, 40px, 1px); stickerIconRight: sprite(342px, 73px, 40px, 1px); stickerIconMove: 400; +botKbDuration: 200; +botKbBg: #f7f7f7; +botKbOverBg: #e8ecef; +botKbDownBg: #dfe3e6; +botKbColor: #8a8a8f; +botKbFont: font(16px); +botKbButton: botKeyboardButton { + margin: 10px; + padding: 14px; + height: 46px; + textTop: 13px; + downTextTop: 14px; +} +botKbTinyButton: botKeyboardButton { + margin: 4px; + padding: 3px; + height: 25px; + textTop: 2px; + downTextTop: 3px; +} +botKbScroll: flatScroll(newScroll) { + deltax: 3px; + width: 10px; +} + mvBgColor: #222; mvBgOpacity: 0.92; mvThickFont: font(fsize semibold); @@ -1887,6 +1924,8 @@ mentionPadding: margins(8px, 5px, 8px, 5px); mentionTop: 11px; mentionFont: linkFont; mentionPhotoSize: msgPhotoSize; +botCommandFont: font(fsize semibold); +botDescFont: font(fsize italic); sessionsHeight: 440px; sessionHeight: 70px; diff --git a/Telegram/Resources/style_classes.txt b/Telegram/Resources/style_classes.txt index 69dc94fe9..2f06f4a85 100644 --- a/Telegram/Resources/style_classes.txt +++ b/Telegram/Resources/style_classes.txt @@ -259,3 +259,11 @@ dropdown { duration: number; width: number; } + +botKeyboardButton { + margin: number; + padding: number; + height: number; + textTop: number; + downTextTop: number; +} diff --git a/Telegram/SourceFiles/apiwrap.cpp b/Telegram/SourceFiles/apiwrap.cpp index a87b1bd57..56fcd6f5d 100644 --- a/Telegram/SourceFiles/apiwrap.cpp +++ b/Telegram/SourceFiles/apiwrap.cpp @@ -119,6 +119,19 @@ void ApiWrap::gotChatFull(PeerData *peer, const MTPmessages_ChatFull &result) { App::feedUsers(d.vusers); App::feedChats(d.vchats); App::feedParticipants(f.vparticipants); + const QVector &v(f.vbot_info.c_vector().v); + for (QVector::const_iterator i = v.cbegin(), e = v.cend(); i < e; ++i) { + switch (i->type()) { + case mtpc_botInfo: { + const MTPDbotInfo &b(i->c_botInfo()); + UserData *user = App::userLoaded(b.vuser_id.v); + if (user) { + user->setBotInfo(*i); + emit fullPeerUpdated(user); + } + } break; + } + } PhotoData *photo = App::feedPhoto(f.vchat_photo); ChatData *chat = peer->asChat(); if (chat) { @@ -132,7 +145,7 @@ void ApiWrap::gotChatFull(PeerData *peer, const MTPmessages_ChatFull &result) { App::main()->gotNotifySetting(MTP_inputNotifyPeer(peer->input), f.vnotify_settings); _fullRequests.remove(peer); - emit fullPeerLoaded(peer); + emit fullPeerUpdated(peer); } void ApiWrap::gotUserFull(PeerData *peer, const MTPUserFull &result) { @@ -145,7 +158,7 @@ void ApiWrap::gotUserFull(PeerData *peer, const MTPUserFull &result) { peer->asUser()->setBotInfo(d.vbot_info); _fullRequests.remove(peer); - emit fullPeerLoaded(peer); + emit fullPeerUpdated(peer); } bool ApiWrap::gotPeerFailed(PeerData *peer, const RPCError &error) { diff --git a/Telegram/SourceFiles/apiwrap.h b/Telegram/SourceFiles/apiwrap.h index 6a8b80cd7..5b7daff30 100644 --- a/Telegram/SourceFiles/apiwrap.h +++ b/Telegram/SourceFiles/apiwrap.h @@ -40,7 +40,7 @@ public: signals: - void fullPeerLoaded(PeerData *peer); + void fullPeerUpdated(PeerData *peer); public slots: diff --git a/Telegram/SourceFiles/app.cpp b/Telegram/SourceFiles/app.cpp index a8cb9df03..38d7df130 100644 --- a/Telegram/SourceFiles/app.cpp +++ b/Telegram/SourceFiles/app.cpp @@ -55,6 +55,9 @@ namespace { typedef QHash WebPagesData; WebPagesData webPagesData; + typedef QMap ReplyMarkups; + ReplyMarkups replyMarkups; + VideoItems videoItems; AudioItems audioItems; DocumentItems documentItems; @@ -208,7 +211,7 @@ namespace App { int32 onlineForSort(UserData *user, int32 now) { if (isServiceUser(user->id) || user->botInfo) { - return now - 1; + return -1; } int32 online = user->onlineTill; if (online <= 0) { @@ -343,6 +346,7 @@ namespace App { data->setName(lang(lng_deleted), QString(), QString(), QString()); data->setPhoto(MTP_userProfilePhotoEmpty()); data->access = UserNoAccess; + data->setBotInfoVersion(-1); wasContact = (data->contact > 0); status = &emptyStatus; data->contact = -1; @@ -382,7 +386,13 @@ namespace App { status = d.has_status() ? &d.vstatus : &emptyStatus; } wasContact = (data->contact > 0); - if (d.has_bot_info_version()) data->setBotInfoVersion(d.vbot_info_version.v); + if (d.has_bot_info_version()) { + data->setBotInfoVersion(d.vbot_info_version.v); + data->botInfo->readsAllHistory = (d.vflags.v & MTPDuser_flag_bot_reads_all); + data->botInfo->cantJoinGroups = (d.vflags.v & MTPDuser_flag_bot_cant_join); + } else { + data->setBotInfoVersion(-1); + } data->contact = (flags & (MTPDuser_flag_contact | MTPDuser_flag_mutual_contact)) ? 1 : (data->phone.isEmpty() ? -1 : 0); if ((flags & MTPDuser_flag_self) && ::self != data) { ::self = data; @@ -446,6 +456,7 @@ namespace App { if (data->version < d.vversion.v) { data->version = d.vversion.v; data->participants = ChatData::Participants(); + data->botStatus = 0; } } break; case mtpc_chatForbidden: { @@ -479,6 +490,7 @@ namespace App { if (data->version < d.vversion.v) { data->version = d.vversion.v; data->participants = ChatData::Participants(); + data->botStatus = 0; }/**/ } break; } @@ -492,7 +504,7 @@ namespace App { return data; } - void feedParticipants(const MTPChatParticipants &p) { + void feedParticipants(const MTPChatParticipants &p, bool requestBotInfos) { switch (p.type()) { case mtpc_chatParticipantsForbidden: { const MTPDchatParticipantsForbidden &d(p.c_chatParticipantsForbidden()); @@ -519,17 +531,24 @@ namespace App { } } else { chat->participants = ChatData::Participants(); + chat->botStatus = 0; break; } } if (!chat->participants.isEmpty()) { + int32 botStatus = -1; for (ChatData::Participants::iterator i = chat->participants.begin(), e = chat->participants.end(); i != e;) { if (i.value() < pversion) { i = chat->participants.erase(i); } else { + if (i.key()->botInfo) { + botStatus = (botStatus > 0 || i.key()->botInfo->readsAllHistory) ? 2 : 1; + if (requestBotInfos && !i.key()->botInfo->inited) App::api()->requestFullPeer(i.key()); + } ++i; } } + chat->botStatus = botStatus; } if (App::main()) App::main()->peerUpdated(chat); } @@ -545,6 +564,7 @@ namespace App { if (user) { if (chat->participants.isEmpty() && chat->count) { chat->count++; + chat->botStatus = 0; } else if (chat->participants.find(user) == chat->participants.end()) { chat->participants[user] = (chat->participants.isEmpty() ? 1 : chat->participants.begin().value()); if (d.vinviter_id.v == MTP::authedId()) { @@ -553,9 +573,14 @@ namespace App { chat->cankick.remove(user); } chat->count++; + if (user->botInfo) { + chat->botStatus = (chat->botStatus > 0 || !user->botInfo->readsAllHistory) ? 2 : 1; + if (!user->botInfo->inited) App::api()->requestFullPeer(user); + } } } else { chat->participants = ChatData::Participants(); + chat->botStatus = 0; chat->count++; } if (App::main()) App::main()->peerUpdated(chat); @@ -576,9 +601,23 @@ namespace App { chat->participants.erase(i); chat->count--; } + if (chat->botStatus > 0 && user->botInfo) { + int32 botStatus = -1; + for (ChatData::Participants::const_iterator j = chat->participants.cbegin(), e = chat->participants.cend(); j != e; ++j) { + if (j.key()->botInfo) { + if (botStatus > 0 || !j.key()->botInfo->readsAllHistory) { + botStatus = 2; + break; + } + botStatus = 1; + } + } + chat->botStatus = botStatus; + } } } else { chat->participants = ChatData::Participants(); + chat->botStatus = 0; chat->count--; } if (App::main()) App::main()->peerUpdated(chat); @@ -1479,6 +1518,7 @@ namespace App { } ::maxMsgId = 0; ::hoveredItem = ::pressedItem = ::hoveredLinkItem = ::pressedLinkItem = ::contextItem = 0; + replyMarkups.clear(); } void historyClearItems() { @@ -1634,6 +1674,9 @@ namespace App { prepareCorners(MediaviewSaveCorners, st::msgRadius, st::emojiPanHover); prepareCorners(EmojiHoverCorners, st::msgRadius, st::emojiPanHover); prepareCorners(StickerHoverCorners, st::msgRadius, st::emojiPanHover); + prepareCorners(BotKeyboardCorners, st::msgRadius, st::botKbBg); + prepareCorners(BotKeyboardOverCorners, st::msgRadius, st::botKbOverBg); + prepareCorners(BotKeyboardDownCorners, st::msgRadius, st::botKbDownBg); prepareCorners(MessageInCorners, st::msgRadius, st::msgInBg, &st::msgInShadow); prepareCorners(MessageInSelectedCorners, st::msgRadius, st::msgInSelectBg, &st::msgInSelectShadow); @@ -1928,6 +1971,106 @@ namespace App { if (changeInMin) App::main()->updateMutedIn(changeInMin); } + void feedReplyMarkup(MsgId msgId, const MTPReplyMarkup &markup) { + ReplyMarkup data; + switch (markup.type()) { + case mtpc_replyKeyboardMarkup: { + const MTPDreplyKeyboardMarkup &d(markup.c_replyKeyboardMarkup()); + const QVector &v(d.vrows.c_vector().v); + if (!v.isEmpty()) { + data.reserve(v.size()); + for (int32 i = 0, l = v.size(); i < l; ++i) { + switch (v.at(i).type()) { + case mtpc_keyboardButtonRow: { + const MTPDkeyboardButtonRow &r(v.at(i).c_keyboardButtonRow()); + const QVector &b(r.vbuttons.c_vector().v); + if (!b.isEmpty()) { + QList btns; + btns.reserve(b.size()); + for (int32 j = 0, s = b.size(); j < s; ++j) { + switch (b.at(j).type()) { + case mtpc_keyboardButton: { + btns.push_back(qs(b.at(j).c_keyboardButton().vtext)); + } break; + } + } + if (!btns.isEmpty()) data.push_back(btns); + } + } break; + } + } + if (!data.isEmpty()) { + replyMarkups.insert(msgId, data); + } + } + } break; + } + } + + void clearReplyMarkup(MsgId msgId) { + replyMarkups.remove(msgId); + } + + const ReplyMarkup &replyMarkup(MsgId msgId) { + static ReplyMarkup zeroMarkup; + if (zeroMarkup.isEmpty()) { + QList cmds; + cmds.push_back("Test command 1Test comma"); + cmds.push_back("Test comma" + emojiGetSequence(0)); + zeroMarkup.push_back(cmds); + cmds.clear(); + cmds.push_back("123 Test command 1"); + cmds.push_back("321 Test command 3"); + cmds.push_back("123 Test command 4"); + zeroMarkup.push_back(cmds); + cmds.clear(); + cmds.push_back("Test command 11111"); + cmds.push_back("Test command 222222"); + cmds.push_back("Test command 33333"); + cmds.push_back("Test command 444444"); + cmds.push_back("Test command 55555"); + zeroMarkup.push_back(cmds); + cmds.clear(); + cmds.push_back("123 1"); + cmds.push_back("321 3"); + zeroMarkup.push_back(cmds); + cmds.clear(); + cmds.push_back("Test command 11111"); + cmds.push_back("Test command 222222"); + cmds.push_back("Test command 33333"); + cmds.push_back("Test command 444444"); + cmds.push_back("Test command 55555"); + cmds.push_back("123 Test command 1"); + cmds.push_back("321 Test command 3"); + cmds.push_back("123 Test command 4"); + zeroMarkup.push_back(cmds); + cmds.clear(); + cmds.push_back("Test command 11111"); + cmds.push_back("Test command 222222"); + cmds.push_back("Test command 33333"); + cmds.push_back("Test command 444444"); + cmds.push_back("Test command 55555"); + cmds.push_back("123 Test command 1"); + cmds.push_back("321 Test command 3"); + cmds.push_back("123 Test command 4"); + zeroMarkup.push_back(cmds); + cmds.clear(); + cmds.push_back("Test command 11111"); + cmds.push_back("Test command 222222"); + cmds.push_back("Test command 33333"); + cmds.push_back("Test command 444444"); + cmds.push_back("Test command 55555"); + cmds.push_back("123 Test command 1"); + cmds.push_back("321 Test command 3"); + cmds.push_back("123 Test command 4"); + zeroMarkup.push_back(cmds); + cmds.clear(); + } + ReplyMarkups::const_iterator i = replyMarkups.constFind(msgId); + if (i == replyMarkups.cend() || true) return zeroMarkup; + return i.value(); + } + void setProxySettings(QNetworkAccessManager &manager) { if (cConnectionType() == dbictHttpProxy) { const ConnectionProxy &p(cConnectionProxy()); @@ -1946,9 +2089,9 @@ namespace App { } } - void sendBotCommand(const QString &cmd) { + void sendBotCommand(const QString &cmd, MsgId replyTo) { if (App::main()) { - App::main()->sendBotCommand(cmd); + App::main()->sendBotCommand(cmd, replyTo); } } diff --git a/Telegram/SourceFiles/app.h b/Telegram/SourceFiles/app.h index 86639d91d..056db79c9 100644 --- a/Telegram/SourceFiles/app.h +++ b/Telegram/SourceFiles/app.h @@ -35,6 +35,7 @@ typedef QHash VideoItems; typedef QHash AudioItems; typedef QHash DocumentItems; typedef QHash WebPageItems; +typedef QList > ReplyMarkup; enum RoundCorners { MaskCorners = 0x00, // for images @@ -48,6 +49,9 @@ enum RoundCorners { MediaviewSaveCorners, EmojiHoverCorners, StickerHoverCorners, + BotKeyboardCorners, + BotKeyboardOverCorners, + BotKeyboardDownCorners, InShadowCorners, // for photos without bg InSelectedShadowCorners, @@ -99,7 +103,7 @@ namespace App { UserData *feedUsers(const MTPVector &users); // returns last user ChatData *feedChats(const MTPVector &chats); // returns last chat - void feedParticipants(const MTPChatParticipants &p); + void feedParticipants(const MTPChatParticipants &p, bool requestBotInfos = false); void feedParticipantAdd(const MTPDupdateChatParticipantAdd &d); void feedParticipantDelete(const MTPDupdateChatParticipantDelete &d); void feedMsgs(const MTPVector &msgs, int msgsState = 0); // 2 - new read message, 1 - new unread message, 0 - not new message, -1 - searched message @@ -222,10 +226,14 @@ namespace App { void unregMuted(PeerData *peer); void updateMuted(); + void feedReplyMarkup(MsgId msgId, const MTPReplyMarkup &markup); + void clearReplyMarkup(MsgId msgId); + const ReplyMarkup &replyMarkup(MsgId msgId); + void setProxySettings(QNetworkAccessManager &manager); void setProxySettings(QTcpSocket &socket); - void sendBotCommand(const QString &cmd); + void sendBotCommand(const QString &cmd, MsgId replyTo = 0); void searchByHashtag(const QString &tag); void openUserByName(const QString &username, bool toProfile = false); void joinGroupByHash(const QString &hash); diff --git a/Telegram/SourceFiles/application.cpp b/Telegram/SourceFiles/application.cpp index f86acc229..c1b086843 100644 --- a/Telegram/SourceFiles/application.cpp +++ b/Telegram/SourceFiles/application.cpp @@ -640,10 +640,10 @@ void Application::checkMapVersion() { psRegisterCustomScheme(); if (Local::oldMapVersion()) { QString versionFeatures; - if (DevChannel && Local::oldMapVersion() < 8023) { - versionFeatures = QString::fromUtf8("\xe2\x80\x94 Improved sticker panel\n\xe2\x80\x94 Bug fixes and minor stuff");// .replace('@', qsl("@") + QChar(0x200D)); + if (DevChannel && Local::oldMapVersion() < 8025) { + versionFeatures = QString::fromUtf8("\xe2\x80\x94 IPv6 connections support\n\xe2\x80\x94 Bug fixes and minor stuff");// .replace('@', qsl("@") + QChar(0x200D)); } else if (!DevChannel && Local::oldMapVersion() < 8024) { - versionFeatures = lang(lng_new_version_text).trimmed(); + versionFeatures = lng_new_version_text(lt_blog_link, qsl("https://telegram.org/blog/bot-revolution"));// lang(lng_new_version_text).trimmed(); } if (!versionFeatures.isEmpty()) { versionFeatures = lng_new_version_wrap(lt_version, QString::fromStdWString(AppVersionStr), lt_changes, versionFeatures, lt_link, qsl("https://desktop.telegram.org/#changelog")); diff --git a/Telegram/SourceFiles/art/sprite.png b/Telegram/SourceFiles/art/sprite.png index 9840c1ea6..329aeef7b 100644 Binary files a/Telegram/SourceFiles/art/sprite.png and b/Telegram/SourceFiles/art/sprite.png differ diff --git a/Telegram/SourceFiles/art/sprite_200x.png b/Telegram/SourceFiles/art/sprite_200x.png index c6b647f0c..0786e72ae 100644 Binary files a/Telegram/SourceFiles/art/sprite_200x.png and b/Telegram/SourceFiles/art/sprite_200x.png differ diff --git a/Telegram/SourceFiles/boxes/contactsbox.cpp b/Telegram/SourceFiles/boxes/contactsbox.cpp index db9a9b674..579a0cd07 100644 --- a/Telegram/SourceFiles/boxes/contactsbox.cpp +++ b/Telegram/SourceFiles/boxes/contactsbox.cpp @@ -23,7 +23,9 @@ Copyright (c) 2014 John Preston, https://desktop.telegram.org #include "mainwidget.h" #include "window.h" -ContactsInner::ContactsInner(bool creatingChat) : _chat(0), _creatingChat(creatingChat), +#include "confirmbox.h" + +ContactsInner::ContactsInner(bool creatingChat) : _chat(0), _bot(0), _creatingChat(creatingChat), _addToChat(0), _contacts(&App::main()->contactsList()), _sel(0), _filteredSel(-1), @@ -35,7 +37,7 @@ _addContactLnk(this, lang(lng_add_contact_button)) { init(); } -ContactsInner::ContactsInner(ChatData *chat) : _chat(chat), _creatingChat(false), +ContactsInner::ContactsInner(ChatData *chat) : _chat(chat), _bot(0), _creatingChat(false), _addToChat(0), _contacts(&App::main()->contactsList()), _sel(0), _filteredSel(-1), @@ -47,6 +49,24 @@ _addContactLnk(this, lang(lng_add_contact_button)) { init(); } +ContactsInner::ContactsInner(UserData *bot) : _chat(0), _bot(bot), _creatingChat(false), _addToChat(0), +_contacts(new DialogsIndexed(DialogsSortByAdd)), +_sel(0), +_filteredSel(-1), +_mouseSel(false), +_selCount(0), +_searching(false), +_byUsernameSel(-1), +_addContactLnk(this, lang(lng_add_contact_button)) { + DialogsIndexed &v(App::main()->dialogsList()); + for (DialogRow *r = v.list.begin; r != v.list.end; r = r->next) { + if (r->history->peer->chat && !r->history->peer->asChat()->forbidden) { + _contacts->addToEnd(r->history); + } + } + init(); +} + void ContactsInner::init() { connect(&_addContactLnk, SIGNAL(clicked()), App::wnd(), SLOT(onShowAddContact())); @@ -59,10 +79,21 @@ void ContactsInner::init() { connect(App::main(), SIGNAL(dialogRowReplaced(DialogRow *, DialogRow *)), this, SLOT(onDialogRowReplaced(DialogRow *, DialogRow *))); connect(App::main(), SIGNAL(peerUpdated(PeerData*)), this, SLOT(peerUpdated(PeerData *))); - connect(App::main(), SIGNAL(peerNameChanged(PeerData *, const PeerData::Names &, const PeerData::NameFirstChars &)), this, SLOT(peerUpdated(PeerData *))); + connect(App::main(), SIGNAL(peerNameChanged(PeerData *, const PeerData::Names &, const PeerData::NameFirstChars &)), this, SLOT(onPeerNameChanged(PeerData *, const PeerData::Names &, const PeerData::NameFirstChars &))); connect(App::main(), SIGNAL(peerPhotoChanged(PeerData *)), this, SLOT(peerUpdated(PeerData *))); } +void ContactsInner::onPeerNameChanged(PeerData *peer, const PeerData::Names &oldNames, const PeerData::NameFirstChars &oldChars) { + if (bot()) { + _contacts->peerNameChanged(peer, oldNames, oldChars); + } + peerUpdated(peer); +} + +void ContactsInner::onAddBot() { + App::main()->addParticipants(_addToChat, QVector(1, _bot)); +} + void ContactsInner::peerUpdated(PeerData *peer) { if (_chat && (!peer || peer == _chat)) { if (_chat->forbidden) { @@ -81,8 +112,8 @@ void ContactsInner::peerUpdated(PeerData *peer) { } } } - } else if (!peer->chat) { - ContactsData::iterator i = _contactsData.find(peer->asUser()); + } else { + ContactsData::iterator i = _contactsData.find(peer); if (i != _contactsData.cend()) { for (DialogRow *row = _contacts->list.begin; row->next; row = row->next) { if (row->attached == i.value()) row->attached = 0; @@ -136,14 +167,23 @@ void ContactsInner::loadProfilePhotos(int32 yFrom) { ContactsInner::ContactData *ContactsInner::contactData(DialogRow *row) { ContactData *data = (ContactData*)row->attached; if (!data) { - UserData *user = row->history->peer->asUser(); - ContactsData::const_iterator i = _contactsData.constFind(user); + PeerData *peer = row->history->peer; + ContactsData::const_iterator i = _contactsData.constFind(peer); if (i == _contactsData.cend()) { - _contactsData.insert(user, data = new ContactData()); - data->inchat = _chat ? _chat->participants.contains(user) : false; + _contactsData.insert(peer, data = new ContactData()); + data->inchat = (_chat && !peer->chat) ? _chat->participants.contains(peer->asUser()) : false; data->check = false; - data->name.setText(st::profileListNameFont, user->name, _textNameOptions); - data->online = App::onlineText(user, _time); + data->name.setText(st::profileListNameFont, peer->name, _textNameOptions); + if (peer->chat) { + ChatData *chat = peer->asChat(); + if (chat->forbidden) { + data->online = lang(lng_chat_status_unaccessible); + } else { + data->online = lng_chat_status_members(lt_count, chat->count); + } + } else { + data->online = App::onlineText(peer->asUser(), _time); + } } else { data = i.value(); } @@ -152,9 +192,11 @@ ContactsInner::ContactData *ContactsInner::contactData(DialogRow *row) { return data; } -void ContactsInner::paintDialog(QPainter &p, UserData *user, ContactData *data, bool sel) { +void ContactsInner::paintDialog(QPainter &p, PeerData *peer, ContactData *data, bool sel) { int32 left = st::profileListPadding.width(); + UserData *user = peer->chat ? 0 : peer->asUser(); + if (data->inchat || data->check || _selCount + (_chat ? _chat->count : 0) >= cMaxGroupCount()) { sel = false; } @@ -163,7 +205,7 @@ void ContactsInner::paintDialog(QPainter &p, UserData *user, ContactData *data, p.fillRect(0, 0, width(), 2 * st::profileListPadding.height() + st::profileListPhotoSize, ((data->inchat || data->check) ? st::profileActiveBG : st::profileHoverBG)->b); } - p.drawPixmap(left, st::profileListPadding.height(), user->photo->pix(st::profileListPhotoSize)); + p.drawPixmap(left, st::profileListPadding.height(), peer->photo->pix(st::profileListPhotoSize)); if (data->inchat || data->check) { p.setPen(st::white->p); @@ -181,8 +223,7 @@ void ContactsInner::paintDialog(QPainter &p, UserData *user, ContactData *data, p.drawPixmap(QPoint(width() - st::contactsImg.pxWidth() - st::profileCheckDeltaX, st::profileListPadding.height() + (st::profileListPhotoSize - st::contactsImg.pxHeight()) / 2 - st::profileCheckDeltaY), App::sprite(), st::contactsImg); } - - bool uname = (data->online.at(0) == '@'); + bool uname = user && (data->online.at(0) == '@'); p.setFont(st::profileSubFont->f); if (uname && !data->inchat && !data->check && !_lastQuery.isEmpty() && user->username.startsWith(_lastQuery, Qt::CaseInsensitive)) { int32 availw = width() - (left + st::profileListPhotoSize + st::profileListPadding.width() * 2); @@ -200,8 +241,10 @@ void ContactsInner::paintDialog(QPainter &p, UserData *user, ContactData *data, } else { if (data->inchat || data->check) { p.setPen(st::white->p); + } else if (user && (uname || App::onlineColorUse(user->onlineTill, _time))) { + p.setPen(st::profileOnlineColor->p); } else { - p.setPen(((uname || App::onlineColorUse(user->onlineTill, _time)) ? st::profileOnlineColor : st::profileOfflineColor)->p); + p.setPen(st::profileOfflineColor->p); } p.drawText(left + st::profileListPhotoSize + st::profileListPadding.width(), st::profileListPadding.height() + st::profileListPhotoSize - st::profileListStatusBottom, data->online); } @@ -224,7 +267,7 @@ void ContactsInner::paintEvent(QPaintEvent *e) { DialogRow *drawFrom = _contacts->list.current; p.translate(0, drawFrom->pos * rh); while (drawFrom != _contacts->list.end && drawFrom->pos * rh < yTo) { - paintDialog(p, drawFrom->history->peer->asUser(), contactData(drawFrom), (drawFrom == _sel)); + paintDialog(p, drawFrom->history->peer, contactData(drawFrom), (drawFrom == _sel)); p.translate(0, rh); drawFrom = drawFrom->next; } @@ -253,13 +296,29 @@ void ContactsInner::paintEvent(QPaintEvent *e) { } else { p.setFont(st::noContactsFont->f); p.setPen(st::noContactsColor->p); - p.drawText(QRect(0, 0, width(), st::noContactsHeight - ((cContactsReceived() && !_searching) ? st::noContactsFont->height : 0)), lang((cContactsReceived() && !_searching) ? lng_no_contacts : lng_contacts_loading), style::al_center); + QString text; + int32 skip = 0; + if (bot()) { + text = lang(cDialogsReceived() ? lng_bot_no_groups : lng_contacts_loading); + } else if (cContactsReceived() && !_searching) { + text = lang(lng_no_contacts); + skip = st::noContactsFont->height; + } else { + text = lang(lng_contacts_loading); + } + p.drawText(QRect(0, 0, width(), st::noContactsHeight - skip), text, style::al_center); } } else { if (_filtered.isEmpty() && _byUsernameFiltered.isEmpty()) { p.setFont(st::noContactsFont->f); p.setPen(st::noContactsColor->p); - p.drawText(QRect(0, 0, width(), st::noContactsHeight), lang((cContactsReceived() && !_searching) ? lng_contacts_not_found : lng_contacts_loading), style::al_center); + QString text; + if (bot()) { + text = lang(cDialogsReceived() ? lng_bot_groups_not_found : lng_contacts_loading); + } else { + text = lang((cContactsReceived() && !_searching) ? lng_contacts_not_found : lng_contacts_loading); + } + p.drawText(QRect(0, 0, width(), st::noContactsHeight), text, style::al_center); } else { if (!_filtered.isEmpty()) { int32 from = (yFrom >= 0) ? (yFrom / rh) : 0; @@ -269,7 +328,7 @@ void ContactsInner::paintEvent(QPaintEvent *e) { p.translate(0, from * rh); for (; from < to; ++from) { - paintDialog(p, _filtered[from]->history->peer->asUser(), contactData(_filtered[from]), (_filteredSel == from)); + paintDialog(p, _filtered[from]->history->peer, contactData(_filtered[from]), (_filteredSel == from)); p.translate(0, rh); } } @@ -371,25 +430,32 @@ void ContactsInner::chooseParticipant() { } } else { int32 rh = st::profileListPhotoSize + st::profileListPadding.height() * 2, from; - PeerId peer = 0; + PeerData *peer = 0; if (_filter.isEmpty()) { if (_byUsernameSel >= 0 && _byUsernameSel < _byUsername.size()) { - peer = _byUsername[_byUsernameSel]->id; - } else { - peer = _sel ? _sel->history->peer->id : 0; + peer = _byUsername[_byUsernameSel]; + } else if (_sel) { + peer = _sel->history->peer; } } else { if (_byUsernameSel >= 0 && _byUsernameSel < _byUsernameFiltered.size()) { - peer = _byUsernameFiltered[_byUsernameSel]->id; + peer = _byUsernameFiltered[_byUsernameSel]; } else { if (_filteredSel < 0 || _filteredSel >= _filtered.size()) return; - peer = _filtered[_filteredSel]->history->peer->id; + peer = _filtered[_filteredSel]->history->peer; } } if (peer) { - App::wnd()->hideSettings(true); - App::main()->showPeer(peer, 0, false, true); - App::wnd()->hideLayer(); + if (bot() && peer->chat) { + _addToChat = peer->asChat(); + ConfirmBox *box = new ConfirmBox(lng_bot_sure_invite(lt_group, peer->name)); + connect(box, SIGNAL(confirmed()), this, SLOT(onAddBot())); + App::wnd()->replaceLayer(box); + } else { + App::wnd()->hideSettings(true); + App::main()->showPeer(peer->id, 0, false, true); + App::wnd()->hideLayer(); + } } } parentWidget()->update(); @@ -562,8 +628,10 @@ void ContactsInner::updateFilter(QString filter) { refresh(); - _searching = true; - emit searchByUsername(); + if (!bot()) { + _searching = true; + emit searchByUsername(); + } } if (parentWidget()) parentWidget()->update(); loadProfilePhotos(0); @@ -612,6 +680,8 @@ void ContactsInner::peopleReceived(const QString &query, const QVectorbotInfo && u->botInfo->cantJoinGroups && (_chat || _creatingChat)) continue; // skip bot's that can't be invited to groups + ContactData *d = new ContactData(); _byUsernameDatas.push_back(d); d->inchat = _chat ? _chat->participants.contains(u) : false; @@ -634,7 +704,7 @@ void ContactsInner::refresh() { if (!_addContactLnk.isHidden()) _addContactLnk.hide(); resize(width(), (_contacts->list.count * rh) + (_byUsername.isEmpty() ? 0 : (st::searchedBarHeight + _byUsername.size() * rh))); } else { - if (cContactsReceived()) { + if (cContactsReceived() && !bot()) { if (_addContactLnk.isHidden()) _addContactLnk.show(); } else { if (!_addContactLnk.isHidden()) _addContactLnk.hide(); @@ -656,6 +726,10 @@ ChatData *ContactsInner::chat() const { return _chat; } +UserData *ContactsInner::bot() const { + return _bot; +} + bool ContactsInner::creatingChat() const { return _creatingChat; } @@ -664,6 +738,7 @@ ContactsInner::~ContactsInner() { for (ContactsData::iterator i = _contactsData.begin(), e = _contactsData.end(); i != e; ++i) { delete *i; } + if (_bot) delete _contacts; } void ContactsInner::resizeEvent(QResizeEvent *e) { @@ -796,8 +871,8 @@ QVector ContactsInner::selected() { QVector result; result.reserve(_contactsData.size()); for (ContactsData::const_iterator i = _contactsData.cbegin(), e = _contactsData.cend(); i != e; ++i) { - if (i.value()->check) { - result.push_back(i.key()); + if (i.value()->check && !i.key()->chat) { + result.push_back(i.key()->asUser()); } } for (int32 i = 0, l = _byUsername.size(); i < l; ++i) { @@ -812,7 +887,7 @@ QVector ContactsInner::selectedInputs() { QVector result; result.reserve(_contactsData.size()); for (ContactsData::const_iterator i = _contactsData.cbegin(), e = _contactsData.cend(); i != e; ++i) { - if (i.value()->check) { + if (i.value()->check && !i.key()->chat) { result.push_back(i.key()->inputUser); } } @@ -854,6 +929,14 @@ _cancel(this, lang(lng_cancel), st::btnSelectCancel) { init(); } +ContactsBox::ContactsBox(UserData *bot) : ItemListBox(st::boxNoTopScroll), _inner(bot), +_addContact(this, lang(lng_add_contact_button), st::contactsAdd), +_filter(this, st::contactsFilter, lang(lng_participant_filter)), +_next(this, lang(lng_create_group_next), st::btnSelectDone), +_cancel(this, lang(lng_cancel), st::contactsClose) { + init(); +} + void ContactsBox::init() { ItemListBox::init(&_inner, _cancel.height(), st::contactsAdd.height + st::newGroupNamePadding.top() + _filter.height() + st::newGroupNamePadding.bottom()); @@ -961,14 +1044,17 @@ void ContactsBox::hideAll() { void ContactsBox::showAll() { ItemListBox::showAll(); - _addContact.show(); _filter.show(); if (_inner.chat() || _inner.creatingChat()) { _next.show(); _addContact.hide(); } else { _next.hide(); - _addContact.show(); + if (_inner.bot()) { + _addContact.hide(); + } else { + _addContact.show(); + } } _cancel.show(); } @@ -1010,6 +1096,8 @@ void ContactsBox::paintEvent(QPaintEvent *e) { // paint button sep p.fillRect(st::btnSelectCancel.width, size().height() - st::btnSelectCancel.height, st::lineWidth, st::btnSelectCancel.height, st::btnSelectSep->b); + } else if (_inner.bot()) { + paintTitle(p, lang(lng_bot_choose_group), true); } else { paintTitle(p, lang(lng_contacts_header), true); } diff --git a/Telegram/SourceFiles/boxes/contactsbox.h b/Telegram/SourceFiles/boxes/contactsbox.h index a6b1612fa..7fd8fc834 100644 --- a/Telegram/SourceFiles/boxes/contactsbox.h +++ b/Telegram/SourceFiles/boxes/contactsbox.h @@ -30,6 +30,7 @@ public: ContactsInner(bool creatingChat); ContactsInner(ChatData *chat); + ContactsInner(UserData *bot); void init(); void paintEvent(QPaintEvent *e); @@ -39,7 +40,7 @@ public: void mousePressEvent(QMouseEvent *e); void resizeEvent(QResizeEvent *e); - void paintDialog(QPainter &p, UserData *user, ContactData *data, bool sel); + void paintDialog(QPainter &p, PeerData *peer, ContactData *data, bool sel); void updateFilter(QString filter = QString()); void selectSkip(int32 dir); @@ -59,6 +60,7 @@ public: void refresh(); ChatData *chat() const; + UserData *bot() const; bool creatingChat() const; ~ContactsInner(); @@ -75,11 +77,17 @@ public slots: void updateSel(); void peerUpdated(PeerData *peer); + void onPeerNameChanged(PeerData *peer, const PeerData::Names &oldNames, const PeerData::NameFirstChars &oldChars); + + void onAddBot(); private: ChatData *_chat; + UserData *_bot; bool _creatingChat; + + ChatData *_addToChat; int32 _time; @@ -99,7 +107,7 @@ private: bool inchat; bool check; }; - typedef QMap ContactsData; + typedef QMap ContactsData; ContactsData _contactsData; ContactData *contactData(DialogRow *row); @@ -125,6 +133,7 @@ public: ContactsBox(bool creatingChat = false); ContactsBox(ChatData *chat); + ContactsBox(UserData *bot); void keyPressEvent(QKeyEvent *e); void paintEvent(QPaintEvent *e); void resizeEvent(QResizeEvent *e); diff --git a/Telegram/SourceFiles/config.h b/Telegram/SourceFiles/config.h index ed4ff4b95..f669db538 100644 --- a/Telegram/SourceFiles/config.h +++ b/Telegram/SourceFiles/config.h @@ -17,9 +17,9 @@ Copyright (c) 2014 John Preston, https://desktop.telegram.org */ #pragma once -static const int32 AppVersion = 8024; -static const wchar_t *AppVersionStr = L"0.8.24"; -static const bool DevChannel = false; +static const int32 AppVersion = 8025; +static const wchar_t *AppVersionStr = L"0.8.25"; +static const bool DevChannel = true; static const wchar_t *AppNameOld = L"Telegram Win (Unofficial)"; static const wchar_t *AppName = L"Telegram Desktop"; diff --git a/Telegram/SourceFiles/dialogswidget.cpp b/Telegram/SourceFiles/dialogswidget.cpp index 9384227d3..ba2300413 100644 --- a/Telegram/SourceFiles/dialogswidget.cpp +++ b/Telegram/SourceFiles/dialogswidget.cpp @@ -28,9 +28,9 @@ Copyright (c) 2014 John Preston, https://desktop.telegram.org #include "localstorage.h" DialogsListWidget::DialogsListWidget(QWidget *parent, MainWidget *main) : QWidget(parent), -dialogs(false), -contactsNoDialogs(true), -contacts(true), +dialogs(DialogsSortByDate), +contactsNoDialogs(DialogsSortByName), +contacts(DialogsSortByName), sel(0), contactSel(false), selByMouse(false), @@ -64,6 +64,8 @@ int32 DialogsListWidget::searchedOffset() const { } void DialogsListWidget::paintEvent(QPaintEvent *e) { + if (!App::main()) return; + QRect r(e->rect()); bool trivial = (rect() == r); @@ -95,7 +97,7 @@ void DialogsListWidget::paintEvent(QPaintEvent *e) { } p.translate(0, from * st::mentionHeight); if (from < hashtagResults.size()) { - int32 to = (r.bottom() / int32(st::mentionHeight)) + 1, w = width(); + int32 to = (r.bottom() / int32(st::mentionHeight)) + 1, w = width(), htagwidth = w - st::dlgPaddingHor * 2; if (to > hashtagResults.size()) to = hashtagResults.size(); p.setFont(st::mentionFont->f); p.setPen(st::black->p); @@ -106,8 +108,27 @@ void DialogsListWidget::paintEvent(QPaintEvent *e) { int skip = (st::mentionHeight - st::notifyClose.icon.pxHeight()) / 2; p.drawPixmap(QPoint(w - st::notifyClose.icon.pxWidth() - skip, skip), App::sprite(), st::notifyClose.icon); } - QString tag = st::mentionFont->m.elidedText('#' + hashtagResults.at(from), Qt::ElideRight, w - st::dlgPaddingHor * 2); - p.drawText(st::dlgPaddingHor, st::mentionTop + st::mentionFont->ascent, tag); + + QString first = (_hashtagFilter.size() < 2) ? QString() : ('#' + hashtagResults.at(from).mid(0, _hashtagFilter.size() - 1)), second = (_hashtagFilter.size() < 2) ? ('#' + hashtagResults.at(from)) : hashtagResults.at(from).mid(_hashtagFilter.size() - 1); + int32 firstwidth = st::mentionFont->m.width(first), secondwidth = st::mentionFont->m.width(second); + if (htagwidth < firstwidth + secondwidth) { + if (htagwidth < firstwidth + st::mentionFont->elidew) { + first = st::mentionFont->m.elidedText(first + second, Qt::ElideRight, htagwidth); + second = QString(); + } else { + second = st::mentionFont->m.elidedText(second, Qt::ElideRight, htagwidth - firstwidth); + } + } + + p.setFont(st::mentionFont->f); + if (!first.isEmpty()) { + p.setPen(st::profileOnlineColor->p); + p.drawText(st::dlgPaddingHor, st::mentionTop + st::mentionFont->ascent, first); + } + if (!second.isEmpty()) { + p.setPen(st::profileOfflineColor->p); + p.drawText(st::dlgPaddingHor + firstwidth, st::mentionTop + st::mentionFont->ascent, second); + } p.translate(0, st::mentionHeight); } } @@ -607,8 +628,9 @@ void DialogsListWidget::onFilterUpdate(QString newFilter, bool force) { } } -void DialogsListWidget::onHashtagFilterUpdate(QString newFilter) { +void DialogsListWidget::onHashtagFilterUpdate(QStringRef newFilter) { if (newFilter.isEmpty() || newFilter.at(0) != '#') { + _hashtagFilter = QString(); if (!hashtagResults.isEmpty()) { hashtagResults.clear(); refresh(true); @@ -616,6 +638,7 @@ void DialogsListWidget::onHashtagFilterUpdate(QString newFilter) { } return; } + _hashtagFilter = newFilter.toString(); if (cRecentSearchHashtags().isEmpty() && cRecentWriteHashtags().isEmpty()) { Local::readRecentHashtags(); } @@ -624,7 +647,7 @@ void DialogsListWidget::onHashtagFilterUpdate(QString newFilter) { if (!recent.isEmpty()) { hashtagResults.reserve(qMin(recent.size(), 5)); for (RecentHashtagPack::const_iterator i = recent.cbegin(), e = recent.cend(); i != e; ++i) { - if (i->first.startsWith(newFilter.midRef(1), Qt::CaseInsensitive) && i->first.size() + 1 != newFilter.size()) { + if (i->first.startsWith(_hashtagFilter.midRef(1), Qt::CaseInsensitive) && i->first.size() + 1 != newFilter.size()) { hashtagResults.push_back(i->first); if (hashtagResults.size() == 5) break; } @@ -1604,7 +1627,10 @@ void DialogsWidget::onSearchMore(MsgId minMsgId) { void DialogsWidget::loadDialogs() { if (dlgPreloading) return; - if (dlgCount >= 0 && dlgOffset >= dlgCount) return; + if (dlgCount >= 0 && dlgOffset >= dlgCount) { + cSetDialogsReceived(true); + return; + } int32 loadCount = dlgOffset ? DialogsPerPage : DialogsFirstLoad; dlgPreloading = MTP::send(MTPmessages_GetDialogs(MTP_int(dlgOffset), MTP_int(0), MTP_int(loadCount)), rpcDone(&DialogsWidget::dialogsReceived), rpcFail(&DialogsWidget::dialogsFailed)); @@ -1752,12 +1778,13 @@ void DialogsWidget::onFilterUpdate(bool force) { void DialogsWidget::onFilterCursorMoved(int from, int to) { if (to < 0) to = _filter.cursorPosition(); - QString t = _filter.text(), r; + QString t = _filter.text(); + QStringRef r; for (int start = to; start > 0;) { --start; if (t.size() <= start) break; if (t.at(start) == '#') { - r = t.mid(start, to - start); + r = t.midRef(start, to - start); break; } if (!t.at(start).isLetterOrNumber() && t.at(start) != '_') break; @@ -1886,6 +1913,10 @@ DialogsIndexed &DialogsWidget::contactsList() { return list.contactsList(); } +DialogsIndexed &DialogsWidget::dialogsList() { + return list.dialogsList(); +} + void DialogsWidget::onAddContact() { App::wnd()->replaceLayer(new AddContactBox()); } diff --git a/Telegram/SourceFiles/dialogswidget.h b/Telegram/SourceFiles/dialogswidget.h index 308225034..7d6fb8372 100644 --- a/Telegram/SourceFiles/dialogswidget.h +++ b/Telegram/SourceFiles/dialogswidget.h @@ -94,7 +94,7 @@ public: bool hasFilteredResults() const; void onFilterUpdate(QString newFilter, bool force = false); - void onHashtagFilterUpdate(QString newFilter); + void onHashtagFilterUpdate(QStringRef newFilter); void itemRemoved(HistoryItem *item); void itemReplaced(HistoryItem *oldItem, HistoryItem *newItem); @@ -130,7 +130,7 @@ private: bool contactSel; bool selByMouse; - QString filter; + QString filter, _hashtagFilter; QStringList hashtagResults; int32 hashtagSel; @@ -197,6 +197,7 @@ public: void removeContact(UserData *user); DialogsIndexed &contactsList(); + DialogsIndexed &dialogsList(); void enableShadow(bool enable = true); diff --git a/Telegram/SourceFiles/dropdown.cpp b/Telegram/SourceFiles/dropdown.cpp index bdc0f937d..5c566bed7 100644 --- a/Telegram/SourceFiles/dropdown.cpp +++ b/Telegram/SourceFiles/dropdown.cpp @@ -779,7 +779,7 @@ void EmojiPanInner::paintEvent(QPaintEvent *e) { int32 index = i * EmojiPanPerRow + j; if (index >= size) break; - float64 hover = (!_picker.isHidden() && c * emojiTabShift + index == _pickerSel) ? 1 : _hovers[c][index]; + float64 hover = (!_picker.isHidden() && c * MatrixRowShift + index == _pickerSel) ? 1 : _hovers[c][index]; QPoint w(st::emojiPanPadding + j * st::emojiPanSize.width(), y + i * st::emojiPanSize.height()); if (hover > 0) { @@ -817,7 +817,7 @@ void EmojiPanInner::mousePressEvent(QMouseEvent *e) { _pressedSel = _selected; if (_selected >= 0 && _selected != SwitcherSelected) { - int tab = (_selected / emojiTabShift), sel = _selected % emojiTabShift; + int tab = (_selected / MatrixRowShift), sel = _selected % MatrixRowShift; if (tab < emojiTabCount && sel < _emojis[tab].size() && _emojis[tab][sel]->color) { _pickerSel = _selected; if (cEmojiVariants().constFind(_emojis[tab][sel]->code) == cEmojiVariants().cend()) { @@ -838,7 +838,7 @@ void EmojiPanInner::mouseReleaseEvent(QMouseEvent *e) { if (_picker.rect().contains(_picker.mapFromGlobal(_lastMousePos))) { return _picker.mouseReleaseEvent(0); } else if (_pickerSel >= 0) { - int tab = (_pickerSel / emojiTabShift), sel = _pickerSel % emojiTabShift; + int tab = (_pickerSel / MatrixRowShift), sel = _pickerSel % MatrixRowShift; if (tab < emojiTabCount && sel < _emojis[tab].size() && _emojis[tab][sel]->color) { if (cEmojiVariants().constFind(_emojis[tab][sel]->code) != cEmojiVariants().cend()) { _picker.hideStart(); @@ -860,11 +860,11 @@ void EmojiPanInner::mouseReleaseEvent(QMouseEvent *e) { return; } - if (_selected >= emojiTabCount * emojiTabShift) { + if (_selected >= emojiTabCount * MatrixRowShift) { return; } - int tab = (_selected / emojiTabShift), sel = _selected % emojiTabShift; + int tab = (_selected / MatrixRowShift), sel = _selected % MatrixRowShift; if (sel < _emojis[tab].size()) { EmojiPtr emoji(_emojis[tab][sel]); if (emoji->color && !_picker.isHidden()) return; @@ -917,7 +917,7 @@ void EmojiPanInner::onSaveConfig() { } void EmojiPanInner::onShowPicker() { - int tab = (_pickerSel / emojiTabShift), sel = _pickerSel % emojiTabShift; + int tab = (_pickerSel / MatrixRowShift), sel = _pickerSel % MatrixRowShift; if (tab < emojiTabCount && sel < _emojis[tab].size() && _emojis[tab][sel]->color) { int32 y = 0; for (int c = 0; c <= tab; ++c) { @@ -952,7 +952,7 @@ void EmojiPanInner::onColorSelected(EmojiPtr emoji) { cRefEmojiVariants().insert(emoji->code, emojiKey(emoji)); } if (_pickerSel >= 0) { - int tab = (_pickerSel / emojiTabShift), sel = _pickerSel % emojiTabShift; + int tab = (_pickerSel / MatrixRowShift), sel = _pickerSel % MatrixRowShift; if (tab >= 0 && tab < emojiTabCount) { _emojis[tab][sel] = emoji; update(); @@ -991,7 +991,7 @@ void EmojiPanInner::clearSelection(bool fast) { _lastMousePos = mapToGlobal(QPoint(-10, -10)); if (fast) { for (Animations::const_iterator i = _animations.cbegin(); i != _animations.cend(); ++i) { - int index = qAbs(i.key()) - 1, tab = (index / emojiTabShift), sel = index % emojiTabShift; + int index = qAbs(i.key()) - 1, tab = (index / MatrixRowShift), sel = index % MatrixRowShift; (index == SwitcherSelected ? _switcherHover : _hovers[tab][sel]) = 0; } _animations.clear(); @@ -1055,7 +1055,7 @@ void EmojiPanInner::updateSelected() { if (selIndex >= _emojis[c].size()) { selIndex = -1; } else { - selIndex += c * emojiTabShift; + selIndex += c * MatrixRowShift; } } break; @@ -1098,7 +1098,7 @@ void EmojiPanInner::updateSelected() { bool EmojiPanInner::animStep(float64 ms) { uint64 now = getms(); for (Animations::iterator i = _animations.begin(); i != _animations.end();) { - int index = qAbs(i.key()) - 1, tab = (index / emojiTabShift), sel = index % emojiTabShift; + int index = qAbs(i.key()) - 1, tab = (index / MatrixRowShift), sel = index % MatrixRowShift; float64 dt = float64(now - i.value()) / st::emojiPanDuration; if (dt >= 1) { (index == SwitcherSelected ? _switcherHover : _hovers[tab][sel]) = (i.key() > 0) ? 1 : 0; @@ -1293,11 +1293,11 @@ void StickerPanInner::mouseReleaseEvent(QMouseEvent *e) { emit switchToEmoji(); return; } - if (_selected >= emojiTabShift * _setIds.size()) { + if (_selected >= MatrixRowShift * _setIds.size()) { return; } - int tab = (_selected / emojiTabShift), sel = _selected % emojiTabShift; + int tab = (_selected / MatrixRowShift), sel = _selected % MatrixRowShift; if (_setIds[tab] == RecentStickerSetId && sel >= _sets[tab].size() && sel < _sets[tab].size() * 2 && _custom.at(sel - _sets[tab].size())) { clearSelection(true); bool refresh = false; @@ -1362,7 +1362,7 @@ void StickerPanInner::clearSelection(bool fast) { _lastMousePos = mapToGlobal(QPoint(-10, -10)); if (fast) { for (Animations::const_iterator i = _animations.cbegin(); i != _animations.cend(); ++i) { - int index = qAbs(i.key()) - 1, tab = (index / emojiTabShift), sel = index % emojiTabShift; + int index = qAbs(i.key()) - 1, tab = (index / MatrixRowShift), sel = index % MatrixRowShift; (index == SwitcherSelected ? _switcherHover : _hovers[tab][sel]) = 0; } _animations.clear(); @@ -1554,7 +1554,7 @@ void StickerPanInner::updateSelected() { ytill = y + st::emojiPanHeader + ((cnt / StickerPanPerRow) + ((cnt % StickerPanPerRow) ? 1 : 0)) * st::stickerPanSize.height(); if (p.y() >= y && p.y() < ytill) { if (!special && p.y() >= y && p.y() < y + st::emojiPanHeader && sx + st::stickerPanPadding >= width() - st::emojiPanHeaderLeft - st::notifyClose.icon.pxWidth() && sx + st::stickerPanPadding < width() - st::emojiPanHeaderLeft) { - selIndex = c * emojiTabShift + _sets[c].size(); + selIndex = c * MatrixRowShift + _sets[c].size(); } else { y += st::emojiPanHeader; if (p.y() >= y && sx >= 0 && sx < StickerPanPerRow * st::stickerPanSize.width()) { @@ -1568,7 +1568,7 @@ void StickerPanInner::updateSelected() { selIndex += _sets[c].size(); } } - selIndex += c * emojiTabShift; + selIndex += c * MatrixRowShift; } } } @@ -1578,12 +1578,12 @@ void StickerPanInner::updateSelected() { } bool startanim = false; - int oldSel = _selected, oldSelTab = oldSel / emojiTabShift, xOldSel = -1, newSel = selIndex, newSelTab = newSel / emojiTabShift, xNewSel = -1; - if (oldSel >= 0 && oldSelTab < _setIds.size() && _setIds[oldSelTab] == RecentStickerSetId && oldSel >= oldSelTab * emojiTabShift + _sets[oldSelTab].size()) { + int oldSel = _selected, oldSelTab = oldSel / MatrixRowShift, xOldSel = -1, newSel = selIndex, newSelTab = newSel / MatrixRowShift, xNewSel = -1; + if (oldSel >= 0 && oldSelTab < _setIds.size() && _setIds[oldSelTab] == RecentStickerSetId && oldSel >= oldSelTab * MatrixRowShift + _sets[oldSelTab].size()) { xOldSel = oldSel; oldSel -= _sets[oldSelTab].size(); } - if (newSel >= 0 && newSelTab < _setIds.size() && _setIds[newSelTab] == RecentStickerSetId && newSel >= newSelTab * emojiTabShift + _sets[newSelTab].size()) { + if (newSel >= 0 && newSelTab < _setIds.size() && _setIds[newSelTab] == RecentStickerSetId && newSel >= newSelTab * MatrixRowShift + _sets[newSelTab].size()) { xNewSel = newSel; newSel -= _sets[newSelTab].size(); } @@ -1627,7 +1627,7 @@ void StickerPanInner::updateSelected() { bool StickerPanInner::animStep(float64 ms) { uint64 now = getms(); for (Animations::iterator i = _animations.begin(); i != _animations.end();) { - int index = qAbs(i.key()) - 1, tab = (index / emojiTabShift), sel = index % emojiTabShift; + int index = qAbs(i.key()) - 1, tab = (index / MatrixRowShift), sel = index % MatrixRowShift; float64 dt = float64(now - i.value()) / st::emojiPanDuration; if (dt >= 1) { (index == SwitcherSelected ? _switcherHover : _hovers[tab][sel]) = (i.key() > 0) ? 1 : 0; @@ -2403,7 +2403,7 @@ void EmojiPan::onDelayedHide() { _removingSetId = 0; } -MentionsInner::MentionsInner(MentionsDropdown *parent, MentionRows *rows, HashtagRows *hrows) : _parent(parent), _rows(rows), _hrows(hrows), _sel(-1), _mouseSel(false), _overDelete(false) { +MentionsInner::MentionsInner(MentionsDropdown *parent, MentionRows *rows, HashtagRows *hrows, BotCommandRows *crows) : _parent(parent), _rows(rows), _hrows(hrows), _crows(crows), _sel(-1), _mouseSel(false), _overDelete(false) { } void MentionsInner::paintEvent(QPaintEvent *e) { @@ -2413,28 +2413,26 @@ void MentionsInner::paintEvent(QPaintEvent *e) { int32 availwidth = width() - 2 * st::mentionPadding.left() - st::mentionPhotoSize - 2 * st::mentionPadding.right(); int32 htagleft = st::btnAttachPhoto.width + st::taMsgField.textMrg.left() - st::dlgShadow, htagwidth = width() - st::mentionPadding.right() - htagleft; - int32 from = qFloor(e->rect().top() / st::mentionHeight), to = qFloor(e->rect().bottom() / st::mentionHeight) + 1, last = _rows->isEmpty() ? _hrows->size() : _rows->size(); + int32 from = qFloor(e->rect().top() / st::mentionHeight), to = qFloor(e->rect().bottom() / st::mentionHeight) + 1; + int32 last = _rows->isEmpty() ? (_hrows->isEmpty() ? _crows->size() : _hrows->size()) : _rows->size(); + bool hasUsername = _parent->filter().indexOf('@') > 1; for (int32 i = from; i < to; ++i) { if (i >= last) break; if (i == _sel) { p.fillRect(0, i * st::mentionHeight, width(), st::mentionHeight, st::dlgHoverBG->b); int skip = (st::mentionHeight - st::notifyClose.icon.pxHeight()) / 2; - if (_rows->isEmpty()) p.drawPixmap(QPoint(width() - st::notifyClose.icon.pxWidth() - skip, i * st::mentionHeight + skip), App::sprite(), st::notifyClose.icon); + if (!_hrows->isEmpty()) p.drawPixmap(QPoint(width() - st::notifyClose.icon.pxWidth() - skip, i * st::mentionHeight + skip), App::sprite(), st::notifyClose.icon); } p.setPen(st::black->p); - if (_rows->isEmpty()) { - QString tag = st::mentionFont->m.elidedText('#' + _hrows->at(last - i - 1), Qt::ElideRight, htagwidth); - p.setFont(st::mentionFont->f); - p.drawText(htagleft, i * st::mentionHeight + st::mentionTop + st::mentionFont->ascent, tag); - } else { + if (!_rows->isEmpty()) { UserData *user = _rows->at(last - i - 1); QString first = (_parent->filter().size() < 2) ? QString() : ('@' + user->username.mid(0, _parent->filter().size() - 1)), second = (_parent->filter().size() < 2) ? ('@' + user->username) : user->username.mid(_parent->filter().size() - 1); int32 firstwidth = st::mentionFont->m.width(first), secondwidth = st::mentionFont->m.width(second), unamewidth = firstwidth + secondwidth, namewidth = user->nameText.maxWidth(); if (availwidth < unamewidth + namewidth) { namewidth = (availwidth * namewidth) / (namewidth + unamewidth); unamewidth = availwidth - namewidth; - if (firstwidth <= unamewidth) { + if (firstwidth < unamewidth + st::mentionFont->elidew) { if (firstwidth < unamewidth) { first = st::mentionFont->m.elidedText(first, Qt::ElideRight, unamewidth); } else if (!second.isEmpty()) { @@ -2448,14 +2446,96 @@ void MentionsInner::paintEvent(QPaintEvent *e) { user->photo->load(); p.drawPixmap(st::mentionPadding.left(), i * st::mentionHeight + st::mentionPadding.top(), user->photo->pixRounded(st::mentionPhotoSize)); user->nameText.drawElided(p, 2 * st::mentionPadding.left() + st::mentionPhotoSize, i * st::mentionHeight + st::mentionTop, namewidth); + p.setFont(st::mentionFont->f); - p.setPen(st::profileOnlineColor->p); p.drawText(2 * st::mentionPadding.left() + st::mentionPhotoSize + namewidth + st::mentionPadding.right(), i * st::mentionHeight + st::mentionTop + st::mentionFont->ascent, first); if (!second.isEmpty()) { p.setPen(st::profileOfflineColor->p); p.drawText(2 * st::mentionPadding.left() + st::mentionPhotoSize + namewidth + st::mentionPadding.right() + firstwidth, i * st::mentionHeight + st::mentionTop + st::mentionFont->ascent, second); } + } else if (!_hrows->isEmpty()) { + QString first = (_parent->filter().size() < 2) ? QString() : ('#' + _hrows->at(last - i - 1).mid(0, _parent->filter().size() - 1)), second = (_parent->filter().size() < 2) ? ('#' + _hrows->at(last - i - 1)) : _hrows->at(last - i - 1).mid(_parent->filter().size() - 1); + int32 firstwidth = st::mentionFont->m.width(first), secondwidth = st::mentionFont->m.width(second); + if (htagwidth < firstwidth + secondwidth) { + if (htagwidth < firstwidth + st::mentionFont->elidew) { + first = st::mentionFont->m.elidedText(first + second, Qt::ElideRight, htagwidth); + second = QString(); + } else { + second = st::mentionFont->m.elidedText(second, Qt::ElideRight, htagwidth - firstwidth); + } + } + + p.setFont(st::mentionFont->f); + if (!first.isEmpty()) { + p.setPen(st::profileOnlineColor->p); + p.drawText(htagleft, i * st::mentionHeight + st::mentionTop + st::mentionFont->ascent, first); + } + if (!second.isEmpty()) { + p.setPen(st::profileOfflineColor->p); + p.drawText(htagleft + firstwidth, i * st::mentionHeight + st::mentionTop + st::mentionFont->ascent, second); + } + } else { + UserData *user = _crows->at(last - i - 1).first; + + const BotCommand &command = _crows->at(last - i - 1).second; + QString toHighlight = command.command; + int32 botStatus = _parent->chat() ? _parent->chat()->botStatus : -1; + if (hasUsername || botStatus == 0 || botStatus == 2) { + toHighlight += '@' + user->username; + if (botStatus == 0 || botStatus == 2) { + user->photo->load(); + p.drawPixmap(st::mentionPadding.left(), i * st::mentionHeight + st::mentionPadding.top(), user->photo->pixRounded(st::mentionPhotoSize)); + } + } + + int32 addleft = 0, widthleft = htagwidth; + QString first = (_parent->filter().size() < 2) ? QString() : ('/' + toHighlight.mid(0, _parent->filter().size() - 1)), second = (_parent->filter().size() < 2) ? ('/' + toHighlight) : toHighlight.mid(_parent->filter().size() - 1); + int32 firstwidth = st::botCommandFont->m.width(first), secondwidth = st::botCommandFont->m.width(second); + if (htagwidth < firstwidth + secondwidth) { + if (htagwidth < firstwidth + st::botCommandFont->elidew) { + first = st::botCommandFont->m.elidedText(first + second, Qt::ElideRight, htagwidth); + second = QString(); + } else { + second = st::botCommandFont->m.elidedText(second, Qt::ElideRight, htagwidth - firstwidth); + } + } + p.setFont(st::botCommandFont->f); + if (!first.isEmpty()) { + p.setPen(st::profileOnlineColor->p); + p.drawText(htagleft, i * st::mentionHeight + st::mentionTop + st::mentionFont->ascent, first); + } + if (!second.isEmpty()) { + p.setPen(st::profileOfflineColor->p); + p.drawText(htagleft + firstwidth, i * st::mentionHeight + st::mentionTop + st::mentionFont->ascent, second); + } + addleft += firstwidth + secondwidth + st::mentionPadding.left(); + widthleft -= firstwidth + secondwidth + st::mentionPadding.left(); + + QString params = command.params; + if (widthleft > st::mentionFont->elidew && !params.isEmpty()) { + p.setFont(st::mentionFont->f); + int32 paramswidth = st::mentionFont->m.width(params); + if (widthleft < paramswidth) { + params = st::mentionFont->m.elidedText(params, Qt::ElideRight, widthleft); + } + p.setPen(st::profileOfflineColor->p); + p.drawText(htagleft + addleft, i * st::mentionHeight + st::mentionTop + st::mentionFont->ascent, params); + + addleft += paramswidth + st::mentionPadding.left(); + widthleft -= paramswidth + st::mentionPadding.left(); + } + QString description = command.description; + if (widthleft > st::botDescFont->elidew && !description.isEmpty()) { + p.setFont(st::botDescFont->f); + int32 descwidth = st::botDescFont->m.width(description); + if (widthleft < descwidth) { + description = st::botDescFont->m.elidedText(description, Qt::ElideRight, widthleft); + descwidth = st::botDescFont->m.width(description); + } + p.setPen(st::profileOfflineColor->p); + p.drawText(htagleft + addleft + (widthleft - descwidth), i * st::mentionHeight + st::mentionTop + st::mentionFont->ascent, description); + } } } @@ -2476,7 +2556,7 @@ void MentionsInner::clearSel() { bool MentionsInner::moveSel(int direction) { _mouseSel = false; - int32 maxSel = (_rows->isEmpty() ? _hrows->size() : _rows->size()); + int32 maxSel = (_rows->isEmpty() ? (_hrows->isEmpty() ? _crows->size() : _hrows->size()) : _rows->size()); if (_sel >= maxSel || _sel < 0) { if (direction < 0) setSel(maxSel - 1, true); return (_sel >= 0 && _sel < maxSel); @@ -2488,9 +2568,23 @@ bool MentionsInner::moveSel(int direction) { } bool MentionsInner::select() { - int32 maxSel = (_rows->isEmpty() ? _hrows->size() : _rows->size()); + int32 maxSel = (_rows->isEmpty() ? (_hrows->isEmpty() ? _crows->size() : _hrows->size()) : _rows->size()); if (_sel >= 0 && _sel < maxSel) { - QString result = _rows->isEmpty() ? ('#' + _hrows->at(_hrows->size() - _sel - 1)) : ('@' + _rows->at(_rows->size() - _sel - 1)->username); + QString result; + if (!_rows->isEmpty()) { + result = '@' + _rows->at(_rows->size() - _sel - 1)->username; + } else if (!_hrows->isEmpty()) { + result = '#' + _hrows->at(_hrows->size() - _sel - 1); + } else { + UserData *user = _crows->at(_crows->size() - _sel - 1).first; + const BotCommand &command(_crows->at(_crows->size() - _sel - 1).second); + int32 botStatus = _parent->chat() ? _parent->chat()->botStatus : -1; + if (botStatus == 0 || botStatus == 2 || _parent->filter().indexOf('@') > 1) { + result = '/' + command.command + '@' + user->username; + } else { + result = '/' + command.command; + } + } emit chosen(result); return true; } @@ -2542,7 +2636,7 @@ void MentionsInner::leaveEvent(QEvent *e) { void MentionsInner::setSel(int sel, bool scroll) { _sel = sel; parentWidget()->update(); - int32 maxSel = _rows->isEmpty() ? _hrows->size() : _rows->size(); + int32 maxSel = _rows->isEmpty() ? (_hrows->isEmpty() ? _crows->size() : _hrows->size()) : _rows->size(); if (scroll && _sel >= 0 && _sel < maxSel) emit mustScrollTo(_sel * st::mentionHeight, (_sel + 1) * st::mentionHeight); } @@ -2552,7 +2646,7 @@ void MentionsInner::onUpdateSelected(bool force) { int w = width(), mouseY = mouse.y(); _overDelete = _rows->isEmpty() && (mouse.x() >= w - st::mentionHeight); - int32 sel = mouseY / int32(st::mentionHeight), maxSel = _rows->isEmpty() ? _hrows->size() : _rows->size(); + int32 sel = mouseY / int32(st::mentionHeight), maxSel = _rows->isEmpty() ? (_hrows->isEmpty() ? _crows->size() : _hrows->size()) : _rows->size(); if (sel < 0 || sel >= maxSel) { sel = -1; } @@ -2570,7 +2664,7 @@ void MentionsInner::onParentGeometryChanged() { } MentionsDropdown::MentionsDropdown(QWidget *parent) : QWidget(parent), -_scroll(this, st::mentionScroll), _inner(this, &_rows, &_hrows), _chat(0), _hiding(false), a_opacity(0), _shadow(st::dropdownDef.shadow) { +_scroll(this, st::mentionScroll), _inner(this, &_rows, &_hrows, &_crows), _chat(0), _hiding(false), a_opacity(0), _shadow(st::dropdownDef.shadow) { _hideTimer.setSingleShot(true); connect(&_hideTimer, SIGNAL(timeout()), this, SLOT(hideStart())); connect(&_inner, SIGNAL(chosen(QString)), this, SIGNAL(chosen(QString))); @@ -2608,8 +2702,9 @@ void MentionsDropdown::paintEvent(QPaintEvent *e) { } -void MentionsDropdown::showFiltered(ChatData *chat, QString start) { - _chat = chat; +void MentionsDropdown::showFiltered(PeerData *peer, QString start) { + _chat = peer->chat ? peer->asChat() : 0; + _user = peer->chat ? 0 : peer->asUser(); start = start.toLower(); bool toDown = (_filter != start); if (toDown) { @@ -2621,10 +2716,11 @@ void MentionsDropdown::showFiltered(ChatData *chat, QString start) { void MentionsDropdown::updateFiltered(bool toDown) { int32 now = unixtime(); - QMultiMap ordered; MentionRows rows; HashtagRows hrows; + BotCommandRows crows; if (_filter.at(0) == '@') { + QMultiMap ordered; rows.reserve(_chat->participants.isEmpty() ? _chat->lastAuthors.size() : _chat->participants.size()); if (_chat->participants.isEmpty()) { if (_chat->count > 0) { @@ -2653,23 +2749,79 @@ void MentionsDropdown::updateFiltered(bool toDown) { rows.push_back(i.value()); } } - } else { + } else if (_filter.at(0) == '#') { const RecentHashtagPack &recent(cRecentWriteHashtags()); hrows.reserve(recent.size()); for (RecentHashtagPack::const_iterator i = recent.cbegin(), e = recent.cend(); i != e; ++i) { if (_filter.size() > 1 && (!i->first.startsWith(_filter.midRef(1), Qt::CaseInsensitive) || i->first.size() + 1 == _filter.size())) continue; hrows.push_back(i->first); } + } else if (_filter.at(0) == '/') { + bool hasUsername = _filter.indexOf('@') > 1; + QMap bots; + int32 cnt = 0; + if (_chat) { + if (_chat->participants.isEmpty()) { + if (_chat->count > 0) { + App::api()->requestFullPeer(_chat); + } + } else { + int32 index = 0; + for (ChatData::Participants::const_iterator i = _chat->participants.cbegin(), e = _chat->participants.cend(); i != e; ++i) { + UserData *user = i.key(); + if (!user->botInfo || user->botInfo->commands.isEmpty()) continue; + bots.insert(user, true); + cnt += user->botInfo->commands.size(); + } + } + } else if (_user->botInfo) { + cnt = _user->botInfo->commands.size(); + bots.insert(_user, true); + } + if (cnt) { + crows.reserve(cnt); + int32 botStatus = _chat ? _chat->botStatus : -1; + if (_chat) { + for (MentionRows::const_iterator i = _chat->lastAuthors.cbegin(), e = _chat->lastAuthors.cend(); i != e; ++i) { + UserData *user = *i; + if (!user->botInfo || user->botInfo->commands.isEmpty()) continue; + for (int32 j = 0, l = user->botInfo->commands.size(); j < l; ++j) { + if (_filter.size() > 1) { + QString toFilter = (hasUsername || botStatus == 0 || botStatus == 2) ? user->botInfo->commands.at(j).command + '@' + user->username : user->botInfo->commands.at(j).command; + if (!toFilter.startsWith(_filter.midRef(1), Qt::CaseInsensitive) || toFilter.size() + 1 == _filter.size()) continue; + } + crows.push_back(qMakePair(user, user->botInfo->commands.at(j))); + } + if (!bots.isEmpty()) { + bots.remove(user); + } + } + } + if (!bots.isEmpty()) { + for (QMap::const_iterator i = bots.cbegin(), e = bots.cend(); i != e; ++i) { + UserData *user = i.key(); + for (int32 j = 0, l = user->botInfo->commands.size(); j < l; ++j) { + if (_filter.size() > 1) { + QString toFilter = (hasUsername || botStatus == 0 || botStatus == 2) ? user->botInfo->commands.at(j).command + '@' + user->username : user->botInfo->commands.at(j).command; + if (!toFilter.startsWith(_filter.midRef(1), Qt::CaseInsensitive) || toFilter.size() + 1 == _filter.size()) continue; + } + crows.push_back(qMakePair(user, user->botInfo->commands.at(j))); + } + } + } + } } - if (rows.isEmpty() && hrows.isEmpty()) { + if (rows.isEmpty() && hrows.isEmpty() && crows.isEmpty()) { if (!isHidden()) { hideStart(); _rows.clear(); _hrows.clear(); + _crows.clear(); } } else { _rows = rows; _hrows = hrows; + _crows = crows; bool hidden = _hiding || isHidden(); if (hidden) { show(); @@ -2692,7 +2844,7 @@ void MentionsDropdown::setBoundings(QRect boundings) { } void MentionsDropdown::recount(bool toDown) { - int32 h = (_rows.isEmpty() ? _hrows.size() : _rows.size()) * st::mentionHeight, oldst = _scroll.scrollTop(), st = oldst; + int32 h = (_rows.isEmpty() ? (_hrows.isEmpty() ? _crows.size() : _hrows.size()) : _rows.size()) * st::mentionHeight, oldst = _scroll.scrollTop(), st = oldst; if (_inner.height() != h) { st += h - _inner.height(); @@ -2780,6 +2932,10 @@ const QString &MentionsDropdown::filter() const { return _filter; } +ChatData *MentionsDropdown::chat() const { + return _chat; +} + int32 MentionsDropdown::innerTop() { return _scroll.scrollTop(); } diff --git a/Telegram/SourceFiles/dropdown.h b/Telegram/SourceFiles/dropdown.h index 7a136f72c..c54b19126 100644 --- a/Telegram/SourceFiles/dropdown.h +++ b/Telegram/SourceFiles/dropdown.h @@ -467,6 +467,7 @@ private: typedef QList MentionRows; typedef QList HashtagRows; +typedef QList > BotCommandRows; class MentionsDropdown; class MentionsInner : public QWidget { @@ -474,7 +475,7 @@ class MentionsInner : public QWidget { public: - MentionsInner(MentionsDropdown *parent, MentionRows *rows, HashtagRows *hrows); + MentionsInner(MentionsDropdown *parent, MentionRows *rows, HashtagRows *hrows, BotCommandRows *crows); void paintEvent(QPaintEvent *e); @@ -505,6 +506,7 @@ private: MentionsDropdown *_parent; MentionRows *_rows; HashtagRows *_hrows; + BotCommandRows *_crows; int32 _sel; bool _mouseSel; QPoint _mousePos; @@ -523,13 +525,14 @@ public: void fastHide(); - void showFiltered(ChatData *chat, QString start); + void showFiltered(PeerData *peer, QString start); void updateFiltered(bool toDown = false); void setBoundings(QRect boundings); bool animStep(float64 ms); const QString &filter() const; + ChatData *chat() const; int32 innerTop(); int32 innerBottom(); @@ -556,11 +559,13 @@ private: QPixmap _cache; MentionRows _rows; HashtagRows _hrows; + BotCommandRows _crows; ScrollArea _scroll; MentionsInner _inner; ChatData *_chat; + UserData *_user; QString _filter; QRect _boundings; diff --git a/Telegram/SourceFiles/gui/flattextarea.cpp b/Telegram/SourceFiles/gui/flattextarea.cpp index e0b5cf28b..3b5a18e3a 100644 --- a/Telegram/SourceFiles/gui/flattextarea.cpp +++ b/Telegram/SourceFiles/gui/flattextarea.cpp @@ -179,7 +179,7 @@ EmojiPtr FlatTextarea::getSingleEmoji() const { return 0; } -void FlatTextarea::getMentionHashtagStart(QString &start) const { +void FlatTextarea::getMentionHashtagBotCommandStart(QString &start) const { int32 pos = textCursor().position(); if (textCursor().anchor() != pos) return; @@ -195,11 +195,16 @@ void FlatTextarea::getMentionHashtagStart(QString &start) const { QTextCharFormat f = fr.charFormat(); if (f.isImageFormat()) continue; + bool mentionInCommand = false; QString t(fr.text()); for (int i = pos - p; i > 0; --i) { if (t.at(i - 1) == '@') { if ((pos - p - i < 1 || t.at(i).isLetter()) && (i < 2 || !(t.at(i - 2).isLetterOrNumber() || t.at(i - 2) == '_'))) { start = t.mid(i - 1, pos - p - i + 1); + } else if ((pos - p - i < 1 || t.at(i).isLetter()) && i > 2 && (t.at(i - 2).isLetterOrNumber() || t.at(i - 2) == '_') && !mentionInCommand) { + mentionInCommand = true; + --i; + continue; } return; } else if (t.at(i - 1) == '#') { @@ -207,15 +212,20 @@ void FlatTextarea::getMentionHashtagStart(QString &start) const { start = t.mid(i - 1, pos - p - i + 1); } return; + } else if (t.at(i - 1) == '/') { + if (i < 2) { + start = t.mid(i - 1, pos - p - i + 1); + } + return; } - if (pos - p - i > 63) break; + if (pos - p - i > 127 || (!mentionInCommand && (pos - p - i > 63))) break; if (!t.at(i - 1).isLetterOrNumber() && t.at(i - 1) != '_') break; } return; } } -void FlatTextarea::onMentionOrHashtagInsert(QString mentionOrHashtag) { +void FlatTextarea::onMentionHashtagOrBotCommandInsert(QString str) { QTextCursor c(textCursor()); int32 pos = c.position(); @@ -231,31 +241,37 @@ void FlatTextarea::onMentionOrHashtagInsert(QString mentionOrHashtag) { QTextCharFormat f = fr.charFormat(); if (f.isImageFormat()) continue; + bool mentionInCommand = false; QString t(fr.text()); for (int i = pos - p; i > 0; --i) { - if (t.at(i - 1) == '@' || t.at(i - 1) == '#') { + if (t.at(i - 1) == '@' || t.at(i - 1) == '#' || t.at(i - 1) == '/') { if ((i == pos - p || t.at(i).isLetter() || t.at(i - 1) == '#') && (i < 2 || !(t.at(i - 2).isLetterOrNumber() || t.at(i - 2) == '_'))) { c.setPosition(p + i - 1, QTextCursor::MoveAnchor); int till = p + i; - for (; (till < e) && (till - p - i + 1 < mentionOrHashtag.size()); ++till) { - if (t.at(till - p).toLower() != mentionOrHashtag.at(till - p - i + 1).toLower()) { + for (; (till < e) && (till - p - i + 1 < str.size()); ++till) { + if (t.at(till - p).toLower() != str.at(till - p - i + 1).toLower()) { break; } } - if (till - p - i + 1 == mentionOrHashtag.size() && till < e && t.at(till - p) == ' ') { + if (till - p - i + 1 == str.size() && till < e && t.at(till - p) == ' ') { ++till; } c.setPosition(till, QTextCursor::KeepAnchor); - c.insertText(mentionOrHashtag + ' '); + c.insertText(str + ' '); return; + } else if ((i == pos - p || t.at(i).isLetter()) && t.at(i - 1) == '@' && i > 2 && (t.at(i - 2).isLetterOrNumber() || t.at(i - 2) == '_') && !mentionInCommand) { + mentionInCommand = true; + --i; + continue; } break; } - if (pos - p - i > 63) break; + if (pos - p - i > 127 || (!mentionInCommand && (pos - p - i > 63))) break; if (!t.at(i - 1).isLetterOrNumber() && t.at(i - 1) != '_') break; } + break; } - c.insertText(mentionOrHashtag + ' '); + c.insertText(str + ' '); } void FlatTextarea::getSingleEmojiFragment(QString &text, QTextFragment &fragment) const { diff --git a/Telegram/SourceFiles/gui/flattextarea.h b/Telegram/SourceFiles/gui/flattextarea.h index 4df93d25c..fc69f0e9f 100644 --- a/Telegram/SourceFiles/gui/flattextarea.h +++ b/Telegram/SourceFiles/gui/flattextarea.h @@ -49,7 +49,7 @@ public: QSize minimumSizeHint() const; EmojiPtr getSingleEmoji() const; - void getMentionHashtagStart(QString &start) const; + void getMentionHashtagBotCommandStart(QString &start) const; void removeSingleEmoji(); QString getText(int32 start = 0, int32 end = -1) const; bool hasText() const; @@ -72,7 +72,7 @@ public slots: void onUndoAvailable(bool avail); void onRedoAvailable(bool avail); - void onMentionOrHashtagInsert(QString mentionOrHashtag); + void onMentionHashtagOrBotCommandInsert(QString str); signals: diff --git a/Telegram/SourceFiles/gui/scrollarea.cpp b/Telegram/SourceFiles/gui/scrollarea.cpp index 6ba255889..d7ecbceec 100644 --- a/Telegram/SourceFiles/gui/scrollarea.cpp +++ b/Telegram/SourceFiles/gui/scrollarea.cpp @@ -661,3 +661,7 @@ void ScrollArea::updateColors(const style::color &bar, const style::color &bg, c hor.update(); vert.update(); } + +ScrollArea::~ScrollArea() { + takeWidget(); +} diff --git a/Telegram/SourceFiles/gui/scrollarea.h b/Telegram/SourceFiles/gui/scrollarea.h index 014a7a391..5408d52a5 100644 --- a/Telegram/SourceFiles/gui/scrollarea.h +++ b/Telegram/SourceFiles/gui/scrollarea.h @@ -131,6 +131,8 @@ public: void updateColors(const style::color &bar, const style::color &bg, const style::color &barOver, const style::color &bgOver); + ~ScrollArea(); + public slots: void scrollToY(int toTop, int toBottom = -1); diff --git a/Telegram/SourceFiles/gui/text.cpp b/Telegram/SourceFiles/gui/text.cpp index 9611fc93e..97b22da04 100644 --- a/Telegram/SourceFiles/gui/text.cpp +++ b/Telegram/SourceFiles/gui/text.cpp @@ -30,7 +30,7 @@ namespace { const QRegularExpression _reMailStart(qsl("^[a-zA-Z\\-_\\.0-9]{1,256}\\@")); const QRegularExpression _reHashtag(qsl("(^|[\\s\\.,:;<>|'\"\\[\\]\\{\\}`\\~\\!\\%\\^\\*\\(\\)\\-\\+=\\x10])#[\\w]{2,64}([\\W]|$)"), QRegularExpression::UseUnicodePropertiesOption); const QRegularExpression _reMention(qsl("(^|[\\s\\.,:;<>|'\"\\[\\]\\{\\}`\\~\\!\\%\\^\\*\\(\\)\\-\\+=\\x10])@[A-Za-z_0-9]{5,32}([\\W]|$)"), QRegularExpression::UseUnicodePropertiesOption); - const QRegularExpression _reBotCommand(qsl("(^|[\\s\\.,:;<>|'\"\\[\\]\\{\\}`\\~\\!\\%\\^\\*\\(\\)\\-\\+=\\x10])/[\\w]{1,64}([\\W]|$)"), QRegularExpression::UseUnicodePropertiesOption); + const QRegularExpression _reBotCommand(qsl("(^|[\\s\\.,:;<>|'\"\\[\\]\\{\\}`\\~\\!\\%\\^\\*\\(\\)\\-\\+=\\x10])/[\\w]{1,64}(@[A-Za-z_0-9]{5,32})?([\\W]|$)"), QRegularExpression::UseUnicodePropertiesOption); QSet _validProtocols, _validTopDomains; const style::textStyle *_textStyle = 0; @@ -1104,9 +1104,33 @@ public: if (_yTo >= 0 && _y + _yDelta >= _yTo) return false; if (_y + _yDelta + _fontHeight <= _yFrom) return true; + uint16 trimmedLineEnd = _lineEnd; + for (; trimmedLineEnd > _lineStart; --trimmedLineEnd) { + QChar ch = _t->_text.at(trimmedLineEnd - 1); + if ((ch != QChar::Space || trimmedLineEnd == _lineStart + 1) && ch != QChar::LineFeed) { + break; + } + } + ITextBlock *_endBlock = (_endBlockIter == _end) ? 0 : (*_endBlockIter); bool elidedLine = _elideLast && _endBlock && (_y + _lineHeight >= _yTo); + int blockIndex = _lineStartBlock; + ITextBlock *currentBlock = _t->_blocks[blockIndex]; + ITextBlock *nextBlock = (++blockIndex < _blocksSize) ? _t->_blocks[blockIndex] : 0; + + int32 delta = (currentBlock->from() < _lineStart ? qMin(_lineStart - currentBlock->from(), 2) : 0); + _localFrom = _lineStart - delta; + int32 lineEnd = (_endBlock && _endBlock->from() < trimmedLineEnd && !elidedLine) ? qMin(uint16(trimmedLineEnd + 2), _blockEnd(_t, _endBlockIter, _end)) : trimmedLineEnd; + + QString lineText = _t->_text.mid(_localFrom, lineEnd - _localFrom); + int32 lineStart = delta, lineLength = trimmedLineEnd - _lineStart; + + if (elidedLine) { + initParagraphBidi(); + prepareElidedLine(lineText, lineStart, lineLength, _endBlock); + } + QFixed x = _x; if (_align & Qt::AlignHCenter) { x += (_wLeft / 2).toInt(); @@ -1154,34 +1178,9 @@ public: } } - /* // lpadding is counted to _wLeft - for (; _lineStart < _lineEnd; ++_lineStart) { - if (_t->_text.at(_lineStart) != QChar::Space) { - break; - } - }/**/ - for (; _lineEnd > _lineStart; --_lineEnd) { - QChar ch = _t->_text.at(_lineEnd - 1); - if ((ch != QChar::Space || _lineEnd == _lineStart + 1) && ch != QChar::LineFeed) { - break; - } - }/**/ - if (_lineEnd == _lineStart && !elidedLine) return true; + if (trimmedLineEnd == _lineStart && !elidedLine) return true; - initParagraphBidi(); // if was not inited - - int blockIndex = _lineStartBlock; - ITextBlock *currentBlock = _t->_blocks[blockIndex]; - ITextBlock *nextBlock = (++blockIndex < _blocksSize) ? _t->_blocks[blockIndex] : 0; - - int32 delta = (currentBlock->from() < _lineStart ? qMin(_lineStart - currentBlock->from(), 2) : 0); - _localFrom = _lineStart - delta; - int32 lineEnd = (_endBlock && _endBlock->from() < _lineEnd && !elidedLine) ? qMin(uint16(_lineEnd + 2), _blockEnd(_t, _endBlockIter, _end)) : _lineEnd; - - QString lineText = _t->_text.mid(_localFrom, lineEnd - _localFrom); - int32 lineStart = delta, lineLength = _lineEnd - _lineStart; - - if (elidedLine) prepareElidedLine(lineText, lineStart, lineLength, _endBlock); + if (!elidedLine) initParagraphBidi(); // if was not inited _f = _t->_font; QStackTextEngine engine(lineText, _f->f); @@ -1218,7 +1217,7 @@ public: } if (si.analysis.flags == QScriptAnalysis::Object) { if (_type == TextBlockEmoji || _type == TextBlockSkip) { - si.width = currentBlock->f_width() + (nextBlock == _endBlock && (!nextBlock || nextBlock->from() >= _lineEnd) ? 0 : currentBlock->f_rpadding()); + si.width = currentBlock->f_width() + (nextBlock == _endBlock && (!nextBlock || nextBlock->from() >= trimmedLineEnd) ? 0 : currentBlock->f_rpadding()); } } } @@ -1266,8 +1265,8 @@ public: *_getSymbolAfter = false; *_getSymbolUpon = false; } else { - *_getSymbol = (_lineEnd > _lineStart) ? (_lineEnd - 1) : _lineStart; - *_getSymbolAfter = (_lineEnd > _lineStart) ? true : false; + *_getSymbol = (trimmedLineEnd > _lineStart) ? (trimmedLineEnd - 1) : _lineStart; + *_getSymbolAfter = (trimmedLineEnd > _lineStart) ? true : false; *_getSymbolUpon = false; } return false; @@ -4152,7 +4151,7 @@ LinkRanges textParseLinks(const QString &text, int32 flags, bool rich) { // some if (!mBotCommand.capturedRef(1).isEmpty()) { ++botCommandOffset; } - if (!mBotCommand.capturedRef(2).isEmpty()) { + if (!mBotCommand.capturedRef(3).isEmpty()) { --botCommandEnd; } } diff --git a/Telegram/SourceFiles/gui/text.h b/Telegram/SourceFiles/gui/text.h index a16f4c782..55d11368a 100644 --- a/Telegram/SourceFiles/gui/text.h +++ b/Telegram/SourceFiles/gui/text.h @@ -435,8 +435,7 @@ struct TextParseOptions { int32 maxh; Qt::LayoutDirection dir; }; -extern const TextParseOptions _defaultOptions; -extern const TextParseOptions _textPlainOptions; +extern const TextParseOptions _defaultOptions, _textPlainOptions; enum TextSelectType { TextSelectLetters = 0x01, diff --git a/Telegram/SourceFiles/history.cpp b/Telegram/SourceFiles/history.cpp index 2de3d31a6..4ac7c817c 100644 --- a/Telegram/SourceFiles/history.cpp +++ b/Telegram/SourceFiles/history.cpp @@ -109,6 +109,14 @@ namespace { inline const HistoryForwarded *toHistoryForwarded(const HistoryItem *item) { return item ? item->toHistoryForwarded() : 0; } + inline const TextParseOptions &itemTextParseOptions(HistoryItem *item) { + History *h = item->history(); + UserData *f = item->from(); + if ((!h->peer->chat && h->peer->asUser()->botInfo) || (!f->chat && f->asUser()->botInfo) || (h->peer->chat && h->peer->asChat()->botStatus >= 0)) { + return _historyBotOptions; + } + return _historyTextOptions; + } } void historyInit() { @@ -155,7 +163,7 @@ void DialogRow::paint(QPainter &p, int32 w, bool act, bool sel) const { rectForName.setLeft(rectForName.left() + st::dlgChatImgSkip); } - HistoryItem *last = history->last; + HistoryItem *last = history->lastMsg; if (!last) { p.setFont(st::dlgHistFont->f); p.setPen((act ? st::dlgActiveColor : st::dlgSystemColor)->p); @@ -296,9 +304,11 @@ History::History(const PeerId &peerId) : width(0), height(0) , peer(App::peer(peerId)) , oldLoaded(false) , newLoaded(true) -, last(0) +, lastMsg(0) , activeMsgId(0) , draftToId(0) +, lastKeyboardId(0) +, lastKeyboardFrom(0) , lastWidth(0) , lastScrollTop(History::ScrollMax) , mute(isNotifyMuted(peer->notify)) @@ -377,7 +387,7 @@ bool DialogsList::del(const PeerId &peerId, DialogRow *replacedBy) { } void DialogsIndexed::peerNameChanged(PeerData *peer, const PeerData::Names &oldNames, const PeerData::NameFirstChars &oldChars) { - if (byName) { + if (sortMode == DialogsSortByName) { DialogRow *mainRow = list.adjustByName(peer); if (!mainRow) return; @@ -406,7 +416,7 @@ void DialogsIndexed::peerNameChanged(PeerData *peer, const PeerData::Names &oldN for (PeerData::NameFirstChars::const_iterator i = toAdd.cbegin(), e = toAdd.cend(); i != e; ++i) { DialogsIndex::iterator j = index.find(*i); if (j == index.cend()) { - j = index.insert(*i, new DialogsList(byName)); + j = index.insert(*i, new DialogsList(sortMode)); } j.value()->addByName(history); } @@ -428,7 +438,7 @@ void DialogsIndexed::peerNameChanged(PeerData *peer, const PeerData::Names &oldN } } for (PeerData::NameFirstChars::const_iterator i = toRemove.cbegin(), e = toRemove.cend(); i != e; ++i) { - history->dialogs.remove(*i); + if (sortMode == DialogsSortByDate) history->dialogs.remove(*i); DialogsIndex::iterator j = index.find(*i); if (j != index.cend()) { j.value()->del(peer->id, mainRow); @@ -437,9 +447,13 @@ void DialogsIndexed::peerNameChanged(PeerData *peer, const PeerData::Names &oldN for (PeerData::NameFirstChars::const_iterator i = toAdd.cbegin(), e = toAdd.cend(); i != e; ++i) { DialogsIndex::iterator j = index.find(*i); if (j == index.cend()) { - j = index.insert(*i, new DialogsList(byName)); + j = index.insert(*i, new DialogsList(sortMode)); + } + if (sortMode == DialogsSortByDate) { + history->dialogs.insert(*i, j.value()->addByPos(history)); + } else { + j.value()->addToEnd(history); } - history->dialogs.insert(*i, j.value()->addByPos(history)); } } } @@ -535,7 +549,7 @@ HistoryItem *Histories::addToBack(const MTPmessage &msg, int msgState) { if (!h.value()->loadedAtBottom()) { HistoryItem *item = h.value()->addToHistory(msg); if (item) { - h.value()->last = item; + h.value()->lastMsg = item; if (msgState > 0) { h.value()->newItemAdded(item); } @@ -586,6 +600,9 @@ HistoryItem *History::createItem(HistoryBlock *block, const MTPmessage &msg, boo } else { result = new HistoryMessage(this, block, msg.c_message()); } + if (msg.c_message().has_reply_markup()) { + App::feedReplyMarkup(msgId, msg.c_message().vreply_markup); + } break; case mtpc_messageService: { @@ -762,7 +779,7 @@ HistoryItem *History::doAddToBack(HistoryBlock *to, bool newBlock, HistoryItem * } } to->push_back(adding); - last = adding; + lastMsg = adding; adding->y = to->height; if (width) { int32 dh = adding->resize(width); @@ -785,12 +802,23 @@ HistoryItem *History::doAddToBack(HistoryBlock *to, bool newBlock, HistoryItem * } } } - if (peer->chat && adding->from()->id) { - QList *lastAuthors = &(peer->asChat()->lastAuthors); - int prev = lastAuthors->indexOf(adding->from()); - if (prev > 0) { - lastAuthors->removeAt(prev); - lastAuthors->push_front(adding->from()); + if (adding->from()->id) { + if (peer->chat) { + QList *lastAuthors = &(peer->asChat()->lastAuthors); + int prev = lastAuthors->indexOf(adding->from()); + if (prev > 0) { + lastAuthors->removeAt(prev); + } + if (prev) { + lastAuthors->push_front(adding->from()); + } + } + if (adding->hasReplyMarkup()) { + lastKeyboardId = adding->id; + lastKeyboardFrom = adding->from()->id; + } else if (lastKeyboardFrom == adding->from()->id) { + lastKeyboardId = 0; + lastKeyboardFrom = 0; } } return adding; @@ -894,7 +922,15 @@ void History::addToFront(const QVector &slice) { } } } - if (lastAuthors && item->from()->id && !lastAuthors->contains(item->from())) lastAuthors->push_back(item->from()); + if (item->from()->id) { + if (lastAuthors && !lastAuthors->contains(item->from())) { + if (item->hasReplyMarkup() && !lastKeyboardId) { + lastKeyboardId = item->id; + lastKeyboardFrom = item->from()->id; + } + lastAuthors->push_back(item->from()); + } + } } if (App::wnd()) App::wnd()->mediaOverviewUpdated(peer); } @@ -1143,9 +1179,9 @@ void History::fixLastMessage(bool wasAtBottom) { wasAtBottom = false; } if (wasAtBottom) { - last = back()->back(); + lastMsg = back()->back(); } else { - last = 0; + lastMsg = 0; if (App::main()) { App::main()->checkPeerHistory(peer); } @@ -1161,12 +1197,12 @@ void History::loadAround(MsgId msgId) { if (!item || !item->block()) { clear(true); } - newLoaded = last && !last->detached(); + newLoaded = lastMsg && !lastMsg->detached(); } else { if (!loadedAtBottom()) { clear(true); } - newLoaded = isEmpty() || (last && !last->detached()); + newLoaded = isEmpty() || (lastMsg && !lastMsg->detached()); } } } @@ -1252,7 +1288,7 @@ void History::clear(bool leaveItems) { setMsgCount(0); if (!leaveItems) { setUnreadCount(0); - last = 0; + lastMsg = 0; } height = 0; oldLoaded = false; @@ -1478,7 +1514,7 @@ void HistoryItem::destroy() { bool wasAtBottom = history()->loadedAtBottom(); _history->removeNotification(this); detach(); - if (history()->last == this) { + if (history()->lastMsg == this) { history()->fixLastMessage(wasAtBottom); } HistoryMedia *m = getMedia(true); @@ -1554,8 +1590,7 @@ HistoryPhoto::HistoryPhoto(const MTPDphoto &photo, const QString &caption, Histo , _caption(st::minPhotoSize) , openl(new PhotoLink(data)) { if (!caption.isEmpty()) { - bool bot = (!parent->history()->peer->chat && parent->history()->peer->asUser()->botInfo) || (!parent->from()->chat && parent->from()->asUser()->botInfo); - _caption.setText(st::msgFont, caption + textcmdSkipBlock(parent->timeWidth(true), st::msgDateFont->height - st::msgDateDelta.y()), bot ? _historyBotOptions : _historyTextOptions); + _caption.setText(st::msgFont, caption + textcmdSkipBlock(parent->timeWidth(true), st::msgDateFont->height - st::msgDateDelta.y()), itemTextParseOptions(parent)); } init(); } @@ -1955,8 +1990,7 @@ HistoryVideo::HistoryVideo(const MTPDvideo &video, const QString &caption, Histo , _uplDone(0) { if (!caption.isEmpty()) { - bool bot = (!parent->history()->peer->chat && parent->history()->peer->asUser()->botInfo) || (!parent->from()->chat && parent->from()->asUser()->botInfo); - _caption.setText(st::msgFont, caption + textcmdSkipBlock(parent->timeWidth(true), st::msgDateFont->height - st::msgDateDelta.y()), bot ? _historyBotOptions : _historyTextOptions); + _caption.setText(st::msgFont, caption + textcmdSkipBlock(parent->timeWidth(true), st::msgDateFont->height - st::msgDateDelta.y()), itemTextParseOptions(parent)); } _size = formatDurationAndSizeText(data->duration, data->size); @@ -4664,11 +4698,10 @@ void HistoryMessage::initMediaFromDocument(DocumentData *doc) { void HistoryMessage::initDimensions(const QString &text) { if (!_media || !text.isEmpty()) { // !justMedia() - bool bot = (!history()->peer->chat && history()->peer->asUser()->botInfo) || (!from()->chat && from()->asUser()->botInfo); if (_media && _media->isDisplayed()) { - _text.setText(st::msgFont, text, bot ? _historyBotOptions : _historyTextOptions); + _text.setText(st::msgFont, text, itemTextParseOptions(this)); } else { - _text.setText(st::msgFont, text + textcmdSkipBlock(timeWidth(true), st::msgDateFont->height - st::msgDateDelta.y()), bot ? _historyBotOptions : _historyTextOptions); + _text.setText(st::msgFont, text + textcmdSkipBlock(timeWidth(true), st::msgDateFont->height - st::msgDateDelta.y()), itemTextParseOptions(this)); } } } @@ -4684,18 +4717,17 @@ void HistoryMessage::initDimensions(const HistoryItem *parent) { _maxw += st::msgPadding.left() + st::msgPadding.right(); if (_media) { _media->initDimensions(this); - bool bot = (!history()->peer->chat && history()->peer->asUser()->botInfo) || (!from()->chat && from()->asUser()->botInfo); if (_media->isDisplayed() && _text.hasSkipBlock()) { QString was = HistoryMessage::selectedText(FullItemSel); if (!was.isEmpty()) { - _text.setText(st::msgFont, was, bot ? _historyBotOptions : _historyTextOptions); // without date skip + _text.setText(st::msgFont, was, itemTextParseOptions(this)); // without date skip _textWidth = 0; _textHeight = 0; } } else if (!_media->isDisplayed() && !_text.hasSkipBlock()) { QString was = HistoryMessage::selectedText(FullItemSel); if (!was.isEmpty()) { - _text.setText(st::msgFont, was + textcmdSkipBlock(timeWidth(true), st::msgDateFont->height - st::msgDateDelta.y()), bot ? _historyBotOptions : _historyTextOptions); // without date skip + _text.setText(st::msgFont, was + textcmdSkipBlock(timeWidth(true), st::msgDateFont->height - st::msgDateDelta.y()), itemTextParseOptions(this)); // without date skip _textWidth = 0; _textHeight = 0; } @@ -4749,18 +4781,17 @@ void HistoryMessage::setMedia(const MTPmessageMedia &media) { } QString t; initMedia(media, t); - bool bot = (!history()->peer->chat && history()->peer->asUser()->botInfo) || (!from()->chat && from()->asUser()->botInfo); if (_media && _media->isDisplayed() && !mediaWasDisplayed) { QString was = HistoryMessage::selectedText(FullItemSel); if (!was.isEmpty()) { - _text.setText(st::msgFont, was, bot ? _historyBotOptions : _historyTextOptions); // without date skip + _text.setText(st::msgFont, was, itemTextParseOptions(this)); // without date skip _textWidth = 0; _textHeight = 0; } } else if (mediaWasDisplayed && (!_media || !_media->isDisplayed())) { QString was = HistoryMessage::selectedText(FullItemSel); if (!was.isEmpty()) { - _text.setText(st::msgFont, was + textcmdSkipBlock(timeWidth(true), st::msgDateFont->height - st::msgDateDelta.y()), bot ? _historyBotOptions : _historyTextOptions); // without date skip + _text.setText(st::msgFont, was + textcmdSkipBlock(timeWidth(true), st::msgDateFont->height - st::msgDateDelta.y()), itemTextParseOptions(this)); // without date skip _textWidth = 0; _textHeight = 0; } @@ -5069,6 +5100,9 @@ HistoryMessage::~HistoryMessage() { _media->unregItem(this); delete _media; } + if (_flags & MTPDmessage::flag_reply_markup) { + App::clearReplyMarkup(id); + } } HistoryForwarded::HistoryForwarded(History *history, HistoryBlock *block, const MTPDmessage &msg) : HistoryMessage(history, block, msg.vid.v, msg.vflags.v, ::date(msg.vdate), msg.vfrom_id.v, textClean(qs(msg.vmessage)), msg.vmedia) diff --git a/Telegram/SourceFiles/history.h b/Telegram/SourceFiles/history.h index 7dbf50316..0a0adfeb5 100644 --- a/Telegram/SourceFiles/history.h +++ b/Telegram/SourceFiles/history.h @@ -198,7 +198,7 @@ struct History : public QList { PeerData *peer; bool oldLoaded, newLoaded; - HistoryItem *last; + HistoryItem *lastMsg; MsgId activeMsgId; typedef QList NotifyQueue; @@ -238,8 +238,8 @@ struct History : public QList { } } } - if (last == old) { - last = item; + if (lastMsg == old) { + lastMsg = item; } // showFrom can't be detached } @@ -251,6 +251,9 @@ struct History : public QList { int32 lastWidth, lastScrollTop; bool mute; + MsgId lastKeyboardId; + PeerId lastKeyboardFrom; + mtpRequestId sendRequestId; // for dialog drawing @@ -284,8 +287,14 @@ struct History : public QList { static const int32 ScrollMax = INT_MAX; }; +enum DialogsSortMode { + DialogsSortByDate, + DialogsSortByName, + DialogsSortByAdd +}; + struct DialogsList { - DialogsList(bool sortByName) : begin(&last), end(&last), byName(sortByName), count(0), current(&last) { + DialogsList(DialogsSortMode sortMode) : begin(&last), end(&last), sortMode(sortMode), count(0), current(&last) { } void adjustCurrent(int32 y, int32 h) const { @@ -323,10 +332,10 @@ struct DialogsList { end->pos++; if (begin == end) { begin = current = result; - if (!byName && updatePos) history->posInDialogs = 0; + if (sortMode == DialogsSortByDate && updatePos) history->posInDialogs = 0; } else { end->prev->next = result; - if (!byName && updatePos) history->posInDialogs = end->prev->history->posInDialogs + 1; + if (sortMode == DialogsSortByDate && updatePos) history->posInDialogs = end->prev->history->posInDialogs + 1; } rowByPeer.insert(history->peer->id, result); ++count; @@ -334,7 +343,7 @@ struct DialogsList { } void bringToTop(DialogRow *row, bool updatePos = true) { - if (!byName && updatePos && row != begin) { + if (sortMode == DialogsSortByDate && updatePos && row != begin) { row->history->posInDialogs = begin->history->posInDialogs - 1; } insertBefore(row, begin); @@ -389,7 +398,7 @@ struct DialogsList { } DialogRow *adjustByName(const PeerData *peer) { - if (!byName) return 0; + if (sortMode != DialogsSortByName) return 0; RowByPeer::iterator i = rowByPeer.find(peer->id); if (i == rowByPeer.cend()) return 0; @@ -408,7 +417,7 @@ struct DialogsList { } DialogRow *addByName(History *history) { - if (!byName) return 0; + if (sortMode != DialogsSortByName) return 0; DialogRow *row = addToEnd(history), *change = row; const QString &peerName(history->peer->name); @@ -425,7 +434,7 @@ struct DialogsList { } void adjustByPos(DialogRow *row) { - if (byName) return; + if (sortMode != DialogsSortByDate) return; DialogRow *change = row; while (change->prev && change->prev->history->posInDialogs > row->history->posInDialogs) { @@ -440,7 +449,7 @@ struct DialogsList { } DialogRow *addByPos(History *history) { - if (byName) return 0; + if (sortMode != DialogsSortByDate) return 0; DialogRow *row = addToEnd(history, false); adjustByPos(row); @@ -475,7 +484,7 @@ struct DialogsList { DialogRow last; DialogRow *begin, *end; - bool byName; + DialogsSortMode sortMode; int32 count; typedef QHash RowByPeer; @@ -485,7 +494,7 @@ struct DialogsList { }; struct DialogsIndexed { - DialogsIndexed(bool sortByName) : byName(sortByName), list(byName) { + DialogsIndexed(DialogsSortMode sortMode) : sortMode(sortMode), list(sortMode) { } History::DialogLinks addToEnd(History *history) { @@ -499,7 +508,7 @@ struct DialogsIndexed { for (PeerData::NameFirstChars::const_iterator i = history->peer->chars.cbegin(), e = history->peer->chars.cend(); i != e; ++i) { DialogsIndex::iterator j = index.find(*i); if (j == index.cend()) { - j = index.insert(*i, new DialogsList(byName)); + j = index.insert(*i, new DialogsList(sortMode)); } result.insert(*i, j.value()->addToEnd(history)); } @@ -517,7 +526,7 @@ struct DialogsIndexed { for (PeerData::NameFirstChars::const_iterator i = history->peer->chars.cbegin(), e = history->peer->chars.cend(); i != e; ++i) { DialogsIndex::iterator j = index.find(*i); if (j == index.cend()) { - j = index.insert(*i, new DialogsList(byName)); + j = index.insert(*i, new DialogsList(sortMode)); } j.value()->addByName(history); } @@ -556,7 +565,7 @@ struct DialogsIndexed { void clear(); - bool byName; + DialogsSortMode sortMode; DialogsList list; typedef QMap DialogsIndex; DialogsIndex index; @@ -678,6 +687,9 @@ public: void markMediaRead() { _flags &= ~MTPDmessage_flag_media_unread; } + bool hasReplyMarkup() const { + return _flags & MTPDmessage::flag_reply_markup; + } virtual bool needCheck() const { return true; } diff --git a/Telegram/SourceFiles/historywidget.cpp b/Telegram/SourceFiles/historywidget.cpp index b5194d55f..f47960b82 100644 --- a/Telegram/SourceFiles/historywidget.cpp +++ b/Telegram/SourceFiles/historywidget.cpp @@ -1163,24 +1163,30 @@ void HistoryList::onTouchSelect() { } void HistoryList::onUpdateSelected() { - if (!hist || hist->isEmpty()) return; + if (!hist) return; QPoint mousePos(mapFromGlobal(_dragPos)); QPoint point(historyWidget->clampMousePosition(mousePos)); - adjustCurrent(point.y()); + HistoryBlock *block = 0; + HistoryItem *item = 0; + QPoint m; + if (!hist->isEmpty()) { + adjustCurrent(point.y()); - HistoryBlock *block = (*hist)[currentBlock]; - HistoryItem *item = (*block)[currentItem]; - App::mousedItem(item); - QPoint m = mapMouseToItem(point, item); - if (item->hasPoint(m.x(), m.y())) { - updateMsg(App::hoveredItem()); - App::hoveredItem(item); - updateMsg(App::hoveredItem()); - } else if (App::hoveredItem()) { - updateMsg(App::hoveredItem()); - App::hoveredItem(0); + block = (*hist)[currentBlock]; + item = (*block)[currentItem]; + + App::mousedItem(item); + m = mapMouseToItem(point, item); + if (item->hasPoint(m.x(), m.y())) { + updateMsg(App::hoveredItem()); + App::hoveredItem(item); + updateMsg(App::hoveredItem()); + } else if (App::hoveredItem()) { + updateMsg(App::hoveredItem()); + App::hoveredItem(0); + } } linkTipTimer.start(1000); @@ -1193,7 +1199,7 @@ void HistoryList::onUpdateSelected() { botInfo->text.getState(lnk, inText, point.x() - botDescRect.left() - st::msgPadding.left(), point.y() - botDescRect.top() - st::msgPadding.top() - st::botDescSkip - st::msgNameFont->height, botDescWidth); lnkInDesc = true; } - } else { + } else if (item) { item->getState(lnk, inText, m.x(), m.y()); } if (lnk != textlnkOver()) { @@ -1223,7 +1229,7 @@ void HistoryList::onUpdateSelected() { } else if (inText && (_selected.isEmpty() || _selected.cbegin().value() != FullItemSel)) { cur = style::cur_text; } - } else { + } else if (item) { if (item != _dragItem || (m - _dragStartPos).manhattanLength() >= QApplication::startDragDistance()) { if (_dragAction == PrepareDrag) { _dragAction = Dragging; @@ -1372,14 +1378,27 @@ void HistoryList::onParentGeometryChanged() { } } -MessageField::MessageField(HistoryWidget *history, const style::flatTextarea &st, const QString &ph, const QString &val) : FlatTextarea(history, st, ph, val), history(history) { +MessageField::MessageField(HistoryWidget *history, const style::flatTextarea &st, const QString &ph, const QString &val) : FlatTextarea(history, st, ph, val), history(history), _maxHeight(st::maxFieldHeight) { connect(this, SIGNAL(changed()), this, SLOT(onChange())); } +void MessageField::setMaxHeight(int32 maxHeight) { + _maxHeight = maxHeight; + int newh = ceil(document()->size().height()) + 2 * fakeMargin(), minh = st::btnSend.height - 2 * st::sendPadding; + if (newh > _maxHeight) { + newh = _maxHeight; + } else if (newh < minh) { + newh = minh; + } + if (height() != newh) { + resize(width(), newh); + } +} + void MessageField::onChange() { int newh = ceil(document()->size().height()) + 2 * fakeMargin(), minh = st::btnSend.height - 2 * st::sendPadding; - if (newh > st::maxFieldHeight) { - newh = st::maxFieldHeight; + if (newh > _maxHeight) { + newh = _maxHeight; } else if (newh < minh) { newh = minh; } @@ -1427,6 +1446,273 @@ void MessageField::focusInEvent(QFocusEvent *e) { emit focused(); } +BotKeyboard::BotKeyboard() : _wasForMsgId(0), _hoverAnim(animFunc(this, &BotKeyboard::hoverStep)), _st(&st::botKbButton), _sel(-1), _down(-1) { + setGeometry(0, 0, _st->margin, _st->margin); + setMouseTracking(true); + + _cmdTipTimer.setSingleShot(true); + connect(&_cmdTipTimer, SIGNAL(timeout()), this, SLOT(showCommandTip())); +} + +void BotKeyboard::paintEvent(QPaintEvent *e) { + Painter p(this); + + QRect r(e->rect()); + p.setClipRect(r); + p.fillRect(r, st::white->b); + + p.setPen(st::botKbColor->p); + p.setFont(st::botKbFont->f); + for (int32 i = 0, l = _btns.size(); i != l; ++i) { + int32 j = 0, s = _btns.at(i).size(); + for (; j != s; ++j) { + const Button &btn(_btns.at(i).at(j)); + QRect rect(btn.rect); + if (rect.top() >= r.bottom()) break; + if (rect.bottom() < r.top()) continue; + + if (rtl()) rect.moveLeft(width() - rect.left() - rect.width()); + + if (_down == i * MatrixRowShift + j) { + App::roundRect(p, rect, st::botKbDownBg, BotKeyboardDownCorners); + btn.text.drawElided(p, rect.x(), rect.y() + _st->downTextTop, rect.width(), 1, style::al_top); + } else { + App::roundRect(p, rect, st::botKbBg, BotKeyboardCorners); + float64 hover = btn.hover; + if (hover > 0) { + p.setOpacity(hover); + App::roundRect(p, rect, st::botKbOverBg, BotKeyboardOverCorners); + p.setOpacity(1); + } + btn.text.drawElided(p, rect.x(), rect.y() + _st->textTop, rect.width(), 1, style::al_top); + } + } + if (j < s) break; + } +} + +void BotKeyboard::resizeEvent(QResizeEvent *e) { + updateStyle(); + int32 h = (_btns.size() + 1) * _st->margin + _btns.size() * _st->height; + if (height() != h) { + resize(width(), h); + return; + } + + int32 y = _st->margin; + for (int32 i = 0, l = _btns.size(); i != l; ++i) { + int32 j = 0, s = _btns.at(i).size(); + + float64 widthForText = width() - (s * _st->margin + st::botKbScroll.width + s * 2 * _st->padding), widthOfText = 0.; + for (; j != s; ++j) { + Button &btn(_btns[i][j]); + if (btn.text.isEmpty()) btn.text.setText(st::botKbFont, textOneLine(btn.cmd), _textPlainOptions); + if (!btn.cwidth) btn.cwidth = btn.cmd.size(); + if (!btn.cwidth) btn.cwidth = 1; + widthOfText += qMax(btn.text.maxWidth(), 1); + } + + float64 x = _st->margin, coef = widthForText / widthOfText; + for (j = 0; j != s; ++j) { + Button &btn(_btns[i][j]); + float64 tw = widthForText / float64(s)/*qMax(btn.text.maxWidth(), 1) * coef*/, w = 2 * _st->padding + tw; + if (w < _st->padding) w = _st->padding; + + btn.rect = QRect(qRound(x), y, qRound(w), _st->height); + x += w + _st->margin; + + btn.full = tw >= btn.text.maxWidth(); + } + y += _st->height + _st->margin; + } +} + +void BotKeyboard::mousePressEvent(QMouseEvent *e) { + _lastMousePos = e->globalPos(); + updateSelected(); + _down = _sel; + update(); +} + +void BotKeyboard::mouseMoveEvent(QMouseEvent *e) { + _lastMousePos = e->globalPos(); + updateSelected(); +} + +void BotKeyboard::mouseReleaseEvent(QMouseEvent *e) { + int32 down = _down; + _down = -1; + + _lastMousePos = e->globalPos(); + updateSelected(); + if (_sel == down && down >= 0) { + int row = (down / MatrixRowShift), col = down % MatrixRowShift; + App::sendBotCommand(_btns.at(row).at(col).cmd, _wasForMsgId); + } +} + +void BotKeyboard::leaveEvent(QEvent *e) { + _lastMousePos = QPoint(-1, -1); + updateSelected(); +} + +bool BotKeyboard::updateMarkup(HistoryItem *to) { + if (to && to->hasReplyMarkup()) { + if (_wasForMsgId == to->id) return false; + + _wasForMsgId = to->id; + clearSelection(); + _btns.clear(); + const ReplyMarkup &markup(App::replyMarkup(to->id)); + if (!markup.isEmpty()) { + int32 i = 0, l = qMin(markup.size(), 32); + _btns.reserve(l); + for (; i != l; ++i) { + const QList &row(markup.at(i)); + QList