Skip to content

Commit

Permalink
Add initial support for notifications
Browse files Browse the repository at this point in the history
  • Loading branch information
mythz committed Apr 5, 2024
1 parent b13c6b3 commit cb52d49
Show file tree
Hide file tree
Showing 23 changed files with 865 additions and 72 deletions.
212 changes: 183 additions & 29 deletions MyApp.ServiceInterface/BackgroundMqServices.cs
Original file line number Diff line number Diff line change
Expand Up @@ -91,9 +91,12 @@ await Db.InsertAsync(new Achievement

if (request.CreatePost != null)
{
var postId = (int)await Db.InsertAsync(request.CreatePost, selectIdentity:true);
var createdBy = request.CreatePost.CreatedBy;
if (createdBy != null && request.CreatePost.PostTypeId == 1)
var post = request.CreatePost;
var body = post.Body;
post.Body = null;
post.Id = (int)await Db.InsertAsync(post, selectIdentity:true);
var createdBy = post.CreatedBy;
if (createdBy != null && post.PostTypeId == 1)
{
await appConfig.ResetUserQuestionsAsync(Db, createdBy);
}
Expand All @@ -102,22 +105,55 @@ await Db.InsertAsync(new Achievement
{
await Db.InsertAsync(new StatTotals
{
Id = $"{postId}",
PostId = postId,
Id = $"{post.Id}",
PostId = post.Id,
UpVotes = 0,
DownVotes = 0,
StartingUpVotes = 0,
CreatedBy = request.CreatePost.CreatedBy,
CreatedBy = post.CreatedBy,
});
}
catch (Exception e)
{
log.LogWarning("Couldn't insert StatTotals for Post {PostId}: '{Message}', updating instead...", postId, e.Message);
log.LogWarning("Couldn't insert StatTotals for Post {PostId}: '{Message}', updating instead...", post.Id, e.Message);
await Db.UpdateOnlyAsync(() => new StatTotals
{
PostId = postId,
CreatedBy = request.CreatePost.CreatedBy,
}, x => x.Id == $"{postId}");
PostId = post.Id,
CreatedBy = post.CreatedBy,
}, x => x.Id == $"{post.Id}");
}

if (!string.IsNullOrEmpty(body))
{
var cleanBody = body.StripHtml();
var userNameMentions = cleanBody.FindUserNameMentions()
.Where(x => x != createdBy).ToList();
if (userNameMentions.Count > 0)
{
var existingUsers = await Db.SelectAsync(Db.From<ApplicationUser>()
.Where(x => userNameMentions.Contains(x.UserName!)));

foreach (var existingUser in existingUsers)
{
var firstMentionPos = cleanBody.IndexOf(existingUser.UserName!, StringComparison.Ordinal);
if (firstMentionPos < 0) continue;

var startPos = Math.Max(0, firstMentionPos - 50);
await Db.InsertAsync(new Notification
{
UserName = existingUser.UserName!,
Type = NotificationType.QuestionMention,
RefId = $"{post.Id}",
PostId = post.Id,
CreatedDate = post.CreationDate,
PostTitle = post.Title.SubstringWithEllipsis(0,100),
Summary = cleanBody.SubstringWithEllipsis(startPos,100),
Href = $"/questions/{post.Id}/{post.Slug}",
RefUserName = createdBy,
});
appConfig.IncrNotificationsFor(existingUser.UserName!);
}
}
}
}

Expand Down Expand Up @@ -214,14 +250,15 @@ await Db.UpdateOnlyAsync(() =>
}

