diff --git a/TwitchDownloaderCore/ChatRenderer.cs b/TwitchDownloaderCore/ChatRenderer.cs
index d9bb9276..e24c160a 100644
--- a/TwitchDownloaderCore/ChatRenderer.cs
+++ b/TwitchDownloaderCore/ChatRenderer.cs
@@ -614,11 +614,11 @@ private SKBitmap CombineImages(List<(SKImageInfo info, SKBitmap bitmap)> section
var finalBitmapInfo = finalBitmap.Info;
using (SKCanvas finalCanvas = new SKCanvas(finalBitmap))
{
- if (highlightType is HighlightType.PayingForward or HighlightType.ChannelPointHighlight)
+ if (highlightType is HighlightType.PayingForward or HighlightType.ChannelPointHighlight or HighlightType.WatchStreak)
{
var accentColor = highlightType is HighlightType.PayingForward
- ? new SKColor(0x26, 0x26, 0x2C, 0xFF) // #26262C (RRGGBB)
- : new SKColor(0x80, 0x80, 0x8C, 0xFF); // #80808C (RRGGBB)
+ ? new SKColor(0xFF26262C) // AARRGGBB
+ : new SKColor(0xFF80808C); // AARRGGBB
using var paint = new SKPaint { Color = accentColor };
finalCanvas.DrawRect(renderOptions.SidePadding, 0, renderOptions.AccentStrokeWidth, finalBitmapInfo.Height, paint);
@@ -630,7 +630,7 @@ private SKBitmap CombineImages(List<(SKImageInfo info, SKBitmap bitmap)> section
(renderOptions.AlternateMessageBackgrounds && renderOptions.AlternateBackgroundColor.Alpha < OPAQUE_THRESHOLD)))
{
// Draw the highlight background only if the message background is opaque enough
- var backgroundColor = new SKColor(0x6B, 0x6B, 0x6E, 0x1A); // #1A6B6B6E (AARRGGBB)
+ var backgroundColor = new SKColor(0x1A6B6B6E); // AARRGGBB
using var backgroundPaint = new SKPaint { Color = backgroundColor };
finalCanvas.DrawRect(renderOptions.SidePadding, 0, finalBitmapInfo.Width - renderOptions.SidePadding * 2, finalBitmapInfo.Height, backgroundPaint);
}
@@ -706,6 +706,9 @@ private void DrawAccentedMessage(Comment comment, List<(SKImageInfo info, SKBitm
case HighlightType.BitBadgeTierNotification:
DrawBitsBadgeTierMessage(comment, sectionImages, emotePositionList, ref drawPos, defaultPos, highlightIcon, iconPoint);
break;
+ case HighlightType.WatchStreak:
+ DrawWatchStreakMessage(comment, sectionImages, emotePositionList, ref drawPos, defaultPos, highlightIcon, iconPoint);
+ break;
case HighlightType.GiftedMany:
case HighlightType.GiftedSingle:
case HighlightType.GiftedAnonymous:
@@ -802,6 +805,45 @@ private void DrawBitsBadgeTierMessage(Comment comment, List<(SKImageInfo info, S
DrawMessage(comment, sectionImages, emotePositionList, false, ref drawPos, defaultPos);
}
+ private void DrawWatchStreakMessage(Comment comment, List<(SKImageInfo info, SKBitmap bitmap)> sectionImages, List<(Point, TwitchEmote)> emotePositionList, ref Point drawPos, Point defaultPos, SKImage highlightIcon, Point iconPoint)
+ {
+ using SKCanvas canvas = new(sectionImages.Last().bitmap);
+ canvas.DrawImage(highlightIcon, iconPoint.X, iconPoint.Y);
+
+ Point customMessagePos = drawPos;
+ drawPos.X += highlightIcon.Width + renderOptions.WordSpacing;
+ defaultPos.X = drawPos.X;
+
+ DrawUsername(comment, sectionImages, ref drawPos, defaultPos, false, Purple);
+ AddImageSection(sectionImages, ref drawPos, defaultPos);
+
+ // Remove the commenter's name from the watch streak message
+ comment.message.body = comment.message.body[(comment.commenter.display_name.Length + 1)..];
+ if (comment.message.fragments[0].text.Equals(comment.commenter.display_name, StringComparison.OrdinalIgnoreCase))
+ {
+ // This is necessary for sub messages. We'll keep it around just in case.
+ comment.message.fragments.RemoveAt(0);
+ }
+ else
+ {
+ comment.message.fragments[0].text = comment.message.fragments[0].text[(comment.commenter.display_name.Length + 1)..];
+ }
+
+ var (streakMessage, customMessage) = HighlightIcons.SplitWatchStreakComment(comment);
+ DrawMessage(streakMessage, sectionImages, emotePositionList, false, ref drawPos, defaultPos);
+
+ // Return if there is no custom message to draw
+ if (customMessage is null)
+ {
+ return;
+ }
+
+ AddImageSection(sectionImages, ref drawPos, defaultPos);
+ drawPos = customMessagePos;
+ defaultPos = customMessagePos;
+ DrawNonAccentedMessage(customMessage, sectionImages, emotePositionList, false, ref drawPos, ref defaultPos);
+ }
+
private void DrawGiftMessage(Comment comment, List<(SKImageInfo info, SKBitmap bitmap)> sectionImages, List<(Point, TwitchEmote)> emotePositionList, ref Point drawPos, Point defaultPos, SKImage highlightIcon, Point iconPoint)
{
using SKCanvas canvas = new(sectionImages.Last().bitmap);
diff --git a/TwitchDownloaderCore/Tools/HighlightIcons.cs b/TwitchDownloaderCore/Tools/HighlightIcons.cs
index 71100dfe..649c079a 100644
--- a/TwitchDownloaderCore/Tools/HighlightIcons.cs
+++ b/TwitchDownloaderCore/Tools/HighlightIcons.cs
@@ -19,6 +19,7 @@ public enum HighlightType
ChannelPointHighlight,
Raid,
BitBadgeTierNotification,
+ WatchStreak,
Unknown
}
@@ -32,9 +33,11 @@ public sealed class HighlightIcons : IDisposable
private const string GIFTED_MANY_ICON_URL = "https://static-cdn.jtvnw.net/subs-image-assets/gift-illus.png";
private const string GIFTED_ANONYMOUS_ICON_SVG = "m 54.571425,64.514958 a 4.3531428,4.2396967 0 0 1 -1.273998,-0.86096 l -1.203426,-1.172067 a 7.0051428,6.822584 0 0 0 -9.90229,0 c -3.417139,3.328092 -8.962569,3.328092 -12.383427,0 l -0.159707,-0.155553 a 7.1871427,6.9998405 0 0 0 -9.854005,-0.28216 l -1.894286,1.635103 a 4.9362858,4.8076423 0 0 1 -3.276,1.215474 H 10 V 32.337399 a 26.000001,25.322423 0 0 1 52,0 v 32.557396 h -5.627146 c -0.627714,0 -1.240569,-0.133847 -1.801429,-0.379837 z M 35.999996,14.249955 A 18.571428,18.087444 0 0 0 17.428572,32.337399 v 22.515245 a 14.619428,14.238435 0 0 1 17.471998,2.358609 l 0.163448,0.155554 c 0.516285,0.50645 1.355715,0.50645 1.875712,0 a 14.437428,14.061179 0 0 1 17.631712,-2.11623 V 32.337399 A 18.571428,18.087444 0 0 0 35.999996,14.249955 Z M 24.857142,35.954887 a 3.7142855,3.6174889 0 1 1 7.42857,0 3.7142855,3.6174889 0 0 1 -7.42857,0 z m 18.571432,-3.617488 a 3.7142859,3.6174892 0 1 0 0,7.234978 3.7142859,3.6174892 0 0 0 0,-7.234978 z";
private const string BIT_BADGE_TIER_NOTIFICATION_ICON_SVG = "M 14.242705,42.37453 36,11.292679 57.757295,42.37453 36,61.023641 Z M 22.566425,41.323963 36,22.13092 49.433577,41.317747 46.79162,43.580506 36,39.266345 25.205273,43.586723 22.566425,41.320854 Z";
+ private const string WATCH_STREAK_ICON_SVG = "M 38.84325,21.169078 33.156748,14.060989 21.215093,27.992844 a 21.267516,21.267402 0 0 0 -5.11785,13.846557 c 0,9.752298 7.961102,17.713358 17.713453,17.713358 H 38.50206 A 17.400696,17.400602 0 0 0 55.902755,42.152157 c 0,-5.288419 -1.848114,-10.406242 -5.231581,-14.500501 L 41.686501,16.904225 Z m -13.306415,10.519973 7.619913,-9.098354 5.686502,7.108089 2.843251,-4.264854 4.606066,5.885497 a 16.945776,16.945684 0 0 1 3.923686,10.832728 c 0,5.91393 -4.407039,10.804296 -10.121973,11.600401 1.02357,-1.336321 1.592221,-2.985397 1.592221,-4.719771 0,-1.478483 -0.511786,-2.900101 -1.421626,-4.065827 l -4.264877,-5.316851 -4.264876,5.316851 c -0.90984,1.137294 -1.421625,2.587344 -1.421625,4.065827 0,1.705941 0.56865,3.355018 1.535355,4.662906 A 12.026952,12.026887 0 0 1 21.783744,41.839401 c 0,-3.72464 1.336328,-7.335548 3.753091,-10.15035 z";
private static readonly Regex SubMessageRegex = new(@"^(subscribed (?:with Prime|at Tier \d)\. They've subscribed for \d{1,3} months(?:, currently on a \d{1,3} month streak)?! )(.+)$", RegexOptions.Compiled);
private static readonly Regex GiftAnonymousRegex = new(@"^An anonymous user (?:gifted a|is gifting \d{1,4}) Tier \d", RegexOptions.Compiled);
+ private static readonly Regex WatchStreakRegex = new(@"^(watched \d+ consecutive streams this month and sparked a watch streak! )(.*)$", RegexOptions.Compiled);
private SKImage _subscribedTierIcon;
private SKImage _subscribedPrimeIcon;
@@ -42,6 +45,7 @@ public sealed class HighlightIcons : IDisposable
private SKImage _giftManyIcon;
private SKImage _giftAnonymousIcon;
private SKImage _bitBadgeTierNotificationIcon;
+ private SKImage _watchStreakIcon;
private readonly string _cachePath;
private readonly SKColor _purple;
@@ -86,6 +90,9 @@ public static HighlightType GetHighlightType(Comment comment)
if (bodyWithoutName.StartsWith(" is paying forward the Gift they got from"))
return HighlightType.PayingForward;
+ if (bodyWithoutName.Contains(" consecutive streams this month and sparked a watch streak!", StringComparison.Ordinal))
+ return HighlightType.WatchStreak;
+
if (bodyWithoutName.StartsWith(" converted from a"))
{
// TODO: use bodyWithoutName when .NET 7
@@ -134,6 +141,7 @@ public SKImage GetHighlightIcon(HighlightType highlightType, SKColor textColor,
HighlightType.GiftedMany => _giftManyIcon ??= GenerateGiftedManyIcon(fontSize, _cachePath, _offline),
HighlightType.GiftedAnonymous => _giftAnonymousIcon ??= GenerateSvgIcon(GIFTED_ANONYMOUS_ICON_SVG, textColor, fontSize),
HighlightType.BitBadgeTierNotification => _bitBadgeTierNotificationIcon ??= GenerateSvgIcon(BIT_BADGE_TIER_NOTIFICATION_ICON_SVG, textColor, fontSize),
+ HighlightType.WatchStreak => _watchStreakIcon ??= GenerateSvgIcon(WATCH_STREAK_ICON_SVG, textColor, fontSize),
_ => null
};
}
@@ -196,13 +204,16 @@ private static SKImage GenerateSvgIcon(string iconSvgString, SKColor iconColor,
///
public static (Comment subMessage, Comment customMessage) SplitSubComment(Comment comment)
{
- var (subMessage, customMessage) = SplitSubMessage(comment.message.body);
- // Return the original comment + null if there is no custom sub message
- if (customMessage is null)
+ var subMessageMatch = SubMessageRegex.Match(comment.message.body);
+ if (!subMessageMatch.Success)
{
+ // Return the original comment + null if there is no custom sub message
return (comment, null);
}
+ var subMessage = subMessageMatch.Groups[1].Value;
+ var customMessage = subMessageMatch.Groups[2].Value;
+
// If we don't clone then both new comments reference the original commenter object, message object, fragment list, etc.
var subMessageComment = comment.Clone();
subMessageComment.message.body = subMessage;
@@ -232,18 +243,53 @@ public static (Comment subMessage, Comment customMessage) SplitSubComment(Commen
return (subMessageComment, customMessageComment);
}
- /// The split re-sub details and user's custom re-sub message if there is one, else the re-sub details and null
- public static (string subMessage, string customMessage) SplitSubMessage(string commentMessage)
+ ///
+ /// Splits a comment into 2 comments based on the start index of a custom re-sub message
+ ///
+ ///
+ /// 2 clones of whose and contain the split re-sub details and
+ /// the user's custom re-sub message if there is one, else the original and null
+ ///
+ public static (Comment subMessage, Comment customMessage) SplitWatchStreakComment(Comment comment)
{
- var subMessageMatch = SubMessageRegex.Match(commentMessage);
- if (!subMessageMatch.Success)
+ var watchStreakMatch = WatchStreakRegex.Match(comment.message.body);
+ if (!watchStreakMatch.Success)
{
- return (commentMessage, null);
+ // Return the original comment + null if there is no custom watch streak message
+ return (comment, null);
}
- return (subMessageMatch.Groups[1].Value, subMessageMatch.Groups[2].Value);
- }
+ var streakMessage = watchStreakMatch.Groups[1].Value;
+ var customMessage = watchStreakMatch.Groups[2].Value;
+
+ // If we don't clone then both new comments reference the original commenter object, message object, fragment list, etc.
+ var streakMessageComment = comment.Clone();
+ streakMessageComment.message.body = streakMessage;
+ streakMessageComment.message.fragments[0].text = streakMessage;
+ var customMessageComment = comment.Clone();
+ customMessageComment.message.body = customMessage;
+
+ // If only one fragment then we are done
+ if (comment.message.fragments.Count == 1)
+ {
+ customMessageComment.message.fragments[0].text = customMessage;
+ return (streakMessageComment, customMessageComment);
+ }
+
+ streakMessageComment.message.fragments.RemoveRange(1, comment.message.fragments.Count - 1);
+ streakMessageComment.message.emoticons.Clear();
+ // Check to see if there is a custom message before the next fragment
+ // i.e. Foobar watched 3 consecutive streams this month and sparked a watch streak! Hey PogChamp
+ if (!customMessage.StartsWith(comment.message.fragments[1].text)) // If yes
+ {
+ customMessageComment.message.fragments[0].text = customMessage[..(customMessage.IndexOf(comment.message.fragments[1].text, StringComparison.Ordinal) - 1)];
+ return (streakMessageComment, customMessageComment);
+ }
+
+ customMessageComment.message.fragments.RemoveAt(0);
+ return (streakMessageComment, customMessageComment);
+ }
#region ImplementIDisposable
public void Dispose()