diff --git a/Telegram.Msix/Package.appxmanifest b/Telegram.Msix/Package.appxmanifest index 63979f0554..6763f06e0a 100644 --- a/Telegram.Msix/Package.appxmanifest +++ b/Telegram.Msix/Package.appxmanifest @@ -1,6 +1,6 @@  - + Unigram—Telegram for Windows diff --git a/Telegram.Native/ParticlesAnimation.cpp b/Telegram.Native/ParticlesAnimation.cpp index 98698c84d4..13e2dc39e7 100644 --- a/Telegram.Native/ParticlesAnimation.cpp +++ b/Telegram.Native/ParticlesAnimation.cpp @@ -79,6 +79,8 @@ namespace winrt::Telegram::Native::implementation } } +#pragma region Circle + // 1px diameter circles (0.5px radius) inline void draw_circle_1px_100(int32_t* pixels, int width, int height, int cx, int cy, int sa, int sr, int sg, int sb) @@ -360,6 +362,260 @@ namespace winrt::Telegram::Native::implementation } } +#pragma endregion + +#pragma region Plus + + // 1px diameter plus shapes (0.5px radius equivalent) + inline void draw_plus_1px_100(int32_t* pixels, int width, int height, + int cx, int cy, int sa, int sr, int sg, int sb) + { + // Single pixel + set_pixel(pixels, cx, cy, width, height, sa, sr, sg, sb); + } + + inline void draw_plus_1px_125(int32_t* pixels, int width, int height, + int cx, int cy, int sa, int sr, int sg, int sb) + { + // 1.25px effective size - center + light arms + set_pixel(pixels, cx, cy, width, height, sa, sr, sg, sb); + set_pixel_alpha(pixels, cx - 1, cy, width, height, sa, sr, sg, sb, 64); + set_pixel_alpha(pixels, cx + 1, cy, width, height, sa, sr, sg, sb, 64); + set_pixel_alpha(pixels, cx, cy - 1, width, height, sa, sr, sg, sb, 64); + set_pixel_alpha(pixels, cx, cy + 1, width, height, sa, sr, sg, sb, 64); + } + + inline void draw_plus_1px_150(int32_t* pixels, int width, int height, + int cx, int cy, int sa, int sr, int sg, int sb) + { + // 1.5px effective size + set_pixel(pixels, cx, cy, width, height, sa, sr, sg, sb); + set_pixel_alpha(pixels, cx - 1, cy, width, height, sa, sr, sg, sb, 128); + set_pixel_alpha(pixels, cx + 1, cy, width, height, sa, sr, sg, sb, 128); + set_pixel_alpha(pixels, cx, cy - 1, width, height, sa, sr, sg, sb, 128); + set_pixel_alpha(pixels, cx, cy + 1, width, height, sa, sr, sg, sb, 128); + } + + inline void draw_plus_1px_200(int32_t* pixels, int width, int height, + int cx, int cy, int sa, int sr, int sg, int sb) + { + // 2px effective size - solid plus + set_pixel(pixels, cx, cy, width, height, sa, sr, sg, sb); + set_pixel(pixels, cx - 1, cy, width, height, sa, sr, sg, sb); + set_pixel(pixels, cx + 1, cy, width, height, sa, sr, sg, sb); + set_pixel(pixels, cx, cy - 1, width, height, sa, sr, sg, sb); + set_pixel(pixels, cx, cy + 1, width, height, sa, sr, sg, sb); + } + + inline void draw_plus_1px_250(int32_t* pixels, int width, int height, + int cx, int cy, int sa, int sr, int sg, int sb) + { + // 2.5px effective size with anti-aliasing + set_pixel(pixels, cx, cy, width, height, sa, sr, sg, sb); + set_pixel(pixels, cx - 1, cy, width, height, sa, sr, sg, sb); + set_pixel(pixels, cx + 1, cy, width, height, sa, sr, sg, sb); + set_pixel(pixels, cx, cy - 1, width, height, sa, sr, sg, sb); + set_pixel(pixels, cx, cy + 1, width, height, sa, sr, sg, sb); + + // Extended arms with anti-aliasing + set_pixel_alpha(pixels, cx - 2, cy, width, height, sa, sr, sg, sb, 96); + set_pixel_alpha(pixels, cx + 2, cy, width, height, sa, sr, sg, sb, 96); + set_pixel_alpha(pixels, cx, cy - 2, width, height, sa, sr, sg, sb, 96); + set_pixel_alpha(pixels, cx, cy + 2, width, height, sa, sr, sg, sb, 96); + } + + inline void draw_plus_1px_400(int32_t* pixels, int width, int height, + int cx, int cy, int sa, int sr, int sg, int sb) + { + // 4px effective size with anti-aliasing + // Solid center cross + set_pixel(pixels, cx, cy, width, height, sa, sr, sg, sb); + set_pixel(pixels, cx - 1, cy, width, height, sa, sr, sg, sb); + set_pixel(pixels, cx + 1, cy, width, height, sa, sr, sg, sb); + set_pixel(pixels, cx, cy - 1, width, height, sa, sr, sg, sb); + set_pixel(pixels, cx, cy + 1, width, height, sa, sr, sg, sb); + + // Extended arms + set_pixel(pixels, cx - 2, cy, width, height, sa, sr, sg, sb); + set_pixel(pixels, cx + 2, cy, width, height, sa, sr, sg, sb); + set_pixel(pixels, cx, cy - 2, width, height, sa, sr, sg, sb); + set_pixel(pixels, cx, cy + 2, width, height, sa, sr, sg, sb); + + // Anti-aliased outer tips + set_pixel_alpha(pixels, cx - 3, cy, width, height, sa, sr, sg, sb, 128); + set_pixel_alpha(pixels, cx + 3, cy, width, height, sa, sr, sg, sb, 128); + set_pixel_alpha(pixels, cx, cy - 3, width, height, sa, sr, sg, sb, 128); + set_pixel_alpha(pixels, cx, cy + 3, width, height, sa, sr, sg, sb, 128); + } + + // 2px diameter plus shapes (1px radius equivalent) + inline void draw_plus_2px_100(int32_t* pixels, int width, int height, + int cx, int cy, int sa, int sr, int sg, int sb) + { + // Classic 2px plus pattern + set_pixel(pixels, cx, cy, width, height, sa, sr, sg, sb); + set_pixel(pixels, cx - 1, cy, width, height, sa, sr, sg, sb); + set_pixel(pixels, cx + 1, cy, width, height, sa, sr, sg, sb); + set_pixel(pixels, cx, cy - 1, width, height, sa, sr, sg, sb); + set_pixel(pixels, cx, cy + 1, width, height, sa, sr, sg, sb); + } + + inline void draw_plus_2px_125(int32_t* pixels, int width, int height, + int cx, int cy, int sa, int sr, int sg, int sb) + { + // 2.5px effective size with light anti-aliasing + set_pixel(pixels, cx, cy, width, height, sa, sr, sg, sb); + set_pixel(pixels, cx - 1, cy, width, height, sa, sr, sg, sb); + set_pixel(pixels, cx + 1, cy, width, height, sa, sr, sg, sb); + set_pixel(pixels, cx, cy - 1, width, height, sa, sr, sg, sb); + set_pixel(pixels, cx, cy + 1, width, height, sa, sr, sg, sb); + + // Light anti-aliasing on arm tips + set_pixel_alpha(pixels, cx - 2, cy, width, height, sa, sr, sg, sb, 64); + set_pixel_alpha(pixels, cx + 2, cy, width, height, sa, sr, sg, sb, 64); + set_pixel_alpha(pixels, cx, cy - 2, width, height, sa, sr, sg, sb, 64); + set_pixel_alpha(pixels, cx, cy + 2, width, height, sa, sr, sg, sb, 64); + } + + inline void draw_plus_2px_150(int32_t* pixels, int width, int height, + int cx, int cy, int sa, int sr, int sg, int sb) + { + // 3px effective size + set_pixel(pixels, cx, cy, width, height, sa, sr, sg, sb); + set_pixel(pixels, cx - 1, cy, width, height, sa, sr, sg, sb); + set_pixel(pixels, cx + 1, cy, width, height, sa, sr, sg, sb); + set_pixel(pixels, cx, cy - 1, width, height, sa, sr, sg, sb); + set_pixel(pixels, cx, cy + 1, width, height, sa, sr, sg, sb); + + // Medium anti-aliasing on arm tips + set_pixel_alpha(pixels, cx - 2, cy, width, height, sa, sr, sg, sb, 128); + set_pixel_alpha(pixels, cx + 2, cy, width, height, sa, sr, sg, sb, 128); + set_pixel_alpha(pixels, cx, cy - 2, width, height, sa, sr, sg, sb, 128); + set_pixel_alpha(pixels, cx, cy + 2, width, height, sa, sr, sg, sb, 128); + } + + inline void draw_plus_2px_200(int32_t* pixels, int width, int height, + int cx, int cy, int sa, int sr, int sg, int sb) + { + // 4px effective size - thicker plus + set_pixel(pixels, cx, cy, width, height, sa, sr, sg, sb); + set_pixel(pixels, cx - 1, cy, width, height, sa, sr, sg, sb); + set_pixel(pixels, cx + 1, cy, width, height, sa, sr, sg, sb); + set_pixel(pixels, cx, cy - 1, width, height, sa, sr, sg, sb); + set_pixel(pixels, cx, cy + 1, width, height, sa, sr, sg, sb); + + // Extended solid arms + set_pixel(pixels, cx - 2, cy, width, height, sa, sr, sg, sb); + set_pixel(pixels, cx + 2, cy, width, height, sa, sr, sg, sb); + set_pixel(pixels, cx, cy - 2, width, height, sa, sr, sg, sb); + set_pixel(pixels, cx, cy + 2, width, height, sa, sr, sg, sb); + + // Light anti-aliasing on outer tips + set_pixel_alpha(pixels, cx - 3, cy, width, height, sa, sr, sg, sb, 96); + set_pixel_alpha(pixels, cx + 3, cy, width, height, sa, sr, sg, sb, 96); + set_pixel_alpha(pixels, cx, cy - 3, width, height, sa, sr, sg, sb, 96); + set_pixel_alpha(pixels, cx, cy + 3, width, height, sa, sr, sg, sb, 96); + } + + inline void draw_plus_2px_250(int32_t* pixels, int width, int height, + int cx, int cy, int sa, int sr, int sg, int sb) + { + // 5px effective size with good anti-aliasing + // Solid center cross + set_pixel(pixels, cx, cy, width, height, sa, sr, sg, sb); + set_pixel(pixels, cx - 1, cy, width, height, sa, sr, sg, sb); + set_pixel(pixels, cx + 1, cy, width, height, sa, sr, sg, sb); + set_pixel(pixels, cx, cy - 1, width, height, sa, sr, sg, sb); + set_pixel(pixels, cx, cy + 1, width, height, sa, sr, sg, sb); + + // Extended solid arms + set_pixel(pixels, cx - 2, cy, width, height, sa, sr, sg, sb); + set_pixel(pixels, cx + 2, cy, width, height, sa, sr, sg, sb); + set_pixel(pixels, cx, cy - 2, width, height, sa, sr, sg, sb); + set_pixel(pixels, cx, cy + 2, width, height, sa, sr, sg, sb); + + // Anti-aliased outer tips + set_pixel_alpha(pixels, cx - 3, cy, width, height, sa, sr, sg, sb, 160); + set_pixel_alpha(pixels, cx + 3, cy, width, height, sa, sr, sg, sb, 160); + set_pixel_alpha(pixels, cx, cy - 3, width, height, sa, sr, sg, sb, 160); + set_pixel_alpha(pixels, cx, cy + 3, width, height, sa, sr, sg, sb, 160); + } + + inline void draw_plus_2px_400(int32_t* pixels, int width, int height, + int cx, int cy, int sa, int sr, int sg, int sb) + { + // 8px effective size with full anti-aliasing + // Solid center cross (3 pixels thick) + for (int i = -1; i <= 1; i++) + { + // Horizontal arm + set_pixel(pixels, cx - 3, cy + i, width, height, sa, sr, sg, sb); + set_pixel(pixels, cx - 2, cy + i, width, height, sa, sr, sg, sb); + set_pixel(pixels, cx - 1, cy + i, width, height, sa, sr, sg, sb); + set_pixel(pixels, cx, cy + i, width, height, sa, sr, sg, sb); + set_pixel(pixels, cx + 1, cy + i, width, height, sa, sr, sg, sb); + set_pixel(pixels, cx + 2, cy + i, width, height, sa, sr, sg, sb); + set_pixel(pixels, cx + 3, cy + i, width, height, sa, sr, sg, sb); + + // Vertical arm + set_pixel(pixels, cx + i, cy - 3, width, height, sa, sr, sg, sb); + set_pixel(pixels, cx + i, cy - 2, width, height, sa, sr, sg, sb); + set_pixel(pixels, cx + i, cy + 2, width, height, sa, sr, sg, sb); + set_pixel(pixels, cx + i, cy + 3, width, height, sa, sr, sg, sb); + } + + // Anti-aliased outer tips + set_pixel_alpha(pixels, cx - 4, cy, width, height, sa, sr, sg, sb, 128); + set_pixel_alpha(pixels, cx + 4, cy, width, height, sa, sr, sg, sb, 128); + set_pixel_alpha(pixels, cx, cy - 4, width, height, sa, sr, sg, sb, 128); + set_pixel_alpha(pixels, cx, cy + 4, width, height, sa, sr, sg, sb, 128); + + // Side anti-aliasing for smoother edges + set_pixel_alpha(pixels, cx - 4, cy - 1, width, height, sa, sr, sg, sb, 64); + set_pixel_alpha(pixels, cx - 4, cy + 1, width, height, sa, sr, sg, sb, 64); + set_pixel_alpha(pixels, cx + 4, cy - 1, width, height, sa, sr, sg, sb, 64); + set_pixel_alpha(pixels, cx + 4, cy + 1, width, height, sa, sr, sg, sb, 64); + set_pixel_alpha(pixels, cx - 1, cy - 4, width, height, sa, sr, sg, sb, 64); + set_pixel_alpha(pixels, cx + 1, cy - 4, width, height, sa, sr, sg, sb, 64); + set_pixel_alpha(pixels, cx - 1, cy + 4, width, height, sa, sr, sg, sb, 64); + set_pixel_alpha(pixels, cx + 1, cy + 4, width, height, sa, sr, sg, sb, 64); + } + + // Dispatch function for easy usage + inline void draw_plus_scaled(int32_t* pixels, int width, int height, Particle* particle, Color color, int scale, double rasterizationScale) + { + int cx = particle->X; + int cy = particle->Y; + float radius = particle->Radius; + + if (radius == 0.5) + { + switch (scale) + { + case 100: draw_plus_1px_100(pixels, width, height, cx, cy, color.A, color.R, color.G, color.B); break; + case 125: draw_plus_1px_125(pixels, width, height, cx, cy, color.A, color.R, color.G, color.B); break; + case 150: draw_plus_1px_150(pixels, width, height, cx, cy, color.A, color.R, color.G, color.B); break; + case 200: draw_plus_1px_200(pixels, width, height, cx, cy, color.A, color.R, color.G, color.B); break; + case 250: draw_plus_1px_250(pixels, width, height, cx, cy, color.A, color.R, color.G, color.B); break; + case 400: draw_plus_1px_400(pixels, width, height, cx, cy, color.A, color.R, color.G, color.B); break; + } + } + else if (radius == 1) + { + switch (scale) + { + case 100: draw_plus_2px_100(pixels, width, height, cx, cy, color.A, color.R, color.G, color.B); break; + case 125: draw_plus_2px_125(pixels, width, height, cx, cy, color.A, color.R, color.G, color.B); break; + case 150: draw_plus_2px_150(pixels, width, height, cx, cy, color.A, color.R, color.G, color.B); break; + case 200: draw_plus_2px_200(pixels, width, height, cx, cy, color.A, color.R, color.G, color.B); break; + case 250: draw_plus_2px_250(pixels, width, height, cx, cy, color.A, color.R, color.G, color.B); break; + case 400: draw_plus_2px_400(pixels, width, height, cx, cy, color.A, color.R, color.G, color.B); break; + } + } + } + +#pragma endregion + void ParticlesAnimation::RenderSync(IBuffer bitmap) { auto add = 0.04; @@ -379,12 +635,19 @@ namespace winrt::Telegram::Native::implementation auto easedOpacity = (byte)(std::clamp(dot->Opacity, 0., 1.) * 255); auto color = premultiply_color(m_foreground.R, m_foreground.G, m_foreground.B, easedOpacity); - draw_circle_scaled(pixels, m_width, m_height, dot, color, m_scalePercent, m_rasterizationScale); + if (m_type == ParticlesType::Status) + { + draw_plus_scaled(pixels, m_width, m_height, dot, color, m_scalePercent, m_rasterizationScale); + } + else + { + draw_circle_scaled(pixels, m_width, m_height, dot, color, m_scalePercent, m_rasterizationScale); + } if (dot->Opacity <= 0) { dot->Adding = true; - m_particles[i] = GenerateParticle(dot->Adding); + m_particles[i] = GenerateParticle(dot->Adding, NextPoint(m_width, m_height)); } else if (dot->Opacity >= 1) { @@ -398,18 +661,117 @@ namespace winrt::Telegram::Native::implementation return x > y ? y : x; } - void ParticlesAnimation::Prepare() + inline double max(double x, double y) { - auto w = m_width * (1 / m_rasterizationScale); - auto h = m_height * (1 / m_rasterizationScale); + return x > y ? x : y; + } - auto count = round(w * h / (35 * (IS_MOBILE ? 2 : 1))); - count *= m_text ? 4 : 1; - count = min(/*!liteMode.isAvailable('chat_spoilers') ? 400 :*/ IS_MOBILE ? 1000 : 2200, count); + constexpr float PI = 3.14159265358979323846f; + + std::vector ParticlesAnimation::NextPoints(int count, float width, float height, float noiseFactor) + { + std::random_device rd; + std::mt19937 gen(rd()); + + // Pre-calculate constants + const float centerX = width * 0.5f; + const float centerY = height * 0.5f; + const float semiMajor = width * 0.5f; + const float semiMinor = height * 0.5f; + const float invTwoPi = 1.0f / (2.0f * PI); + + std::vector particles; + particles.reserve(count); + + // Generate all random numbers at once for better cache performance + std::uniform_real_distribution uniformDist(0.0f, 1.0f); + std::uniform_real_distribution noiseDist(-noiseFactor, noiseFactor); for (int i = 0; i < count; ++i) { - m_particles.push_back(GenerateParticle(-1)); + if (m_type == ParticlesType::Status) + { + // Generate angle and radius + float angle = uniformDist(gen) * 2.0f * PI; + float r = std::sqrt(uniformDist(gen)); // sqrt for uniform area distribution + + // Fast trigonometry + float cosAngle = std::cos(angle); + float sinAngle = std::sin(angle); + + // Calculate base position + float baseX = r * semiMajor * cosAngle; + float baseY = r * semiMinor * sinAngle; + + // Add noise + float noiseX = noiseDist(gen) * semiMajor; + float noiseY = noiseDist(gen) * semiMinor; + + // Final position + float x = centerX + baseX + noiseX; + float y = centerY + baseY + noiseY; + + // Clamp to bounds (branchless) + x = std::max(0.0f, std::min(width, x)); + y = std::max(0.0f, std::min(height, y)); + + particles.emplace_back(Point{ x, y }); + } + else + { + particles.emplace_back(Point{ uniformDist(gen) * m_width, uniformDist(gen) * m_height }); + } + } + + return particles; + } + + Point ParticlesAnimation::NextPoint(float width, float height, float noiseFactor) + { + static std::random_device rd; + static std::mt19937 gen(rd()); + + if (m_type == ParticlesType::Status) + { + // Pre-calculate constants + const float centerX = width * 0.5f; + const float centerY = height * 0.5f; + const float semiMajor = width * 0.5f; + const float semiMinor = height * 0.5f; + + std::uniform_real_distribution uniformDist(0.0f, 1.0f); + std::uniform_real_distribution noiseDist(-noiseFactor, noiseFactor); + + // Generate angle and radius + float angle = uniformDist(gen) * 2.0f * PI; + float r = std::sqrt(uniformDist(gen)); // sqrt for uniform area distribution + + // Fast trigonometry + float cosAngle = std::cos(angle); + float sinAngle = std::sin(angle); + + // Calculate base position + float baseX = r * semiMajor * cosAngle; + float baseY = r * semiMinor * sinAngle; + + // Add noise + float noiseX = noiseDist(gen) * semiMajor; + float noiseY = noiseDist(gen) * semiMinor; + + // Final position + float x = centerX + baseX + noiseX; + float y = centerY + baseY + noiseY; + + // Clamp to bounds + x = std::max(0.0f, std::min(width, x)); + y = std::max(0.0f, std::min(height, y)); + + return { x, y }; + } + else + { + static std::uniform_real_distribution dis(0.0, 1.0); + return { dis(gen) * m_width, dis(gen) * m_height }; } } @@ -421,19 +783,40 @@ namespace winrt::Telegram::Native::implementation return dis(gen); } - Particle ParticlesAnimation::GenerateParticle(int32_t type) + void ParticlesAnimation::Prepare() { - auto x = floor(NextDouble() * m_width); - auto y = floor(NextDouble() * m_height); + auto w = m_width * (1 / m_rasterizationScale); + auto h = m_height * (1 / m_rasterizationScale); + + auto count = round(w * h / (35 * (IS_MOBILE ? 2 : 1))); + count *= m_type == ParticlesType::Text ? 4 : 1; + count = min(/*!liteMode.isAvailable('chat_spoilers') ? 400 :*/ IS_MOBILE ? 1000 : 2200, count); + + auto particles = NextPoints(count, m_width, m_height); + m_particles.reserve(count); + + for (const auto& particle : particles) + { + m_particles.emplace_back(GenerateParticle(-1, particle)); + } + } + + Particle ParticlesAnimation::GenerateParticle(int32_t type, const Point& position) + { + const auto threshold = m_type == ParticlesType::Status ? .2f : .8f; + auto opacity = type == 1 ? 0 : NextDouble(); - auto radius = (NextDouble() >= .8 ? 1 : 0.5); + auto radius = (NextDouble() >= threshold ? 1.f : 0.5f); auto adding = type == -1 ? NextDouble() >= .5 : type; + + auto padding = ceil(radius * m_rasterizationScale / 2); + return Particle( - (float)x, - (float)y, - (float)radius, + max(padding, min(m_width - padding - 1, round(position.X))), + max(padding, min(m_height - padding - 1, round(position.Y))), + radius, opacity, adding); } diff --git a/Telegram.Native/ParticlesAnimation.h b/Telegram.Native/ParticlesAnimation.h index 5a146f0573..613a8d48f2 100644 --- a/Telegram.Native/ParticlesAnimation.h +++ b/Telegram.Native/ParticlesAnimation.h @@ -19,13 +19,17 @@ namespace winrt::Telegram::Native::implementation { } - float X; - float Y; + float X, Y; float Radius; double Opacity; bool Adding; }; + struct Point + { + float X, Y; + }; + inline static double findClosestScale(double target) { static double rasterizationScales[6] = { 1.0, 1.25, 1.5, 2.0, 2.5, 4.0 }; @@ -58,12 +62,12 @@ namespace winrt::Telegram::Native::implementation struct ParticlesAnimation : ParticlesAnimationT { - ParticlesAnimation(int32_t width, int32_t height, double rasterizationScale, bool text, Color foreground, Color background) + ParticlesAnimation(int32_t width, int32_t height, double rasterizationScale, ParticlesType type, Color foreground, Color background) : m_width(width) , m_height(height) , m_scalePercent(findClosestScale(rasterizationScale) * 100) , m_rasterizationScale(rasterizationScale) - , m_text(text) + , m_type(type) , m_foreground(foreground) , m_background(premultiply_background(background)) { @@ -84,13 +88,16 @@ namespace winrt::Telegram::Native::implementation private: void Prepare(); - Particle GenerateParticle(int32_t type); + Particle GenerateParticle(int32_t type, const Point& position); + + std::vector NextPoints(int count, float width, float height, float noiseFactor = 0.1f); + Point NextPoint(float width, float height, float noiseFactor = 0.1f); int32_t m_width; int32_t m_height; int32_t m_scalePercent; double m_rasterizationScale; - bool m_text; + ParticlesType m_type; Color m_foreground; int32_t m_background; diff --git a/Telegram.Native/ParticlesAnimation.idl b/Telegram.Native/ParticlesAnimation.idl index 229a3f3075..e4519d2f1f 100644 --- a/Telegram.Native/ParticlesAnimation.idl +++ b/Telegram.Native/ParticlesAnimation.idl @@ -1,9 +1,16 @@ namespace Telegram.Native { + enum ParticlesType + { + Media, + Text, + Status + }; + [default_interface] runtimeclass ParticlesAnimation { - ParticlesAnimation(Int32 width, Int32 height, Double rasterizationScale, Boolean text, Windows.UI.Color foreground, Windows.UI.Color background); + ParticlesAnimation(Int32 width, Int32 height, Double rasterizationScale, ParticlesType type, Windows.UI.Color foreground, Windows.UI.Color background); void RenderSync(Windows.Storage.Streams.IBuffer bitmap); diff --git a/Telegram/App.xaml.cs b/Telegram/App.xaml.cs index 7e4b2b0755..417f73222b 100644 --- a/Telegram/App.xaml.cs +++ b/Telegram/App.xaml.cs @@ -209,9 +209,7 @@ public override UIElement CreateRootElement(IActivatedEventArgs e, WindowContext } var sessionId = TypeResolver.Current.Lifetime.ActiveItem.Id; - - var navigationFrame = new Frame(); - var navigationService = NavigationServiceFactory(window, BackButton.Ignore, navigationFrame, sessionId, $"{sessionId}", true) as NavigationService; + var navigationService = NavigationServiceFactory(window, BackButton.Ignore, sessionId, $"{sessionId}", true) as NavigationService; if (e is ShareTargetActivatedEventArgs) { diff --git a/Telegram/Assets/Fonts/Telegram.json b/Telegram/Assets/Fonts/Telegram.json index 39bd3f6a61..069d368c2c 100644 --- a/Telegram/Assets/Fonts/Telegram.json +++ b/Telegram/Assets/Fonts/Telegram.json @@ -10984,13 +10984,29 @@ }, { "selection": [ + { + "order": 964, + "id": 173, + "name": "tl_fluent_poll_undo_20_regular", + "prevSize": 32, + "code": 60100, + "tempChar": "" + }, + { + "order": 963, + "id": 172, + "name": "tl_copy_as_path_20_regular", + "prevSize": 32, + "code": 60099, + "tempChar": "" + }, { "order": 957, "id": 171, "name": "tl_fluent_diamond_20_regular", "prevSize": 32, "code": 60093, - "tempChar": "" + "tempChar": "" }, { "order": 935, @@ -10998,7 +11014,7 @@ "name": "tl_fluent_number_symbol_arrow_up_16_regular", "prevSize": 32, "code": 60075, - "tempChar": "" + "tempChar": "" }, { "order": 936, @@ -11006,7 +11022,7 @@ "name": "tl_fluent_number_symbol_arrow_up_20_regular", "prevSize": 32, "code": 60076, - "tempChar": "" + "tempChar": "" }, { "order": 937, @@ -11014,7 +11030,7 @@ "name": "tl_fluent_dollar_arrow_up_16_regular", "prevSize": 32, "code": 60077, - "tempChar": "" + "tempChar": "" }, { "order": 938, @@ -11022,7 +11038,7 @@ "name": "tl_fluent_dollar_arrow_up_20_regular", "prevSize": 32, "code": 60078, - "tempChar": "" + "tempChar": "" }, { "order": 939, @@ -11030,7 +11046,7 @@ "name": "tl_fluent_calendar_arrow_up_16_regular", "prevSize": 32, "code": 60079, - "tempChar": "" + "tempChar": "" }, { "order": 940, @@ -11038,7 +11054,7 @@ "name": "tl_fluent_calendar_arrow_up_20_regular", "prevSize": 32, "code": 60080, - "tempChar": "" + "tempChar": "" }, { "order": 929, @@ -11046,7 +11062,7 @@ "name": "tl_fluent_home_dismiss_20_regular", "prevSize": 32, "code": 60069, - "tempChar": "" + "tempChar": "" }, { "order": 926, @@ -11054,7 +11070,7 @@ "name": "tl_fluent_chat_stars_20_filled", "prevSize": 32, "code": 59772, - "tempChar": "" + "tempChar": "" }, { "order": 925, @@ -11062,7 +11078,7 @@ "name": "tl_fluent_spoiler_20_regular", "prevSize": 32, "code": 60065, - "tempChar": "" + "tempChar": "" }, { "order": 924, @@ -11070,7 +11086,7 @@ "name": "tl_fluent_media_spoiler_20_regular", "prevSize": 32, "code": 60066, - "tempChar": "" + "tempChar": "" }, { "order": 918, @@ -11078,7 +11094,7 @@ "name": "tl_fluent_clock_arrow_forward_20_regular", "prevSize": 32, "code": 60063, - "tempChar": "" + "tempChar": "" }, { "order": 917, @@ -11086,7 +11102,7 @@ "name": "tl_fluent_clock_edit_20_regular", "prevSize": 32, "code": 60062, - "tempChar": "" + "tempChar": "" }, { "order": 899, @@ -11094,7 +11110,7 @@ "name": "ic_fluent_person_add_20_regular", "prevSize": 32, "code": 60049, - "tempChar": "" + "tempChar": "" }, { "order": 897, @@ -11102,7 +11118,7 @@ "name": "tl_fluent_fragment_20_filled", "prevSize": 32, "code": 60046, - "tempChar": "" + "tempChar": "" }, { "order": 895, @@ -11110,7 +11126,7 @@ "name": "tl_fluent_coin_20_regular", "prevSize": 32, "code": 60045, - "tempChar": "" + "tempChar": "" }, { "order": 890, @@ -11118,7 +11134,7 @@ "name": "Premium-1", "prevSize": 32, "code": 60039, - "tempChar": "" + "tempChar": "" }, { "order": 886, @@ -11126,7 +11142,7 @@ "name": "tl_fluent_chat_link_20_filled", "prevSize": 32, "code": 60035, - "tempChar": "" + "tempChar": "" }, { "order": 885, @@ -11134,7 +11150,7 @@ "name": "tl_fluent_chat_info_20_filled", "prevSize": 32, "code": 60034, - "tempChar": "" + "tempChar": "" }, { "order": 884, @@ -11142,7 +11158,7 @@ "name": "tl_fluent_fifty_fifty_20_regular", "prevSize": 32, "code": 60031, - "tempChar": "" + "tempChar": "" }, { "order": 883, @@ -11150,7 +11166,7 @@ "name": "tl_fluent_ton_coin_20_regular", "prevSize": 32, "code": 60033, - "tempChar": "" + "tempChar": "" }, { "order": 881, @@ -11158,7 +11174,7 @@ "name": "tl_fluent_arrow_reply_20_filled", "prevSize": 32, "code": 60028, - "tempChar": "" + "tempChar": "" }, { "order": 880, @@ -11166,7 +11182,7 @@ "name": "tl_fluent_chat_snooze_20_filled", "prevSize": 32, "code": 60029, - "tempChar": "" + "tempChar": "" }, { "order": 877, @@ -11174,7 +11190,7 @@ "name": "tl_fluent_chat_unread_20_filled", "prevSize": 32, "code": 60025, - "tempChar": "" + "tempChar": "" }, { "order": 874, @@ -11182,7 +11198,7 @@ "name": "tl_fluent_chat_snooze_20_regular", "prevSize": 32, "code": 60022, - "tempChar": "" + "tempChar": "" }, { "order": 871, @@ -11195,7 +11211,7 @@ 60020, 60021 ], - "tempChar": "" + "tempChar": "" }, { "order": 864, @@ -11203,7 +11219,7 @@ "name": "tl_fluent_last_seen_20_filled", "prevSize": 32, "code": 60013, - "tempChar": "" + "tempChar": "" }, { "order": 853, @@ -11211,7 +11227,7 @@ "name": "tl_fluent_my_notes_20_filled", "prevSize": 32, "code": 60001, - "tempChar": "" + "tempChar": "" }, { "order": 852, @@ -11219,7 +11235,7 @@ "name": "tl_fluent_author_hidden_20_filled", "prevSize": 32, "code": 60002, - "tempChar": "" + "tempChar": "" }, { "order": 847, @@ -11227,7 +11243,7 @@ "name": "tl_fluent_view_once_20_regular", "prevSize": 32, "code": 59672, - "tempChar": "" + "tempChar": "" }, { "order": 836, @@ -11235,7 +11251,7 @@ "name": "tl_fluent_videochat_20_filled", "prevSize": 32, "code": 59983, - "tempChar": "" + "tempChar": "" }, { "order": 810, @@ -11243,7 +11259,7 @@ "name": "tl_fluent_move_down_20_regular", "prevSize": 32, "code": 59971, - "tempChar": "" + "tempChar": "" }, { "order": 811, @@ -11251,7 +11267,7 @@ "name": "tl_fluent_move_up_20_regular", "prevSize": 32, "code": 59972, - "tempChar": "" + "tempChar": "" }, { "order": 809, @@ -11259,7 +11275,7 @@ "name": "tl_fluent_enlarge_20_regular", "prevSize": 32, "code": 59973, - "tempChar": "" + "tempChar": "" }, { "order": 808, @@ -11267,7 +11283,7 @@ "name": "tl_fluent_shrink_20_regular", "prevSize": 32, "code": 59974, - "tempChar": "" + "tempChar": "" }, { "order": 807, @@ -11275,7 +11291,7 @@ "name": "tl_fluent_id_rectangle_20_regular", "prevSize": 32, "code": 59970, - "tempChar": "" + "tempChar": "" }, { "order": 804, @@ -11283,7 +11299,7 @@ "name": "tl_fluent_reply_another_chat_20_regular", "prevSize": 32, "code": 59967, - "tempChar": "" + "tempChar": "" }, { "order": 803, @@ -11291,7 +11307,7 @@ "name": "tl_fluent_quote_block_20_regular", "prevSize": 32, "code": 59966, - "tempChar": "" + "tempChar": "" }, { "order": 800, @@ -11299,7 +11315,7 @@ "name": "tl_fluent_text_quote_20_regular", "prevSize": 32, "code": 59964, - "tempChar": "", + "tempChar": "", "codes": [ 59964 ] @@ -11310,7 +11326,7 @@ "name": "tl_fluent_boost_20_regular", "prevSize": 32, "code": 59961, - "tempChar": "" + "tempChar": "" }, { "order": 785, @@ -11318,7 +11334,7 @@ "name": "tl_fluent_stories_off_20_regular", "prevSize": 32, "code": 59949, - "tempChar": "" + "tempChar": "" }, { "order": 784, @@ -11326,7 +11342,7 @@ "name": "tl_fluent_stealth_20_filled", "prevSize": 32, "code": 59948, - "tempChar": "" + "tempChar": "" }, { "order": 783, @@ -11334,7 +11350,7 @@ "name": "tl_fluent_stealth_20_regular", "prevSize": 32, "code": 59944, - "tempChar": "" + "tempChar": "" }, { "order": 782, @@ -11342,7 +11358,7 @@ "name": "tl_fluent_stealth_locked_20_regular", "prevSize": 32, "code": 59945, - "tempChar": "" + "tempChar": "" }, { "order": 781, @@ -11350,7 +11366,7 @@ "name": "tl_fluent_eye_on_20_regular", "prevSize": 32, "code": 59946, - "tempChar": "" + "tempChar": "" }, { "order": 780, @@ -11358,7 +11374,7 @@ "name": "tl_fluent_save_locked_20_regular", "prevSize": 32, "code": 59947, - "tempChar": "" + "tempChar": "" }, { "order": 764, @@ -11366,7 +11382,7 @@ "name": "tl_fluent_unarchive_20_regular", "prevSize": 32, "code": 59940, - "tempChar": "" + "tempChar": "" }, { "order": 759, @@ -11377,7 +11393,7 @@ "codes": [ 59859 ], - "tempChar": "" + "tempChar": "" }, { "order": 757, @@ -11385,7 +11401,7 @@ "name": "Arrow", "prevSize": 32, "code": 59710, - "tempChar": "" + "tempChar": "" }, { "order": 753, @@ -11393,7 +11409,7 @@ "name": "tl_fluent_arrow_forward_20_filled", "prevSize": 32, "code": 59668, - "tempChar": "" + "tempChar": "" }, { "order": 752, @@ -11401,7 +11417,7 @@ "name": "tl_fluent_arrow_forward_20_regular", "prevSize": 32, "code": 59181, - "tempChar": "" + "tempChar": "" }, { "order": 751, @@ -11409,7 +11425,7 @@ "name": "tl_fluent_arrow_reply_20_regular", "prevSize": 32, "code": 57928, - "tempChar": "" + "tempChar": "" }, { "order": 749, @@ -11417,7 +11433,7 @@ "name": "VoiceRecognition1", "prevSize": 32, "code": 59667, - "tempChar": "" + "tempChar": "" }, { "order": 750, @@ -11425,7 +11441,7 @@ "name": "VoiceRecognition2", "prevSize": 32, "code": 59659, - "tempChar": "" + "tempChar": "" }, { "order": 725, @@ -11433,7 +11449,7 @@ "name": "ic_fluent_speed_template_20_regular", "prevSize": 32, "code": 59923, - "tempChar": "" + "tempChar": "" }, { "order": 722, @@ -11441,7 +11457,7 @@ "name": "ic_fluent_folder_arrow_download_20_regular", "prevSize": 32, "code": 59920, - "tempChar": "" + "tempChar": "" }, { "order": 717, @@ -11449,7 +11465,7 @@ "name": "ShieldStar", "prevSize": 32, "code": 59779, - "tempChar": "" + "tempChar": "" }, { "order": 713, @@ -11457,7 +11473,7 @@ "name": "bot_command_0", "prevSize": 32, "code": 59915, - "tempChar": "" + "tempChar": "" }, { "order": 696, @@ -11468,7 +11484,7 @@ "codes": [ 59895 ], - "tempChar": "" + "tempChar": "" }, { "order": 697, @@ -11476,7 +11492,7 @@ "name": "link_1", "prevSize": 32, "code": 59896, - "tempChar": "" + "tempChar": "" }, { "order": 692, @@ -11491,7 +11507,7 @@ 59896, 59897 ], - "tempChar": "" + "tempChar": "" }, { "order": 691, @@ -11506,7 +11522,7 @@ 59901, 59902 ], - "tempChar": "" + "tempChar": "" }, { "order": 690, @@ -11521,7 +11537,7 @@ 59906, 59907 ], - "tempChar": "" + "tempChar": "" }, { "order": 689, @@ -11536,7 +11552,7 @@ 59911, 59912 ], - "tempChar": "" + "tempChar": "" }, { "order": 652, @@ -11544,7 +11560,7 @@ "name": "gift_premium", "prevSize": 32, "code": 59860, - "tempChar": "" + "tempChar": "" }, { "order": 630, @@ -11552,7 +11568,7 @@ "name": "Circle", "prevSize": 32, "code": 59963, - "tempChar": "" + "tempChar": "" }, { "order": 621, @@ -11560,7 +11576,7 @@ "name": "Premium", "prevSize": 32, "code": 59829, - "tempChar": "" + "tempChar": "" }, { "order": 603, @@ -11568,7 +11584,7 @@ "name": "ic_fluent_clock_alarm_hour_20_regular", "prevSize": 32, "code": 59811, - "tempChar": "" + "tempChar": "" }, { "order": 601, @@ -11576,7 +11592,7 @@ "name": "never", "prevSize": 32, "code": 59807, - "tempChar": "" + "tempChar": "" }, { "order": 599, @@ -11584,7 +11600,7 @@ "name": "day", "prevSize": 32, "code": 59808, - "tempChar": "" + "tempChar": "" }, { "order": 602, @@ -11592,7 +11608,7 @@ "name": "week", "prevSize": 32, "code": 59809, - "tempChar": "" + "tempChar": "" }, { "order": 600, @@ -11600,7 +11616,7 @@ "name": "month", "prevSize": 32, "code": 59810, - "tempChar": "" + "tempChar": "" }, { "order": 592, @@ -11608,7 +11624,7 @@ "name": "mask_outline_2", "prevSize": 32, "code": 59799, - "tempChar": "" + "tempChar": "" }, { "order": 521, @@ -11616,7 +11632,7 @@ "name": "Reactions", "prevSize": 32, "code": 59776, - "tempChar": "" + "tempChar": "" }, { "order": 513, @@ -11624,7 +11640,7 @@ "name": "ic_fluent_dismiss_24_regular", "prevSize": 32, "code": 59768, - "tempChar": "" + "tempChar": "" }, { "order": 497, @@ -11632,7 +11648,7 @@ "name": "Seen", "prevSize": 32, "code": 59755, - "tempChar": "" + "tempChar": "" }, { "order": 494, @@ -11640,7 +11656,7 @@ "name": "ArrowDownSmall", "prevSize": 32, "code": 60892, - "tempChar": "" + "tempChar": "" }, { "order": 492, @@ -11648,7 +11664,7 @@ "name": "ArrowLeftSmall", "prevSize": 32, "code": 60889, - "tempChar": "" + "tempChar": "" }, { "order": 491, @@ -11656,7 +11672,7 @@ "name": "ArrowRightSmall", "prevSize": 32, "code": 60890, - "tempChar": "" + "tempChar": "" }, { "order": 493, @@ -11664,7 +11680,7 @@ "name": "ArrowUpSmall", "prevSize": 32, "code": 60891, - "tempChar": "" + "tempChar": "" }, { "order": 489, @@ -11672,7 +11688,7 @@ "name": "DismissSmall", "prevSize": 32, "code": 57610, - "tempChar": "" + "tempChar": "" }, { "order": 403, @@ -11680,7 +11696,7 @@ "name": "tl_fluent_videochat_20_regular", "prevSize": 32, "code": 59648, - "tempChar": "" + "tempChar": "" }, { "order": 397, @@ -11688,7 +11704,7 @@ "name": "ic_fluent_play_20_regular", "prevSize": 32, "code": 59240, - "tempChar": "" + "tempChar": "" }, { "order": 394, @@ -11696,7 +11712,7 @@ "name": "uniE769", "prevSize": 32, "code": 59241, - "tempChar": "" + "tempChar": "" }, { "order": 0, @@ -11768,7 +11784,7 @@ "name": "CancelSmall", "prevSize": 32, "code": 59711, - "tempChar": "" + "tempChar": "" }, { "order": 237, @@ -11776,7 +11792,7 @@ "name": "DownloadSmall", "prevSize": 32, "code": 59690, - "tempChar": "" + "tempChar": "" }, { "order": 0, @@ -11856,7 +11872,7 @@ "prevSize": 32, "code": 58880, "name": "uniE600", - "tempChar": "" + "tempChar": "" }, { "id": 9, @@ -11865,7 +11881,7 @@ "prevSize": 32, "code": 58881, "name": "uniE601", - "tempChar": "" + "tempChar": "" }, { "id": 10, @@ -11874,7 +11890,7 @@ "prevSize": 32, "code": 58882, "name": "uniE602", - "tempChar": "" + "tempChar": "" }, { "id": 11, @@ -11883,7 +11899,7 @@ "prevSize": 32, "code": 58883, "name": "uniE603", - "tempChar": "" + "tempChar": "" }, { "id": 12, @@ -11919,7 +11935,7 @@ "prevSize": 32, "code": 58885, "name": "uniE60B", - "tempChar": "" + "tempChar": "" }, { "id": 16, @@ -11928,7 +11944,7 @@ "prevSize": 32, "code": 58892, "name": "uniE60C", - "tempChar": "" + "tempChar": "" }, { "order": 235, @@ -11936,7 +11952,7 @@ "name": "Trending", "prevSize": 32, "code": 58893, - "tempChar": "" + "tempChar": "" }, { "id": 18, @@ -11980,7 +11996,7 @@ "prevSize": 32, "code": 58894, "name": "uniE611", - "tempChar": "" + "tempChar": "" }, { "id": 23, @@ -11989,7 +12005,7 @@ "prevSize": 32, "code": 58898, "name": "uniE612", - "tempChar": "" + "tempChar": "" }, { "order": 0, @@ -12087,7 +12103,7 @@ "prevSize": 32, "code": 59654, "name": "uniE906", - "tempChar": "" + "tempChar": "" }, { "id": 34, @@ -12105,7 +12121,7 @@ "prevSize": 32, "code": 59656, "name": "uniE908", - "tempChar": "" + "tempChar": "" }, { "id": 36, @@ -12123,7 +12139,7 @@ "prevSize": 32, "code": 59658, "name": "uniE90A", - "tempChar": "" + "tempChar": "" }, { "id": 38, @@ -12195,7 +12211,7 @@ "prevSize": 32, "code": 59666, "name": "uniE912", - "tempChar": "" + "tempChar": "" }, { "id": 46, @@ -12267,7 +12283,7 @@ "prevSize": 32, "code": 59674, "name": "uniE91A", - "tempChar": "" + "tempChar": "" }, { "id": 55, @@ -12303,7 +12319,7 @@ "prevSize": 32, "code": 942080, "name": "uE6000", - "tempChar": "" + "tempChar": "" } ], "id": 2, @@ -12317,6 +12333,38 @@ "height": 1024, "prevSize": 32, "icons": [ + { + "id": 173, + "paths": [ + "M365.901 109.906c9.996-9.996 26.203-9.993 36.2 0 9.998 9.997 9.998 26.203 0 36.2l-58.7 58.7h347.799c55.148 0 101.207 13.806 133.299 45.9s45.901 78.155 45.901 133.3v358.399c0 55.142-13.804 101.207-45.901 133.299-32.092 32.092-78.157 45.901-133.299 45.901h-358.399c-83.031 0-131.261-28.237-156.35-70.052-23.368-38.953-22.85-84.495-22.85-109.148v-358.399c0-14.138 11.462-25.599 25.6-25.6 14.138 0 25.6 11.462 25.6 25.6v358.399c0 26.542 0.529 57.815 15.55 82.852 13.316 22.18 41.905 45.148 112.45 45.148h358.399c47.252 0 77.993-11.796 97.101-30.899 19.103-19.103 30.899-49.848 30.899-97.101v-358.399c0-47.252-11.796-77.994-30.899-97.1-19.108-19.106-49.848-30.9-97.101-30.9h-347.799l58.7 58.7c9.998 9.997 9.998 26.203 0 36.2-9.998 9.987-26.206 9.994-36.2 0l-102.4-102.4c-9.987-9.995-9.983-26.204 0-36.2l102.4-102.4z", + "M647.501 442.706c9.999-9.998 26.199-9.998 36.198 0 9.994 9.997 9.994 26.203 0 36.2l-204.798 204.798c-9.997 9.994-26.203 9.994-36.2 0l-102.4-102.4c-9.997-9.999-9.994-26.199 0-36.198 9.997-9.999 26.203-9.999 36.2 0l84.3 84.301 186.7-186.701z" + ], + "attrs": [ + {}, + {} + ], + "grid": 0, + "tags": [ + "tl_fluent_poll_undo_20_regular" + ], + "isMulticolor": false, + "isMulticolor2": false + }, + { + "id": 172, + "paths": [ + "M768 204.8c84.833 0 153.6 68.769 153.6 153.6v307.2c0 84.833-68.767 153.6-153.6 153.6h-512c-84.831 0-153.6-68.767-153.6-153.6v-307.2c0-84.831 68.769-153.6 153.6-153.6h512zM256 256c-56.554 0-102.4 45.846-102.4 102.4v307.2c0 56.556 45.846 102.4 102.4 102.4h512c56.556 0 102.4-45.844 102.4-102.4v-307.2c0-56.554-45.844-102.4-102.4-102.4h-512zM640 665.6c14.136 0 25.6 11.464 25.6 25.6s-11.464 25.6-25.6 25.6c-14.136 0-25.6-11.464-25.6-25.6s11.464-25.6 25.6-25.6zM742.4 665.6c14.136 0 25.6 11.464 25.6 25.6s-11.464 25.6-25.6 25.6c-14.136 0-25.6-11.464-25.6-25.6s11.464-25.6 25.6-25.6zM223.35 308.2c11.896-3.399 24.25 2.188 29.75 12.75l1.9 4.8 102.4 358.4c3.882 13.588-3.967 27.756-17.55 31.652-11.896 3.395-24.251-2.191-29.75-12.754l-1.9-4.797-102.4-358.4-0.95-5.1c-0.919-11.873 6.61-23.143 18.5-26.55zM376.95 308.2c11.896-3.399 24.25 2.188 29.75 12.75l1.9 4.8 102.4 358.4c3.882 13.588-3.967 27.756-17.55 31.652-11.896 3.395-24.251-2.191-29.75-12.754l-1.9-4.797-102.4-358.4-0.95-5.1c-0.919-11.873 6.61-23.143 18.5-26.55z" + ], + "attrs": [ + {} + ], + "isMulticolor": false, + "isMulticolor2": false, + "grid": 0, + "tags": [ + "tl_copy_as_path_20_regular" + ] + }, { "id": 171, "paths": [ @@ -14689,7 +14737,7 @@ "metadata": { "fontFamily": "Telegram", "majorVersion": 2, - "minorVersion": 149 + "minorVersion": 151 }, "metrics": { "emSize": 1024, @@ -14714,9 +14762,7 @@ "name": "icomoon" }, "historySize": 100, - "quickUsageToken": { - "Telegram": "OGVhN2VmMzRiZCMxNzUxMzcxNDcyI2J5bDA3Qm1uT1FiVVVTTmM0OXRsVWdhOEZqWDVvSWViWDdxSEZiN3JhUndJ" - }, + "quickUsageToken": {}, "gridSize": 16 }, "uid": -1 diff --git a/Telegram/Assets/Fonts/Telegram.ttf b/Telegram/Assets/Fonts/Telegram.ttf index 262b03b34f..c5c887e931 100644 Binary files a/Telegram/Assets/Fonts/Telegram.ttf and b/Telegram/Assets/Fonts/Telegram.ttf differ diff --git a/Telegram/Collections/Experimental/ItemCacheManager.cs b/Telegram/Collections/Experimental/ItemCacheManager.cs index 90673c6e0e..f9b9bd5ed9 100644 --- a/Telegram/Collections/Experimental/ItemCacheManager.cs +++ b/Telegram/Collections/Experimental/ItemCacheManager.cs @@ -39,6 +39,8 @@ class ItemCacheManager internal ItemIndexRangeList _requests; internal ItemIndexRangeList _visibleRanges; + private ItemIndexRange[] _trackedRanges; + // list of ranges for items that are present in the cache private ItemIndexRangeList _cachedResults; // Range of items that is currently being requested @@ -93,7 +95,7 @@ public T this[int index] // iterates through the cache blocks to find the item foreach (CacheEntryBlock block in _cacheBlocks) { - if (index >= block.FirstIndex && index <= block.lastIndex) + if (index >= block.FirstIndex && index <= block.LastIndex) { return block.Items[index - block.FirstIndex]; } @@ -106,7 +108,7 @@ public T this[int index] for (int i = 0; i < _cacheBlocks.Count; i++) { CacheEntryBlock block = _cacheBlocks[i]; - if (index >= block.FirstIndex && index <= block.lastIndex) + if (index >= block.FirstIndex && index <= block.LastIndex) { block.Items[index - block.FirstIndex] = value; //register that we have the result in the cache @@ -132,17 +134,16 @@ private void AddOrExtendBlock(int index, T value, int insertBeforeBlock) if (insertBeforeBlock > 0) { CacheEntryBlock block = _cacheBlocks[insertBeforeBlock - 1]; - if (block.lastIndex == index - 1) + if (block.LastIndex == index - 1) { T[] newItems = new T[block.Length + 1]; Array.Copy(block.Items, newItems, (int)block.Length); newItems[block.Length] = value; - block.Length++; - block.Items = newItems; + _cacheBlocks[insertBeforeBlock - 1] = new CacheEntryBlock(block.FirstIndex, newItems); return; } } - CacheEntryBlock newBlock = new CacheEntryBlock() { FirstIndex = index, Length = 1, Items = new T[] { value } }; + CacheEntryBlock newBlock = new CacheEntryBlock(index, new T[] { value }); _cacheBlocks.Insert(insertBeforeBlock, newBlock); } @@ -159,74 +160,17 @@ public void UpdateRanges(ItemIndexRange visibleRange, ItemIndexRange[] ranges) // Fail fast if the ranges haven't changed if (!HasRangesChanged(ranges)) { return; } - //To make the cache update easier, we'll create a new set of CacheEntryBlocks - List> newCacheBlocks = new List>(); - foreach (ItemIndexRange range in ranges) - { - CacheEntryBlock newBlock = new CacheEntryBlock() { FirstIndex = range.FirstIndex, Length = range.Length, Items = new T[range.Length] }; - newCacheBlocks.Add(newBlock); - } - -#if TRACE_DATASOURCE - string s = "┌ " + debugName + ".UpdateRanges: "; - foreach (ItemIndexRange range in ranges) - { - s += range.FirstIndex + "->" + range.LastIndex + " "; - } - Debug.WriteLine(s); -#endif - //Copy over data to the new cache blocks from the old ones where there is overlap - int lastTransferred = 0; - for (int i = 0; i < ranges.Length; i++) - { - CacheEntryBlock newBlock = newCacheBlocks[i]; - ItemIndexRange range = ranges[i]; - int j = lastTransferred; - while (j < _cacheBlocks.Count && _cacheBlocks[j].FirstIndex <= ranges[i].LastIndex) - { - ItemIndexRange overlap, oldEntryRange; - ItemIndexRange[] added, removed; - CacheEntryBlock oldBlock = _cacheBlocks[j]; - oldEntryRange = new ItemIndexRange(oldBlock.FirstIndex, oldBlock.Length); - bool hasOverlap = oldEntryRange.DiffRanges(range, out overlap, out removed, out added); - if (hasOverlap) - { - Array.Copy(oldBlock.Items, overlap.FirstIndex - oldBlock.FirstIndex, newBlock.Items, overlap.FirstIndex - range.FirstIndex, (int)overlap.Length); -#if TRACE_DATASOURCE - Debug.WriteLine("│ Transfering cache items " + overlap.FirstIndex + "->" + overlap.LastIndex); -#endif - } - j++; - if (ranges.Length > i + 1 && oldBlock.lastIndex < ranges[i + 1].FirstIndex) { lastTransferred = j; } - } - } - //swap over to the new cache - _cacheBlocks = newCacheBlocks; - //figure out what items need to be fetched because we don't have them in the cache _visibleRanges = new ItemIndexRangeList(visibleRange); _requests = new ItemIndexRangeList(ranges); - ItemIndexRangeList newCachedResults = new ItemIndexRangeList(); + _trackedRanges = ranges; - // Use the previous knowlege of what we have cached to form the new list - foreach (ItemIndexRange range in ranges) - { - foreach (ItemIndexRange cached in _cachedResults) - { - ItemIndexRange overlap; - ItemIndexRange[] added, removed; - bool hasOverlap = cached.DiffRanges(range, out overlap, out removed, out added); - if (hasOverlap) { newCachedResults.Add(overlap); } - } - } - // remove the data we know we have cached from the results - foreach (ItemIndexRange range in newCachedResults) + foreach (CacheEntryBlock cached in _cacheBlocks) { - _requests.Subtract(range); + _requests.Subtract(cached); } - _cachedResults = newCachedResults; - startFetchData(); + StartFetchData(); #if TRACE_DATASOURCE s = "└ Pending requests: "; @@ -241,19 +185,21 @@ public void UpdateRanges(ItemIndexRange visibleRange, ItemIndexRange[] ranges) // Compares the new ranges against the previous ones to see if they have changed private bool HasRangesChanged(ItemIndexRange[] ranges) { - if (ranges.Length != _cacheBlocks.Count) + if (_trackedRanges?.Length != ranges.Length) { return true; } + for (int i = 0; i < ranges.Length; i++) { ItemIndexRange r = ranges[i]; - CacheEntryBlock block = _cacheBlocks[i]; - if (r.FirstIndex != block.FirstIndex || r.LastIndex != block.lastIndex) + ItemIndexRange block = _trackedRanges[i]; + if (r.FirstIndex != block.FirstIndex || r.LastIndex != block.LastIndex) { return true; } } + return false; } @@ -281,10 +227,9 @@ public ItemIndexRange GetFirstRequestBlock(int maxsize = 50) return null; } - // Throttling function for fetching data. Forces a wait of 20ms before making the request. // If another fetch is requested in that time, it will reset the timer, so we don't fetch data if the view is actively scrolling - public void startFetchData() + public void StartFetchData() { // Verify if an active request is still needed if (_requestInProgress != null) @@ -432,13 +377,15 @@ public int IndexOf(T value) } // Type for the cache blocks - class CacheEntryBlock + class CacheEntryBlock : ItemIndexRange { - public int FirstIndex; - public uint Length; - public ITEMTYPE[] Items; + public ITEMTYPE[] Items { get; } - public int lastIndex { get { return FirstIndex + (int)Length - 1; } } + public CacheEntryBlock(int firstIndex, ITEMTYPE[] items) + : base(firstIndex, (uint)items.Length) + { + Items = items; + } } } } diff --git a/Telegram/Collections/Experimental/ItemIndexRangeList.cs b/Telegram/Collections/Experimental/ItemIndexRangeList.cs index 0f1bf612f6..84947b1642 100644 --- a/Telegram/Collections/Experimental/ItemIndexRangeList.cs +++ b/Telegram/Collections/Experimental/ItemIndexRangeList.cs @@ -17,33 +17,33 @@ class ItemIndexRangeList : IList public ItemIndexRangeList() { - this._ranges = new List(); + _ranges = new List(); } public ItemIndexRangeList(ItemIndexRange range) { - this._ranges = new List(); - this._ranges.Add(range); + _ranges = new List(); + _ranges.Add(range); } public ItemIndexRangeList(List ranges) { - this._ranges = NormalizeRanges(ranges); + _ranges = NormalizeRanges(ranges); } public ItemIndexRangeList(ItemIndexRange[] ranges) { - this._ranges = NormalizeRanges(ranges); + _ranges = NormalizeRanges(ranges); } public List ToList() { - return this._ranges; + return _ranges; } public ItemIndexRange[] ToArray() { - return this._ranges.ToArray(); + return _ranges.ToArray(); } /// @@ -52,31 +52,31 @@ public ItemIndexRange[] ToArray() /// Range to merge into the collection public void Add(ItemIndexRange newrange) { - for (int i = 0; i < this._ranges.Count; i++) + for (int i = 0; i < _ranges.Count; i++) { - ItemIndexRange existing = this._ranges[i]; + ItemIndexRange existing = _ranges[i]; if (newrange.ContiguousOrOverlaps(existing)) { existing = existing.Combine(newrange); - for (int j = i + 1; j < this._ranges.Count; j++) + for (int j = i + 1; j < _ranges.Count; j++) { - ItemIndexRange next = this._ranges[j]; + ItemIndexRange next = _ranges[j]; if (existing.ContiguousOrOverlaps(next)) { existing = existing.Combine(next); - this._ranges.RemoveAt(i + 1); + _ranges.RemoveAt(i + 1); } } - this._ranges[i] = existing; + _ranges[i] = existing; return; } else if (newrange.LastIndex < existing.FirstIndex) { - this._ranges.Insert(i, newrange); + _ranges.Insert(i, newrange); return; } } - this._ranges.Add(newrange); + _ranges.Add(newrange); } /// @@ -84,7 +84,7 @@ public void Add(ItemIndexRange newrange) /// public void Add(uint FirstIndex, uint Length) { - this.Add(new ItemIndexRange((int)FirstIndex, Length)); + Add(new ItemIndexRange((int)FirstIndex, Length)); } /// @@ -92,9 +92,9 @@ public void Add(uint FirstIndex, uint Length) /// public void Subtract(ItemIndexRange range) { - for (int idx = 0; idx < this._ranges.Count; idx++) + for (int idx = 0; idx < _ranges.Count; idx++) { - ItemIndexRange existing = this._ranges[idx]; + ItemIndexRange existing = _ranges[idx]; if (existing.FirstIndex > range.LastIndex) return; int i, j; @@ -106,25 +106,25 @@ public void Subtract(ItemIndexRange range) if (existing.FirstIndex < i && existing.LastIndex > j) { //range is in the middle of existing range, so split existing into two - this._ranges[idx] = (new ItemIndexRange(existing.FirstIndex, (uint)(i - existing.FirstIndex))); - this._ranges.Insert(idx + 1, new ItemIndexRange(j + 1, (uint)(existing.LastIndex - j))); + _ranges[idx] = (new ItemIndexRange(existing.FirstIndex, (uint)(i - existing.FirstIndex))); + _ranges.Insert(idx + 1, new ItemIndexRange(j + 1, (uint)(existing.LastIndex - j))); return; } else if (existing.LastIndex > j) { //range ends before existing so trim existing to be the remainder - this._ranges[idx] = new ItemIndexRange(j + 1, (uint)(existing.LastIndex - j)); + _ranges[idx] = new ItemIndexRange(j + 1, (uint)(existing.LastIndex - j)); return; } else if (existing.FirstIndex < i) { //range starts after existing so trim existing to the part before range - this._ranges[idx] = new ItemIndexRange(existing.FirstIndex, (uint)(i - existing.FirstIndex)); + _ranges[idx] = new ItemIndexRange(existing.FirstIndex, (uint)(i - existing.FirstIndex)); } else { //existing is overlapped by range, so remove it. - this._ranges.RemoveAt(idx); + _ranges.RemoveAt(idx); idx--; } //trim the subtracted range to the remainder, and exit if complete @@ -139,12 +139,12 @@ public void Subtract(ItemIndexRange range) public void Subtract(uint FirstIndex, uint Length) { - this.Subtract(new ItemIndexRange((int)FirstIndex, Length)); + Subtract(new ItemIndexRange((int)FirstIndex, Length)); } public bool Intersects(ItemIndexRange range) { - foreach (ItemIndexRange r in this._ranges) + foreach (ItemIndexRange r in _ranges) { if (r.Intersects(range)) { diff --git a/Telegram/Collections/Experimental/MediaDataSource.cs b/Telegram/Collections/Experimental/MediaDataSource.cs index 3a1a1af5cf..f422c94877 100644 --- a/Telegram/Collections/Experimental/MediaDataSource.cs +++ b/Telegram/Collections/Experimental/MediaDataSource.cs @@ -1,15 +1,12 @@ using System; using System.Collections.Generic; using System.Collections.Specialized; -using System.Diagnostics; using System.Linq; using System.Threading; using System.Threading.Tasks; using Telegram.Services; using Telegram.Td.Api; using Telegram.ViewModels; -using Windows.Storage.Search; -using Windows.UI.Core; using Windows.UI.Xaml.Data; namespace Telegram.Collections @@ -124,7 +121,7 @@ public bool HasPositions { get { - if (_positions == null) + if (_positions == null || _positions.Count < 2) { return false; } @@ -135,18 +132,18 @@ public bool HasPositions public MessagePosition GetByDate(int targetDate) { - return _positions?.Values.LastOrDefault(x => x.Date <= targetDate); + return _positions?.Values.LastOrDefault(x => x.Date >= targetDate); } public MessagePosition GetByOffset(double offset) { offset = Math.Clamp(offset, 0, 1); - return _positions?.Values.LastOrDefault(x => x.Position <= offset * _positions.Count); + return _positions?.Values.LastOrDefault(x => x.Position <= offset * _count); } public MessagePosition GetByIndex(int targetIndex, out int index) { - if (_positions == null) + if (_positions == null || _positions.Count == 0) { index = -1; return null; @@ -198,7 +195,7 @@ public MessagePositionRange(long fromMessageId, int offset, int limit, int first private async Task GetPositionAsync(ItemIndexRange batch, bool retry) { var position = GetByIndex(batch.FirstIndex, out int index); - if (position == null || (position.Position < batch.FirstIndex && index == -1 && retry)) + if (_positions == null || (position?.Position < batch.FirstIndex && index == -1 && retry)) { await _gettingPositions.WaitAsync(); @@ -228,6 +225,11 @@ private async Task GetPositionAsync(ItemIndexRange batch, } } + if (position == null) + { + position = new MessagePosition(0, 0, 0); + } + var offset = batch.FirstIndex - position.Position; var limit = (int)batch.Length; @@ -327,42 +329,36 @@ private async Task> FetchDataCallback(ItemIndex _gettingPositions.Release(); var position = await GetPositionAsync(batch, true); - if (position.FromMessageId != 0) + + // Check if request has been cancelled, if so abort getting additional data + if (ct.IsCancellationRequested) { - // Check if request has been cancelled, if so abort getting additional data - if (ct.IsCancellationRequested) - { - return null; - } + return null; + } - MessageTopic messageTopic = null; - if (_savedMessagesTopicId != 0) - { - messageTopic = new MessageTopicSavedMessages(_savedMessagesTopicId); - } + MessageTopic messageTopic = null; + if (_savedMessagesTopicId != 0) + { + messageTopic = new MessageTopicSavedMessages(_savedMessagesTopicId); + } - var response = await _clientService.SendAsync(new SearchChatMessages(_chatId, messageTopic, string.Empty, null, position.FromMessageId, position.Offset, position.Limit, _filter)); - if (response is FoundChatMessages foundChatMessages) + var response = await _clientService.SendAsync(new SearchChatMessages(_chatId, messageTopic, string.Empty, null, position.FromMessageId, position.Offset, position.Limit, _filter)); + if (response is FoundChatMessages foundChatMessages) + { + for (int i = 0; i < foundChatMessages.Messages.Count; i++) { - for (int i = 0; i < foundChatMessages.Messages.Count; i++) + // Check if request has been cancelled, if so abort getting additional data + if (ct.IsCancellationRequested) { - // Check if request has been cancelled, if so abort getting additional data - if (ct.IsCancellationRequested) - { - return null; - } - - messages.Add(new MessageWithOwner(_clientService, foundChatMessages.Messages[i])); + return null; } - } - else - { - Logger.Info(response); + + messages.Add(new MessageWithOwner(_clientService, foundChatMessages.Messages[i])); } } else { - Debugger.Break(); + Logger.Info(response); } return new ItemCacheRange(position.FirstIndex, messages.Count, messages); diff --git a/Telegram/Collections/FlatteningCollection.cs b/Telegram/Collections/FlatteningCollection.cs index ae17fac0eb..319815d993 100644 --- a/Telegram/Collections/FlatteningCollection.cs +++ b/Telegram/Collections/FlatteningCollection.cs @@ -7,6 +7,7 @@ using System.Collections; using System.Collections.Generic; using System.Collections.Specialized; +using System.Diagnostics; namespace Telegram.Collections { @@ -66,12 +67,42 @@ private void OnCollectionChanged(object sender, NotifyCollectionChangedEventArgs case NotifyCollectionChangedAction.Remove: Remove(collection, e.OldStartingIndex, e.OldItems.Count); break; + case NotifyCollectionChangedAction.Replace: + Replace(collection, e.OldStartingIndex, e.OldItems[0], e.NewItems[0]); + break; case NotifyCollectionChangedAction.Reset: Reset(collection); break; } UpdateIndexes(collection); + Assert(); + } + + [Conditional("DEBUG")] + private void Assert() + { + var temp = new List(); + + foreach (var collection in _groups) + { + if (collection.Count > 0) + { + temp.Add(collection); + } + + foreach (var item in collection) + { + temp.Add(item); + } + } + + Debug.Assert(temp.Count == Count); + + for (int i = 0; i < Count; i++) + { + Debug.Assert(temp[i] == this[i]); + } } private void Insert(IKeyedCollection collection, int newStartingIndex, IList newItems) @@ -92,7 +123,7 @@ private void Remove(IKeyedCollection collection, int oldStartingIndex, int oldIt { for (int i = oldStartingIndex; i < oldStartingIndex + oldItemsCount; i++) { - RemoveAt(collection.TotalIndex + i); + RemoveAt(collection.TotalIndex + oldStartingIndex); } if (collection.Count == 0 && oldItemsCount > 0 && collection.Key != null) @@ -101,6 +132,11 @@ private void Remove(IKeyedCollection collection, int oldStartingIndex, int oldIt } } + private void Replace(IKeyedCollection collection, int oldStartingIndex, object oldItem, object newItem) + { + this[collection.TotalIndex + oldStartingIndex] = newItem; + } + private void Reset(IKeyedCollection collection) { var index = _groups.IndexOf(collection); diff --git a/Telegram/Collections/Handlers/SearchResultDiffHandler.cs b/Telegram/Collections/Handlers/SearchResultDiffHandler.cs index a4e9dabc47..c02e4836ac 100644 --- a/Telegram/Collections/Handlers/SearchResultDiffHandler.cs +++ b/Telegram/Collections/Handlers/SearchResultDiffHandler.cs @@ -13,7 +13,7 @@ public partial class SearchResultDiffHandler : IDiffHandler { public bool CompareItems(SearchResult oldItem, SearchResult newItem) { - if (oldItem.IsPublic != newItem.IsPublic) + if (oldItem.Type != newItem.Type) { return false; } diff --git a/Telegram/Collections/MvxObservableCollection.cs b/Telegram/Collections/MvxObservableCollection.cs index 54091f4624..7fb48a9620 100644 --- a/Telegram/Collections/MvxObservableCollection.cs +++ b/Telegram/Collections/MvxObservableCollection.cs @@ -73,6 +73,11 @@ public MvxObservableCollection(IDiffHandler diffHandler, DiffOptions options) { } + public MvxObservableCollection(IEnumerable items, IDiffHandler diffHandler, DiffOptions options) + : base(items, diffHandler, options) + { + } + /// /// Initializes a new instance of the class. /// @@ -119,7 +124,7 @@ public void RaiseCollectionChanged(NotifyCollectionChangedEventArgs args) /// /// The collection from which the items are copied. /// The items list is null. - public void AddRange(IEnumerable items) + public new void AddRange(IEnumerable items) { if (items == null) { @@ -169,12 +174,17 @@ public void InsertRange(int index, IList items) OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, changedItems: items, startingIndex: index)); } + public void ReplaceWith(IEnumerable items) + { + ReplaceWith(items.Cast()); + } + /// /// Replaces the current instance items with the ones specified in the items collection, raising a single event. /// /// The collection from which the items are copied. /// The items list is null. - public void ReplaceWith(IEnumerable items) + public void ReplaceWith(IEnumerable items) { if (items == null) { @@ -184,7 +194,7 @@ public void ReplaceWith(IEnumerable items) using (SuppressEvents()) { Clear(); - AddRange(items.Cast()); + AddRange(items); } OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); @@ -303,7 +313,7 @@ public void RemoveItems(IEnumerable items) /// The start index. /// The count of items to remove. /// Start index or count incorrect - public void RemoveRange(int start, int count) + public new void RemoveRange(int start, int count) { if (start < 0) { diff --git a/Telegram/Collections/SearchCollection.cs b/Telegram/Collections/SearchCollection.cs index 3741e47d50..22313310f3 100644 --- a/Telegram/Collections/SearchCollection.cs +++ b/Telegram/Collections/SearchCollection.cs @@ -41,10 +41,10 @@ public SearchCollection(Func factory, object sender, ID { _factory = factory; _sender = sender; - _query = new DebouncedProperty(Constants.TypingTimeout, UpdateQuery); + _query = new DebouncedPropertyWithToken(Constants.TypingTimeout, UpdateQuery); } - private readonly DebouncedProperty _query; + private readonly DebouncedPropertyWithToken _query; public string Query { get => _query; @@ -69,8 +69,13 @@ public void UpdateSender(object sender) Update(_factory((_sender = sender) ?? this, _query.Value)); } - public void UpdateQuery(string value) + public void UpdateQuery(string value, CancellationToken token = default) { + if (token.IsCancellationRequested) + { + return; + } + Update(_factory(_sender ?? this, _query.Value = value)); } diff --git a/Telegram/Collections/SynchronizedList.cs b/Telegram/Collections/SynchronizedList.cs index 197f1dfd42..351603b515 100644 --- a/Telegram/Collections/SynchronizedList.cs +++ b/Telegram/Collections/SynchronizedList.cs @@ -6,6 +6,7 @@ // using System.Collections.ObjectModel; using System.Collections.Specialized; +using System.Linq; namespace Telegram.Collections { @@ -17,8 +18,9 @@ public interface ISynchronizedList public partial class SynchronizedList : MvxObservableCollection, ISynchronizedList { private ObservableCollection _source; + private bool _reverse; - public void UpdateSource(ObservableCollection source) + public void UpdateSource(ObservableCollection source, bool reverse) { if (_source != null) { @@ -26,11 +28,12 @@ public void UpdateSource(ObservableCollection source) } _source = source; + _reverse = reverse; if (_source != null) { _source.CollectionChanged += OnCollectionChanged; - ReplaceWith(_source); + ReplaceWith(reverse ? _source.Reverse() : _source); } else { @@ -56,13 +59,13 @@ private void OnCollectionChanged(object sender, NotifyCollectionChangedEventArgs switch (e.Action) { case NotifyCollectionChangedAction.Add: - InsertRange(e.NewStartingIndex, e.NewItems); + InsertRange(_reverse ? Count - e.NewStartingIndex : e.NewStartingIndex, e.NewItems); break; case NotifyCollectionChangedAction.Remove: - RemoveRange(e.OldStartingIndex, e.OldItems.Count); + RemoveRange(_reverse ? Count - e.OldStartingIndex : e.OldStartingIndex, e.OldItems.Count); break; case NotifyCollectionChangedAction.Reset: - ReplaceWith(_source); + ReplaceWith(_reverse ? _source.Reverse() : _source); break; } } diff --git a/Telegram/Common/ApiInfo.cs b/Telegram/Common/ApiInfo.cs index 229f51d4a1..de9f6849ea 100644 --- a/Telegram/Common/ApiInfo.cs +++ b/Telegram/Common/ApiInfo.cs @@ -68,7 +68,7 @@ public static bool IsBuildOrGreater(ulong compare) public static NavigationCacheMode NavigationCacheMode => IsXbox ? NavigationCacheMode.Disabled : Constants.DEBUG - ? NavigationCacheMode.Required - : NavigationCacheMode.Required; + ? NavigationCacheMode.Enabled + : NavigationCacheMode.Enabled; } } diff --git a/Telegram/Common/CommonStyles.xaml b/Telegram/Common/CommonStyles.xaml index b3273c2b37..b28cbf0a43 100644 --- a/Telegram/Common/CommonStyles.xaml +++ b/Telegram/Common/CommonStyles.xaml @@ -3732,189 +3732,6 @@ - - - - + + + + diff --git a/Telegram/Controls/Messages/Content/PollOptionContent.xaml.cs b/Telegram/Controls/Messages/Content/PollOptionContent.xaml.cs new file mode 100644 index 0000000000..f8a63399d5 --- /dev/null +++ b/Telegram/Controls/Messages/Content/PollOptionContent.xaml.cs @@ -0,0 +1,289 @@ +// +// Copyright Fela Ameghino & Contributors 2015-2025 +// +// Distributed under the GNU General Public License v3.0. (See accompanying +// file LICENSE or copy at https://www.gnu.org/licenses/gpl-3.0.txt) +// +using Microsoft.UI.Xaml.Controls; +using System; +using System.Linq; +using Telegram.Assets.Icons; +using Telegram.Common; +using Telegram.Composition; +using Telegram.Navigation; +using Telegram.Td.Api; +using Telegram.ViewModels; +using Windows.UI.Composition; +using Windows.UI.Xaml; +using Windows.UI.Xaml.Automation; +using Windows.UI.Xaml.Controls; +using Windows.UI.Xaml.Hosting; +using Windows.UI.Xaml.Media; +using Windows.UI.Xaml.Shapes; + +namespace Telegram.Controls.Messages.Content +{ + public sealed partial class PollOptionContent : ToggleButtonEx + { + private bool _allowToggle; + + private MessageViewModel _message; + private Poll _poll; + private PollOption _option; + + public PollOptionContent() + { + DefaultStyleKey = typeof(PollOptionContent); + + Connected += OnLoaded; + Disconnected += OnUnloaded; + } + + #region InitializeComponent + + private Ellipse Ellipse; + private Microsoft.UI.Xaml.Controls.ProgressRing Loading; + private TextBlock Percentage; + private FormattedTextBlock TextText; + private Grid Tick; + private Ellipse Zero; + private Windows.UI.Xaml.Controls.ProgressBar Votes; + private Border VotesLine; + private Border CheckmarkIcon; + + private bool _templateApplied; + + protected override void OnApplyTemplate() + { + Ellipse = GetTemplateChild(nameof(Ellipse)) as Ellipse; + Loading = GetTemplateChild(nameof(Loading)) as Microsoft.UI.Xaml.Controls.ProgressRing; + Percentage = GetTemplateChild(nameof(Percentage)) as TextBlock; + TextText = GetTemplateChild(nameof(TextText)) as FormattedTextBlock; + Tick = GetTemplateChild(nameof(Tick)) as Grid; + Zero = GetTemplateChild(nameof(Zero)) as Ellipse; + Votes = GetTemplateChild(nameof(Votes)) as Windows.UI.Xaml.Controls.ProgressBar; + VotesLine = GetTemplateChild(nameof(VotesLine)) as Border; + CheckmarkIcon = GetTemplateChild(nameof(CheckmarkIcon)) as Border; + + TextText.TextEntityClick += TextText_TextEntityClick; + + _templateApplied = true; + + if (_message != null && _poll != null && _option != null) + { + UpdatePollOption(_message, _poll, _option); + } + } + + #endregion + + private void OnLoaded(object sender, RoutedEventArgs e) + { + _selectionStrokeBrush?.Register(); + } + + private void OnUnloaded(object sender, RoutedEventArgs e) + { + _selectionStrokeBrush?.Unregister(); + } + + private void TextText_TextEntityClick(object sender, TextEntityClickEventArgs e) + { + MessageBubble.TextEntityClick(_message, TextText, e); + } + + public PollOption Option { get; private set; } + + private long _chatId; + private long _messageId; + private int _optionId; + + public void UpdatePollOption(MessageViewModel message, Poll poll, PollOption option) + { + _message = message; + _poll = poll; + _option = option; + + if (!_templateApplied) + { + return; + } + + var optionId = poll.Options.IndexOf(option); + var recycled = _chatId == message.ChatId + && _messageId == message.Id + && _optionId == optionId; + + var results = poll.IsClosed || poll.Options.Any(x => x.IsChosen); + var correct = poll.Type is PollTypeQuiz quiz && quiz.CorrectOptionId == poll.Options.IndexOf(option); + + var votes = Locale.Declension(poll.Type is PollTypeQuiz ? Strings.R.Answer : Strings.R.Vote, option.VoterCount); + + Option = option; + IsThreeState = results; + + if (results || !recycled) + { + IsChecked = results ? null : new bool?(false); + } + + _allowToggle = poll.Type is PollTypeRegular regular && regular.AllowMultipleAnswers && !results; + + if (_allowToggle) + { + CreateIcon(); + } + else + { + UpdateIcon(false, false); + CheckmarkIcon.Visibility = Visibility.Collapsed; + } + + Ellipse.Opacity = results || option.IsBeingChosen ? 0 : 1; + + Percentage.Visibility = results ? Visibility.Visible : Visibility.Collapsed; + Percentage.Text = $"{option.VotePercentage}%"; + + Extensions.SetToolTip(Percentage, results ? votes : null); + + TextText.SetText(message.ClientService, option.Text); + + Zero.Visibility = results ? Visibility.Visible : Visibility.Collapsed; + + Votes.Maximum = results ? Math.Max(poll.Options.Max(x => x.VoterCount), 1) : 1; + Votes.Value = results ? option.VoterCount : 0; + VotesLine.Opacity = results ? 0 : 0.3; + + Loading.IsActive = option.IsBeingChosen; + + Tick.Visibility = (results && correct) || option.IsChosen ? Visibility.Visible : Visibility.Collapsed; + + if (option.IsChosen && poll.Type is PollTypeQuiz) + { + VisualStateManager.GoToState(this, correct ? "Correct" : "Wrong", false); + } + else + { + VisualStateManager.GoToState(this, "Unknown", false); + } + + if (results) + { + AutomationProperties.SetName(this, $"{option.Text.Text}, {votes}, {option.VotePercentage}%"); + } + else + { + AutomationProperties.SetName(this, option.Text.Text); + } + + _chatId = message.ChatId; + _messageId = message.Id; + _optionId = optionId; + } + + protected override void OnToggle() + { + if (_allowToggle) + { + base.OnToggle(); + UpdateIcon(IsChecked is true, true); + } + } + + private void CreateIcon() + { + if (_source != null) + { + CheckmarkIcon.Visibility = Visibility.Visible; + return; + } + + var visual = GetVisual(BootStrapper.Current.Compositor, out var source, out _props); + + _source = source; + _previous = visual; + _selectionStrokeBrush = new CompositionVisualColorSource(SelectionStroke, source, "Color_FF0000", IsConnected); + + ElementCompositionPreview.SetElementChildVisual(CheckmarkIcon, visual?.RootVisual); + } + + private void UpdateIcon(bool selected, bool animate) + { + if (_props != null && _previous != null) + { + if (animate) + { + var linearEasing = _props.Compositor.CreateLinearEasingFunction(); + var animation = _props.Compositor.CreateScalarKeyFrameAnimation(); + animation.Duration = _previous.Duration; + animation.InsertKeyFrame(1, selected ? 1 : 0, linearEasing); + + _props.StartAnimation("Progress", animation); + } + else + { + _props.InsertScalar("Progress", selected ? 1.0F : 0.0F); + } + } + } + + // This should be held in memory, or animation will stop + private CompositionPropertySet _props; + + private IAnimatedVisual _previous; + private IAnimatedVisualSource2 _source; + + private IAnimatedVisual GetVisual(Compositor compositor, out IAnimatedVisualSource2 source, out CompositionPropertySet properties) + { + source = new ChecklistSelect(); + + if (source == null) + { + properties = null; + return null; + } + + var visual = source.TryCreateAnimatedVisual(compositor, out _); + if (visual == null) + { + properties = null; + return null; + } + + properties = compositor.CreatePropertySet(); + properties.InsertScalar("Progress", 0.0F); + + var progressAnimation = compositor.CreateExpressionAnimation("_.Progress"); + progressAnimation.SetReferenceParameter("_", properties); + visual.RootVisual.Properties.InsertScalar("Progress", 0.0F); + visual.RootVisual.Properties.StartAnimation("Progress", progressAnimation); + + return visual; + } + + #region SelectionStroke + + private CompositionVisualColorSource _selectionStrokeBrush; + + public SolidColorBrush SelectionStroke + { + get { return (SolidColorBrush)GetValue(SelectionStrokeProperty); } + set { SetValue(SelectionStrokeProperty, value); } + } + + public static readonly DependencyProperty SelectionStrokeProperty = + DependencyProperty.Register("SelectionStroke", typeof(SolidColorBrush), typeof(PollOptionContent), new PropertyMetadata(null, OnSelectionStrokeChanged)); + + private static void OnSelectionStrokeChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + ((PollOptionContent)d).OnSelectionStrokeChanged(e.NewValue as SolidColorBrush, e.OldValue as SolidColorBrush); + } + + private void OnSelectionStrokeChanged(SolidColorBrush newValue, SolidColorBrush oldValue) + { + _selectionStrokeBrush?.PropertyChanged(newValue, IsConnected); + } + + #endregion + } +} diff --git a/Telegram/Controls/Messages/MessageBubble.xaml.cs b/Telegram/Controls/Messages/MessageBubble.xaml.cs index 9816086c8a..63d95c583d 100644 --- a/Telegram/Controls/Messages/MessageBubble.xaml.cs +++ b/Telegram/Controls/Messages/MessageBubble.xaml.cs @@ -112,10 +112,14 @@ public bool HasFloatingElements } } - public void UpdateQuery(string text) + public void UpdateQuery(string text, bool invalidate = true) { _query = text; - Message?.SetQuery(text); + + if (invalidate) + { + Message?.SetQuery(text); + } } #region InitializeComponent @@ -224,7 +228,7 @@ public void Recycle() public void UpdateMessage(MessageViewModel message) { - if (_message?.Id != message?.Id && Message != null) + if (Message != null && (_message?.Id != message?.Id || _message?.ChatId != message?.ChatId)) { Message.IgnoreSpoilers = false; } @@ -470,18 +474,21 @@ public void UpdateAttach(MessageViewModel message) var bottomOutgoing = false; var bottomIncoming = false; + var isFirst = message.Delegate.IsSavedMessagesTab ? message.IsLast : message.IsFirst; + var isLast = message.Delegate.IsSavedMessagesTab ? message.IsFirst : message.IsLast; + var outgoing = (message.IsOutgoing && !message.IsChannelPost) || (message.IsSaved && message.ForwardInfo?.Source is { IsOutgoing: true }); if (outgoing) { - if (message.IsFirst && message.IsLast) + if (isFirst && isLast) { bottomOutgoing = SettingsService.Current.Diagnostics.BubbleTailDebug; } - else if (message.IsFirst) + else if (isFirst) { bottomRight = small; } - else if (message.IsLast) + else if (isLast) { topRight = small; bottomOutgoing = SettingsService.Current.Diagnostics.BubbleTailDebug; @@ -494,15 +501,15 @@ public void UpdateAttach(MessageViewModel message) } else { - if (message.IsFirst && message.IsLast) + if (isFirst && isLast) { bottomIncoming = SettingsService.Current.Diagnostics.BubbleTailDebug; } - else if (message.IsFirst) + else if (isFirst) { bottomLeft = small; } - else if (message.IsLast) + else if (isLast) { topLeft = small; bottomIncoming = SettingsService.Current.Diagnostics.BubbleTailDebug; @@ -549,7 +556,7 @@ public void UpdateAttach(MessageViewModel message) if (message.Delegate != null && message.Delegate.IsDialog) { - var top = message.IsFirst ? 4 : 2; + var top = isFirst ? 4 : 2; var action = message.IsSaved || message.CanBeShared; if (message.IsSaved || (chat != null && (chat.Type is ChatTypeBasicGroup || chat.Type is ChatTypeSupergroup)) && !message.IsChannelPost) @@ -654,7 +661,8 @@ private void UpdatePhoto(MessageViewModel message) { if (message.HasSenderPhoto) { - if (message.IsLast) + var isLast = message.Delegate.IsSavedMessagesTab ? message.IsFirst : message.IsLast; + if (isLast) { if (message.Id != _photoId || PhotoRoot == null || PhotoRoot.Visibility == Visibility.Collapsed) { @@ -927,7 +935,9 @@ or MessageBigEmoji var header = false; var forward = false; - if (!light && message.IsFirst && (message.IsSaved || message.IsVerificationCode) && !outgoing) + var isFirst = message.Delegate.IsSavedMessagesTab ? message.IsLast : message.IsFirst; + + if (!light && isFirst && (message.IsSaved || message.IsVerificationCode) && !outgoing) { var title = string.Empty; var foreground = default(SolidColorBrush); @@ -972,7 +982,7 @@ or MessageBigEmoji HeaderLinkRun.Text = title; Identity.ClearStatus(); } - else if (!light && message.IsFirst && !outgoing && (message.HasSenderPhoto || (!message.IsChannelPost && !message.IsDirectMessagesChatTopicMessage)) && (chat.Type is ChatTypeBasicGroup || chat.Type is ChatTypeSupergroup)) + else if (!light && isFirst && !outgoing && (message.HasSenderPhoto || (!message.IsChannelPost && !message.IsDirectMessagesChatTopicMessage)) && (chat.Type is ChatTypeBasicGroup || chat.Type is ChatTypeSupergroup)) { if (message.ClientService.TryGetUser(message.SenderId, out User senderUser)) { @@ -1441,8 +1451,7 @@ public void UpdateMessageInteractionInfo(MessageViewModel message) } else { - RecentRepliers.Items.Clear(); - RecentRepliers.Items.AddRange(info.RecentReplierIds); + RecentRepliers.Items.ReplaceWith(info.RecentReplierIds); } _recentRepliersChatId = message.ChatId; @@ -1580,11 +1589,13 @@ public void UpdateMessageContent(MessageViewModel message) var top = 0; var bottom = 0; - if (message.IsFirst && !outgoing && !message.IsChannelPost && (chat.Type is ChatTypeBasicGroup || chat.Type is ChatTypeSupergroup)) + var isFirst = message.Delegate.IsSavedMessagesTab ? message.IsLast : message.IsFirst; + + if (isFirst && !outgoing && !message.IsChannelPost && (chat.Type is ChatTypeBasicGroup || chat.Type is ChatTypeSupergroup)) { top = 4; } - if (message.IsFirst && message.IsSaved) + if (isFirst && message.IsSaved) { top = 4; } @@ -1713,7 +1724,7 @@ public void UpdateMessageContent(MessageViewModel message) Panel.Children.Insert(0, new MessageFactCheck(message)); } - if (Media.Child is IContent media && media.IsValid(content, true)) + if (Media.Child is IContent media) { if (media.IsValid(content, true)) { @@ -1726,137 +1737,50 @@ public void UpdateMessageContent(MessageViewModel message) } } - if (Media.Child is StickerContent or VideoNoteContent) - { - UpdateAttach(message); - } + //if (Media.Child is StickerContent or VideoNoteContent) + //{ + // UpdateAttach(message); + //} - if (content is MessageText textMessage && textMessage.LinkPreview != null) - { - Media.Child = new WebPageContent(message); - } - else if (content is MessageAlbum) - { - Media.Child = new AlbumContent(message); - } - else if (content is MessagePaidAlbum) - { - Media.Child = new PaidMediaContent(message); - } - else if (content is MessageAnimation) - { - Media.Child = new AnimationContent(message); - } - else if (content is MessageAudio) - { - Media.Child = new AudioContent(message); - } - else if (content is MessageCall or MessageGroupCall) - { - Media.Child = new CallContent(message); - } - else if (content is MessageContact) - { - Media.Child = new ContactContent(message); - } - else if (content is MessageDice) - { - Media.Child = new DiceContent(message); - } - else if (content is MessageDocument) - { - Media.Child = new DocumentContent(message); - } - else if (content is MessageGame) - { - Media.Child = new GameContent(message); - } - else if (content is MessageInvoice invoice) - { - if (invoice.PaidMedia is PaidMediaPhoto) - { - Media.Child = new PhotoContent(message); - } - else if (invoice.PaidMedia is PaidMediaVideo) - { - Media.Child = new VideoContent(message); - } - else if (invoice.PaidMedia is PaidMediaPreview) - { - Media.Child = new InvoicePreviewContent(message); - } - else if (invoice.ProductInfo.Photo != null) - { - Media.Child = new InvoicePhotoContent(message); - } - else - { - Media.Child = new InvoiceContent(message); - } - } - else if (content is MessageLocation) - { - Media.Child = new LocationContent(message); - } - else if (content is MessagePhoto) - { - Media.Child = new PhotoContent(message); - } - else if (content is MessagePoll) - { - Media.Child = new PollContent(message); - } - else if (content is MessageChecklist) - { - Media.Child = new ChecklistContent(message); - } - else if (content is MessageSticker) - { - Media.Child = new StickerContent(message); - } - else if (content is MessageVenue) - { - Media.Child = new VenueContent(message); - } - else if (content is MessageVideo) - { - Media.Child = new VideoContent(message); - } - else if (content is MessageVideoNote) - { - Media.Child = new VideoNoteContent(message); - } - else if (content is MessageVoiceNote) - { - Media.Child = new VoiceNoteContent(message); - } - else if (content is MessageGiveaway or MessageGiveawayWinners) - { - Media.Child = new GiveawayContent(message); - } - else if (content is MessageAsyncStory story && story.State != MessageStoryState.Expired) - { - Media.Child = new AspectView + Media.Child = content switch + { + MessageText textMessage when textMessage.LinkPreview != null => new WebPageContent(message), + MessageAlbum => new AlbumContent(message), + MessagePaidAlbum => new PaidMediaContent(message), + MessageAnimation => new AnimationContent(message), + MessageAudio => new AudioContent(message), + MessageCall or MessageGroupCall => new CallContent(message), + MessageContact => new ContactContent(message), + MessageDice => new DiceContent(message), + MessageDocument => new DocumentContent(message), + MessageGame => new GameContent(message), + MessageInvoice invoice when invoice.PaidMedia is PaidMediaPhoto => new PhotoContent(message), + MessageInvoice invoice when invoice.PaidMedia is PaidMediaVideo => new VideoContent(message), + MessageInvoice invoice when invoice.PaidMedia is PaidMediaPreview => new InvoicePreviewContent(message), + MessageInvoice invoice when invoice.ProductInfo.Photo != null => new InvoicePhotoContent(message), + MessageInvoice => new InvoiceContent(message), + MessageLocation => new LocationContent(message), + MessagePhoto => new PhotoContent(message), + MessagePoll => new PollContent(message), + MessageChecklist => new ChecklistContent(message), + MessageSticker => new StickerContent(message), + MessageVenue => new VenueContent(message), + MessageVideo => new VideoContent(message), + MessageVideoNote => new VideoNoteContent(message), + MessageVoiceNote => new VoiceNoteContent(message), + MessageGiveaway or MessageGiveawayWinners => new GiveawayContent(message), + MessageAsyncStory story when story.State != MessageStoryState.Expired => new AspectView { Constraint = message - }; - } - else if (content is MessageAnimatedEmoji) - { - Media.Child = new Border + }, + MessageAnimatedEmoji => new Border { Width = 180 * message.ClientService.Config.GetNamedNumber("emojies_animated_zoom", 0.625f), Height = 180 * message.ClientService.Config.GetNamedNumber("emojies_animated_zoom", 0.625f) - }; - } - else if (content is MessageUnsupported) - { - Media.Child = new UnsupportedContent(message); - } - else - { - Media.Child = null; - } + }, + MessageUnsupported => new UnsupportedContent(message), + _ => null + }; } public IPlayerView GetPlaybackElement() diff --git a/Telegram/Controls/Messages/MessageFooter.xaml.cs b/Telegram/Controls/Messages/MessageFooter.xaml.cs index f9d114bdae..f02b1fce85 100644 --- a/Telegram/Controls/Messages/MessageFooter.xaml.cs +++ b/Telegram/Controls/Messages/MessageFooter.xaml.cs @@ -435,7 +435,8 @@ public void UpdateMessageState(MessageViewModel message) return; } - if (message.IsOutgoing && !message.IsChannelPost && !message.IsSaved) + var outgoing = (message.IsOutgoing && !message.IsChannelPost) || (message.IsSaved && message.ForwardInfo?.Source is { IsOutgoing: true }); + if (outgoing) { var maxId = message.LastReadOutboxMessageId; var messageHash = message.ChatId ^ message.Id; diff --git a/Telegram/Controls/Messages/MessageReferenceBase.cs b/Telegram/Controls/Messages/MessageReferenceBase.cs index e3d51131cb..4c8a3d2c82 100644 --- a/Telegram/Controls/Messages/MessageReferenceBase.cs +++ b/Telegram/Controls/Messages/MessageReferenceBase.cs @@ -22,6 +22,8 @@ namespace Telegram.Controls.Messages { public abstract class MessageReferenceBase : HyperlinkButton { + protected MessageComposerHeader _composerHeader; + protected MessageViewModel _messageReply; protected MessageViewModel _message; @@ -34,26 +36,14 @@ public MessageReferenceBase() { } - public long MessageId { get; private set; } + public MessageViewModel Message { get; private set; } #region Message - public object Message + public void UpdateComposerHeader(MessageComposerHeader embedded) { - get => GetValue(MessageProperty); - set => SetValue(MessageProperty, value); - } - - public static readonly DependencyProperty MessageProperty = - DependencyProperty.Register("Message", typeof(object), typeof(MessageReferenceBase), new PropertyMetadata(null, OnMessageChanged)); + _composerHeader = embedded; - private static void OnMessageChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) - { - ((MessageReferenceBase)d).OnMessageChanged(e.NewValue as MessageComposerHeader); - } - - protected void OnMessageChanged(MessageComposerHeader embedded) - { if (embedded == null || !_templateApplied) { return; @@ -61,7 +51,7 @@ protected void OnMessageChanged(MessageComposerHeader embedded) if (embedded.LinkPreview != null && !embedded.LinkPreviewDisabled) { - MessageId = 0; + Message = null; Visibility = Visibility.Visible; HideThumbnail(); @@ -89,12 +79,12 @@ protected void OnMessageChanged(MessageComposerHeader embedded) } else if (embedded.EditingMessage != null) { - MessageId = embedded.EditingMessage.Id; + Message = embedded.EditingMessage; GetMessageTemplate(embedded.EditingMessage, null, false, Strings.Edit, true, false, false); } else if (embedded.ReplyToMessage != null) { - MessageId = embedded.ReplyToMessage.Id; + Message = embedded.ReplyToMessage; GetMessageTemplate(embedded.ReplyToMessage, embedded.ReplyToQuote?.Text, false, embedded.ReplyToQuote != null ? Strings.ReplyToQuote : Strings.ReplyTo, true, false, false); } } @@ -167,11 +157,12 @@ public void UpdateMessage(MessageViewModel message, bool loading, string title) if (loading) { + Message = null; SetLoadingTemplate(message, null, title, true, false); } else { - MessageId = message.Id; + Message = message; GetMessageTemplate(message, null, false, title, true, false, message.ForwardInfo != null); } } @@ -868,28 +859,14 @@ private void SetVideoNoteTemplate(MessageViewModel message, MessageSender sender private void SetAnimatedEmojiTemplate(MessageViewModel message, MessageSender sender, MessageAnimatedEmoji animatedEmoji, string title, bool outgoing, bool white) { - if (animatedEmoji.AnimatedEmoji?.Sticker?.FullType is StickerFullTypeCustomEmoji) - { - SetText(message, - outgoing, - sender, - title, - string.Empty, - animatedEmoji.AnimatedEmoji.Sticker.ToFormattedText(), - false, - white); - } - else - { - SetText(message, - outgoing, - sender, - title, - animatedEmoji.Emoji, - null, - false, - white); - } + SetText(message, + outgoing, + sender, + title, + string.Empty, + null, + false, + white); HideThumbnail(); } diff --git a/Telegram/Controls/Messages/MessageReply.xaml b/Telegram/Controls/Messages/MessageReply.xaml index 59649f1a76..4214414833 100644 --- a/Telegram/Controls/Messages/MessageReply.xaml +++ b/Telegram/Controls/Messages/MessageReply.xaml @@ -117,9 +117,10 @@ diff --git a/Telegram/Controls/Messages/MessageReply.xaml.cs b/Telegram/Controls/Messages/MessageReply.xaml.cs index 2f94564212..57e0229cb2 100644 --- a/Telegram/Controls/Messages/MessageReply.xaml.cs +++ b/Telegram/Controls/Messages/MessageReply.xaml.cs @@ -215,9 +215,9 @@ protected override void OnApplyTemplate() { UpdateMessage(_message, _loading, _title); } - else if (Message != null) + else if (_composerHeader != null) { - OnMessageChanged(Message as MessageComposerHeader); + UpdateComposerHeader(_composerHeader); } } diff --git a/Telegram/Controls/Messages/MessageSelector.xaml.cs b/Telegram/Controls/Messages/MessageSelector.xaml.cs index 72357e65de..cd6045e93e 100644 --- a/Telegram/Controls/Messages/MessageSelector.xaml.cs +++ b/Telegram/Controls/Messages/MessageSelector.xaml.cs @@ -427,11 +427,11 @@ private IAnimatedVisual GetVisual(Compositor compositor, out IAnimatedVisualSour } properties = compositor.CreatePropertySet(); - properties.InsertScalar("Progress", 1.0F); + properties.InsertScalar("Progress", 0.0F); var progressAnimation = compositor.CreateExpressionAnimation("_.Progress"); progressAnimation.SetReferenceParameter("_", properties); - visual.RootVisual.Properties.InsertScalar("Progress", 1.0F); + visual.RootVisual.Properties.InsertScalar("Progress", 0.0F); visual.RootVisual.Properties.StartAnimation("Progress", progressAnimation); return visual; diff --git a/Telegram/Controls/Messages/MessageService.cs b/Telegram/Controls/Messages/MessageService.cs index d2824f2e9d..85eb3fe059 100644 --- a/Telegram/Controls/Messages/MessageService.cs +++ b/Telegram/Controls/Messages/MessageService.cs @@ -691,7 +691,7 @@ private static FormattedText UpdateHeaderDate(MessageWithOwner message, MessageH return Strings.MessageScheduledUntilOnline.AsFormattedText(); } - return Formatter.DayGrouping(Formatter.ToLocalTime(message.Date)).AsFormattedText(); + return Formatter.DayGrouping(Formatter.ToLocalTime(headerDate.Date)).AsFormattedText(); } #endregion @@ -2247,7 +2247,7 @@ private static FormattedText UpdatePinMessage(MessageWithOwner message, MessageP } else if (reply.Content is MessageText text) { - var mess = text.Text.ReplaceSpoilers(); + var mess = text.Text.Clone(); if (mess.Text.Length > 20) { mess = TdExtensions.Concat(mess.Substring(0, 20), "...".AsFormattedText()); diff --git a/Telegram/Controls/Messages/ReactionButton.cs b/Telegram/Controls/Messages/ReactionButton.cs index c747da10a7..e11192b79a 100644 --- a/Telegram/Controls/Messages/ReactionButton.cs +++ b/Telegram/Controls/Messages/ReactionButton.cs @@ -178,8 +178,7 @@ protected virtual void UpdateInteraction(MessageViewModel message, MessageReacti } else { - destination.Clear(); - destination.AddRange(origin); + destination.ReplaceWith(origin); } if (Count != null) diff --git a/Telegram/Controls/PatternBackground.xaml.cs b/Telegram/Controls/PatternBackground.xaml.cs index 98ae793fa6..a9c22f9b36 100644 --- a/Telegram/Controls/PatternBackground.xaml.cs +++ b/Telegram/Controls/PatternBackground.xaml.cs @@ -1,7 +1,10 @@ using Microsoft.Graphics.Canvas.Effects; using Microsoft.UI.Xaml.Media; +using System; +using System.Collections.Generic; using System.Numerics; using Telegram.Common; +using Telegram.Native; using Telegram.Navigation; using Telegram.Services; using Telegram.Streams; @@ -15,6 +18,206 @@ namespace Telegram.Controls { + // TODO: Rewrite + public partial class ProfileHeaderPattern : Control + { + public ProfileHeaderPattern() + { + DefaultStyleKey = typeof(ProfileHeaderPattern); + } + + protected override void OnApplyTemplate() + { + var animated = GetTemplateChild("Animated") as AnimatedImage; + var layoutRoot = GetTemplateChild("LayoutRoot") as Border; + + animated.Ready += OnReady; + + var visual = ElementComposition.GetElementVisual(animated); + var compositor = visual.Compositor; + + // Create a VisualSurface positioned at the same location as this control and feed that + // through the color effect. + var surfaceBrush = compositor.CreateSurfaceBrush(); + var surface = compositor.CreateVisualSurface(); + + // Select the source visual and the offset/size of this control in that element's space. + surface.SourceVisual = visual; + surface.SourceOffset = new Vector2(0, 0); + surface.SourceSize = new Vector2(37, 37); + surfaceBrush.HorizontalAlignmentRatio = 0.5f; + surfaceBrush.VerticalAlignmentRatio = 0.5f; + surfaceBrush.Surface = surface; + surfaceBrush.Stretch = CompositionStretch.Fill; + surfaceBrush.BitmapInterpolationMode = CompositionBitmapInterpolationMode.NearestNeighbor; + surfaceBrush.SnapToPixels = true; + + var container = compositor.CreateContainerVisual(); + container.Size = new Vector2(1000, 320); + + var clones = Generate(0); + + for (int i = 1; i < clones.Count; i++) + { + Vector4 clone = clones[i]; + + var redirect = compositor.CreateSpriteVisual(); + redirect.Size = new Vector2(clone.Z); + redirect.Offset = new Vector3(clone.X, clone.Y, 0); + redirect.CenterPoint = new Vector3(clone.Z / 2); + redirect.Opacity = clone.W; + redirect.Brush = surfaceBrush; + + container.Children.InsertAtTop(redirect); + } + + ElementCompositionPreview.SetElementChildVisual(layoutRoot, container); + } + + private void OnReady(object sender, EventArgs e) + { + var layoutRoot = GetTemplateChild("LayoutRoot") as Border; + var container = ElementCompositionPreview.GetElementChildVisual(layoutRoot) as ContainerVisual; + + var scale = container.Compositor.CreateVector3KeyFrameAnimation(); + scale.InsertKeyFrame(0, Vector3.Zero); + scale.InsertKeyFrame(1, Vector3.One); + + var batch = container.Compositor.CreateScopedBatch(CompositionBatchTypes.Animation); + + foreach (var redirect in container.Children) + { + redirect.StartAnimation("Scale", scale); + } + + batch.End(); + } + + public void Update(float avatarTransitionFraction) + { + var layoutRoot = GetTemplateChild("LayoutRoot") as Border; + var container = ElementCompositionPreview.GetElementChildVisual(layoutRoot) as ContainerVisual; + + var clones = Generate(avatarTransitionFraction); + var i = 0; + + foreach (var redirect in container.Children) + { + Vector4 clone = clones[i++]; + + redirect.Size = new Vector2(clone.Z); + redirect.Offset = new Vector3(clone.X, clone.Y, 0); + redirect.Opacity = clone.W; + } + } + + private float windowFunction(float t) + { + return BezierPoint.Calculate(0.6f, 0.0f, 0.4f, 1.0f, t); + } + + private float patternScaleValueAt(float fraction, float t, bool reverse) + { + float windowSize = 0.8f; + + float effectiveT; + float windowStartOffset; + float windowEndOffset; + if (reverse) + { + effectiveT = 1.0f - t; + windowStartOffset = 1.0f; + windowEndOffset = -windowSize; + } + else + { + effectiveT = t; + windowStartOffset = -0.3f; + windowEndOffset = 1.0f; + } + + float windowPosition = (1.0f - fraction) * windowStartOffset + fraction * windowEndOffset; + float windowT = MathF.Max(0.0f, MathF.Min(windowSize, effectiveT - windowPosition)) / windowSize; + float localT = 1.0f - windowFunction(t: windowT); + + return localT; + } + + private IList Generate(float avatarTransitionFraction) + { + var results = new List(); + + var avatarPatternFrame = new Vector2(1000 - 36, 86 + 36 * 2); + //var avatarPatternFrame = new Vector2(500, 500); + + var lokiRng = new LokiRng(seed0: 123, seed1: 0, seed2: 0); + var numRows = 5; + + for (int row = 0; row < numRows; row++) + { + int avatarPatternCount = 7; + float avatarPatternAngleSpan = MathF.PI * 2.0f / (avatarPatternCount - 1f); + + for (int i = 0; i < avatarPatternCount - 1; i++) + { + float baseItemDistance; + float itemDistanceFraction; + float itemScaleFraction; + float itemDistance; + + if (IsSmall) + { + baseItemDistance = 72.0f + row * 28.0f; + + itemDistanceFraction = MathF.Max(0.0f, MathF.Min(1.0f, baseItemDistance / 140.0f)); + itemScaleFraction = patternScaleValueAt(fraction: avatarTransitionFraction, t: itemDistanceFraction, reverse: false); + itemDistance = baseItemDistance * (1.0f - itemScaleFraction) + 20.0f * itemScaleFraction; + } + else + { + baseItemDistance = 100.0f + row * 40.0f; + + itemDistanceFraction = MathF.Max(0.0f, MathF.Min(1.0f, baseItemDistance / 196.0f)); + itemScaleFraction = patternScaleValueAt(fraction: avatarTransitionFraction, t: itemDistanceFraction, reverse: false); + itemDistance = baseItemDistance * (1.0f - itemScaleFraction) + 28.0f * itemScaleFraction; + } + + + float itemAngle = -MathF.PI * 0.5f + i * avatarPatternAngleSpan; + + if (row % 2 != 0) + { + itemAngle += avatarPatternAngleSpan * 0.5f; + } + + Vector2 itemPosition = new Vector2(avatarPatternFrame.X * 0.5f + MathF.Cos(itemAngle) * itemDistance, avatarPatternFrame.Y * 0.5f + MathF.Sin(itemAngle) * itemDistance); + + float itemScale = 0.7f + lokiRng.Next() * (1.0f - 0.7f); + float itemSize = MathF.Floor((IsSmall ? 32 : 36) * itemScale); + + results.Add(new Vector4(itemPosition.X, itemPosition.Y, itemSize, 1.0f - itemScaleFraction)); + } + } + + return results; + } + + public bool IsSmall { get; set; } = false; + + #region Source + + public AnimatedImageSource Source + { + get { return (AnimatedImageSource)GetValue(SourceProperty); } + set { SetValue(SourceProperty, value); } + } + + public static readonly DependencyProperty SourceProperty = + DependencyProperty.Register("Source", typeof(AnimatedImageSource), typeof(ProfileHeaderPattern), new PropertyMetadata(null)); + + #endregion + } + public partial class PatternBackground : ContentControl { public PatternBackground() diff --git a/Telegram/Controls/PollOptionControl.xaml b/Telegram/Controls/PollOptionControl.xaml deleted file mode 100644 index e1ceabdb58..0000000000 --- a/Telegram/Controls/PollOptionControl.xaml +++ /dev/null @@ -1,154 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Telegram/Controls/PollOptionControl.xaml.cs b/Telegram/Controls/PollOptionControl.xaml.cs deleted file mode 100644 index 5dc22cd9e6..0000000000 --- a/Telegram/Controls/PollOptionControl.xaml.cs +++ /dev/null @@ -1,93 +0,0 @@ -// -// Copyright Fela Ameghino & Contributors 2015-2025 -// -// Distributed under the GNU General Public License v3.0. (See accompanying -// file LICENSE or copy at https://www.gnu.org/licenses/gpl-3.0.txt) -// -using System; -using System.Linq; -using Telegram.Common; -using Telegram.Services; -using Telegram.Td.Api; -using Windows.UI.Xaml; -using Windows.UI.Xaml.Automation; -using Windows.UI.Xaml.Controls.Primitives; - -namespace Telegram.Controls -{ - public sealed partial class PollOptionControl : ToggleButton - { - private bool _allowToggle; - - public PollOptionControl() - { - InitializeComponent(); - } - - public void UpdatePollOption(IClientService clientService, Poll poll, PollOption option) - { - var results = poll.IsClosed || poll.Options.Any(x => x.IsChosen); - var correct = poll.Type is PollTypeQuiz quiz && quiz.CorrectOptionId == poll.Options.IndexOf(option); - - var votes = Locale.Declension(poll.Type is PollTypeQuiz ? Strings.R.Answer : Strings.R.Vote, option.VoterCount); - - IsThreeState = results; - IsChecked = results ? null : new bool?(false); - Tag = option; - - _allowToggle = poll.Type is PollTypeRegular regular && regular.AllowMultipleAnswers && !results; - - Ellipse.Opacity = results || option.IsBeingChosen ? 0 : 1; - - Percentage.Visibility = results ? Visibility.Visible : Visibility.Collapsed; - Percentage.Text = $"{option.VotePercentage}%"; - - Extensions.SetToolTip(Percentage, results ? votes : null); - - CustomEmojiIcon.Add(TextText, Text.Inlines, clientService, option.Text); - - Zero.Visibility = results ? Visibility.Visible : Visibility.Collapsed; - - Votes.Maximum = results ? Math.Max(poll.Options.Max(x => x.VoterCount), 1) : 1; - Votes.Value = results ? option.VoterCount : 0; - VotesLine.Opacity = results ? 0 : 0.3; - - Loading.IsActive = option.IsBeingChosen; - - Tick.Visibility = (results && correct) || option.IsChosen ? Visibility.Visible : Visibility.Collapsed; - - if (option.IsChosen && poll.Type is PollTypeQuiz) - { - VisualStateManager.GoToState(LayoutRoot, correct ? "Correct" : "Wrong", false); - } - else - { - VisualStateManager.GoToState(LayoutRoot, "Normal", false); - } - - if (results) - { - AutomationProperties.SetName(this, $"{option.Text.Text}, {votes}, {option.VotePercentage}%"); - } - else - { - AutomationProperties.SetName(this, option.Text.Text); - } - } - - protected override void OnToggle() - { - if (!_allowToggle) - { - return; - } - - base.OnToggle(); - } - - private Visibility ConvertCheckMark(bool? check) - { - return check == true ? Visibility.Visible : Visibility.Collapsed; - } - } -} diff --git a/Telegram/Controls/ProfileGiftsCover.xaml b/Telegram/Controls/ProfileGiftsCover.xaml new file mode 100644 index 0000000000..798e6ba2ec --- /dev/null +++ b/Telegram/Controls/ProfileGiftsCover.xaml @@ -0,0 +1,15 @@ + + + + + + diff --git a/Telegram/Controls/ProfileGiftsCover.xaml.cs b/Telegram/Controls/ProfileGiftsCover.xaml.cs new file mode 100644 index 0000000000..7092a54308 --- /dev/null +++ b/Telegram/Controls/ProfileGiftsCover.xaml.cs @@ -0,0 +1,511 @@ +// +// Copyright (c) Fela Ameghino 2015-2025 +// +// Distributed under the GNU General Public License v3.0. (See accompanying +// file LICENSE or copy at https://www.gnu.org/licenses/gpl-3.0.txt) +// + +using Microsoft.UI.Xaml.Media; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; +using Telegram.Common; +using Telegram.Native; +using Telegram.Streams; +using Telegram.Td.Api; +using Telegram.ViewModels; +using Windows.Foundation; +using Windows.UI; +using Windows.UI.Composition; +using Windows.UI.Xaml; +using Windows.UI.Xaml.Controls; +using Windows.UI.Xaml.Hosting; +using Windows.UI.Xaml.Media; + +namespace Telegram.Controls +{ + public sealed partial class ProfileGiftsCover : UserControl + { + private readonly uint _seed; + + private List _positions; + private long _gifts; + private float _frameWidth; + private float _frameHeight; + + public ProfileViewModel ViewModel => DataContext as ProfileViewModel; + + public ProfileGiftsCover() + { + InitializeComponent(); + + _seed = (uint)DateTime.Now.ToTimestamp(); + } + + public void Update(float avatarTransitionFraction, UIElement titleRoot) + { + var newSize = new Vector2(ActualSize.X + 36, ActualSize.Y); + var seed = _seed; + + var gifts = GetPinnedGifts(out long hash); + + var avatarSize = new Vector2(120, 120); + var centerFrame = new RectangleF((-72 + newSize.X - avatarSize.X) / 2f, (-36 + 204 - avatarSize.Y) / 2f, avatarSize.X, avatarSize.Y); + + if (_gifts != hash || _positions == null || _frameWidth != newSize.X || _frameHeight != newSize.Y) + { + var titleTransform = titleRoot.TransformToVector2(this); + + var excludeRects = new RectangleF[] + { + new RectangleF(titleTransform.X - 4, titleTransform.Y, titleRoot.ActualSize.X + 8, titleRoot.ActualSize.Y), + }; + + var positionGenerator = new OrbitGenerator( + containerSize: newSize, + centerFrame: centerFrame, + exclusionZones: excludeRects, + minimumDistance: 42.0f, + edgePadding: 5.0f, + seed: seed + ); + + _positions = positionGenerator.GeneratePositions(count: 12, itemSize: new Vector2(28)); + _gifts = hash; + _frameWidth = newSize.X; + _frameHeight = newSize.Y; + + RootGrid.Children.Clear(); + } + + var iconPositions = _positions; + if (iconPositions == null) + { + return; + } + + for (int i = 0; i < Math.Max(iconPositions.Count, gifts.Count); i++) + { + if (i >= gifts.Count || i >= iconPositions.Count) + { + if (RootGrid.Children.Count > i) + { + RootGrid.Children.RemoveAt(i); + } + + continue; + } + + OrbitGenerator.Position iconPosition = iconPositions[i]; + var itemDistanceFraction = Math.Max(0.0f, Math.Min(0.5f, (iconPosition.Distance - avatarSize.X / 2.0f) / 144.0f)); + var itemScaleFraction = OrbitGenerator.PatternScaleValueAt(fraction: Math.Min(1.0f, avatarTransitionFraction * 1.33f), t: itemDistanceFraction, reverse: false); + + var toAngle = MathF.PI * 0.18f; + var centerPosition = new OrbitGenerator.Position(distance: 0.0f, angle: iconPosition.Angle + toAngle, scale: iconPosition.Scale); + var effectivePosition = OrbitGenerator.InterpolatePosition(from: iconPosition, to: centerPosition, t: itemScaleFraction); + var effectiveAngle = toAngle * itemScaleFraction; + + var absolutePosition = effectivePosition.GetAbsolutePosition(centerFrame.Center); + + if (gifts[i].Gift is SentGiftUpgraded upgraded) + { + Visual visual; + + if (RootGrid.Children.Count - 1 < i) + { + static Color MakeLuminous(Color color, double intensity) + { + intensity = Math.Max(0, Math.Min(1, intensity)); + + var hsl = color.ToHSL(); + + // Increase lightness and reduce saturation for a glowing effect + hsl.L = Math.Min(1, hsl.L + intensity * (1 - hsl.L) * 0.8); + hsl.S = hsl.S * (1 - intensity * 0.4); + + return hsl.ToRGB(); + } + + var centerColor = MakeLuminous(upgraded.Gift.Backdrop.Colors.CenterColor.ToColor(), 0.3); + + var gradient = new RadialGradientBrush(); + gradient.Center = new Point(0.5, 0.5); + gradient.GradientStops.Add(new GradientStop { Color = Color.FromArgb(166, centerColor.R, centerColor.G, centerColor.B) }); + gradient.GradientStops.Add(new GradientStop { Color = Color.FromArgb(166, centerColor.R, centerColor.G, centerColor.B), Offset = 0.3 }); + gradient.GradientStops.Add(new GradientStop { Color = Color.FromArgb(0, centerColor.R, centerColor.G, centerColor.B), Offset = 1 }); + + var particles = new AnimatedImage + { + Source = new ParticlesImageSource(Colors.White, ParticlesType.Status), + IsViewportAware = false, + Stretch = Stretch.UniformToFill, + DecodeFrameType = Windows.UI.Xaml.Media.Imaging.DecodePixelType.Logical, + FrameSize = new Size(36, 36), + Width = 36, + Height = 36, + Margin = new Thickness(-4) + }; + + var icon = new CustomEmojiIcon + { + Source = DelayedFileSource.FromSticker(ViewModel.ClientService, upgraded.Gift.Model.Sticker), + Width = 28, + Height = 28, + FrameSize = new Size(28, 28), + IsViewportAware = false + }; + + icon.Ready += OnReady; + + var root = new Grid + { + Opacity = 0, + Width = 28, + Height = 28, + HorizontalAlignment = HorizontalAlignment.Left, + VerticalAlignment = VerticalAlignment.Top + }; + + root.Children.Add(new Border + { + Background = gradient, + Width = 32, + Height = 32, + Margin = new Thickness(-2) + }); + + root.Children.Add(particles); + root.Children.Add(icon); + + RootGrid.Children.Add(root); + + visual = ElementComposition.GetElementVisual(root); + } + else + { + visual = ElementComposition.GetElementVisual(RootGrid.Children[i]); + } + + visual.Offset = new Vector3(absolutePosition, 0); + visual.Scale = new Vector3(iconPosition.Scale * (1.0f - itemScaleFraction)); + visual.RotationAngle = effectiveAngle; + } + } + } + + private void OnReady(object sender, EventArgs e) + { + var icon = sender as CustomEmojiIcon; + var root = icon.Parent as Grid; + var visual = ElementComposition.GetElementVisual(root); + + var scale = visual.Compositor.CreateVector3KeyFrameAnimation(); + scale.InsertKeyFrame(0, Vector3.Zero); + scale.InsertKeyFrame(1, visual.Scale); + + root.Opacity = 1; + + visual.CenterPoint = new Vector3(14); + visual.StartAnimation("Scale", scale); + } + + private IList GetPinnedGifts(out long hash) + { + hash = 0; + + var itemsView = ViewModel?.GiftsTab?.Items; + if (itemsView == null) + { + return Array.Empty(); + } + + var items = new List(); + + foreach (var gift in itemsView) + { + if (gift.IsPinned && gift.Gift is SentGiftUpgraded upgraded) + { + items.Add(gift); + hash = ((hash * 20261) + 0x80000000L + upgraded.Gift.Id) % 0x80000000L; + } + } + + return items; + } + } + + public class OrbitGenerator + { + public struct Position + { + public float Distance { get; } + public float Angle { get; } + public float Scale { get; } + + public Position(float distance, float angle, float scale) + { + Distance = distance; + Angle = angle; + Scale = scale; + } + + public Vector2 RelativeCartesian + { + get + { + return new Vector2( + Distance * (float)Math.Cos(Angle), + Distance * (float)Math.Sin(Angle) + ); + } + } + + public Vector2 GetAbsolutePosition(Vector2 centerPoint) + { + return new Vector2( + centerPoint.X + Distance * (float)Math.Cos(Angle), + centerPoint.Y + Distance * (float)Math.Sin(Angle) + ); + } + } + + private readonly Vector2 containerSize; + private readonly RectangleF centerFrame; + private readonly RectangleF[] exclusionZones; + private readonly float minimumDistance; + private readonly float edgePadding; + private readonly (float min, float max) scaleRange; + + private readonly (float min, float max) innerOrbitRange; + private readonly (float min, float max) outerOrbitRange; + private readonly int innerOrbitCount; + + private readonly LokiRng lokiRng; + + public OrbitGenerator( + Vector2 containerSize, + RectangleF centerFrame, + RectangleF[] exclusionZones, + float minimumDistance, + float edgePadding, + uint seed, + (float min, float max) scaleRange = default, + (float min, float max) innerOrbitRange = default, + (float min, float max) outerOrbitRange = default, + int innerOrbitCount = 4) + { + this.containerSize = containerSize; + this.centerFrame = centerFrame; + this.exclusionZones = exclusionZones; + this.minimumDistance = minimumDistance; + this.edgePadding = edgePadding; + this.scaleRange = scaleRange == default ? (0.7f, 1.15f) : scaleRange; + this.innerOrbitRange = innerOrbitRange == default ? (1.4f, 2.2f) : innerOrbitRange; + this.outerOrbitRange = outerOrbitRange == default ? (2.5f, 3.6f) : outerOrbitRange; + this.innerOrbitCount = innerOrbitCount; + this.lokiRng = new LokiRng(seed, 0, 0); + } + + public List GeneratePositions(int count, Vector2 itemSize) + { + var positions = new List(); + + var centerPoint = new Vector2( + centerFrame.X + centerFrame.Width / 2f, + centerFrame.Y + centerFrame.Height / 2f + ); + var centerRadius = Math.Min(centerFrame.Width, centerFrame.Height) / 2f; + + int maxAttempts = count * 200; + int attempts = 0; + + int leftPositions = 0; + int rightPositions = 0; + + int innerCount = Math.Min(innerOrbitCount, count); + + // Generate inner orbit positions + while (positions.Count < innerCount && attempts < maxAttempts) + { + attempts++; + + bool placeOnLeftSide = rightPositions > leftPositions; + + float orbitRangeSize = innerOrbitRange.max - innerOrbitRange.min; + float orbitDistanceFactor = innerOrbitRange.min + orbitRangeSize * lokiRng.Next(); + float distance = orbitDistanceFactor * centerRadius; + + float angleRange = (float)Math.PI; + float angleOffset = placeOnLeftSide ? (float)Math.PI / 2 : -(float)Math.PI / 2; + float angle = angleOffset + angleRange * lokiRng.Next(); + + var absolutePosition = GetAbsolutePosition(distance, angle, centerPoint); + + if (absolutePosition.X - itemSize.X / 2 < edgePadding || + absolutePosition.X + itemSize.X / 2 > containerSize.X - edgePadding || + absolutePosition.Y - itemSize.Y / 2 < edgePadding || + absolutePosition.Y + itemSize.Y / 2 > containerSize.Y - edgePadding) + { + continue; + } + + var itemRect = new RectangleF( + absolutePosition.X - itemSize.X / 2, + absolutePosition.Y - itemSize.Y / 2, + itemSize.X, + itemSize.Y + ); + + if (IsValidPosition(itemRect, positions.Select(p => + GetAbsolutePosition(p.Distance, p.Angle, centerPoint)).ToList(), itemSize)) + { + float scaleRangeSize = Math.Max(scaleRange.min + 0.1f, 0.75f) - scaleRange.max; + float scale = scaleRange.max + scaleRangeSize * lokiRng.Next(); + positions.Add(new Position(distance, angle, scale)); + + if (absolutePosition.X < centerPoint.X) + leftPositions++; + else + rightPositions++; + } + } + + float maxPossibleDistance = (float)Math.Sqrt(containerSize.X * containerSize.X + + containerSize.Y * containerSize.Y) / 2; + + // Generate outer orbit positions + while (positions.Count < count && attempts < maxAttempts) + { + attempts++; + + bool placeOnLeftSide = rightPositions >= leftPositions; + + float orbitRangeSize = outerOrbitRange.max - outerOrbitRange.min; + float orbitDistanceFactor = outerOrbitRange.min + orbitRangeSize * lokiRng.Next(); + float distance = orbitDistanceFactor * centerRadius; + + float angleRange = (float)Math.PI; + float angleOffset = placeOnLeftSide ? (float)Math.PI / 2 : -(float)Math.PI / 2; + float angle = angleOffset + angleRange * lokiRng.Next(); + + var absolutePosition = GetAbsolutePosition(distance, angle, centerPoint); + + if (absolutePosition.X - itemSize.X / 2 < edgePadding || + absolutePosition.X + itemSize.X / 2 > containerSize.X - edgePadding || + absolutePosition.Y - itemSize.Y / 2 < edgePadding || + absolutePosition.Y + itemSize.Y / 2 > containerSize.Y - edgePadding) + { + continue; + } + + var itemRect = new RectangleF( + absolutePosition.X - itemSize.X / 2, + absolutePosition.Y - itemSize.Y / 2, + itemSize.X, + itemSize.Y + ); + + if (IsValidPosition(itemRect, positions.Select(p => + GetAbsolutePosition(p.Distance, p.Angle, centerPoint)).ToList(), itemSize)) + { + float normalizedDistance = Math.Min(distance / maxPossibleDistance, 1.0f); + float scale = scaleRange.max - normalizedDistance * (scaleRange.max - scaleRange.min); + positions.Add(new Position(distance, angle, scale)); + + if (absolutePosition.X < centerPoint.X) + leftPositions++; + else + rightPositions++; + } + } + + return positions; + } + + public static Vector2 GetAbsolutePosition(float distance, float angle, Vector2 centerPoint) + { + return new Vector2( + centerPoint.X + distance * (float)Math.Cos(angle), + centerPoint.Y + distance * (float)Math.Sin(angle) + ); + } + + private bool IsValidPosition(RectangleF rect, List existingPositions, Vector2 itemSize) + { + if (rect.Left < edgePadding || rect.Right > containerSize.X - edgePadding || + rect.Top < edgePadding || rect.Bottom > containerSize.Y - edgePadding) + { + return false; + } + + foreach (var zone in exclusionZones) + { + if (rect.IntersectsWith(zone)) + { + return false; + } + } + + float effectiveMinDistance = existingPositions.Count > 5 ? + Math.Max(minimumDistance * 0.7f, 10.0f) : minimumDistance; + + foreach (var existingPosition in existingPositions) + { + float distance = (float)Math.Sqrt( + Math.Pow(existingPosition.X - (rect.X + rect.Width / 2), 2) + + Math.Pow(existingPosition.Y - (rect.Y + rect.Height / 2), 2) + ); + if (distance < effectiveMinDistance) + { + return false; + } + } + + return true; + } + + public static Position InterpolatePosition(Position from, Position to, float t) + { + var clampedT = Math.Max(0, Math.Min(1, t)); + + var interpolatedDistance = from.Distance + (to.Distance - from.Distance) * clampedT; + var interpolatedAngle = from.Angle + (to.Angle - from.Angle) * clampedT; + + return new OrbitGenerator.Position(distance: interpolatedDistance, angle: interpolatedAngle, scale: from.Scale); + } + + public static float WindowFunction(float t) + { + return BezierPoint.Calculate(0.6f, 0.0f, 0.4f, 1.0f, t); + } + + public static float PatternScaleValueAt(float fraction, float t, bool reverse) + { + float windowSize = 0.8f; + + float effectiveT; + float windowStartOffset; + float windowEndOffset; + + if (reverse) + { + effectiveT = 1.0f - t; + windowStartOffset = 1.0f; + windowEndOffset = -windowSize; + } + else + { + effectiveT = t; + windowStartOffset = -windowSize; + windowEndOffset = 1.0f; + } + + float windowPosition = (1.0f - fraction) * windowStartOffset + fraction * windowEndOffset; + float windowT = Math.Max(0.0f, Math.Min(windowSize, effectiveT - windowPosition)) / windowSize; + float localT = 1.0f - WindowFunction(windowT); + + return localT; + } + } +} diff --git a/Telegram/Controls/ProfileHeader.xaml b/Telegram/Controls/ProfileHeader.xaml index d662e90b35..24fe241ee0 100644 --- a/Telegram/Controls/ProfileHeader.xaml +++ b/Telegram/Controls/ProfileHeader.xaml @@ -9,10 +9,10 @@ Background="{ThemeResource ApplicationPageBackgroundThemeBrush}"> - + + + + + + + + + + + + + + + + + + + + @@ -151,45 +183,6 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + - + diff --git a/Telegram/Views/Profile/ProfileAnimationsTabPage.xaml.cs b/Telegram/Views/Profile/ProfileAnimationsTabPage.xaml.cs index e837face01..bf73666bde 100644 --- a/Telegram/Views/Profile/ProfileAnimationsTabPage.xaml.cs +++ b/Telegram/Views/Profile/ProfileAnimationsTabPage.xaml.cs @@ -9,10 +9,13 @@ using Telegram.Td.Api; using Telegram.ViewModels; using Telegram.ViewModels.Chats; +using Windows.UI.Xaml; using Windows.UI.Xaml.Automation; using Windows.UI.Xaml.Controls; using Windows.UI.Xaml.Controls.Primitives; +using Windows.UI.Xaml.Media.Animation; using Windows.UI.Xaml.Media.Imaging; +using Windows.UI.Xaml.Navigation; namespace Telegram.Views.Profile { @@ -23,31 +26,53 @@ public ProfileAnimationsTabPage() InitializeComponent(); } + protected override void OnNavigatedTo(NavigationEventArgs e) + { + base.OnNavigatedTo(e); + + if (!IsProfile) + { + ScrollingHost.Padding = new Thickness(12, 0, 4, 8); + } + + if (ViewModel.Animations.Empty()) + { + ScrollingHost.ItemContainerTransitions.Add(new EntranceThemeTransition { IsStaggeringEnabled = false }); + } + } + private void OnContainerContentChanging(ListViewBase sender, ContainerContentChangingEventArgs args) { - if (args.InRecycleQueue) + if (args.InRecycleQueue || ViewModel == null) { return; } - else if (args.ItemContainer.ContentTemplateRoot is Grid content && args.Item is MessageWithOwner message) + else if (args.ItemContainer.ContentTemplateRoot is Grid content) { - AutomationProperties.SetName(args.ItemContainer, Automation.GetSummaryWithName(message, true)); - var photo = content.Children[0] as ImageView; - if (message.Content is MessageAnimation animation) + if (args.Item is MessageWithOwner message) { - if (animation.Animation.Thumbnail is { Format: ThumbnailFormatJpeg }) - { - photo.SetSource(ViewModel.ClientService, animation.Animation.Thumbnail.File, animation.Animation.Minithumbnail); - } - else if (animation.Animation.Minithumbnail != null) + AutomationProperties.SetName(args.ItemContainer, Automation.GetSummaryWithName(message, true)); + + if (message.Content is MessageAnimation animation) { - var bitmap = new BitmapImage(); - PlaceholderHelper.GetBlurred(bitmap, animation.Animation.Minithumbnail.Data); - photo.Source = bitmap; + if (animation.Animation.Thumbnail is { Format: ThumbnailFormatJpeg }) + { + photo.SetSource(ViewModel.ClientService, animation.Animation.Thumbnail.File, animation.Animation.Minithumbnail); + } + else if (animation.Animation.Minithumbnail != null) + { + var bitmap = new BitmapImage(); + PlaceholderHelper.GetBlurred(bitmap, animation.Animation.Minithumbnail.Data); + photo.Source = bitmap; + } } } + else + { + photo.Clear(); + } args.Handled = true; } diff --git a/Telegram/Views/Profile/ProfileBotsTabPage.xaml b/Telegram/Views/Profile/ProfileBotsTabPage.xaml index 79cdc58a73..66c26220c6 100644 --- a/Telegram/Views/Profile/ProfileBotsTabPage.xaml +++ b/Telegram/Views/Profile/ProfileBotsTabPage.xaml @@ -41,7 +41,7 @@ - diff --git a/Telegram/Views/Profile/ProfileChannelsTabPage.xaml b/Telegram/Views/Profile/ProfileChannelsTabPage.xaml index a12817b05c..912b0af5ff 100644 --- a/Telegram/Views/Profile/ProfileChannelsTabPage.xaml +++ b/Telegram/Views/Profile/ProfileChannelsTabPage.xaml @@ -41,7 +41,7 @@ - diff --git a/Telegram/Views/Profile/ProfileFilesTabPage.xaml b/Telegram/Views/Profile/ProfileFilesTabPage.xaml index a87ba7c737..d33b4133ad 100644 --- a/Telegram/Views/Profile/ProfileFilesTabPage.xaml +++ b/Telegram/Views/Profile/ProfileFilesTabPage.xaml @@ -12,7 +12,7 @@ @@ -38,7 +38,7 @@ - + - + diff --git a/Telegram/Views/Profile/ProfileGroupsTabPage.xaml b/Telegram/Views/Profile/ProfileGroupsTabPage.xaml index 730756215d..a60e45afe3 100644 --- a/Telegram/Views/Profile/ProfileGroupsTabPage.xaml +++ b/Telegram/Views/Profile/ProfileGroupsTabPage.xaml @@ -39,7 +39,7 @@ - diff --git a/Telegram/Views/Profile/ProfileLinksTabPage.xaml b/Telegram/Views/Profile/ProfileLinksTabPage.xaml index 9f1623436d..7d3c8d3b88 100644 --- a/Telegram/Views/Profile/ProfileLinksTabPage.xaml +++ b/Telegram/Views/Profile/ProfileLinksTabPage.xaml @@ -14,7 +14,7 @@ @@ -40,7 +40,7 @@ - + - + diff --git a/Telegram/Views/Profile/ProfileMediaTabPage.xaml.cs b/Telegram/Views/Profile/ProfileMediaTabPage.xaml.cs index 253a34b65b..5a031d8131 100644 --- a/Telegram/Views/Profile/ProfileMediaTabPage.xaml.cs +++ b/Telegram/Views/Profile/ProfileMediaTabPage.xaml.cs @@ -130,7 +130,7 @@ public void Zoom(int factor) //parent.RegisterAnchorCandidate(container); //parent.anchor - var y = -(float)parent.VerticalOffset + (float)ViewModel.HeaderHeight - 88; + var y = -(float)parent.VerticalOffset + Header.ActualSize.Y - 88; var childSize = child.ActualSize.X > 0 && child.ActualSize.Y > 0 ? new Vector2(child.ActualSize.X, (float)parent.ViewportHeight) : new Vector2(1, 1); var childOffset = new Vector2(0, Math.Max(-y, 0)); @@ -143,7 +143,7 @@ public void Zoom(int factor) var redirect = visual.Compositor.CreateSpriteVisual(); redirect.RelativeSizeAdjustment = Vector2.One; - redirect.Offset = new Vector3(0, (float)ViewModel.HeaderHeight - Math.Min(y, 0), 0); + redirect.Offset = new Vector3(0, Header.ActualSize.Y - Math.Min(y, 0), 0); //TestGrid.Margin = new Thickness(24, ViewModel.HeaderHeight - Math.Min(y, 0), 16, 0); TestGrid.Height = childSize.Y; diff --git a/Telegram/Views/Profile/ProfileMembersTabPage.xaml b/Telegram/Views/Profile/ProfileMembersTabPage.xaml index 9002e979d4..5f3c5bbda2 100644 --- a/Telegram/Views/Profile/ProfileMembersTabPage.xaml +++ b/Telegram/Views/Profile/ProfileMembersTabPage.xaml @@ -34,7 +34,7 @@ - + diff --git a/Telegram/Views/Profile/ProfileMusicTabPage.xaml b/Telegram/Views/Profile/ProfileMusicTabPage.xaml index f1049193e0..5a0f846b00 100644 --- a/Telegram/Views/Profile/ProfileMusicTabPage.xaml +++ b/Telegram/Views/Profile/ProfileMusicTabPage.xaml @@ -14,7 +14,7 @@ @@ -40,7 +40,7 @@ - + - diff --git a/Telegram/Views/Profile/ProfileSavedMessagesTabPage.xaml b/Telegram/Views/Profile/ProfileSavedMessagesTabPage.xaml new file mode 100644 index 0000000000..899bd452db --- /dev/null +++ b/Telegram/Views/Profile/ProfileSavedMessagesTabPage.xaml @@ -0,0 +1,22 @@ + + + + + + + + + + + diff --git a/Telegram/Views/Profile/ProfileSavedMessagesTabPage.xaml.cs b/Telegram/Views/Profile/ProfileSavedMessagesTabPage.xaml.cs new file mode 100644 index 0000000000..61c5b20043 --- /dev/null +++ b/Telegram/Views/Profile/ProfileSavedMessagesTabPage.xaml.cs @@ -0,0 +1,82 @@ +// +// Copyright Fela Ameghino 2015-2025 +// +// Distributed under the GNU General Public License v3.0. (See accompanying +// file LICENSE or copy at https://www.gnu.org/licenses/gpl-3.0.txt) +// +using Telegram.Common; +using Telegram.Navigation; +using Telegram.Navigation.Services; +using Telegram.ViewModels; +using Telegram.ViewModels.Delegates; + +namespace Telegram.Views.Profile +{ + public sealed partial class ProfileSavedMessagesTabPage : HostedPage, IChatPage + { + public DialogViewModel ViewModel => DataContext as DialogViewModel; + + public ProfileSavedMessagesTabPage() + { + InitializeComponent(); + NavigationCacheMode = ApiInfo.NavigationCacheMode; + } + + public override string GetTitle() + { + return View.ChatTitle; + } + + public override HostedPagePositionBase GetPosition() + { + return null; + } + + public void OnBackRequested(BackRequestedRoutedEventArgs args) + { + View.OnBackRequested(args); + } + + public void Search() + { + View.Search(); + } + + public void Deactivate(bool navigation) + { + View.Deactivate(navigation); + + if (navigation) + { + return; + } + + DataContext = new object(); + } + + public void Activate(INavigationService navigationService) + { + var viewModel = TypeResolver.Current.Resolve(View, navigationService.SessionId); + viewModel.NavigationService = navigationService; + viewModel.IsSavedMessagesTab = true; + DataContext = viewModel; + View.Activate(viewModel); + } + + public void PopupOpened() + { + View.PopupOpened(); + } + + public void PopupClosed() + { + View.PopupClosed(); + } + + public double HeaderHeight + { + get => View.HeaderHeight; + set => View.HeaderHeight = value; + } + } +} diff --git a/Telegram/Views/Profile/ProfileStoriesTabPage.xaml b/Telegram/Views/Profile/ProfileStoriesTabPage.xaml index dc5ae482a3..fe67ed54dc 100644 --- a/Telegram/Views/Profile/ProfileStoriesTabPage.xaml +++ b/Telegram/Views/Profile/ProfileStoriesTabPage.xaml @@ -73,7 +73,7 @@ - +