var answer = request.CreateAnswer;
if (answer is { RefId: not null, ParentId: not null })
if (answer is { ParentId: not null, CreatedBy: not null })
{
var postId = answer.ParentId.Value;
if (!await Db.ExistsAsync(Db.From<StatTotals>().Where(x => x.Id == answer.RefId)))
var refId = $"{postId}-{answer.CreatedBy}";
if (!await Db.ExistsAsync(Db.From<StatTotals>().Where(x => x.Id == refId)))
{
await Db.InsertAsync(new StatTotals
{
Id = answer.RefId,
Id = refId,
PostId = postId,
ViewCount = 0,
FavoriteCount = 0,
Expand All @@ -235,18 +272,58 @@ await Db.InsertAsync(new StatTotals
var post = await Db.SingleByIdAsync<Post>(postId);
if (post?.CreatedBy != null)
{
await Db.InsertAsync(new Notification
var answerHref = $"/questions/{postId}/{post.Slug}#{refId}";
if (post.CreatedBy != answer.CreatedBy)
{
UserName = post.CreatedBy,
Type = NotificationType.NewAnswer,
RefId = answer.RefId,
PostId = postId,
CreatedDate = answer.CreationDate,
PostTitle = post.Title.SubstringWithEllipsis(0,100),
Summary = answer.Summary.SubstringWithEllipsis(0,100),
Href = $"/questions/{postId}/{post.Slug}#{answer.RefId}",
});
await Db.InsertAsync(new Notification
{
UserName = post.CreatedBy,
Type = NotificationType.NewAnswer,
RefId = refId,
PostId = postId,
CreatedDate = answer.CreationDate,
PostTitle = post.Title.SubstringWithEllipsis(0,100),
Summary = answer.Summary.SubstringWithEllipsis(0,100),
Href = answerHref,
RefUserName = answer.CreatedBy,
});
appConfig.IncrNotificationsFor(post.CreatedBy);
}

if (!string.IsNullOrEmpty(answer.Body))
{
var cleanBody = answer.Body.StripHtml();
var userNameMentions = cleanBody.FindUserNameMentions()
.Where(x => x != post.CreatedBy && x != answer.CreatedBy).ToList();
if (userNameMentions.Count > 0)
{
var existingUsers = await Db.SelectAsync(Db.From<ApplicationUser>()
.Where(x => userNameMentions.Contains(x.UserName!)));

foreach (var existingUser in existingUsers)
{
var firstMentionPos = cleanBody.IndexOf(existingUser.UserName!, StringComparison.Ordinal);
if (firstMentionPos < 0) continue;

var startPos = Math.Max(0, firstMentionPos - 50);
await Db.InsertAsync(new Notification
{
UserName = existingUser.UserName!,
Type = NotificationType.AnswerMention,
RefId = $"{postId}",
PostId = postId,
CreatedDate = answer.CreationDate,
PostTitle = post.Title.SubstringWithEllipsis(0,100),
Summary = cleanBody.SubstringWithEllipsis(startPos,100),
Href = answerHref,
RefUserName = answer.CreatedBy,
});
appConfig.IncrNotificationsFor(existingUser.UserName!);
}
}
}
}

}

if (request.AnswerAddedToPost != null)
Expand All @@ -268,30 +345,107 @@ await Db.InsertAsync(new Notification
var createdBy = isAnswer
? (await Db.SingleByIdAsync<StatTotals>(refId))?.CreatedBy
: post.CreatedBy;
if (createdBy != null)

var comment = request.NewComment.Comment;
var commentRefId = $"{refId}-{comment.Created}";
var cleanBody = comment.Body.StripHtml();
var createdDate = DateTimeOffset.FromUnixTimeMilliseconds(comment.Created).DateTime;
var commentHref = $"/questions/{postId}/{post.Slug}#{commentRefId}";

if (createdBy != null && createdBy != comment.CreatedBy)
{
var comment = request.NewComment.Comment;
await Db.InsertAsync(new Notification
{
UserName = createdBy,
Type = NotificationType.NewComment,
RefId = refId,
RefId = commentRefId,
PostId = postId,
CreatedDate = DateTimeOffset.FromUnixTimeMilliseconds(comment.Created).DateTime,
CreatedDate = createdDate,
PostTitle = post.Title.SubstringWithEllipsis(0,100),
Summary = comment.Body.SubstringWithEllipsis(0,100),
Href = $"/questions/{postId}/{post.Slug}#{refId}-{comment.Created}",
Summary = cleanBody.SubstringWithEllipsis(0,100),
Href = commentHref,
RefUserName = comment.CreatedBy,
});
appConfig.IncrNotificationsFor(createdBy);
}

var userNameMentions = cleanBody.FindUserNameMentions()
.Where(x => x != createdBy && x != comment.CreatedBy).ToList();
if (userNameMentions.Count > 0)
{
var existingUsers = await Db.SelectAsync(Db.From<ApplicationUser>()
.Where(x => userNameMentions.Contains(x.UserName!)));

foreach (var existingUser in existingUsers)
{
var firstMentionPos = cleanBody.IndexOf(existingUser.UserName!, StringComparison.Ordinal);
if (firstMentionPos < 0) continue;

var startPos = Math.Max(0, firstMentionPos - 50);
await Db.InsertAsync(new Notification
{
UserName = existingUser.UserName!,
Type = NotificationType.CommentMention,
RefId = commentRefId,
PostId = postId,
CreatedDate = createdDate,
PostTitle = post.Title.SubstringWithEllipsis(0,100),
Summary = cleanBody.SubstringWithEllipsis(startPos,100),
Href = commentHref,
RefUserName = comment.CreatedBy,
});
appConfig.IncrNotificationsFor(existingUser.UserName!);
}
}
}
}

if (request.DeleteComment != null)
{
var refId = $"{request.DeleteComment.Id}-{request.DeleteComment.Created}";
var rowsAffected = await Db.DeleteAsync(Db.From<Notification>()
.Where(x => x.RefId == refId && x.RefUserName == request.DeleteComment.CreatedBy));
if (rowsAffected > 0)
{
appConfig.ResetUsersUnreadNotifications(Db);
}
}

if (request.UpdateReputations == true)
{
// TODO improve
appConfig.UpdateUsersReputation(Db);
appConfig.ResetUsersReputation(Db);
}

if (request.MarkAsRead != null)
{
var userName = request.MarkAsRead.UserName;
if (request.MarkAsRead.AllNotifications == true)
{
await Db.UpdateOnlyAsync(() => new Notification { Read = true }, x => x.UserName == userName);
appConfig.UsersUnreadNotifications[userName] = 0;
}
else if (request.MarkAsRead.NotificationIds?.Count > 0)
{
await Db.UpdateOnlyAsync(() => new Notification { Read = true },
x => x.UserName == userName && request.MarkAsRead.NotificationIds.Contains(x.Id));
appConfig.UsersUnreadNotifications[userName] = (int) await Db.CountAsync(
Db.From<Notification>().Where(x => x.UserName == userName && !x.Read));
}
if (request.MarkAsRead.AllAchievements == true)
{
await Db.UpdateOnlyAsync(() => new Achievement { Read = true }, x => x.UserName == userName);
appConfig.UsersUnreadAchievements[userName] = 0;
}
else if (request.MarkAsRead.AchievementIds?.Count > 0)
{
await Db.UpdateOnlyAsync(() => new Achievement { Read = true },
x => x.UserName == userName && request.MarkAsRead.AchievementIds.Contains(x.Id));
appConfig.UsersUnreadAchievements[userName] = (int) await Db.CountAsync(
Db.From<Achievement>().Where(x => x.UserName == userName && !x.Read));
}
}
}

public async Task Any(AnalyticsTasks request)
Expand Down
36 changes: 36 additions & 0 deletions MyApp.ServiceInterface/Data/AppConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ public class AppConfig
public string? GitPagesBaseUrl { get; set; }
public ConcurrentDictionary<string,int> UsersReputation { get; set; } = new();
public ConcurrentDictionary<string,int> UsersQuestions { get; set; } = new();
public ConcurrentDictionary<string,int> UsersUnreadAchievements { get; set; } = new();
public ConcurrentDictionary<string,int> UsersUnreadNotifications { get; set; } = new();
public HashSet<string> AllTags { get; set; } = [];
public List<ApplicationUser> ModelUsers { get; set; } = [];

Expand Down Expand Up @@ -103,6 +105,9 @@ public void Init(IDbConnection db)

ResetUsersReputation(db);
ResetUsersQuestions(db);

ResetUsersUnreadAchievements(db);
ResetUsersUnreadNotifications(db);
}

