From de69be74745763adba4e583596aad39934d6ac1d Mon Sep 17 00:00:00 2001 From: Fela Date: Thu, 16 Oct 2025 14:59:50 +0400 Subject: [PATCH 1/9] Improve MessageProfilePicture API --- .../Controls/Messages/MessageBubble.xaml.cs | 4 +- .../Messages/MessageProfilePicture.cs | 222 ++++-------------- Telegram/Controls/ProfilePicture.cs | 150 ++++++++++-- 3 files changed, 177 insertions(+), 199 deletions(-) diff --git a/Telegram/Controls/Messages/MessageBubble.xaml.cs b/Telegram/Controls/Messages/MessageBubble.xaml.cs index e9124a19b5..644f46579b 100644 --- a/Telegram/Controls/Messages/MessageBubble.xaml.cs +++ b/Telegram/Controls/Messages/MessageBubble.xaml.cs @@ -3091,11 +3091,11 @@ void layoutUpdated(object o, object e) if (obj is User user) { - Photo.Source = new ProfilePictureSourceUser(clientService, user); + Photo.Source = ProfilePictureSource.User(clientService, user); } else if (obj is Chat chat) { - Photo.Source = new ProfilePictureSourceChat(clientService, chat); + Photo.Source = ProfilePictureSource.Chat(clientService, chat); } PhotoColumn.Width = new GridLength(38, GridUnitType.Pixel); diff --git a/Telegram/Controls/Messages/MessageProfilePicture.cs b/Telegram/Controls/Messages/MessageProfilePicture.cs index 5e59e770b2..a9fd089048 100644 --- a/Telegram/Controls/Messages/MessageProfilePicture.cs +++ b/Telegram/Controls/Messages/MessageProfilePicture.cs @@ -7,8 +7,6 @@ using System; using System.Collections.Generic; using Telegram.Common; -using Telegram.Controls.Media; -using Telegram.Services; using Telegram.Td.Api; using Windows.Foundation; using Windows.UI.Xaml; @@ -102,9 +100,8 @@ private void Load() { _presenter?.Unload(this); _presenter = MessageProfilePictureLoader.Current.GetOrCreate(presentation); + _presenter.Load(this); } - - _presenter.Load(this); } else if (source == null) { @@ -119,43 +116,36 @@ private void Unload() _presenter = null; } - #region Source - + private ProfilePictureSource _source; public ProfilePictureSource Source { - get => (ProfilePictureSource)GetValue(SourceProperty); - set => SetValue(SourceProperty, value); + get => _source; + set + { + if (_source != value) + { + _source = value; + Load(); + } + } } - public static readonly DependencyProperty SourceProperty = - DependencyProperty.Register("Source", typeof(ProfilePictureSource), typeof(MessageProfilePicture), new PropertyMetadata(null, OnPropertyChanged)); - - #endregion - - #region Size - + private int _size; public int Size { - get { return (int)GetValue(SizeProperty); } - set { SetValue(SizeProperty, value); } - } - - public static readonly DependencyProperty SizeProperty = - DependencyProperty.Register("Size", typeof(int), typeof(MessageProfilePicture), new PropertyMetadata(0, OnPropertyChanged)); - - #endregion - - private static void OnPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) - { - if (e.Property == SizeProperty) + get => _size; + set { - ((MessageProfilePicture)d).InvalidateMeasure(); + if (_size != value) + { + _size = value; + InvalidateMeasure(); + Load(); + } } - - ((MessageProfilePicture)d).Load(); } - private void Invalidate(object newValue) + private void Invalidate(object newValue, ProfilePictureShape shape) { if (LayoutRoot == null) { @@ -181,13 +171,11 @@ private void Invalidate(object newValue) else if (newValue is ImageBrush texture) { LayoutRoot.Background = texture; - Initials.Visibility = Visibility.Collapsed; } else { LayoutRoot.Background = null; - Initials.Visibility = Visibility.Collapsed; } @@ -256,6 +244,7 @@ public enum State private long _fileToken; private object _source; + private ProfilePictureShape _shape; public MessageProfilePicturePresenter(MessageProfilePictureLoader loader, MessageProfilePicturePresentation presentation) { @@ -279,7 +268,7 @@ public MessageProfilePicturePresenter(MessageProfilePictureLoader loader, Messag public void Load(MessageProfilePicture picture) { _pictures.Add(picture); - picture.Invalidate(_source); + picture.Invalidate(_source, _presentation.Source.Shape); Invalidate(State.Download); } @@ -287,171 +276,63 @@ public void Load(MessageProfilePicture picture) private void Invalidate(State state) { var source = _presentation.Source; - if (source is ProfilePictureSourceChat sourceChat) + if (source is ProfilePictureSourcePhoto sourcePhoto && _fileId != sourcePhoto.Photo.Id) { - SetChat(source.ClientService, sourceChat.Chat, sourceChat.Chat.Photo?.Small, _presentation.Size, state); - } - else if (source is ProfilePictureSourceUser sourceUser) - { - SetUser(source.ClientService, sourceUser.User, sourceUser.User.ProfilePhoto?.Small, _presentation.Size, state); - } - else if (source is ProfilePictureSourceText sourceText) - { - // Local handling within MessageProfilePicture would be better... + _fileId = sourcePhoto.Photo.Id; UpdateManager.Unsubscribe(this, ref _fileToken); - _fileId = null; - Source = sourceText; + Invalidate(sourcePhoto, _presentation.Size, state); } - } - - private void SetChat(IClientService clientService, Chat chat, File file, int side, State state = State.Download) - { - var fileId = file?.Id ?? 0; - if (fileId != _fileId || /*Source == null ||*/ state != State.Download) + else if (source is ProfilePictureSourceText sourceText) { + _fileId = null; UpdateManager.Unsubscribe(this, ref _fileToken); - _fileId = file?.Id; - Source = GetChat(clientService, chat, file, side, out var shape, state); + Invalidate(sourceText, sourceText.Shape); } } - private object GetChat(IClientService clientService, Chat chat, File file, int side, out ProfilePictureShape shape, State state = State.Download) + private void Invalidate(ProfilePictureSourcePhoto photo, int side, State state = State.Download) { - // TODO: this method may throw a NullReferenceException in some conditions - - shape = ProfilePictureShape.Ellipse; - - if (chat.Id == clientService.Options.MyId) - { - _controller?.Recycle(); - return ProfilePictureSourceText.GetGlyph(Icons.BookmarkFilled, 5); - } - else if (chat.Id == clientService.Options.RepliesBotChatId) + if (photo.Photo.Local.IsDownloadingCompleted) { - _controller?.Recycle(); - return ProfilePictureSourceText.GetGlyph(Icons.ArrowReplyFilled, 5); - } - - //if (IsShapeEnabled && clientService.TryGetSupergroup(chat, out Supergroup supergroup)) - //{ - // if (supergroup.IsForum) - // { - // shape = ProfilePictureShape.Superellipse; - // } - // else if (supergroup.IsDirectMessagesGroup) - // { - // shape = ProfilePictureShape.Tail; - // } - //} - - if (file != null) - { - if (file.Local.IsDownloadingCompleted) - { - _controller.Bitmap(file.Local.Path, side, side, chat.Id); + _controller.Bitmap(photo.Photo.Local.Path, side, side, photo.Id); + Invalidate(_texture, photo.Shape); - return _texture; - } - else - { - if (file.Local.CanBeDownloaded && !file.Local.IsDownloadingActive && state != State.Update) - { - clientService.DownloadFile(file.Id, 1); - } - - UpdateManager.Subscribe(this, clientService, file, ref _fileToken, UpdateFile, true); - } - - var minithumbnail = chat.Photo?.Minithumbnail; - if (minithumbnail != null) - { - _controller.Blur(minithumbnail.Data, 3, chat.Id); - - return _texture; - } + return; } - - _controller.Recycle(); - - if (clientService.TryGetUser(chat, out User user)) + else { - if (user.Type is UserTypeDeleted) + if (photo.Photo.Local.CanBeDownloaded && !photo.Photo.Local.IsDownloadingActive && state != State.Update) { - return ProfilePictureSourceText.GetGlyph(Icons.GhostFilled, long.MinValue); + photo.ClientService.DownloadFile(photo.Photo.Id, 1); } - return ProfilePictureSourceText.GetUser(clientService, user); + UpdateManager.Subscribe(this, photo.ClientService, photo.Photo, ref _fileToken, UpdateFile, true); } - return ProfilePictureSourceText.GetChat(clientService, chat); - } - - private void SetUser(IClientService clientService, User user, File file, int side, State state = State.Download) - { - var fileId = file?.Id ?? 0; - if (fileId != _fileId || /*Source == null ||*/ state != State.Download) + if (photo.Minithumbnail != null) { - UpdateManager.Unsubscribe(this, ref _fileToken); + _controller.Blur(photo.Minithumbnail.Data, 3, photo.Id); + Invalidate(_texture, photo.Shape); - _fileId = fileId; - Source = GetUser(clientService, user, file, side, state); - } - } - - private object GetUser(IClientService clientService, User user, File file, int side, State state = State.Download) - { - if (file != null) - { - if (file.Local.IsDownloadingCompleted) - { - _controller.Bitmap(file.Local.Path, side, side, user.Id); - - return _texture; - } - else - { - if (file.Local.CanBeDownloaded && !file.Local.IsDownloadingActive && state != State.Update) - { - clientService.DownloadFile(file.Id, 1); - } - - UpdateManager.Subscribe(this, clientService, file, ref _fileToken, UpdateFile, true); - } - - var minithumbnail = user.ProfilePhoto?.Minithumbnail; - if (minithumbnail != null) - { - _controller.Blur(minithumbnail.Data, 3, user.Id); - - return _texture; - } + return; } _controller.Recycle(); - - if (user.Type is UserTypeDeleted) - { - return ProfilePictureSourceText.GetGlyph(Icons.GhostFilled, long.MinValue); - } - - return ProfilePictureSourceText.GetUser(clientService, user); + Invalidate(photo.Text, photo.Shape); } - public object Source + private void Invalidate(object value, ProfilePictureShape shape) { - get => _source; - set + if (_source != value || _shape != shape) { - if (_source != value) - { - _source = value; + _source = value; + _shape = shape; - foreach (var picture in _pictures) - { - picture.Invalidate(value); - } + foreach (var picture in _pictures) + { + picture.Invalidate(value, shape); } } } @@ -464,7 +345,7 @@ private void UpdateFile(object target, File file) public void Unload(MessageProfilePicture picture) { _pictures.Remove(picture); - picture.Invalidate(null); + picture.Invalidate(null, _shape); if (_pictures.Empty()) { @@ -480,7 +361,6 @@ private class MessageProfilePictureLoader { [ThreadStatic] private static MessageProfilePictureLoader _current; - public static MessageProfilePictureLoader Current => _current ??= new(); private readonly Dictionary _presenters = new(); diff --git a/Telegram/Controls/ProfilePicture.cs b/Telegram/Controls/ProfilePicture.cs index 3423a28cdc..a06a985cbd 100644 --- a/Telegram/Controls/ProfilePicture.cs +++ b/Telegram/Controls/ProfilePicture.cs @@ -792,7 +792,7 @@ private object GetChatPhoto(IClientService clientService, ChatPhoto photo, File #endregion } - public abstract record ProfilePictureSource(IClientService ClientService) + public abstract record ProfilePictureSource(ProfilePictureShape Shape) { public static ProfilePictureSource Message(MessageViewModel message) { @@ -800,15 +800,15 @@ public static ProfilePictureSource Message(MessageViewModel message) { if (message.ForwardInfo?.Origin is MessageOriginUser fromUser && message.ClientService.TryGetUser(fromUser.SenderUserId, out User fromUserUser)) { - return new ProfilePictureSourceUser(message.ClientService, fromUserUser); + return ProfilePictureSource.User(message.ClientService, fromUserUser); } else if (message.ForwardInfo?.Origin is MessageOriginChat fromChat && message.ClientService.TryGetChat(fromChat.SenderChatId, out Chat fromChatChat)) { - return new ProfilePictureSourceChat(message.ClientService, fromChatChat); + return ProfilePictureSource.Chat(message.ClientService, fromChatChat); } else if (message.ForwardInfo?.Origin is MessageOriginChannel fromChannel && message.ClientService.TryGetChat(fromChannel.ChatId, out Chat fromChannelChat)) { - return new ProfilePictureSourceChat(message.ClientService, fromChannelChat); + return ProfilePictureSource.Chat(message.ClientService, fromChannelChat); } else if (message.ForwardInfo?.Origin is MessageOriginHiddenUser fromHiddenUser) { @@ -821,26 +821,107 @@ public static ProfilePictureSource Message(MessageViewModel message) } else if (message.ClientService.TryGetUser(message.SenderId, out User senderUser)) { - return new ProfilePictureSourceUser(message.ClientService, senderUser); + return ProfilePictureSource.User(message.ClientService, senderUser); } else if (message.ClientService.TryGetChat(message.SenderId, out Chat senderChat)) { - return new ProfilePictureSourceChat(message.ClientService, senderChat); + return ProfilePictureSource.Chat(message.ClientService, senderChat); } return null; } - } - public record ProfilePictureSourceChat(IClientService ClientService, Chat Chat) - : ProfilePictureSource(ClientService); + public static ProfilePictureSource MessageSender(IClientService clientService, MessageSender sender) + { + if (clientService.TryGetUser(sender, out User user)) + { + return ProfilePictureSource.User(clientService, user); + } + else if (clientService.TryGetChat(sender, out Chat chat)) + { + return ProfilePictureSource.Chat(clientService, chat); + } + + return null; + } + + public static ProfilePictureSource User(IClientService clientService, User user) + { + ProfilePictureSourceText text; + if (user.Type is UserTypeDeleted) + { + text = ProfilePictureSourceText.GetGlyph(Icons.GhostFilled, long.MinValue); + } + else + { + text = ProfilePictureSourceText.GetUser(clientService, user); + } + + var photo = user.ProfilePhoto; + if (photo != null) + { + return new ProfilePictureSourcePhoto(clientService, user.Id, photo.Small, photo.Minithumbnail, text, ProfilePictureShape.Ellipse); + } + + return text; + } + + public static ProfilePictureSource Chat(IClientService clientService, Chat chat) + { + if (chat.Id == clientService.Options.MyId) + { + return ProfilePictureSourceText.GetGlyph(Icons.BookmarkFilled, 5); + } + else if (chat.Id == clientService.Options.RepliesBotChatId) + { + return ProfilePictureSourceText.GetGlyph(Icons.ArrowReplyFilled, 5); + } + + var shape = ProfilePictureShape.Ellipse; + if (clientService.TryGetSupergroup(chat, out Supergroup supergroup)) + { + if (supergroup.IsForum) + { + shape = ProfilePictureShape.Superellipse; + } + else if (supergroup.IsDirectMessagesGroup) + { + shape = ProfilePictureShape.Tail; + } + } + + ProfilePictureSourceText text; + if (supergroup == null && clientService.TryGetUser(chat, out User user)) + { + if (user.Type is UserTypeDeleted) + { + text = ProfilePictureSourceText.GetGlyph(Icons.GhostFilled, long.MinValue); + } + else + { + text = ProfilePictureSourceText.GetUser(clientService, user); + } + } + else + { + text = ProfilePictureSourceText.GetChat(clientService, chat, shape); + } - // TODO: this doesn't properly support equality because User is not singleton - public record ProfilePictureSourceUser(IClientService ClientService, User User) - : ProfilePictureSource(ClientService); + var photo = chat.Photo; + if (photo != null) + { + return new ProfilePictureSourcePhoto(clientService, chat.Id, photo.Small, photo.Minithumbnail, text, shape); + } - public record ProfilePictureSourceText(string Initials, bool IsGlyph, Color TopColor, Color BottomColor) - : ProfilePictureSource(ClientService: null) + return text; + } + } + + public record ProfilePictureSourcePhoto(IClientService ClientService, long Id, File Photo, Minithumbnail Minithumbnail, ProfilePictureSourceText Text, ProfilePictureShape Shape) + : ProfilePictureSource(Shape); + + public record ProfilePictureSourceText(string Initials, bool IsGlyph, Color TopColor, Color BottomColor, ProfilePictureShape Shape = ProfilePictureShape.Ellipse) + : ProfilePictureSource(Shape) { private static readonly Color[] _colorsTop = new Color[7] { @@ -890,9 +971,26 @@ public static CompositionBrush GetBrush(Compositor compositor, long i) return compositor.CreateColorBrush(_colors[Math.Abs(i % _colors.Length)]); } - public static ProfilePictureSourceText GetChat(IClientService clientService, Chat chat) + public static ProfilePictureSourceText GetChat(IClientService clientService, Chat chat, ProfilePictureShape shape = ProfilePictureShape.None) { - return ProfilePictureSourceText.FromNameColor(InitialNameStringConverter.Convert(chat.Title), false, clientService.GetAccentColor(chat.AccentColorId)); + if (shape == ProfilePictureShape.None) + { + shape = ProfilePictureShape.Ellipse; + + if (clientService.TryGetSupergroup(chat, out Supergroup supergroup)) + { + if (supergroup.IsForum) + { + shape = ProfilePictureShape.Superellipse; + } + else if (supergroup.IsDirectMessagesGroup) + { + shape = ProfilePictureShape.Tail; + } + } + } + + return ProfilePictureSourceText.FromNameColor(InitialNameStringConverter.Convert(chat.Title), false, clientService.GetAccentColor(chat.AccentColorId), shape); } public static ProfilePictureSourceText GetChat(IClientService clientService, ChatInviteLinkInfo chat) @@ -905,49 +1003,49 @@ public static ProfilePictureSourceText GetUser(IClientService clientService, Use return ProfilePictureSourceText.FromNameColor(InitialNameStringConverter.Convert(user.FirstName, user.LastName), false, clientService.GetAccentColor(user.AccentColorId)); } - public static ProfilePictureSourceText GetNameForUser(string firstName, string lastName, long id = 5) + public static ProfilePictureSourceText GetNameForUser(string firstName, string lastName, long id = 5, ProfilePictureShape shape = ProfilePictureShape.Ellipse) { return ProfilePictureSourceText.FromId(InitialNameStringConverter.Convert(firstName, lastName), false, id); } - public static ProfilePictureSourceText GetNameForUser(string name, long id = 5) + public static ProfilePictureSourceText GetNameForUser(string name, long id = 5, ProfilePictureShape shape = ProfilePictureShape.Ellipse) { return ProfilePictureSourceText.FromId(InitialNameStringConverter.Convert(name), false, id); } - public static ProfilePictureSourceText GetNameForChat(string title, long id = 5) + public static ProfilePictureSourceText GetNameForChat(string title, long id = 5, ProfilePictureShape shape = ProfilePictureShape.Ellipse) { return ProfilePictureSourceText.FromId(InitialNameStringConverter.Convert(title), false, id); } - public static ProfilePictureSourceText GetGlyph(string glyph, long id = 5) + public static ProfilePictureSourceText GetGlyph(string glyph, long id = 5, ProfilePictureShape shape = ProfilePictureShape.Ellipse) { return ProfilePictureSourceText.FromId(glyph, true, id); } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static ProfilePictureSourceText FromNameColor(string initials, bool isGlyph, NameColor color) + private static ProfilePictureSourceText FromNameColor(string initials, bool isGlyph, NameColor color, ProfilePictureShape shape = ProfilePictureShape.Ellipse) { if (color == null) { - return new ProfilePictureSourceText(initials, isGlyph, _disabledTop, _disabled); + return new ProfilePictureSourceText(initials, isGlyph, _disabledTop, _disabled, shape); } else { - return new ProfilePictureSourceText(initials, isGlyph, _colorsTop[Math.Abs(color.BuiltInAccentColorId % _colors.Length)], _colors[Math.Abs(color.BuiltInAccentColorId % _colors.Length)]); + return new ProfilePictureSourceText(initials, isGlyph, _colorsTop[Math.Abs(color.BuiltInAccentColorId % _colors.Length)], _colors[Math.Abs(color.BuiltInAccentColorId % _colors.Length)], shape); } } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static ProfilePictureSourceText FromId(string initials, bool isGlyph, long id) + private static ProfilePictureSourceText FromId(string initials, bool isGlyph, long id, ProfilePictureShape shape = ProfilePictureShape.Ellipse) { if (id == long.MinValue) { - return new ProfilePictureSourceText(initials, isGlyph, _disabledTop, _disabled); + return new ProfilePictureSourceText(initials, isGlyph, _disabledTop, _disabled, shape); } else { - return new ProfilePictureSourceText(initials, isGlyph, _colorsTop[Math.Abs(id % _colors.Length)], _colors[Math.Abs(id % _colors.Length)]); + return new ProfilePictureSourceText(initials, isGlyph, _colorsTop[Math.Abs(id % _colors.Length)], _colors[Math.Abs(id % _colors.Length)], shape); } } } From add0f5f1e372c77a25c14bb04026006dd0161b60 Mon Sep 17 00:00:00 2001 From: Fela Date: Thu, 16 Oct 2025 15:31:38 +0400 Subject: [PATCH 2/9] Support shapes in MessageProfilePicture --- Telegram/Controls/Messages/MessageBubble.xaml | 3 +- .../Messages/MessageProfilePicture.cs | 192 +++++++++++++++--- Telegram/Controls/ProfilePicture.cs | 3 +- Telegram/Views/ChatView.xaml | 6 +- 4 files changed, 168 insertions(+), 36 deletions(-) diff --git a/Telegram/Controls/Messages/MessageBubble.xaml b/Telegram/Controls/Messages/MessageBubble.xaml index 9cb00c08f6..7901d91520 100644 --- a/Telegram/Controls/Messages/MessageBubble.xaml +++ b/Telegram/Controls/Messages/MessageBubble.xaml @@ -161,7 +161,8 @@ Grid.Column="1" Grid.RowSpan="3"> + Size="30" + Shape="Ellipse" /> _shape; + set + { + if (_shape != value) + { + _shape = value; + InvalidateShape(); + } + } + } + + public ProfilePictureShape CalculatedShape + { + get + { + if (_shape == ProfilePictureShape.Auto) + { + if (_source != null) + { + return _source.Shape; + } + + return ProfilePictureShape.Ellipse; + } + + return _shape; + } + } + + private void Invalidate(object newValue) { if (LayoutRoot == null) { @@ -167,6 +221,8 @@ private void Invalidate(object newValue, ProfilePictureShape shape) _glyph = text.IsGlyph; Initials.Margin = new Thickness(0, 1, 0, _glyph ? 0 : 2); } + + InvalidateFontSize(); } else if (newValue is ImageBrush texture) { @@ -178,29 +234,96 @@ private void Invalidate(object newValue, ProfilePictureShape shape) LayoutRoot.Background = null; Initials.Visibility = Visibility.Collapsed; } - - UpdateCornerRadius(); - UpdateFontSize(); } - private void UpdateCornerRadius() + private void InvalidateShape() { - if (LayoutRoot == null || Size == 0) + var shape = CalculatedShape; + var size = Size; + + if (shape == _appliedShape && size == _appliedSize) + { + return; + } + + if (LayoutRoot == null || size == 0) { return; } - LayoutRoot.CornerRadius = new CornerRadius(Size / 2d); + _appliedShape = shape; + _appliedSize = size; + + if (shape == ProfilePictureShape.Tail) + { + _tail = true; + + static CompositionPath GetTail(float radius) + { + CanvasGeometry result; + using (var builder = new CanvasPathBuilder(null)) + { + var cy = radius; + var cx = radius; + var r = radius; + + float b = cy + r; + float x = r / 81.0f; + + float startAngle = -180 * (MathF.PI / 180); + float sweepAngle = 270 * (MathF.PI / 180); + + float x1 = cx + MathF.Cos(startAngle) * r; + float y1 = cy + MathF.Sin(startAngle) * r; + + float x2 = cx + MathF.Cos(startAngle + sweepAngle) * r; + float y2 = cy + MathF.Sin(startAngle + sweepAngle) * r; + + builder.BeginFigure(new Vector2(x1, y1)); + builder.AddArc(new Vector2(x2, y2), r, r, 0, CanvasSweepDirection.Clockwise, CanvasArcSize.Large); + builder.AddCubicBezier(new Vector2(cx - 13 * x, b), new Vector2(cx - 25 * x, b - 3 * x), new Vector2(cx - 36f * x, b - 8.42f * x)); + builder.AddCubicBezier(new Vector2(cx - 52 * x, b - x), new Vector2(cx - 56.5f * x, b - x), new Vector2(cx - 78.02f * x, b - x)); + builder.AddCubicBezier(new Vector2(cx - 80 * x, b - x), new Vector2(cx - 81 * x, b - 3 * x), new Vector2(cx - 79.52f * x, b - 4.5f * x)); + builder.AddCubicBezier(new Vector2(cx - 78 * x, b - 6 * x), new Vector2(cx - 63.73f * x, b - 15 * x), new Vector2(cx - 63.73f * x, b - 31 * x)); + builder.AddCubicBezier(new Vector2(cx - 74.5f * x, b - 44.75f * x), new Vector2(cx - r, cy + 18.87f * x), new Vector2(cx - r, cy)); + builder.EndFigure(CanvasFigureLoop.Closed); + result = CanvasGeometry.CreatePath(builder); + } + return new CompositionPath(result); + } + + var compositor = BootStrapper.Current.Compositor; + + var polygon = compositor.CreatePathGeometry(); + polygon.Path = GetTail(Size / 2f); + + var visual = ElementComposition.GetElementVisual(this); + visual.Clip = compositor.CreateGeometricClip(polygon); + } + else if (_tail) + { + _tail = false; + + var visual = ElementComposition.GetElementVisual(this); + visual.Clip = null; + } + + LayoutRoot.CornerRadius = new CornerRadius(shape switch + { + ProfilePictureShape.Superellipse => size / 4d, + ProfilePictureShape.Ellipse => size / 2d, + _ => 0 + }); } - private void UpdateFontSize() + private void InvalidateFontSize() { - if (Initials == null || double.IsNaN(Width)) + if (Initials == null || Size == 0) { return; } - var fontSize = Width switch + var fontSize = Size switch { < 20 => 10, < 30 => 12, @@ -225,7 +348,6 @@ private class MessageProfilePicturePresenter { public enum State { - Template, Download, Update } @@ -244,7 +366,6 @@ public enum State private long _fileToken; private object _source; - private ProfilePictureShape _shape; public MessageProfilePicturePresenter(MessageProfilePictureLoader loader, MessageProfilePicturePresentation presentation) { @@ -260,7 +381,7 @@ public MessageProfilePicturePresenter(MessageProfilePictureLoader loader, Messag _controller = new ThumbnailController(_texture); - _dispatcherQueue = DispatcherQueue.GetForCurrentThread(); + _dispatcherQueue = loader.DispatcherQueue; } public MessageProfilePicturePresentation Presentation => _presentation; @@ -268,15 +389,15 @@ public MessageProfilePicturePresenter(MessageProfilePictureLoader loader, Messag public void Load(MessageProfilePicture picture) { _pictures.Add(picture); - picture.Invalidate(_source, _presentation.Source.Shape); + picture.Invalidate(_source); - Invalidate(State.Download); + Load(State.Download); } - private void Invalidate(State state) + private void Load(State state) { var source = _presentation.Source; - if (source is ProfilePictureSourcePhoto sourcePhoto && _fileId != sourcePhoto.Photo.Id) + if (source is ProfilePictureSourcePhoto sourcePhoto && (_fileId != sourcePhoto.Photo.Id || state != State.Download)) { _fileId = sourcePhoto.Photo.Id; UpdateManager.Unsubscribe(this, ref _fileToken); @@ -288,7 +409,7 @@ private void Invalidate(State state) _fileId = null; UpdateManager.Unsubscribe(this, ref _fileToken); - Invalidate(sourceText, sourceText.Shape); + Invalidate(sourceText); } } @@ -297,7 +418,7 @@ private void Invalidate(ProfilePictureSourcePhoto photo, int side, State state = if (photo.Photo.Local.IsDownloadingCompleted) { _controller.Bitmap(photo.Photo.Local.Path, side, side, photo.Id); - Invalidate(_texture, photo.Shape); + Invalidate(_texture); return; } @@ -314,38 +435,37 @@ private void Invalidate(ProfilePictureSourcePhoto photo, int side, State state = if (photo.Minithumbnail != null) { _controller.Blur(photo.Minithumbnail.Data, 3, photo.Id); - Invalidate(_texture, photo.Shape); + Invalidate(_texture); return; } _controller.Recycle(); - Invalidate(photo.Text, photo.Shape); + Invalidate(photo.Text); } - private void Invalidate(object value, ProfilePictureShape shape) + private void Invalidate(object value) { - if (_source != value || _shape != shape) + if (_source != value) { _source = value; - _shape = shape; foreach (var picture in _pictures) { - picture.Invalidate(value, shape); + picture.Invalidate(value); } } } private void UpdateFile(object target, File file) { - _dispatcherQueue.TryEnqueue(() => Invalidate(State.Update)); + _dispatcherQueue.TryEnqueue(() => Load(State.Update)); } public void Unload(MessageProfilePicture picture) { _pictures.Remove(picture); - picture.Invalidate(null, _shape); + picture.Invalidate(null); if (_pictures.Empty()) { @@ -364,6 +484,14 @@ private class MessageProfilePictureLoader public static MessageProfilePictureLoader Current => _current ??= new(); private readonly Dictionary _presenters = new(); + private readonly DispatcherQueue _dispatcherQueue; + + private MessageProfilePictureLoader() + { + _dispatcherQueue = DispatcherQueue.GetForCurrentThread(); + } + + public DispatcherQueue DispatcherQueue => _dispatcherQueue; public MessageProfilePicturePresenter GetOrCreate(MessageProfilePicturePresentation presentation) { diff --git a/Telegram/Controls/ProfilePicture.cs b/Telegram/Controls/ProfilePicture.cs index a06a985cbd..c421f40921 100644 --- a/Telegram/Controls/ProfilePicture.cs +++ b/Telegram/Controls/ProfilePicture.cs @@ -29,7 +29,8 @@ public enum ProfilePictureShape None, Ellipse, Superellipse, - Tail + Tail, + Auto } public partial class ProfilePicture : Control diff --git a/Telegram/Views/ChatView.xaml b/Telegram/Views/ChatView.xaml index da7ecd595a..abb83e4765 100644 --- a/Telegram/Views/ChatView.xaml +++ b/Telegram/Views/ChatView.xaml @@ -1632,7 +1632,8 @@ Height="30" Margin="0,0,0,8"> + Size="30" + Shape="Ellipse" /> + Size="30" + Shape="Ellipse" /> From fed079fdb2d36bfb4d514ac1307beb923ab85fc7 Mon Sep 17 00:00:00 2001 From: Fela Date: Thu, 16 Oct 2025 16:42:25 +0400 Subject: [PATCH 3/9] Stop watching device on error --- Telegram.Native/PlaceholderImageHelper.cpp | 13 +++++++++---- Telegram.Native/PlaceholderImageHelper.h | 4 ++-- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/Telegram.Native/PlaceholderImageHelper.cpp b/Telegram.Native/PlaceholderImageHelper.cpp index fa7296ec3e..82c6bf1a96 100644 --- a/Telegram.Native/PlaceholderImageHelper.cpp +++ b/Telegram.Native/PlaceholderImageHelper.cpp @@ -391,7 +391,7 @@ namespace winrt::Telegram::Native::implementation if (result == DXGI_ERROR_DEVICE_REMOVED || result == DXGI_ERROR_DEVICE_RESET) { - ReturnIfFailed(result, HandleDirect3DDeviceLost()); + ReturnIfFailed(result, HandleDirect3DDeviceLost(true)); ReturnIfFailed(result, source->CreateDeviceResources(m_d2dDevice.get())); return Invalidate(imageSource, buffer); } @@ -837,7 +837,7 @@ namespace winrt::Telegram::Native::implementation if ((result = m_d2dContext->EndDraw()) == D2DERR_RECREATE_TARGET) { - ReturnIfFailed(result, HandleDirect3DDeviceLost()); + ReturnIfFailed(result, HandleDirect3DDeviceLost(true)); return DrawBlurredImpl(wicBitmapSource, blurAmount, bitmap, minithumbnail); } @@ -1008,11 +1008,16 @@ namespace winrt::Telegram::Native::implementation void PlaceholderImageHelper::OnDirect3DDeviceLost(DeviceLostHelper const* /* sender */, DeviceLostEventArgs const& /* args */) { - HandleDirect3DDeviceLost(); + HandleDirect3DDeviceLost(false); } - HRESULT PlaceholderImageHelper::HandleDirect3DDeviceLost() + HRESULT PlaceholderImageHelper::HandleDirect3DDeviceLost(bool stop) { + if (stop) + { + m_deviceLostHelper.StopWatchingCurrentDevice(); + } + HRESULT result; ReturnIfFailed(result, CreateDeviceResources()); diff --git a/Telegram.Native/PlaceholderImageHelper.h b/Telegram.Native/PlaceholderImageHelper.h index 8be75fedd7..480d181988 100644 --- a/Telegram.Native/PlaceholderImageHelper.h +++ b/Telegram.Native/PlaceholderImageHelper.h @@ -271,7 +271,7 @@ namespace winrt::Telegram::Native::implementation { if (FAILED(m_d3dDevice->GetDeviceRemovedReason())) { - return HandleDirect3DDeviceLost(); + return HandleDirect3DDeviceLost(true); } return S_OK; @@ -339,7 +339,7 @@ namespace winrt::Telegram::Native::implementation HRESULT CreateTextFormat(double fontSize); void OnDirect3DDeviceLost(DeviceLostHelper const* /* sender */, DeviceLostEventArgs const& /* args */); - HRESULT HandleDirect3DDeviceLost(); + HRESULT HandleDirect3DDeviceLost(bool stop); HRESULT DrawBlurredImpl(IWICBitmapSource* wicBitmapSource, float blurAmount, SoftwareBitmap& bitmap, bool minithumbnail); HRESULT SaveImageToStream(ID2D1Image* image, REFGUID wicFormat, IRandomAccessStream randomAccessStream); From 8121f8c1d01829cc5d1dd55c2bc054950bffe289 Mon Sep 17 00:00:00 2001 From: Fela Date: Thu, 16 Oct 2025 17:14:03 +0400 Subject: [PATCH 4/9] Include all modules in native crash logs --- Telegram.Native/NativeUtils.cpp | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/Telegram.Native/NativeUtils.cpp b/Telegram.Native/NativeUtils.cpp index e0a80272e8..4c1bfba3af 100644 --- a/Telegram.Native/NativeUtils.cpp +++ b/Telegram.Native/NativeUtils.cpp @@ -245,18 +245,6 @@ namespace winrt::Telegram::Native::implementation moduleFilename = moduleFilename.substr(moduleFilenamePos + 1); } - if (moduleFilename.rfind(L"Telegram", 0) != 0) - { - skipping = true; - continue; - } - - if (skipping) - { - skipping = false; - trace += L" ...\n"; - } - trace += wstrprintf(L" at %s+0x%08lx\n", moduleFilename.c_str(), (uint32_t)((unsigned char*)pointer - moduleBase)); frames.Append({ (intptr_t)pointer, (intptr_t)moduleBase }); } From 958fabedd8419d8243f4c3d5741b9a19885b4414 Mon Sep 17 00:00:00 2001 From: Fela Date: Thu, 16 Oct 2025 18:20:21 +0400 Subject: [PATCH 5/9] Disable bubble tails on Windows 10 --- Telegram.Native/Composition/CompositionDevice.cpp | 4 ++++ Telegram/Controls/Messages/MessageBubble.xaml.cs | 6 +++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/Telegram.Native/Composition/CompositionDevice.cpp b/Telegram.Native/Composition/CompositionDevice.cpp index 37ada23a44..5d5c3195aa 100644 --- a/Telegram.Native/Composition/CompositionDevice.cpp +++ b/Telegram.Native/Composition/CompositionDevice.cpp @@ -113,6 +113,10 @@ namespace winrt::Telegram::Native::Composition::implementation return layerVisual; } + // The code below definitely works on late Windows 10 builds, but it definitely crashes on 1909 and earlier + // Thus, for now we just disable bubble tails on Windows 10. + return nullptr; + // We are using the thread ID to verify and ensure that we aren't hooking any other ElementCompositionPreview::GetElementVisual call // that happened to be going in another thread at the same time we are hooking the function to return a LayerVisual, // and we use a lock to ensure that only one thread can be hooking at a time so that thread ID doesn't get changed mid-hook. diff --git a/Telegram/Controls/Messages/MessageBubble.xaml.cs b/Telegram/Controls/Messages/MessageBubble.xaml.cs index 644f46579b..4ecb070440 100644 --- a/Telegram/Controls/Messages/MessageBubble.xaml.cs +++ b/Telegram/Controls/Messages/MessageBubble.xaml.cs @@ -684,10 +684,14 @@ private void SetCorners(float topLeft, float topRight, float bottomRight, float } radius |= bottomLeft != 0 && bottomRight != 0; + radius |= _layerVisual == null; if (radius) { - _layerVisual.Effect = null; + if (_layerVisual != null) + { + _layerVisual.Effect = null; + } _corners = true; ContentPanel.CornerRadius = new CornerRadius(topLeft, topRight, bottomRight, bottomLeft); From 5d677da9a068c7b2416cb250b3d1a10e9a73a5fb Mon Sep 17 00:00:00 2001 From: Fela Date: Thu, 16 Oct 2025 18:20:38 +0400 Subject: [PATCH 6/9] Show audio title before artist --- Telegram/Controls/Cells/PlaybackItemCell.xaml.cs | 2 +- Telegram/Controls/Cells/SharedAudioCell.xaml.cs | 2 +- Telegram/Controls/Messages/Content/AudioContent.xaml.cs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Telegram/Controls/Cells/PlaybackItemCell.xaml.cs b/Telegram/Controls/Cells/PlaybackItemCell.xaml.cs index 0f4a947a2f..415f1e78b3 100644 --- a/Telegram/Controls/Cells/PlaybackItemCell.xaml.cs +++ b/Telegram/Controls/Cells/PlaybackItemCell.xaml.cs @@ -106,7 +106,7 @@ public void UpdateItem(PlaybackItem item) } else { - Title.Text = $"{audio.Performer} - {audio.Title}"; + Title.Text = $"{audio.Title} - {audio.Performer}"; } TitleTrim.Text = string.Empty; diff --git a/Telegram/Controls/Cells/SharedAudioCell.xaml.cs b/Telegram/Controls/Cells/SharedAudioCell.xaml.cs index 87c2808102..5847050949 100644 --- a/Telegram/Controls/Cells/SharedAudioCell.xaml.cs +++ b/Telegram/Controls/Cells/SharedAudioCell.xaml.cs @@ -107,7 +107,7 @@ public void UpdateMessage(MessageWithOwner message) } else { - Title.Text = $"{audio.Performer} - {audio.Title}"; + Title.Text = $"{audio.Title} - {audio.Performer}"; } TitleTrim.Text = string.Empty; diff --git a/Telegram/Controls/Messages/Content/AudioContent.xaml.cs b/Telegram/Controls/Messages/Content/AudioContent.xaml.cs index abfca272ee..a97bbc3647 100644 --- a/Telegram/Controls/Messages/Content/AudioContent.xaml.cs +++ b/Telegram/Controls/Messages/Content/AudioContent.xaml.cs @@ -124,7 +124,7 @@ public void UpdateMessage(MessageViewModel message) } else { - Title.Text = $"{audio.Performer} - {audio.Title}"; + Title.Text = $"{audio.Title} - {audio.Performer}"; } TitleTrim.Text = string.Empty; From 1d1de5a9e8caaf519e37f355bec54c59cae30d64 Mon Sep 17 00:00:00 2001 From: Fela Date: Thu, 16 Oct 2025 18:33:17 +0400 Subject: [PATCH 7/9] Rounded corner as tail replacement --- Telegram/Controls/Messages/MessageBubble.xaml.cs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/Telegram/Controls/Messages/MessageBubble.xaml.cs b/Telegram/Controls/Messages/MessageBubble.xaml.cs index 4ecb070440..072a599a30 100644 --- a/Telegram/Controls/Messages/MessageBubble.xaml.cs +++ b/Telegram/Controls/Messages/MessageBubble.xaml.cs @@ -692,6 +692,18 @@ private void SetCorners(float topLeft, float topRight, float bottomRight, float { _layerVisual.Effect = null; } + else + { + if (bottomRight == 0) + { + bottomRight = 15; + } + + if (bottomLeft == 0) + { + bottomLeft = 15; + } + } _corners = true; ContentPanel.CornerRadius = new CornerRadius(topLeft, topRight, bottomRight, bottomLeft); From 591ee9ae4f677912ea33feb963175e47d6a2396d Mon Sep 17 00:00:00 2001 From: Fela Date: Thu, 16 Oct 2025 18:34:18 +0400 Subject: [PATCH 8/9] Bump version to 12.1.1 --- Telegram.Msix/Package.appxmanifest | 2 +- Telegram/Package.appxmanifest | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Telegram.Msix/Package.appxmanifest b/Telegram.Msix/Package.appxmanifest index cbe88fa608..450bfc89f1 100644 --- a/Telegram.Msix/Package.appxmanifest +++ b/Telegram.Msix/Package.appxmanifest @@ -1,6 +1,6 @@  - + Unigram—Telegram for Windows diff --git a/Telegram/Package.appxmanifest b/Telegram/Package.appxmanifest index 85b7d451e9..f1f4319da5 100644 --- a/Telegram/Package.appxmanifest +++ b/Telegram/Package.appxmanifest @@ -11,7 +11,7 @@ xmlns:desktop="http://schemas.microsoft.com/appx/manifest/desktop/windows10" xmlns:desktop4="http://schemas.microsoft.com/appx/manifest/desktop/windows10/4" IgnorableNamespaces="mp uap uap3 uap4 uap5 uap6 uap11 rescap desktop desktop4"> - + Unigram Experimental From 8d0f81348a754f8b95a9b432e4c6dd11f321419f Mon Sep 17 00:00:00 2001 From: Fela Date: Thu, 16 Oct 2025 19:40:04 +0400 Subject: [PATCH 9/9] Fix d3d1 device lost access violation --- Telegram.Native/MessageBubbleNineGrid.cpp | 2 +- Telegram.Native/PlaceholderImageHelper.cpp | 39 ++++++++++------------ Telegram.Native/PlaceholderImageHelper.h | 5 ++- 3 files changed, 20 insertions(+), 26 deletions(-) diff --git a/Telegram.Native/MessageBubbleNineGrid.cpp b/Telegram.Native/MessageBubbleNineGrid.cpp index d9262d3878..d869df632b 100644 --- a/Telegram.Native/MessageBubbleNineGrid.cpp +++ b/Telegram.Native/MessageBubbleNineGrid.cpp @@ -41,7 +41,7 @@ namespace winrt::Telegram::Native::implementation Invalidate(m_rasterizationScale); m_xamlRootChanged = m_xamlRoot.Changed({ this, &MessageBubbleNineGrid::OnXamlRootChanged }); - m_context->m_compositionDevice.RenderingDeviceReplaced({ this, &MessageBubbleNineGrid::OnRenderingDeviceReplaced }); + m_renderingDeviceReplaced = m_context->m_compositionDevice.RenderingDeviceReplaced({ this, &MessageBubbleNineGrid::OnRenderingDeviceReplaced }); } MessageBubbleNineGrid::~MessageBubbleNineGrid() diff --git a/Telegram.Native/PlaceholderImageHelper.cpp b/Telegram.Native/PlaceholderImageHelper.cpp index 82c6bf1a96..ba1ddd6402 100644 --- a/Telegram.Native/PlaceholderImageHelper.cpp +++ b/Telegram.Native/PlaceholderImageHelper.cpp @@ -391,7 +391,7 @@ namespace winrt::Telegram::Native::implementation if (result == DXGI_ERROR_DEVICE_REMOVED || result == DXGI_ERROR_DEVICE_RESET) { - ReturnIfFailed(result, HandleDirect3DDeviceLost(true)); + ReturnIfFailed(result, CreateDeviceResources()); ReturnIfFailed(result, source->CreateDeviceResources(m_d2dDevice.get())); return Invalidate(imageSource, buffer); } @@ -837,7 +837,7 @@ namespace winrt::Telegram::Native::implementation if ((result = m_d2dContext->EndDraw()) == D2DERR_RECREATE_TARGET) { - ReturnIfFailed(result, HandleDirect3DDeviceLost(true)); + ReturnIfFailed(result, CreateDeviceResources()); return DrawBlurredImpl(wicBitmapSource, blurAmount, bitmap, minithumbnail); } @@ -993,11 +993,20 @@ namespace winrt::Telegram::Native::implementation if (m_compositor) { - auto compositorInterop = m_compositor.as(); - winrt::com_ptr deviceInterop; - ReturnIfFailed(result, compositorInterop->CreateGraphicsDevice(m_d2dDevice.get(), deviceInterop.put())); + // If the composition device already exists, invalidate the rendering device + if (m_compositionDevice) + { + winrt::com_ptr compositionGraphicsDeviceInterop{ m_compositionDevice.as() }; + result = compositionGraphicsDeviceInterop->SetRenderingDevice(m_d2dDevice.get()); + } + else + { + auto compositorInterop = m_compositor.as(); + winrt::com_ptr deviceInterop; + ReturnIfFailed(result, compositorInterop->CreateGraphicsDevice(m_d2dDevice.get(), deviceInterop.put())); - m_compositionDevice = deviceInterop.as(); + m_compositionDevice = deviceInterop.as(); + } } m_deviceLostHelper.WatchDevice(dxgiDevice); @@ -1006,23 +1015,9 @@ namespace winrt::Telegram::Native::implementation return S_OK; } - void PlaceholderImageHelper::OnDirect3DDeviceLost(DeviceLostHelper const* /* sender */, DeviceLostEventArgs const& /* args */) + void PlaceholderImageHelper::OnDirect3DDeviceLost(DeviceLostHelper const* /* sender */, DeviceLostEventArgs const& args) { - HandleDirect3DDeviceLost(false); - } - - HRESULT PlaceholderImageHelper::HandleDirect3DDeviceLost(bool stop) - { - if (stop) - { - m_deviceLostHelper.StopWatchingCurrentDevice(); - } - - HRESULT result; - ReturnIfFailed(result, CreateDeviceResources()); - - winrt::com_ptr compositionGraphicsDeviceInterop{ m_compositionDevice.as() }; - return compositionGraphicsDeviceInterop->SetRenderingDevice(m_d2dDevice.get()); + CreateDeviceResources(); } HRESULT PlaceholderImageHelper::CreateTextFormat(double fontSize) diff --git a/Telegram.Native/PlaceholderImageHelper.h b/Telegram.Native/PlaceholderImageHelper.h index 480d181988..4bd4ae452c 100644 --- a/Telegram.Native/PlaceholderImageHelper.h +++ b/Telegram.Native/PlaceholderImageHelper.h @@ -214,7 +214,7 @@ namespace winrt::Telegram::Native::implementation void StopWatchingCurrentDevice() { - if (m_dxgiDevice) + if (m_dxgiDevice && m_onDeviceLostHandler) { // QI For the ID3D11Device4 interface. auto d3dDevice{ m_dxgiDevice.as<::ID3D11Device4>() }; @@ -271,7 +271,7 @@ namespace winrt::Telegram::Native::implementation { if (FAILED(m_d3dDevice->GetDeviceRemovedReason())) { - return HandleDirect3DDeviceLost(true); + return CreateDeviceResources(); } return S_OK; @@ -339,7 +339,6 @@ namespace winrt::Telegram::Native::implementation HRESULT CreateTextFormat(double fontSize); void OnDirect3DDeviceLost(DeviceLostHelper const* /* sender */, DeviceLostEventArgs const& /* args */); - HRESULT HandleDirect3DDeviceLost(bool stop); HRESULT DrawBlurredImpl(IWICBitmapSource* wicBitmapSource, float blurAmount, SoftwareBitmap& bitmap, bool minithumbnail); HRESULT SaveImageToStream(ID2D1Image* image, REFGUID wicFormat, IRandomAccessStream randomAccessStream);