From 54149fb156bf731bedd09c570ac220c5ace89df7 Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Fri, 3 Jul 2020 21:48:13 +0300 Subject: [PATCH] Moved panel of pinned dialogs for touchbar to separate file. --- Telegram/CMakeLists.txt | 2 + .../touchbar/items/mac_pinned_chats_item.h | 20 + .../touchbar/items/mac_pinned_chats_item.mm | 863 ++++++++++++++++++ 3 files changed, 885 insertions(+) create mode 100644 Telegram/SourceFiles/platform/mac/touchbar/items/mac_pinned_chats_item.h create mode 100644 Telegram/SourceFiles/platform/mac/touchbar/items/mac_pinned_chats_item.mm diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt index e0f17aa22b..9759a3877f 100644 --- a/Telegram/CMakeLists.txt +++ b/Telegram/CMakeLists.txt @@ -891,6 +891,8 @@ PRIVATE platform/mac/specific_mac_p.h platform/mac/window_title_mac.mm platform/mac/window_title_mac.h + platform/mac/touchbar/items/mac_pinned_chats_item.h + platform/mac/touchbar/items/mac_pinned_chats_item.mm platform/mac/touchbar/mac_touchbar_audio.h platform/mac/touchbar/mac_touchbar_audio.mm platform/mac/touchbar/mac_touchbar_common.h diff --git a/Telegram/SourceFiles/platform/mac/touchbar/items/mac_pinned_chats_item.h b/Telegram/SourceFiles/platform/mac/touchbar/items/mac_pinned_chats_item.h new file mode 100644 index 0000000000..c6a4b886fc --- /dev/null +++ b/Telegram/SourceFiles/platform/mac/touchbar/items/mac_pinned_chats_item.h @@ -0,0 +1,20 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#pragma once + +#include + +namespace Main { +class Session; +} // namespace Main + +API_AVAILABLE(macos(10.12.2)) +@interface PinnedDialogsPanel : NSImageView +- (id)init:(not_null)session + destroyEvent:(rpl::producer<>)touchBarSwitches; +@end diff --git a/Telegram/SourceFiles/platform/mac/touchbar/items/mac_pinned_chats_item.mm b/Telegram/SourceFiles/platform/mac/touchbar/items/mac_pinned_chats_item.mm new file mode 100644 index 0000000000..0178ccd310 --- /dev/null +++ b/Telegram/SourceFiles/platform/mac/touchbar/items/mac_pinned_chats_item.mm @@ -0,0 +1,863 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#include "platform/mac/touchbar/items/mac_pinned_chats_item.h" + +#ifndef OS_OSX + +#include "apiwrap.h" +#include "base/call_delayed.h" +#include "base/timer.h" +#include "base/unixtime.h" +#include "core/application.h" +#include "core/sandbox.h" +#include "data/data_changes.h" +#include "data/data_cloud_file.h" +#include "data/data_file_origin.h" +#include "data/data_folder.h" +#include "data/data_peer_values.h" +#include "data/data_session.h" +#include "data/data_user.h" +#include "dialogs/dialogs_layout.h" +#include "history/history.h" +#include "main/main_session.h" +#include "mainwidget.h" +#include "platform/mac/touchbar/mac_touchbar_common.h" +#include "styles/style_dialogs.h" +#include "ui/effects/animations.h" +#include "ui/empty_userpic.h" +#include "window/themes/window_theme.h" +#include "window/window_controller.h" +#include "window/window_session_controller.h" + +#import +#import +#import + +using TouchBar::kCircleDiameter; + +namespace { + +constexpr auto kPinnedButtonsSpace = 30; +constexpr auto kPinnedButtonsLeftSkip = kPinnedButtonsSpace / 2; + +constexpr auto kOnlineCircleSize = 8; +constexpr auto kOnlineCircleStrokeWidth = 1.5; +constexpr auto kUnreadBadgeSize = 15; + +inline bool IsSelfPeer(PeerData *peer) { + return peer && peer->isSelf(); +} + +QImage PrepareImage() { + const auto s = kCircleDiameter * cIntRetinaFactor(); + auto result = QImage(QSize(s, s), QImage::Format_ARGB32_Premultiplied); + result.fill(Qt::transparent); + return result; +} + +QImage SavedMessagesUserpic() { + auto result = PrepareImage(); + Painter paint(&result); + + const auto s = result.width(); + Ui::EmptyUserpic::PaintSavedMessages(paint, 0, 0, s, s); + return result; +} + +QImage ArchiveUserpic(not_null folder) { + auto result = PrepareImage(); + Painter paint(&result); + + auto view = std::shared_ptr(); + folder->paintUserpic(paint, view, 0, 0, result.width()); + return result; +} + +QImage UnreadBadge(not_null peer) { + const auto history = peer->owner().history(peer->id); + const auto count = history->unreadCountForBadge(); + if (!count) { + return QImage(); + } + const auto unread = history->unreadMark() + ? QString() + : QString::number(count); + Dialogs::Layout::UnreadBadgeStyle unreadSt; + unreadSt.sizeId = Dialogs::Layout::UnreadBadgeInTouchBar; + unreadSt.muted = history->mute(); + // Use constant values to draw badge regardless of cConfigScale(). + unreadSt.size = kUnreadBadgeSize * cRetinaFactor(); + unreadSt.padding = 4 * cRetinaFactor(); + unreadSt.font = style::font( + 9.5 * cRetinaFactor(), + unreadSt.font->flags(), + unreadSt.font->family()); + + auto result = QImage( + QSize(kCircleDiameter, kUnreadBadgeSize) * cIntRetinaFactor(), + QImage::Format_ARGB32_Premultiplied); + result.fill(Qt::transparent); + Painter p(&result); + + Dialogs::Layout::paintUnreadCount( + p, + unread, + result.width(), + result.height() - unreadSt.size, + unreadSt, + nullptr, + 2); + return result; +} + +NSRect PeerRectByIndex(int index) { + return NSMakeRect( + index * (kCircleDiameter + kPinnedButtonsSpace) + + kPinnedButtonsLeftSkip, + 0, + kCircleDiameter, + kCircleDiameter); +} + +TimeId CalculateOnlineTill(not_null peer) { + if (peer->isSelf()) { + return 0; + } + if (const auto user = peer->asUser()) { + if (!user->isServiceUser() && !user->isBot()) { + const auto onlineTill = user->onlineTill; + return (onlineTill <= -5) + ? -onlineTill + : (onlineTill <= 0) + ? 0 + : onlineTill; + } + } + return 0; +}; + +} // namespace + +#pragma mark - PinnedDialogsPanel + +@interface PinnedDialogsPanel() +@end // @interface PinnedDialogsPanel + +@implementation PinnedDialogsPanel { + struct Pin { + PeerData *peer = nullptr; + std::shared_ptr userpicView = nullptr; + int index = -1; + QImage userpic; + QImage unreadBadge; + + Ui::Animations::Simple shiftAnimation; + int shift = 0; + int finalShift = 0; + int deltaShift = 0; + int x = 0; + int horizontalShift = 0; + bool onTop = false; + + Ui::Animations::Simple onlineAnimation; + TimeId onlineTill = 0; + }; + rpl::lifetime _lifetime; + Main::Session *_session; + + std::vector> _pins; + QImage _savedMessages; + QImage _archive; + + bool _hasArchive; + bool _selfUnpinned; + + rpl::event_stream> _touches; + rpl::event_stream> _gestures; + + double _r, _g, _b, _a; // The online circle color. +} + +- (void)processHorizontalReorder { + // This method is a simplified version of the VerticalLayoutReorder class + // and is adapatized for horizontal use. + enum class State : uchar { + Started, + Applied, + Cancelled, + }; + + const auto currentStart = _lifetime.make_state(0); + const auto currentPeer = _lifetime.make_state(nullptr); + const auto currentState = _lifetime.make_state(State::Cancelled); + const auto currentDesiredIndex = _lifetime.make_state(-1); + const auto waitForFinish = _lifetime.make_state(false); + const auto isDragging = _lifetime.make_state(false); + + const auto indexOf = [=](PeerData *p) { + const auto i = ranges::find(_pins, p, &Pin::peer); + Assert(i != end(_pins)); + return i - begin(_pins); + }; + + const auto setHorizontalShift = [=](const auto &pin, int shift) { + if (const auto delta = shift - pin->horizontalShift) { + pin->horizontalShift = shift; + pin->x += delta; + + // Redraw a rectangle + // from the beginning point of the pin movement to the end point. + auto rect = PeerRectByIndex(indexOf(pin->peer) + [self shift]); + const auto absDelta = std::abs(delta); + rect.origin.x = pin->x - absDelta; + rect.size.width += absDelta * 2; + [self setNeedsDisplayInRect:rect]; + } + }; + + const auto updateShift = [=](not_null peer, int indexHint) { + Expects(indexHint >= 0 && indexHint < _pins.size()); + + const auto index = (_pins[indexHint]->peer->id == peer->id) + ? indexHint + : indexOf(peer); + const auto &entry = _pins[index]; + entry->shift = entry->deltaShift + + std::round(entry->shiftAnimation.value(entry->finalShift)); + if (entry->deltaShift && !entry->shiftAnimation.animating()) { + entry->finalShift += entry->deltaShift; + entry->deltaShift = 0; + } + setHorizontalShift(entry, entry->shift); + }; + + const auto moveToShift = [=](int index, int shift) { + Core::Sandbox::Instance().customEnterFromEventLoop([=] { + auto &entry = _pins[index]; + if (entry->finalShift + entry->deltaShift == shift) { + return; + } + const auto peer = entry->peer; + entry->shiftAnimation.start( + [=] { updateShift(peer, index); }, + entry->finalShift, + shift - entry->deltaShift, + st::slideWrapDuration); + entry->finalShift = shift - entry->deltaShift; + }); + }; + + const auto cancelCurrentByIndex = [=](int index) { + Expects(*currentPeer != nullptr); + + if (*currentState == State::Started) { + *currentState = State::Cancelled; + } + *currentPeer = nullptr; + for (auto i = 0, count = int(_pins.size()); i != count; ++i) { + moveToShift(i, 0); + } + }; + + const auto cancelCurrent = [=] { + if (*currentPeer) { + cancelCurrentByIndex(indexOf(*currentPeer)); + } + }; + + const auto updateOrder = [=](int index, int positionX) { + const auto shift = positionX - *currentStart; + const auto ¤t = _pins[index]; + current->shiftAnimation.stop(); + current->shift = current->finalShift = shift; + setHorizontalShift(current, shift); + + const auto count = _pins.size(); + const auto currentWidth = current->userpic.width(); + const auto currentMiddle = current->x + currentWidth / 2; + *currentDesiredIndex = index; + if (shift > 0) { + auto top = current->x - shift; + for (auto next = index + 1; next != count; ++next) { + const auto &entry = _pins[next]; + top += entry->userpic.width(); + if (currentMiddle < top) { + moveToShift(next, 0); + } else { + *currentDesiredIndex = next; + moveToShift(next, -currentWidth); + } + } + for (auto prev = index - 1; prev >= 0; --prev) { + moveToShift(prev, 0); + } + } else { + for (auto next = index + 1; next != count; ++next) { + moveToShift(next, 0); + } + for (auto prev = index - 1; prev >= 0; --prev) { + const auto &entry = _pins[prev]; + if (currentMiddle >= entry->x - entry->shift + currentWidth) { + moveToShift(prev, 0); + } else { + *currentDesiredIndex = prev; + moveToShift(prev, currentWidth); + } + } + } + }; + + const auto checkForStart = [=](int positionX) { + const auto shift = positionX - *currentStart; + const auto delta = QApplication::startDragDistance(); + *isDragging = (std::abs(shift) > delta); + if (!*isDragging) { + return; + } + + *currentState = State::Started; + *currentStart += (shift > 0) ? delta : -delta; + + const auto index = indexOf(*currentPeer); + *currentDesiredIndex = index; + + // Raise the pin. + ranges::for_each(_pins, [=](const auto &pin) { + pin->onTop = false; + }); + _pins[index]->onTop = true; + + updateOrder(index, positionX); + }; + + const auto localGuard = _lifetime.make_state(); + + const auto finishCurrent = [=] { + if (!*currentPeer) { + return; + } + const auto index = indexOf(*currentPeer); + if (*currentDesiredIndex == index + || *currentState != State::Started) { + cancelCurrentByIndex(index); + return; + } + const auto result = *currentDesiredIndex; + *currentState = State::Cancelled; + *currentPeer = nullptr; + + const auto ¤t = _pins[index]; + // Since the width of all elements is the same + // we can use a single value. + current->finalShift += (index - result) * current->userpic.width(); + + if (!(current->finalShift + current->deltaShift)) { + current->shift = 0; + setHorizontalShift(current, 0); + } + current->horizontalShift = current->finalShift; + base::reorder(_pins, index, result); + + *waitForFinish = true; + // Call on end of an animation. + base::call_delayed(st::slideWrapDuration, &(*localGuard), [=] { + const auto guard = gsl::finally([=] { + _session->data().notifyPinnedDialogsOrderUpdated(); + *waitForFinish = false; + }); + if (index == result) { + return; + } + const auto &order = _session->data().pinnedChatsOrder( + nullptr, + FilterId()); + const auto d = (index < result) ? 1 : -1; // Direction. + for (auto i = index; i != result; i += d) { + _session->data().chatsList()->pinned()->reorder( + order.at(i).history(), + order.at(i + d).history()); + } + _session->api().savePinnedOrder(nullptr); + }); + + moveToShift(result, 0); + }; + + const auto touchBegan = [=](int touchX) { + *isDragging = false; + cancelCurrent(); + *currentStart = touchX; + if (_pins.size() < 2) { + return; + } + const auto index = [self indexFromX:*currentStart]; + if (index < 0) { + return; + } + *currentPeer = _pins[index]->peer; + }; + + const auto touchMoved = [=](int touchX) { + if (!*currentPeer) { + return; + } else if (*currentState != State::Started) { + checkForStart(touchX); + } else { + updateOrder(indexOf(*currentPeer), touchX); + } + }; + + const auto touchEnded = [=](int touchX) { + if (*isDragging) { + finishCurrent(); + return; + } + const auto step = QApplication::startDragDistance(); + if (std::abs(*currentStart - touchX) < step) { + [self performAction:touchX]; + } + }; + + _gestures.events( + ) | rpl::filter([=] { + return !(*waitForFinish); + }) | rpl::start_with_next([=](not_null gesture) { + const auto currentPosition = [gesture locationInView:self].x; + + switch ([gesture state]) { + case NSGestureRecognizerStateBegan: + return touchBegan(currentPosition); + case NSGestureRecognizerStateChanged: + return touchMoved(currentPosition); + case NSGestureRecognizerStateCancelled: + case NSGestureRecognizerStateEnded: + return touchEnded(currentPosition); + } + }, _lifetime); + + _session->data().pinnedDialogsOrderUpdated( + ) | rpl::start_with_next(cancelCurrent, _lifetime); + + _lifetime.add([=] { + for (const auto &pin : _pins) { + pin->shiftAnimation.stop(); + pin->onlineAnimation.stop(); + } + }); + +} + +- (id)init:(not_null)session + destroyEvent:(rpl::producer<>)touchBarSwitches { + self = [super init]; + _session = session; + _hasArchive = _selfUnpinned = false; + _savedMessages = SavedMessagesUserpic(); + + auto *gesture = [[[NSPressGestureRecognizer alloc] + initWithTarget:self + action:@selector(gestureHandler:)] autorelease]; + gesture.allowedTouchTypes = NSTouchTypeMaskDirect; + gesture.minimumPressDuration = 0; + gesture.allowableMovement = 0; + [self addGestureRecognizer:gesture]; + + // For some reason, sometimes a parent deallocates not immediately, + // but only after the user's input (mouse movement, key pressing, etc.). + // So we have to use a custom event to destroy the current lifetime + // manually, before it leads to crashes. + std::move( + touchBarSwitches + ) | rpl::start_with_next([=] { + _lifetime.destroy(); + }, _lifetime); + + using UpdateFlag = Data::PeerUpdate::Flag; + + const auto downloadLifetime = _lifetime.make_state(); + const auto peerChangedLifetime = _lifetime.make_state(); + const auto lastDialogsCount = _lifetime.make_state>(0); + auto &&peers = ranges::views::all( + _pins + ) | ranges::views::transform(&Pin::peer); + + const auto updatePanelSize = [=] { + const auto size = lastDialogsCount->current(); + if (self.image) { + [self.image release]; + } + // TODO: replace it with NSLayoutConstraint. + self.image = [[NSImage alloc] initWithSize:NSMakeSize( + size * (kCircleDiameter + kPinnedButtonsSpace) + + kPinnedButtonsLeftSkip + - kPinnedButtonsSpace / 2, + kCircleDiameter)]; + }; + lastDialogsCount->changes( + ) | rpl::start_with_next(updatePanelSize, _lifetime); + const auto singleUserpic = [=](const auto &pin) { + if (IsSelfPeer(pin->peer)) { + pin->userpic = _savedMessages; + return; + } + auto userpic = PrepareImage(); + Painter p(&userpic); + + pin->peer->paintUserpic(p, pin->userpicView, 0, 0, userpic.width()); + userpic.setDevicePixelRatio(cRetinaFactor()); + pin->userpic = std::move(userpic); + [self setNeedsDisplayInRect:PeerRectByIndex(pin->index)]; + }; + const auto updateUserpics = [=] { + ranges::for_each(_pins, singleUserpic); + *lastDialogsCount = [self shift] + std::ssize(_pins); + }; + const auto updateBadge = [=](const auto &pin) { + const auto peer = pin->peer; + if (IsSelfPeer(peer)) { + return; + } + pin->unreadBadge = UnreadBadge(peer); + + const auto userpicIndex = pin->index + [self shift]; + [self setNeedsDisplayInRect:PeerRectByIndex(userpicIndex)]; + }; + const auto listenToDownloaderFinished = [=] { + _session->downloaderTaskFinished( + ) | rpl::start_with_next([=] { + const auto all = ranges::all_of(_pins, [=](const auto &pin) { + return (!pin->peer->hasUserpic()) + || (pin->userpicView && pin->userpicView->image()); + }); + if (all) { + downloadLifetime->destroy(); + } + updateUserpics(); + }, *downloadLifetime); + }; + const auto processOnline = [=](const auto &pin) { + // TODO: this should be replaced + // with the global application timer for online statuses. + const auto onlineChanges = + peerChangedLifetime->make_state>(); + const auto peer = pin->peer; + const auto onlineTimer = + peerChangedLifetime->make_state([=] { + onlineChanges->fire_copy({ peer }); + }); + + const auto callTimer = [=](const auto &pin) { + onlineTimer->cancel(); + if (pin->onlineTill) { + const auto time = pin->onlineTill - base::unixtime::now(); + if (time > 0) { + onlineTimer->callOnce(time * crl::time(1000)); + } + } + }; + callTimer(pin); + + using PeerUpdate = Data::PeerUpdate; + auto to_peer = rpl::map([=](const PeerUpdate &update) -> PeerData* { + return update.peer; + }); + rpl::merge( + _session->changes().peerUpdates( + pin->peer, + UpdateFlag::OnlineStatus) | to_peer, + onlineChanges->events() + ) | rpl::start_with_next([=](PeerData *peer) { + const auto it = ranges::find(_pins, peer, &Pin::peer); + if (it == end(_pins)) { + return; + } + const auto &pin = *it; + pin->onlineTill = CalculateOnlineTill(pin->peer); + + callTimer(pin); + + if (![NSApplication sharedApplication].active) { + pin->onlineAnimation.stop(); + return; + } + const auto online = Data::OnlineTextActive( + pin->onlineTill, + base::unixtime::now()); + if (pin->onlineAnimation.animating()) { + pin->onlineAnimation.change( + online ? 1. : 0., + st::dialogsOnlineBadgeDuration); + } else { + const auto s = kOnlineCircleSize + kOnlineCircleStrokeWidth; + const auto index = pin->index; + Core::Sandbox::Instance().customEnterFromEventLoop([=] { + _pins[index]->onlineAnimation.start( + [=] { + [self setNeedsDisplayInRect:NSMakeRect( + _pins[index]->x + kCircleDiameter - s, + 0, + s, + s)]; + }, + online ? 0. : 1., + online ? 1. : 0., + st::dialogsOnlineBadgeDuration); + }); + } + }, *peerChangedLifetime); + }; + + const auto updatePinnedChats = [=] { + _pins = ranges::view::zip( + _session->data().pinnedChatsOrder(nullptr, FilterId()), + ranges::view::ints(0, ranges::unreachable) + ) | ranges::views::transform([=](const auto &pair) { + const auto index = pair.second; + auto peer = pair.first.history()->peer; + auto view = peer->createUserpicView(); + const auto onlineTill = CalculateOnlineTill(peer); + Pin pin = { + .peer = std::move(peer), + .userpicView = std::move(view), + .index = index, + .onlineTill = onlineTill }; + return std::make_unique(std::move(pin)); + }); + _selfUnpinned = ranges::none_of(peers, &PeerData::isSelf); + + peerChangedLifetime->destroy(); + for (const auto &pin : _pins) { + const auto peer = pin->peer; + _session->changes().peerUpdates( + peer, + UpdateFlag::Photo + ) | rpl::start_with_next( + listenToDownloaderFinished, + *peerChangedLifetime); + + if (const auto user = peer->asUser()) { + if (!user->isServiceUser() + && !user->isBot() + && !peer->isSelf()) { + processOnline(pin); + } + } + + const auto index = pin->index; + rpl::merge( + _session->changes().historyUpdates( + _session->data().history(peer), + Data::HistoryUpdate::Flag::UnreadView + ) | rpl::to_empty, + _session->changes().peerFlagsValue( + peer, + UpdateFlag::Notifications + ) | rpl::to_empty + ) | rpl::start_with_next([=] { + updateBadge(_pins[index]); + }, *peerChangedLifetime); + } + + updateUserpics(); + }; + + rpl::single( + rpl::empty_value() + ) | rpl::then( + _session->data().pinnedDialogsOrderUpdated() + ) | rpl::start_with_next(updatePinnedChats, _lifetime); + + const auto ArchiveId = Data::Folder::kId; + rpl::single( + _session->data().folderLoaded(ArchiveId) + ) | rpl::then( + _session->data().chatsListChanges() + ) | rpl::filter([](Data::Folder *folder) { + return folder && (folder->id() == ArchiveId); + }) | rpl::start_with_next([=](Data::Folder *folder) { + _hasArchive = !folder->chatsList()->empty(); + if (_archive.isNull()) { + _archive = ArchiveUserpic(folder); + } + updateUserpics(); + }, _lifetime); + + const auto updateOnlineColor = [=] { + st::dialogsOnlineBadgeFg->c.getRgbF(&_r, &_g, &_b, &_a); + }; + updateOnlineColor(); + + const auto localGuard = _lifetime.make_state(); + + base::ObservableViewer( + *Window::Theme::Background() + ) | rpl::filter([](const Window::Theme::BackgroundUpdate &update) { + return update.paletteChanged(); + }) | rpl::start_with_next([=] { + crl::on_main(&(*localGuard), [=] { + updateOnlineColor(); + if (const auto f = _session->data().folderLoaded(ArchiveId)) { + _archive = ArchiveUserpic(f); + } + _savedMessages = SavedMessagesUserpic(); + updateUserpics(); + }); + }, _lifetime); + + listenToDownloaderFinished(); + [self processHorizontalReorder]; + return self; +} + +- (void)dealloc { + if (self.image) { + [self.image release]; + } + [super dealloc]; +} + +- (int)shift { + return (_hasArchive ? 1 : 0) + (_selfUnpinned ? 1 : 0); +} + +- (void)gestureHandler:(NSPressGestureRecognizer*)gesture { + _gestures.fire(std::move(gesture)); +} + +- (int)indexFromX:(int)position { + const auto x = position + - kPinnedButtonsLeftSkip + + kPinnedButtonsSpace / 2; + return x / (kCircleDiameter + kPinnedButtonsSpace) - [self shift]; +} + +- (void)performAction:(int)xPosition { + const auto index = [self indexFromX:xPosition]; + const auto peer = (index < 0 || index >= std::ssize(_pins)) + ? nullptr + : _pins[index]->peer; + if (!peer && !_hasArchive && !_selfUnpinned) { + return; + } + + const auto active = Core::App().activeWindow(); + const auto controller = active ? active->sessionController() : nullptr; + const auto openFolder = [=] { + const auto folder = _session->data().folderLoaded(Data::Folder::kId); + if (folder && controller) { + controller->openFolder(folder); + } + }; + Core::Sandbox::Instance().customEnterFromEventLoop([=] { + (_hasArchive && (index == (_selfUnpinned ? -2 : -1))) + ? openFolder() + : controller->content()->choosePeer( + (_selfUnpinned && index == -1) + ? _session->userPeerId() + : peer->id, + ShowAtUnreadMsgId); + }); +} + +- (QImage)imageToDraw:(int)i { + Expects(i < std::ssize(_pins)); + if (i < 0) { + if (_hasArchive && (i == -[self shift])) { + return _archive; + } else if (_selfUnpinned) { + return _savedMessages; + } + } + return _pins[i]->userpic; +} + +- (void)drawSinglePin:(int)i rect:(NSRect)dirtyRect { + const auto rect = [&] { + auto rect = PeerRectByIndex(i + [self shift]); + if (i < 0) { + return rect; + } + auto &pin = _pins[i]; + // We can have x = 0 when the pin is dragged. + rect.origin.x = ((!pin->x && !pin->onTop) ? rect.origin.x : pin->x); + pin->x = rect.origin.x; + return rect; + }(); + if (!NSIntersectsRect(rect, dirtyRect)) { + return; + } + CGContextRef context = [[NSGraphicsContext currentContext] CGContext]; + { + CGImageRef image = ([self imageToDraw:i]).toCGImage(); + CGContextDrawImage(context, rect, image); + CGImageRelease(image); + } + + if (i >= 0) { + const auto &pin = _pins[i]; + const auto rectRight = NSMaxX(rect); + if (!pin->unreadBadge.isNull()) { + CGImageRef image = pin->unreadBadge.toCGImage(); + const auto w = CGImageGetWidth(image) / cRetinaFactor(); + const auto borderRect = CGRectMake( + rectRight - w, + 0, + w, + CGImageGetHeight(image) / cRetinaFactor()); + CGContextDrawImage(context, borderRect, image); + CGImageRelease(image); + return; + } + const auto online = Data::OnlineTextActive( + pin->onlineTill, + base::unixtime::now()); + const auto value = pin->onlineAnimation.value(online ? 1. : 0.); + if (value < 0.05) { + return; + } + const auto lineWidth = kOnlineCircleStrokeWidth; + const auto circleSize = kOnlineCircleSize; + const auto progress = value * circleSize; + const auto diff = (circleSize - progress) / 2; + const auto borderRect = CGRectMake( + rectRight - circleSize + diff - lineWidth / 2, + diff, + progress, + progress); + + CGContextSetRGBStrokeColor(context, 0, 0, 0, 1.0); + CGContextSetRGBFillColor(context, _r, _g, _b, _a); + CGContextSetLineWidth(context, lineWidth); + CGContextFillEllipseInRect(context, borderRect); + CGContextStrokeEllipseInRect(context, borderRect); + } +} + +- (void)drawRect:(NSRect)dirtyRect { + const auto shift = [self shift]; + if (_pins.empty() && !shift) { + return; + } + auto indexToTop = -1; + const auto guard = gsl::finally([&] { + if (indexToTop >= 0) { + [self drawSinglePin:indexToTop rect:dirtyRect]; + } + }); + for (auto i = -shift; i < std::ssize(_pins); i++) { + if (i >= 0 && _pins[i]->onTop && (indexToTop < 0)) { + indexToTop = i; + continue; + } + [self drawSinglePin:i rect:dirtyRect]; + } +} + +@end // @@implementation PinnedDialogsPanel + +#endif // OS_OSX