public void UpdateUsersReputation(IDbConnection db)
Expand Down Expand Up @@ -149,6 +154,18 @@ public void ResetUsersQuestions(IDbConnection db)
.Select(x => new { x.UserName, x.QuestionsCount })));
}

public void ResetUsersUnreadNotifications(IDbConnection db)
{
UsersUnreadNotifications = new(db.Dictionary<string, int>(
"SELECT UserName, Count(*) AS Total FROM Notification WHERE Read = false GROUP BY UserName HAVING COUNT(*) > 0"));
}

public void ResetUsersUnreadAchievements(IDbConnection db)
{
UsersUnreadAchievements = new(db.Dictionary<string, int>(
"SELECT UserName, Count(*) AS Total FROM Achievement WHERE Read = false GROUP BY UserName HAVING COUNT(*) > 0"));
}

public async Task ResetUserQuestionsAsync(IDbConnection db, string userName)
{
var questionsCount = (int)await db.CountAsync<Post>(x => x.CreatedBy == userName);
Expand All @@ -174,4 +191,23 @@ public List<string> GetAnswerModelsFor(string? userName)
return models;
}

public void IncrNotificationsFor(string userName)
{
UsersUnreadNotifications.AddOrUpdate(userName, 1, (_, count) => count + 1);
}

