init
Some checks failed
Docker. / Ubuntu (push) Has been cancelled
User-agent updater. / User-agent (push) Failing after 15s
Lock Threads / lock (push) Failing after 10s
Waiting for answer. / waiting-for-answer (push) Failing after 22s
Needs user action. / needs-user-action (push) Failing after 8s
Can't reproduce. / cant-reproduce (push) Failing after 8s
Close stale issues and PRs / stale (push) Has been cancelled

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

View File

@@ -0,0 +1,16 @@
/*
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
#import <AppKit/NSPopoverTouchBarItem.h>
#import <AppKit/NSTouchBar.h>
API_AVAILABLE(macos(10.12.2))
@interface TextFormatPopover : NSPopoverTouchBarItem
- (id)init:(NSTouchBarItemIdentifier)identifier;
@end

View File

@@ -0,0 +1,144 @@
/*
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_formatter_item.h"
#include "base/platform/mac/base_utilities_mac.h"
#include "lang/lang_keys.h"
#include "platform/mac/touchbar/mac_touchbar_common.h"
#import <AppKit/NSCustomTouchBarItem.h>
#import <AppKit/NSScrollView.h>
#import <AppKit/NSSegmentedControl.h>
#include <QtWidgets/QApplication>
#include <QtWidgets/QTextEdit>
namespace {
constexpr auto kCommandBold = 0x010;
constexpr auto kCommandItalic = 0x011;
constexpr auto kCommandUnderline = 0x012;
constexpr auto kCommandStrikeOut = 0x013;
constexpr auto kCommandBlockquote = 0x014;
constexpr auto kCommandMonospace = 0x015;
constexpr auto kCommandClear = 0x016;
constexpr auto kCommandLink = 0x017;
const auto kPopoverFormatter = @"popoverInputFormatter";
void SendKeyEvent(int command) {
auto *focused = qobject_cast<QTextEdit*>(QApplication::focusWidget());
if (!focused) {
return;
}
auto key = 0;
auto modifier = Qt::KeyboardModifiers(0) | Qt::ControlModifier;
switch (command) {
case kCommandBold:
key = Qt::Key_B;
break;
case kCommandItalic:
key = Qt::Key_I;
break;
case kCommandBlockquote:
key = Qt::Key_Period;
modifier |= Qt::ShiftModifier;
break;
case kCommandMonospace:
key = Qt::Key_M;
modifier |= Qt::ShiftModifier;
break;
case kCommandClear:
key = Qt::Key_N;
modifier |= Qt::ShiftModifier;
break;
case kCommandLink:
key = Qt::Key_K;
break;
case kCommandUnderline:
key = Qt::Key_U;
break;
case kCommandStrikeOut:
key = Qt::Key_X;
modifier |= Qt::ShiftModifier;
break;
}
QApplication::postEvent(
focused,
new QKeyEvent(QEvent::KeyPress, key, modifier));
QApplication::postEvent(
focused,
new QKeyEvent(QEvent::KeyRelease, key, modifier));
}
} // namespace
#pragma mark - TextFormatPopover
@implementation TextFormatPopover {
rpl::lifetime _lifetime;
}
- (id)init:(NSTouchBarItemIdentifier)identifier {
self = [super initWithIdentifier:identifier];
if (!self) {
return nil;
}
self.collapsedRepresentationImage = [NSImage
imageNamed:NSImageNameTouchBarTextItalicTemplate]; // autorelease];
auto *secondaryTouchBar = [[[NSTouchBar alloc] init] autorelease];
auto *popover = [[[NSCustomTouchBarItem alloc]
initWithIdentifier:kPopoverFormatter] autorelease];
{
auto *scroll = [[[NSScrollView alloc] init] autorelease];
auto *segment = [[[NSSegmentedControl alloc] init] autorelease];
segment.segmentStyle = NSSegmentStyleRounded;
segment.target = self;
segment.action = @selector(segmentClicked:);
static const auto strings = {
tr::lng_menu_formatting_bold,
tr::lng_menu_formatting_italic,
tr::lng_menu_formatting_underline,
tr::lng_menu_formatting_strike_out,
tr::lng_menu_formatting_blockquote,
tr::lng_menu_formatting_monospace,
tr::lng_menu_formatting_clear,
tr::lng_info_link_label,
};
segment.segmentCount = strings.size();
auto width = 0;
auto count = 0;
for (const auto &s : strings) {
const auto string = Platform::Q2NSString(s(tr::now));
width += TouchBar::WidthFromString(string) * 1.4;
[segment setLabel:string forSegment:count++];
}
segment.frame = NSMakeRect(0, 0, width, TouchBar::kCircleDiameter);
[scroll setDocumentView:segment];
popover.view = scroll;
}
secondaryTouchBar.templateItems = [NSSet setWithArray:@[popover]];
secondaryTouchBar.defaultItemIdentifiers = @[kPopoverFormatter];
self.popoverTouchBar = secondaryTouchBar;
return self;
}
- (void)segmentClicked:(NSSegmentedControl*)sender {
const auto command = int(sender.selectedSegment) + kCommandBold;
sender.selectedSegment = -1;
SendKeyEvent(command);
[self dismissPopover:nil];
}
@end // @implementation TextFormatPopover

View File

@@ -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 <AppKit/NSImageView.h>
namespace Main {
class Session;
} // namespace Main
API_AVAILABLE(macos(10.12.2))
@interface PinnedDialogsPanel : NSImageView
- (id)init:(not_null<Main::Session*>)session
destroyEvent:(rpl::producer<>)touchBarSwitches;
@end

View File

@@ -0,0 +1,873 @@
/*
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"
#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 "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 "ui/userpic_view.h"
#include "ui/unread_badge_paint.h"
#include "ui/painter.h"
#include "window/window_controller.h"
#include "window/window_session_controller.h"
#import <AppKit/NSColor.h>
#import <AppKit/NSGraphicsContext.h>
#import <AppKit/NSPressGestureRecognizer.h>
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();
}
inline bool IsRepliesPeer(PeerData *peer) {
return peer && peer->isRepliesChat();
}
QImage PrepareImage() {
const auto s = kCircleDiameter * style::DevicePixelRatio();
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 RepliesMessagesUserpic() {
auto result = PrepareImage();
Painter paint(&result);
const auto s = result.width();
Ui::EmptyUserpic::PaintRepliesMessages(paint, 0, 0, s, s);
return result;
}
QImage ArchiveUserpic(not_null<Data::Folder*> folder) {
auto result = PrepareImage();
Painter paint(&result);
folder->paintUserpic(paint, 0, 0, result.width());
return result;
}
QImage UnreadBadge(not_null<PeerData*> peer) {
const auto history = peer->owner().history(peer->id);
const auto state = history->chatListBadgesState();
if (!state.unread) {
return QImage();
}
const auto counter = (state.unreadCounter > 0)
? QString::number(state.unreadCounter)
: QString();
Ui::UnreadBadgeStyle unreadSt;
unreadSt.sizeId = Ui::UnreadBadgeSize::TouchBar;
unreadSt.muted = state.unreadMuted;
// Use constant values to draw badge regardless of cConfigScale().
unreadSt.size = kUnreadBadgeSize * float64(style::DevicePixelRatio());
unreadSt.padding = 4 * float64(style::DevicePixelRatio());
unreadSt.font = style::font(
9.5 * float64(style::DevicePixelRatio()),
unreadSt.font->flags(),
unreadSt.font->family());
auto result = QImage(
QSize(kCircleDiameter, kUnreadBadgeSize) * style::DevicePixelRatio(),
QImage::Format_ARGB32_Premultiplied);
result.fill(Qt::transparent);
Painter p(&result);
Ui::PaintUnreadBadge(
p,
counter,
result.width(),
result.height() - unreadSt.size,
unreadSt,
2);
return result;
}
NSRect PeerRectByIndex(int index) {
return NSMakeRect(
index * (kCircleDiameter + kPinnedButtonsSpace)
+ kPinnedButtonsLeftSkip,
0,
kCircleDiameter,
kCircleDiameter);
}
[[nodiscard]] Data::LastseenStatus CalculateLastseenStatus(
not_null<PeerData*> peer) {
if (const auto user = peer->asUser()) {
return user->lastseen();
}
return Data::LastseenStatus();
}
} // namespace
#pragma mark - PinnedDialogsPanel
@interface PinnedDialogsPanel()
@end // @interface PinnedDialogsPanel
@implementation PinnedDialogsPanel {
struct Pin {
PeerData *peer = nullptr;
Ui::PeerUserpicView userpicView;
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;
Data::LastseenStatus lastseen;
};
rpl::lifetime _lifetime;
Main::Session *_session;
std::vector<std::unique_ptr<Pin>> _pins;
QImage _savedMessages;
QImage _repliesMessages;
QImage _archive;
bool _hasArchive;
bool _selfUnpinned;
bool _repliesUnpinned;
rpl::event_stream<not_null<NSEvent*>> _touches;
rpl::event_stream<not_null<NSPressGestureRecognizer*>> _gestures;
CGFloat _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<int>(0);
const auto currentPeer = _lifetime.make_state<PeerData*>(nullptr);
const auto currentState = _lifetime.make_state<State>(State::Cancelled);
const auto currentDesiredIndex = _lifetime.make_state<int>(-1);
const auto waitForFinish = _lifetime.make_state<bool>(false);
const auto isDragging = _lifetime.make_state<bool>(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<PeerData*> 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
+ base::SafeRound(
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 cancelCurrentPeer = [=] {
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) {
cancelCurrentPeer();
}
};
const auto updateOrder = [=](int index, int positionX) {
const auto shift = positionX - *currentStart;
const auto &current = _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<base::has_weak_ptr>();
const auto finishCurrent = [=] {
if (!*currentPeer) {
return;
}
const auto index = indexOf(*currentPeer);
if (*currentDesiredIndex == index
|| *currentState != State::Started) {
cancelCurrentPeer();
return;
}
const auto result = *currentDesiredIndex;
*currentState = State::Cancelled;
*currentPeer = nullptr;
const auto &current = _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);
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::on_next([=](
not_null<NSPressGestureRecognizer*> 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::on_next(cancelCurrent, _lifetime);
_lifetime.add([=] {
for (const auto &pin : _pins) {
pin->shiftAnimation.stop();
pin->onlineAnimation.stop();
}
});
}
- (id)init:(not_null<Main::Session*>)session
destroyEvent:(rpl::producer<>)touchBarSwitches {
self = [super init];
_session = session;
_hasArchive = _selfUnpinned = false;
_savedMessages = SavedMessagesUserpic();
_repliesMessages = RepliesMessagesUserpic();
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::on_next([=] {
_lifetime.destroy();
}, _lifetime);
using UpdateFlag = Data::PeerUpdate::Flag;
const auto downloadLifetime = _lifetime.make_state<rpl::lifetime>();
const auto peerChangedLifetime = _lifetime.make_state<rpl::lifetime>();
const auto lastDialogsCount = _lifetime.make_state<rpl::variable<int>>(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::on_next(updatePanelSize, _lifetime);
const auto singleUserpic = [=](const auto &pin) {
if (IsSelfPeer(pin->peer)) {
pin->userpic = _savedMessages;
return;
} else if (IsRepliesPeer(pin->peer)) {
pin->userpic = _repliesMessages;
return;
}
auto userpic = PrepareImage();
Painter p(&userpic);
pin->peer->paintUserpic(p, pin->userpicView, 0, 0, userpic.width());
userpic.setDevicePixelRatio(style::DevicePixelRatio());
pin->userpic = std::move(userpic);
const auto userpicIndex = pin->index + [self shift];
[self setNeedsDisplayInRect:PeerRectByIndex(userpicIndex)];
};
const auto updateUserpics = [=] {
ranges::for_each(_pins, singleUserpic);
*lastDialogsCount = [self shift] + int(std::size(_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::on_next([=] {
const auto all = ranges::all_of(_pins, [=](const auto &pin) {
return (!pin->peer->hasUserpic())
|| (!Ui::PeerUserpicLoading(pin->userpicView));
});
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<rpl::event_stream<PeerData*>>();
const auto peer = pin->peer;
const auto onlineTimer = peerChangedLifetime->make_state<base::Timer>(
[=] { onlineChanges->fire_copy({ peer }); });
const auto callTimer = [=](const auto &pin) {
onlineTimer->cancel();
if (const auto till = pin->lastseen.onlineTill()) {
const auto left = till - base::unixtime::now();
if (left > 0) {
onlineTimer->callOnce(std::min(86400, left)
* 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::on_next([=](PeerData *peer) {
const auto it = ranges::find(_pins, peer, &Pin::peer);
if (it == end(_pins)) {
return;
}
const auto &pin = *it;
pin->lastseen = CalculateLastseenStatus(pin->peer);
callTimer(pin);
if (![NSApplication sharedApplication].active) {
pin->onlineAnimation.stop();
return;
}
const auto now = base::unixtime::now();
const auto online = pin->lastseen.isOnline(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::views::zip(
_session->data().pinnedChatsOrder(nullptr),
ranges::views::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();
return std::make_unique<Pin>(Pin{
.peer = std::move(peer),
.userpicView = std::move(view),
.index = index,
.lastseen = CalculateLastseenStatus(peer),
});
}) | ranges::to_vector;
_selfUnpinned = ranges::none_of(peers, &PeerData::isSelf);
_repliesUnpinned = ranges::none_of(peers, &PeerData::isRepliesChat);
peerChangedLifetime->destroy();
for (const auto &pin : _pins) {
const auto peer = pin->peer;
const auto index = pin->index;
_session->changes().peerUpdates(
peer,
UpdateFlag::Photo
) | rpl::on_next([=](const Data::PeerUpdate &update) {
_pins[index]->userpicView = update.peer->createUserpicView();
listenToDownloaderFinished();
}, *peerChangedLifetime);
if (const auto user = peer->asUser()) {
if (!user->isServiceUser()
&& !user->isBot()
&& !peer->isSelf()) {
processOnline(pin);
}
}
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::on_next([=] {
updateBadge(_pins[index]);
}, *peerChangedLifetime);
}
updateUserpics();
};
rpl::single(rpl::empty) | rpl::then(
_session->data().pinnedDialogsOrderUpdated()
) | rpl::on_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::on_next([=](Data::Folder *folder) {
_hasArchive = !folder->chatsList()->empty();
if (_archive.isNull()) {
_archive = ArchiveUserpic(folder);
}
updateUserpics();
}, _lifetime);
const auto updateOnlineColor = [=] {
auto r = 0, g = 0, b = 0, a = 0;
st::dialogsOnlineBadgeFg->c.getRgb(&r, &g, &b, &a);
_r = r / 255.;
_g = g / 255.;
_b = b / 255.;
_a = a / 255.;
};
updateOnlineColor();
const auto localGuard = _lifetime.make_state<base::has_weak_ptr>();
style::PaletteChanged(
) | rpl::on_next([=] {
crl::on_main(&(*localGuard), [=] {
updateOnlineColor();
if (const auto f = _session->data().folderLoaded(ArchiveId)) {
_archive = ArchiveUserpic(f);
}
_savedMessages = SavedMessagesUserpic();
_repliesMessages = RepliesMessagesUserpic();
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 >= int(std::size(_pins)))
? nullptr
: _pins[index]->peer;
if (!peer && !_hasArchive && !_selfUnpinned) {
return;
}
const auto active = Core::App().activePrimaryWindow();
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([=] {
if (_hasArchive && (index == (_selfUnpinned ? -2 : -1))) {
openFolder();
} else {
controller->showPeerHistory((_selfUnpinned && index == -1)
? _session->user()
: peer);
}
});
}
- (QImage)imageToDraw:(int)i {
Expects(i < int(std::size(_pins)));
if (i < 0) {
if (_hasArchive && (i == -[self shift])) {
return _archive;
} else if (_selfUnpinned) {
return _savedMessages;
} else if (_repliesUnpinned) {
return _repliesMessages;
}
}
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)
/ float64(style::DevicePixelRatio());
const auto borderRect = CGRectMake(
rectRight - w,
0,
w,
CGImageGetHeight(image)
/ float64(style::DevicePixelRatio()));
CGContextDrawImage(context, borderRect, image);
CGImageRelease(image);
return;
}
const auto now = base::unixtime::now();
const auto online = pin->lastseen.isOnline(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 < int(std::size(_pins)); i++) {
if (i >= 0 && _pins[i]->onTop && (indexToTop < 0)) {
indexToTop = i;
continue;
}
[self drawSinglePin:i rect:dirtyRect];
}
}
@end // @@implementation PinnedDialogsPanel

View File

@@ -0,0 +1,21 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
#import <AppKit/NSPopoverTouchBarItem.h>
#import <AppKit/NSTouchBar.h>
namespace Window {
class Controller;
} // namespace Window
API_AVAILABLE(macos(10.12.2))
@interface StickerEmojiPopover : NSPopoverTouchBarItem<NSTouchBarDelegate>
- (id)init:(not_null<Window::Controller*>)controller
identifier:(NSTouchBarItemIdentifier)identifier;
@end

View File

@@ -0,0 +1,716 @@
/*
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_scrubber_item.h"
#include "api/api_common.h"
#include "api/api_sending.h"
#include "base/call_delayed.h"
#include "base/platform/mac/base_utilities_mac.h"
#include "ui/boxes/confirm_box.h"
#include "ui/painter.h"
#include "chat_helpers/emoji_list_widget.h"
#include "core/sandbox.h"
#include "core/application.h"
#include "core/core_settings.h"
#include "data/data_chat_participant_status.h"
#include "data/data_document.h"
#include "data/data_document_media.h"
#include "data/data_file_origin.h"
#include "data/data_forum_topic.h"
#include "data/data_session.h"
#include "data/stickers/data_stickers.h"
#include "history/history.h"
#include "lang/lang_keys.h"
#include "main/main_session.h"
#include "platform/mac/touchbar/mac_touchbar_common.h"
#include "styles/style_basic.h"
#include "styles/style_settings.h"
#include "ui/widgets/fields/input_field.h"
#include "window/section_widget.h"
#include "window/window_controller.h"
#include "window/window_session_controller.h"
#import <AppKit/NSCustomTouchBarItem.h>
#import <AppKit/NSGestureRecognizer.h>
#import <AppKit/NSImage.h>
#import <AppKit/NSImageView.h>
#import <AppKit/NSPressGestureRecognizer.h>
#import <AppKit/NSScrollView.h>
#import <AppKit/NSScrubber.h>
#import <AppKit/NSScrubberItemView.h>
#import <AppKit/NSScrubberLayout.h>
#import <AppKit/NSSegmentedControl.h>
#import <AppKit/NSTextField.h>
#include <QtWidgets/QTextEdit>
using TouchBar::kCircleDiameter;
using TouchBar::CreateNSImageFromStyleIcon;
namespace {
//https://developer.apple.com/design/human-interface-guidelines/macos/touch-bar/touch-bar-icons-and-images/
constexpr auto kIdealIconSize = 36;
constexpr auto kSegmentIconSize = 25;
constexpr auto kSegmentSize = 92;
constexpr auto kMaxStickerSets = 5;
constexpr auto kGestureStateProcessed = {
NSGestureRecognizerStateChanged,
NSGestureRecognizerStateBegan,
};
constexpr auto kGestureStateFinished = {
NSGestureRecognizerStateEnded,
NSGestureRecognizerStateCancelled,
NSGestureRecognizerStateFailed,
};
const auto kStickersScrubber = @"scrubberStickers";
const auto kEmojiScrubber = @"scrubberEmoji";
const auto kStickerItemIdentifier = @"stickerItem";
const auto kEmojiItemIdentifier = @"emojiItem";
const auto kPickerTitleItemIdentifier = @"pickerTitleItem";
enum ScrubberItemType {
Emoji,
Sticker,
None,
};
inline bool IsSticker(ScrubberItemType type) {
return type == ScrubberItemType::Sticker;
}
struct PickerScrubberItem {
PickerScrubberItem(QString title) : title(title) {
}
PickerScrubberItem(DocumentData *document) : document(document) {
mediaView = document->createMediaView();
mediaView->checkStickerSmall();
updateThumbnail();
}
PickerScrubberItem(EmojiPtr emoji) : emoji(emoji) {
}
void updateThumbnail() {
if (!document || !image.isNull()) {
return;
}
const auto sticker = mediaView->getStickerSmall();
if (!sticker) {
return;
}
const auto size = sticker->size()
.scaled(kCircleDiameter, kCircleDiameter, Qt::KeepAspectRatio);
image = sticker->pixSingle(
size,
{ .outer = { kCircleDiameter, kCircleDiameter } }).toImage();
}
bool isStickerLoaded() const {
return !image.isNull();
}
QString title = QString();
DocumentData *document = nullptr;
std::shared_ptr<Data::DocumentMedia> mediaView = nullptr;
QImage image;
EmojiPtr emoji = nullptr;
};
struct PickerScrubberItemsHolder {
std::vector<PickerScrubberItem> stickers;
std::vector<PickerScrubberItem> emoji;
int size(ScrubberItemType type) {
return IsSticker(type) ? stickers.size() : emoji.size();
}
auto at(int index, ScrubberItemType type) {
return IsSticker(type) ? stickers[index] : emoji[index];
}
};
using Platform::Q2NSString;
using Platform::Q2NSImage;
NSImage *CreateNSImageFromEmoji(EmojiPtr emoji) {
auto image = QImage(
QSize(kIdealIconSize, kIdealIconSize) * style::DevicePixelRatio(),
QImage::Format_ARGB32_Premultiplied);
image.setDevicePixelRatio(style::DevicePixelRatio());
image.fill(Qt::black);
{
Painter paint(&image);
PainterHighQualityEnabler hq(paint);
Ui::Emoji::Draw(
paint,
emoji,
Ui::Emoji::GetSizeTouchbar(),
0,
0);
}
return Q2NSImage(image);
}
auto ActiveChat(not_null<Window::Controller*> controller) {
if (const auto sessionController = controller->sessionController()) {
return sessionController->activeChatCurrent();
}
return Dialogs::Key();
}
bool CanSendToActiveChat(
not_null<Window::Controller*> controller,
ChatRestriction right) {
if (const auto topic = ActiveChat(controller).topic()) {
return Data::CanSend(topic, right);
} else if (const auto history = ActiveChat(controller).history()) {
return Data::CanSend(history->peer, right);
}
return false;
}
std::optional<QString> RestrictionToSend(
not_null<Window::Controller*> controller,
ChatRestriction right) {
if (const auto peer = ActiveChat(controller).peer()) {
if (const auto error = Data::RestrictionError(peer, right)) {
return *error;
}
}
return std::nullopt;
}
QString TitleRecentlyUsed(const Data::StickersSets &sets) {
const auto it = sets.find(Data::Stickers::CloudRecentSetId);
return (it != sets.cend())
? it->second->title
: tr::lng_recent_stickers(tr::now);
}
void AppendStickerSet(
const Data::StickersSets &sets,
std::vector<PickerScrubberItem> &to,
uint64 setId) {
const auto it = sets.find(setId);
if (it == sets.cend() || it->second->stickers.isEmpty()) {
return;
}
const auto set = it->second.get();
if (set->flags & Data::StickersSetFlag::Archived) {
return;
}
if (!(set->flags & Data::StickersSetFlag::Installed)) {
return;
}
to.emplace_back(PickerScrubberItem(set->title.isEmpty()
? set->shortName
: set->title));
for (const auto sticker : set->stickers) {
to.emplace_back(PickerScrubberItem(sticker));
}
}
void AppendRecentStickers(
const Data::StickersSets &sets,
RecentStickerPack &recentPack,
std::vector<PickerScrubberItem> &to) {
const auto cloudIt = sets.find(Data::Stickers::CloudRecentSetId);
const auto cloudCount = (cloudIt != sets.cend())
? cloudIt->second->stickers.size()
: 0;
if (cloudCount > 0) {
to.emplace_back(PickerScrubberItem(cloudIt->second->title));
for (const auto document : cloudIt->second->stickers) {
if (document->owner().stickers().isFaved(document)) {
continue;
}
to.emplace_back(PickerScrubberItem(document));
}
}
for (const auto &recent : recentPack) {
to.emplace_back(PickerScrubberItem(recent.first));
}
}
void AppendFavedStickers(
const Data::StickersSets &sets,
std::vector<PickerScrubberItem> &to) {
const auto it = sets.find(Data::Stickers::FavedSetId);
const auto count = (it != sets.cend())
? it->second->stickers.size()
: 0;
if (!count) {
return;
}
to.emplace_back(PickerScrubberItem(
tr::lng_mac_touchbar_favorite_stickers(tr::now)));
for (const auto document : it->second->stickers) {
to.emplace_back(PickerScrubberItem(document));
}
}
[[nodiscard]] EmojiPack RecentEmojiSection() {
const auto list = Core::App().settings().recentEmoji();
auto result = EmojiPack();
result.reserve(list.size());
for (const auto &emoji : list) {
if (const auto one = std::get_if<EmojiPtr>(&emoji.id.data)) {
result.push_back(*one);
}
}
return result;
}
void AppendEmojiPacks(
const Data::StickersSets &sets,
std::vector<PickerScrubberItem> &to) {
for (auto i = 0; i != ChatHelpers::kEmojiSectionCount; ++i) {
const auto section = static_cast<Ui::Emoji::Section>(i);
const auto list = (section == Ui::Emoji::Section::Recent)
? RecentEmojiSection()
: Ui::Emoji::GetSection(section);
const auto title = (section == Ui::Emoji::Section::Recent)
? TitleRecentlyUsed(sets)
: ChatHelpers::EmojiCategoryTitle(i)(tr::now);
to.emplace_back(title);
for (const auto &emoji : list) {
to.emplace_back(PickerScrubberItem(emoji));
}
}
}
} // namespace
@interface PickerScrubberItemView : NSScrubberImageItemView {
@public
DocumentId documentId;
}
@end // @interface PickerScrubberItemView
@implementation PickerScrubberItemView
@end // @implementation PickerScrubberItemView
#pragma mark - PickerCustomTouchBarItem
@interface PickerCustomTouchBarItem : NSCustomTouchBarItem
<NSScrubberDelegate,
NSScrubberDataSource,
NSScrubberFlowLayoutDelegate>
@end // @interface PickerCustomTouchBarItem
@implementation PickerCustomTouchBarItem {
ScrubberItemType _type;
std::shared_ptr<PickerScrubberItemsHolder> _itemsDataSource;
std::unique_ptr<PickerScrubberItem> _error;
DocumentId _lastPreviewedSticker;
Window::Controller *_controller;
History *_history;
rpl::event_stream<> _closeRequests;
rpl::lifetime _lifetime;
}
- (id)init:(ScrubberItemType)type
controller:(not_null<Window::Controller*>)controller
items:(std::shared_ptr<PickerScrubberItemsHolder>)items {
Expects(controller->sessionController() != nullptr);
self = [super initWithIdentifier:IsSticker(type)
? kStickersScrubber
: kEmojiScrubber];
if (!self) {
return self;
}
_type = type;
_controller = controller;
_itemsDataSource = items;
auto *scrubber = [[[NSScrubber alloc] initWithFrame:NSZeroRect]
autorelease];
auto *layout = [[[NSScrubberFlowLayout alloc] init] autorelease];
layout.itemSpacing = 10;
scrubber.scrubberLayout = layout;
scrubber.mode = NSScrubberModeFree;
scrubber.delegate = self;
scrubber.dataSource = self;
scrubber.floatsSelectionViews = true;
scrubber.showsAdditionalContentIndicators = true;
scrubber.itemAlignment = NSScrubberAlignmentCenter;
[scrubber registerClass:[PickerScrubberItemView class]
forItemIdentifier:kStickerItemIdentifier];
[scrubber registerClass:[NSScrubberTextItemView class]
forItemIdentifier:kPickerTitleItemIdentifier];
[scrubber registerClass:[NSScrubberImageItemView class]
forItemIdentifier:kEmojiItemIdentifier];
if (IsSticker(type)) {
auto *gesture = [[[NSPressGestureRecognizer alloc]
initWithTarget:self
action:@selector(gesturePreviewHandler:)] autorelease];
gesture.allowedTouchTypes = NSTouchTypeMaskDirect;
gesture.minimumPressDuration = QApplication::startDragTime() / 1000.;
gesture.allowableMovement = 0;
[scrubber addGestureRecognizer:gesture];
const auto kRight = ChatRestriction::SendStickers;
if (const auto error = RestrictionToSend(_controller, kRight)) {
_error = std::make_unique<PickerScrubberItem>(
tr::lng_restricted_send_stickers_all(tr::now));
}
} else {
const auto kRight = ChatRestriction::SendOther;
if (const auto error = RestrictionToSend(_controller, kRight)) {
_error = std::make_unique<PickerScrubberItem>(
tr::lng_restricted_send_message_all(tr::now));
}
}
_lastPreviewedSticker = 0;
self.view = scrubber;
return self;
}
- (PickerScrubberItem)itemAt:(int)index {
return _error ? *_error : _itemsDataSource->at(index, _type);
}
- (void)gesturePreviewHandler:(NSPressGestureRecognizer*)gesture {
const auto customEnter = [=](auto &&callback) {
Core::Sandbox::Instance().customEnterFromEventLoop([=] {
if (_controller) {
callback();
}
});
};
const auto checkState = [&](const auto &states) {
return ranges::contains(states, gesture.state);
};
if (checkState(kGestureStateProcessed)) {
NSScrollView *scrollView = self.view;
auto *container = scrollView.documentView.subviews.firstObject;
if (!container) {
return;
}
const auto point = [gesture locationInView:container];
for (PickerScrubberItemView *item in container.subviews) {
if (![item isMemberOfClass:[PickerScrubberItemView class]]
|| (item->documentId == _lastPreviewedSticker)
|| !NSPointInRect(point, item.frame)) {
continue;
}
_lastPreviewedSticker = item->documentId;
auto &owner = _controller->sessionController()->session().data();
const auto doc = owner.document(item->documentId);
customEnter([=] {
_controller->widget()->showMediaPreview(
Data::FileOrigin(),
doc);
});
break;
}
} else if (checkState(kGestureStateFinished)) {
customEnter([=] { _controller->widget()->hideMediaPreview(); });
_lastPreviewedSticker = 0;
}
}
- (void)encodeWithCoder:(nonnull NSCoder*)aCoder {
// Has not been implemented.
}
#pragma mark - NSScrubberDelegate
- (NSInteger)numberOfItemsForScrubber:(NSScrubber*)scrubber {
return _error ? 1 : _itemsDataSource->size(_type);
}
- (NSScrubberItemView*)scrubber:(NSScrubber*)scrubber
viewForItemAtIndex:(NSInteger)index {
const auto item = [self itemAt:index];
if (const auto document = item.document) {
PickerScrubberItemView *itemView = [scrubber
makeItemWithIdentifier:kStickerItemIdentifier
owner:self];
itemView.imageView.image = Q2NSImage(item.image);
itemView->documentId = document->id;
return itemView;
} else if (const auto emoji = item.emoji) {
NSScrubberImageItemView *itemView = [scrubber
makeItemWithIdentifier:kEmojiItemIdentifier
owner:self];
itemView.imageView.image = CreateNSImageFromEmoji(emoji);
return itemView;
} else {
NSScrubberTextItemView *itemView = [scrubber
makeItemWithIdentifier:kPickerTitleItemIdentifier
owner:self];
itemView.textField.stringValue = Q2NSString(item.title);
return itemView;
}
}
- (NSSize)scrubber:(NSScrubber*)scrubber
layout:(NSScrubberFlowLayout*)layout
sizeForItemAtIndex:(NSInteger)index {
const auto t = [self itemAt:index].title;
const auto w = t.isEmpty() ? 0 : TouchBar::WidthFromString(Q2NSString(t));
return NSMakeSize(kCircleDiameter + w, kCircleDiameter);
}
- (void)scrubber:(NSScrubber*)scrubber
didSelectItemAtIndex:(NSInteger)index {
scrubber.selectedIndex = -1;
const auto sticker = _itemsDataSource->at(index, _type);
const auto document = sticker.document;
const auto emoji = sticker.emoji;
const auto kRight = document
? ChatRestriction::SendStickers
: ChatRestriction::SendOther;
if (!CanSendToActiveChat(_controller, kRight) || _error) {
return;
}
auto callback = [=] {
if (document) {
if (const auto error = RestrictionToSend(_controller, kRight)) {
_controller->show(Ui::MakeInformBox(*error));
return true;
} else if (Window::ShowSendPremiumError(_controller->sessionController(), document)) {
return true;
}
Api::SendExistingDocument(
Api::MessageToSend(
Api::SendAction(ActiveChat(_controller).history())),
document);
return true;
} else if (emoji) {
if (const auto error = RestrictionToSend(_controller, kRight)) {
_controller->show(Ui::MakeInformBox(*error));
return true;
} else if (const auto inputField = qobject_cast<QTextEdit*>(
QApplication::focusWidget())) {
Ui::InsertEmojiAtCursor(inputField->textCursor(), emoji);
Core::App().settings().incrementRecentEmoji({ emoji });
return true;
}
}
return false;
};
if (!Core::Sandbox::Instance().customEnterFromEventLoop(
std::move(callback))) {
return;
}
_closeRequests.fire({});
}
- (rpl::producer<>)closeRequests {
return _closeRequests.events();
}
- (rpl::lifetime &)lifetime {
return _lifetime;
}
@end // @implementation PickerCustomTouchBarItem
#pragma mark - StickerEmojiPopover
@implementation StickerEmojiPopover {
Window::Controller *_controller;
Main::Session *_session;
std::shared_ptr<PickerScrubberItemsHolder> _itemsDataSource;
ScrubberItemType _waitingForUpdate;
rpl::lifetime _lifetime;
}
- (id)init:(not_null<Window::Controller*>)controller
identifier:(NSTouchBarItemIdentifier)identifier {
self = [super initWithIdentifier:identifier];
if (!self) {
return nil;
}
_controller = controller;
_session = &controller->sessionController()->session();
_waitingForUpdate = ScrubberItemType::None;
auto *segment = [[[NSSegmentedControl alloc] init] autorelease];
const auto size = kSegmentIconSize;
segment.segmentStyle = NSSegmentStyleSeparated;
segment.segmentCount = 2;
[segment
setImage:CreateNSImageFromStyleIcon(st::settingsIconStickers, size)
forSegment:0];
[segment
setImage:CreateNSImageFromStyleIcon(st::settingsIconEmoji, size)
forSegment:1];
[segment setWidth:kSegmentSize forSegment:0];
[segment setWidth:kSegmentSize forSegment:1];
segment.target = self;
segment.action = @selector(segmentClicked:);
segment.trackingMode = NSSegmentSwitchTrackingMomentary;
self.visibilityPriority = NSTouchBarItemPriorityHigh;
self.collapsedRepresentation = segment;
self.popoverTouchBar = [[[NSTouchBar alloc] init] autorelease];
self.popoverTouchBar.delegate = self;
controller->sessionController()->activeChatValue(
) | rpl::map([](Dialogs::Key k) {
const auto topic = k.topic();
const auto peer = k.peer();
const auto right = ChatRestriction::SendStickers;
return peer
&& (topic
? Data::CanSend(topic, right)
: Data::CanSend(peer, right));
}) | rpl::distinct_until_changed(
) | rpl::on_next([=](bool value) {
[self dismissPopover:nil];
}, _lifetime);
_itemsDataSource = std::make_shared<PickerScrubberItemsHolder>();
const auto localGuard = _lifetime.make_state<base::has_weak_ptr>();
// Workaround.
// A little waiting for the sticker sets and the ending animation.
base::call_delayed(st::slideDuration, &(*localGuard), [=] {
[self updateStickers];
[self updateEmoji];
});
rpl::merge(
rpl::merge(
_session->data().stickers().updated(
Data::StickersType::Stickers),
_session->data().stickers().recentUpdated(
Data::StickersType::Stickers)
) | rpl::map_to(ScrubberItemType::Sticker),
rpl::merge(
Core::App().settings().recentEmojiUpdated(),
Ui::Emoji::Updated()
) | rpl::map_to(ScrubberItemType::Emoji)
) | rpl::on_next([=](ScrubberItemType type) {
_waitingForUpdate = type;
}, _lifetime);
return self;
}
- (NSTouchBarItem*)touchBar:(NSTouchBar*)touchBar
makeItemForIdentifier:(NSTouchBarItemIdentifier)identifier {
if (!touchBar) {
return nil;
}
const auto isEqual = [&](NSString *string) {
return [identifier isEqualToString:string];
};
if (isEqual(kStickersScrubber)) {
auto *item = [[[PickerCustomTouchBarItem alloc]
init:(ScrubberItemType::Sticker)
controller:_controller
items:_itemsDataSource] autorelease];
auto &lifetime = [item lifetime];
[item closeRequests] | rpl::on_next([=] {
[self dismissPopover:nil];
[self updateStickers];
}, lifetime);
return item;
} else if (isEqual(kEmojiScrubber)) {
return [[[PickerCustomTouchBarItem alloc]
init:(ScrubberItemType::Emoji)
controller:_controller
items:_itemsDataSource] autorelease];
}
return nil;
}
- (void)segmentClicked:(NSSegmentedControl*)sender {
self.popoverTouchBar.defaultItemIdentifiers = @[];
const auto identifier = sender.selectedSegment
? kEmojiScrubber
: kStickersScrubber;
if (sender.selectedSegment
&& _waitingForUpdate == ScrubberItemType::Emoji) {
[self updateEmoji];
} else if (!sender.selectedSegment
&& _waitingForUpdate == ScrubberItemType::Sticker) {
[self updateStickers];
}
self.popoverTouchBar.defaultItemIdentifiers = @[identifier];
[self showPopover:nil];
}
- (void)addDownloadHandler {
const auto loadingLifetime = _lifetime.make_state<rpl::lifetime>();
const auto checkLoaded = [=](const auto &sticker) {
return !sticker.document || sticker.isStickerLoaded();
};
const auto isPerformedOnMain = loadingLifetime->make_state<bool>(true);
const auto localGuard = loadingLifetime->make_state<base::has_weak_ptr>();
_session->downloaderTaskFinished(
) | rpl::on_next(crl::guard(&(*localGuard), [=] {
if (*isPerformedOnMain) {
crl::on_main(&(*localGuard), [=] {
for (auto &sticker : _itemsDataSource->stickers) {
sticker.updateThumbnail();
}
if (ranges::all_of(_itemsDataSource->stickers, checkLoaded)) {
loadingLifetime->destroy();
return;
}
*isPerformedOnMain = true;
});
}
*isPerformedOnMain = false;
}), *loadingLifetime);
}
- (void)updateStickers {
auto &stickers = _session->data().stickers();
std::vector<PickerScrubberItem> temp;
AppendFavedStickers(stickers.sets(), temp);
AppendRecentStickers(stickers.sets(), stickers.getRecentPack(), temp);
auto count = 0;
for (const auto setId : stickers.setsOrderRef()) {
AppendStickerSet(stickers.sets(), temp, setId);
if (++count == kMaxStickerSets) {
break;
}
}
if (!temp.size()) {
temp.emplace_back(PickerScrubberItem(
tr::lng_stickers_nothing_found(tr::now)));
}
_itemsDataSource->stickers = std::move(temp);
_waitingForUpdate = ScrubberItemType::None;
[self addDownloadHandler];
}
- (void)updateEmoji {
std::vector<PickerScrubberItem> temp;
AppendEmojiPacks(_session->data().stickers().sets(), temp);
_itemsDataSource->emoji = std::move(temp);
_waitingForUpdate = ScrubberItemType::None;
}
@end // @implementation StickerEmojiPopover