public void IncrAchievementsFor(string userName)
{
UsersUnreadAchievements.AddOrUpdate(userName, 1, (_, count) => count + 1);
}

public bool HasUnreadNotifications(string? userName)
{
return userName != null && UsersUnreadNotifications.TryGetValue(userName, out var count) && count > 0;
}

public bool HasUnreadAchievements(string? userName)
{
return userName != null && UsersUnreadAchievements.TryGetValue(userName, out var count) && count > 0;
}
}
31 changes: 31 additions & 0 deletions MyApp.ServiceInterface/Data/StringExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
using ServiceStack.Text;

namespace MyApp.Data;

public static class StringExtensions
{
static bool IsUserNameChar(char c) => c == '-' || (char.IsLetterOrDigit(c) && char.IsLower(c));

public static List<string> FindUserNameMentions(this string text)
{
var to = new List<string>();
var s = text.AsSpan();
s.AdvancePastChar('@');
while (s.Length > 0)
{
var i = 0;
while (IsUserNameChar(s[i]))
{
if (++i >= s.Length)
break;
}
var candidate = i > 2 ? s[..i].ToString() : "";
if (candidate.Length > 1)
{
to.Add(candidate);
}
s = s.Advance(i).AdvancePastChar('@');
}
return to;
}
}
3 changes: 3 additions & 0 deletions MyApp.ServiceInterface/Data/Tasks.cs
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ public class StartJob

public class NewComment
{
// Post or AnswerId
public string RefId { get; set; }
public Comment Comment { get; set; }
}
Expand All @@ -68,11 +69,13 @@ public class DbWrites
public Post? CreateAnswer { get; set; }
public int? AnswerAddedToPost { get; set; }
public NewComment? NewComment { get; set; }
public DeleteComment? DeleteComment { get; set; }
public List<int>? CompleteJobIds { get; set; }
public FailJob? FailJob { get; set; }
public ApplicationUser? UserRegistered { get; set; }
public ApplicationUser? UserSignedIn { get; set; }
public bool? UpdateReputations { get; set; }
public MarkAsRead? MarkAsRead { get; set; }
}

[Tag(Tag.Tasks)]
Expand Down
Loading

0 comments on commit cb52d49

Please sign in to comment.