🚚 Rename activity in sphere to timeline
In order to leave the activity keyword for pass service user activity
This commit is contained in:
		| @@ -1,34 +0,0 @@ | ||||
| using System.ComponentModel.DataAnnotations; | ||||
| using System.ComponentModel.DataAnnotations.Schema; | ||||
| using NodaTime; | ||||
|  | ||||
| namespace DysonNetwork.Shared.Models; | ||||
|  | ||||
| public interface IActivity | ||||
| { | ||||
|     public SnActivity ToActivity(); | ||||
| } | ||||
|  | ||||
| [NotMapped] | ||||
| public class SnActivity : ModelBase | ||||
| { | ||||
|     public Guid Id { get; set; } | ||||
|     [MaxLength(1024)] public string Type { get; set; } = null!; | ||||
|     [MaxLength(4096)] public string ResourceIdentifier { get; set; } = null!; | ||||
|     [Column(TypeName = "jsonb")] public Dictionary<string, object> Meta { get; set; } = new(); | ||||
|  | ||||
|     public object? Data { get; set; } | ||||
|  | ||||
|     public static SnActivity Empty() | ||||
|     { | ||||
|         var now = SystemClock.Instance.GetCurrentInstant(); | ||||
|         return new SnActivity | ||||
|         { | ||||
|             CreatedAt = now, | ||||
|             UpdatedAt = now, | ||||
|             Id = Guid.NewGuid(), | ||||
|             Type = "empty", | ||||
|             ResourceIdentifier = "none" | ||||
|         }; | ||||
|     } | ||||
| } | ||||
| @@ -2,16 +2,15 @@ using System.ComponentModel.DataAnnotations; | ||||
| using System.ComponentModel.DataAnnotations.Schema; | ||||
| using System.Text.Json.Serialization; | ||||
| using DysonNetwork.Shared.Proto; | ||||
| using NodaTime; | ||||
| using NpgsqlTypes; | ||||
| using Google.Protobuf.WellKnownTypes; | ||||
| using NodaTime; | ||||
|  | ||||
| namespace DysonNetwork.Shared.Models; | ||||
|  | ||||
| public enum PostType | ||||
| { | ||||
|     Moment, | ||||
|     Article | ||||
|     Article, | ||||
| } | ||||
|  | ||||
| public enum PostVisibility | ||||
| @@ -19,7 +18,7 @@ public enum PostVisibility | ||||
|     Public, | ||||
|     Friends, | ||||
|     Unlisted, | ||||
|     Private | ||||
|     Private, | ||||
| } | ||||
|  | ||||
| public enum PostPinMode | ||||
| @@ -29,12 +28,18 @@ public enum PostPinMode | ||||
|     ReplyPage, | ||||
| } | ||||
|  | ||||
| public class SnPost : ModelBase, IIdentifiedResource, IActivity | ||||
| public class SnPost : ModelBase, IIdentifiedResource, ITimelineEvent | ||||
| { | ||||
|     public Guid Id { get; set; } | ||||
|     [MaxLength(1024)] public string? Title { get; set; } | ||||
|     [MaxLength(4096)] public string? Description { get; set; } | ||||
|     [MaxLength(1024)] public string? Slug { get; set; } | ||||
|  | ||||
|     [MaxLength(1024)] | ||||
|     public string? Title { get; set; } | ||||
|  | ||||
|     [MaxLength(4096)] | ||||
|     public string? Description { get; set; } | ||||
|  | ||||
|     [MaxLength(1024)] | ||||
|     public string? Slug { get; set; } | ||||
|     public Instant? EditedAt { get; set; } | ||||
|     public Instant? PublishedAt { get; set; } | ||||
|     public PostVisibility Visibility { get; set; } = PostVisibility.Public; | ||||
| @@ -44,18 +49,30 @@ public class SnPost : ModelBase, IIdentifiedResource, IActivity | ||||
|  | ||||
|     public PostType Type { get; set; } | ||||
|     public PostPinMode? PinMode { get; set; } | ||||
|     [Column(TypeName = "jsonb")] public Dictionary<string, object>? Meta { get; set; } | ||||
|     [Column(TypeName = "jsonb")] public List<ContentSensitiveMark>? SensitiveMarks { get; set; } = []; | ||||
|     [Column(TypeName = "jsonb")] public PostEmbedView? EmbedView { get; set; } | ||||
|  | ||||
|     [Column(TypeName = "jsonb")] | ||||
|     public Dictionary<string, object>? Meta { get; set; } | ||||
|  | ||||
|     [Column(TypeName = "jsonb")] | ||||
|     public List<ContentSensitiveMark>? SensitiveMarks { get; set; } = []; | ||||
|  | ||||
|     [Column(TypeName = "jsonb")] | ||||
|     public PostEmbedView? EmbedView { get; set; } | ||||
|  | ||||
|     public int ViewsUnique { get; set; } | ||||
|     public int ViewsTotal { get; set; } | ||||
|     public int Upvotes { get; set; } | ||||
|     public int Downvotes { get; set; } | ||||
|     public decimal AwardedScore { get; set; } | ||||
|     [NotMapped] public Dictionary<string, int> ReactionsCount { get; set; } = new(); | ||||
|     [NotMapped] public int RepliesCount { get; set; } | ||||
|     [NotMapped] public Dictionary<string, bool>? ReactionsMade { get; set; } | ||||
|  | ||||
|     [NotMapped] | ||||
|     public Dictionary<string, int> ReactionsCount { get; set; } = new(); | ||||
|  | ||||
|     [NotMapped] | ||||
|     public int RepliesCount { get; set; } | ||||
|  | ||||
|     [NotMapped] | ||||
|     public Dictionary<string, bool>? ReactionsMade { get; set; } | ||||
|  | ||||
|     public bool RepliedGone { get; set; } | ||||
|     public bool ForwardedGone { get; set; } | ||||
| @@ -66,22 +83,32 @@ public class SnPost : ModelBase, IIdentifiedResource, IActivity | ||||
|     public SnPost? ForwardedPost { get; set; } | ||||
|  | ||||
|     public Guid? RealmId { get; set; } | ||||
|     [NotMapped] public SnRealm? Realm { get; set; } | ||||
|  | ||||
|     [Column(TypeName = "jsonb")] public List<SnCloudFileReferenceObject> Attachments { get; set; } = []; | ||||
|     [NotMapped] | ||||
|     public SnRealm? Realm { get; set; } | ||||
|  | ||||
|     [Column(TypeName = "jsonb")] | ||||
|     public List<SnCloudFileReferenceObject> Attachments { get; set; } = []; | ||||
|  | ||||
|     public Guid PublisherId { get; set; } | ||||
|     public SnPublisher Publisher { get; set; } = null!; | ||||
|  | ||||
|     public List<SnPostAward> Awards { get; set; } = []; | ||||
|     [JsonIgnore] public List<SnPostReaction> Reactions { get; set; } = []; | ||||
|  | ||||
|     [JsonIgnore] | ||||
|     public List<SnPostReaction> Reactions { get; set; } = []; | ||||
|     public List<SnPostTag> Tags { get; set; } = []; | ||||
|     public List<SnPostCategory> Categories { get; set; } = []; | ||||
|     [JsonIgnore] public List<SnPostCollection> Collections { get; set; } = []; | ||||
|  | ||||
|     [JsonIgnore] | ||||
|     public List<SnPostCollection> Collections { get; set; } = []; | ||||
|     public List<SnPostFeaturedRecord> FeaturedRecords { get; set; } = []; | ||||
|  | ||||
|     [JsonIgnore] public bool Empty => Content == null && Attachments.Count == 0 && ForwardedPostId == null; | ||||
|     [NotMapped] public bool IsTruncated { get; set; } = false; | ||||
|     [JsonIgnore] | ||||
|     public bool Empty => Content == null && Attachments.Count == 0 && ForwardedPostId == null; | ||||
|  | ||||
|     [NotMapped] | ||||
|     public bool IsTruncated { get; set; } = false; | ||||
|  | ||||
|     public string ResourceIdentifier => $"post:{Id}"; | ||||
|  | ||||
| @@ -108,7 +135,7 @@ public class SnPost : ModelBase, IIdentifiedResource, IActivity | ||||
|             PublisherId = PublisherId.ToString(), | ||||
|             Publisher = Publisher.ToProtoValue(), | ||||
|             CreatedAt = Timestamp.FromDateTimeOffset(CreatedAt.ToDateTimeOffset()), | ||||
|             UpdatedAt = Timestamp.FromDateTimeOffset(UpdatedAt.ToDateTimeOffset()) | ||||
|             UpdatedAt = Timestamp.FromDateTimeOffset(UpdatedAt.ToDateTimeOffset()), | ||||
|         }; | ||||
|  | ||||
|         if (EditedAt.HasValue) | ||||
| @@ -195,7 +222,7 @@ public class SnPost : ModelBase, IIdentifiedResource, IActivity | ||||
|             PublisherId = Guid.Parse(proto.PublisherId), | ||||
|             Publisher = SnPublisher.FromProtoValue(proto.Publisher), | ||||
|             CreatedAt = Instant.FromDateTimeOffset(proto.CreatedAt.ToDateTimeOffset()), | ||||
|             UpdatedAt = Instant.FromDateTimeOffset(proto.UpdatedAt.ToDateTimeOffset()) | ||||
|             UpdatedAt = Instant.FromDateTimeOffset(proto.UpdatedAt.ToDateTimeOffset()), | ||||
|         }; | ||||
|  | ||||
|         if (proto.EditedAt is not null) | ||||
| @@ -211,11 +238,14 @@ public class SnPost : ModelBase, IIdentifiedResource, IActivity | ||||
|             post.PinMode = (PostPinMode)(proto.PinMode - 1); | ||||
|  | ||||
|         if (proto.Meta != null) | ||||
|             post.Meta = GrpcTypeHelper.ConvertByteStringToObject<Dictionary<string, object>>(proto.Meta); | ||||
|             post.Meta = GrpcTypeHelper.ConvertByteStringToObject<Dictionary<string, object>>( | ||||
|                 proto.Meta | ||||
|             ); | ||||
|  | ||||
|         if (proto.SensitiveMarks != null) | ||||
|             post.SensitiveMarks = | ||||
|                 GrpcTypeHelper.ConvertByteStringToObject<List<ContentSensitiveMark>>(proto.SensitiveMarks); | ||||
|             post.SensitiveMarks = GrpcTypeHelper.ConvertByteStringToObject< | ||||
|                 List<ContentSensitiveMark> | ||||
|             >(proto.SensitiveMarks); | ||||
|  | ||||
|         if (proto.EmbedView is not null) | ||||
|             post.EmbedView = PostEmbedView.FromProtoValue(proto.EmbedView); | ||||
| @@ -241,19 +271,28 @@ public class SnPost : ModelBase, IIdentifiedResource, IActivity | ||||
|                 post.Realm = SnRealm.FromProtoValue(proto.Realm); | ||||
|         } | ||||
|  | ||||
|         post.Attachments.AddRange(proto.Attachments.Select(SnCloudFileReferenceObject.FromProtoValue)); | ||||
|         post.Awards.AddRange(proto.Awards.Select(a => new SnPostAward | ||||
|         post.Attachments.AddRange( | ||||
|             proto.Attachments.Select(SnCloudFileReferenceObject.FromProtoValue) | ||||
|         ); | ||||
|         post.Awards.AddRange( | ||||
|             proto.Awards.Select(a => new SnPostAward | ||||
|             { | ||||
|             Id = Guid.Parse(a.Id), PostId = Guid.Parse(a.PostId), AccountId = Guid.Parse(a.AccountId), | ||||
|             Amount = (decimal)a.Amount, Attitude = (PostReactionAttitude)((int)a.Attitude - 1), | ||||
|                 Id = Guid.Parse(a.Id), | ||||
|                 PostId = Guid.Parse(a.PostId), | ||||
|                 AccountId = Guid.Parse(a.AccountId), | ||||
|                 Amount = (decimal)a.Amount, | ||||
|                 Attitude = (PostReactionAttitude)((int)a.Attitude - 1), | ||||
|                 Message = string.IsNullOrEmpty(a.Message) ? null : a.Message, | ||||
|                 CreatedAt = Instant.FromDateTimeOffset(a.CreatedAt.ToDateTimeOffset()), | ||||
|             UpdatedAt = Instant.FromDateTimeOffset(a.UpdatedAt.ToDateTimeOffset()) | ||||
|         })); | ||||
|                 UpdatedAt = Instant.FromDateTimeOffset(a.UpdatedAt.ToDateTimeOffset()), | ||||
|             }) | ||||
|         ); | ||||
|         post.Reactions.AddRange(proto.Reactions.Select(SnPostReaction.FromProtoValue)); | ||||
|         post.Tags.AddRange(proto.Tags.Select(SnPostTag.FromProtoValue)); | ||||
|         post.Categories.AddRange(proto.Categories.Select(SnPostCategory.FromProtoValue)); | ||||
|         post.FeaturedRecords.AddRange(proto.FeaturedRecords.Select(SnPostFeaturedRecord.FromProtoValue)); | ||||
|         post.FeaturedRecords.AddRange( | ||||
|             proto.FeaturedRecords.Select(SnPostFeaturedRecord.FromProtoValue) | ||||
|         ); | ||||
|  | ||||
|         if (proto.DeletedAt is not null) | ||||
|             post.DeletedAt = Instant.FromDateTimeOffset(proto.DeletedAt.ToDateTimeOffset()); | ||||
| @@ -261,9 +300,9 @@ public class SnPost : ModelBase, IIdentifiedResource, IActivity | ||||
|         return post; | ||||
|     } | ||||
|  | ||||
|     public SnActivity ToActivity() | ||||
|     public SnTimelineEvent ToActivity() | ||||
|     { | ||||
|         return new SnActivity() | ||||
|         return new SnTimelineEvent() | ||||
|         { | ||||
|             CreatedAt = PublishedAt ?? CreatedAt, | ||||
|             UpdatedAt = UpdatedAt, | ||||
| @@ -271,7 +310,7 @@ public class SnPost : ModelBase, IIdentifiedResource, IActivity | ||||
|             Id = Id, | ||||
|             Type = RepliedPostId is null ? "posts.new" : "posts.new.replies", | ||||
|             ResourceIdentifier = ResourceIdentifier, | ||||
|             Data = this | ||||
|             Data = this, | ||||
|         }; | ||||
|     } | ||||
| } | ||||
| @@ -279,11 +318,18 @@ public class SnPost : ModelBase, IIdentifiedResource, IActivity | ||||
| public class SnPostTag : ModelBase | ||||
| { | ||||
|     public Guid Id { get; set; } | ||||
|     [MaxLength(128)] public string Slug { get; set; } = null!; | ||||
|     [MaxLength(256)] public string? Name { get; set; } | ||||
|     [JsonIgnore] public List<SnPost> Posts { get; set; } = new List<SnPost>(); | ||||
|  | ||||
|     [NotMapped] public int? Usage { get; set; } | ||||
|     [MaxLength(128)] | ||||
|     public string Slug { get; set; } = null!; | ||||
|  | ||||
|     [MaxLength(256)] | ||||
|     public string? Name { get; set; } | ||||
|  | ||||
|     [JsonIgnore] | ||||
|     public List<SnPost> Posts { get; set; } = new List<SnPost>(); | ||||
|  | ||||
|     [NotMapped] | ||||
|     public int? Usage { get; set; } | ||||
|  | ||||
|     public PostTag ToProtoValue() | ||||
|     { | ||||
| @@ -293,7 +339,7 @@ public class SnPostTag : ModelBase | ||||
|             Slug = Slug, | ||||
|             Name = Name ?? string.Empty, | ||||
|             CreatedAt = Timestamp.FromDateTimeOffset(CreatedAt.ToDateTimeOffset()), | ||||
|             UpdatedAt = Timestamp.FromDateTimeOffset(UpdatedAt.ToDateTimeOffset()) | ||||
|             UpdatedAt = Timestamp.FromDateTimeOffset(UpdatedAt.ToDateTimeOffset()), | ||||
|         }; | ||||
|     } | ||||
|  | ||||
| @@ -305,7 +351,7 @@ public class SnPostTag : ModelBase | ||||
|             Slug = proto.Slug, | ||||
|             Name = proto.Name != string.Empty ? proto.Name : null, | ||||
|             CreatedAt = Instant.FromDateTimeOffset(proto.CreatedAt.ToDateTimeOffset()), | ||||
|             UpdatedAt = Instant.FromDateTimeOffset(proto.UpdatedAt.ToDateTimeOffset()) | ||||
|             UpdatedAt = Instant.FromDateTimeOffset(proto.UpdatedAt.ToDateTimeOffset()), | ||||
|         }; | ||||
|     } | ||||
| } | ||||
| @@ -313,11 +359,18 @@ public class SnPostTag : ModelBase | ||||
| public class SnPostCategory : ModelBase | ||||
| { | ||||
|     public Guid Id { get; set; } | ||||
|     [MaxLength(128)] public string Slug { get; set; } = null!; | ||||
|     [MaxLength(256)] public string? Name { get; set; } | ||||
|     [JsonIgnore] public List<SnPost> Posts { get; set; } = new List<SnPost>(); | ||||
|  | ||||
|     [NotMapped] public int? Usage { get; set; } | ||||
|     [MaxLength(128)] | ||||
|     public string Slug { get; set; } = null!; | ||||
|  | ||||
|     [MaxLength(256)] | ||||
|     public string? Name { get; set; } | ||||
|  | ||||
|     [JsonIgnore] | ||||
|     public List<SnPost> Posts { get; set; } = new List<SnPost>(); | ||||
|  | ||||
|     [NotMapped] | ||||
|     public int? Usage { get; set; } | ||||
|  | ||||
|     public PostCategory ToProtoValue() | ||||
|     { | ||||
| @@ -327,7 +380,7 @@ public class SnPostCategory : ModelBase | ||||
|             Slug = Slug, | ||||
|             Name = Name ?? string.Empty, | ||||
|             CreatedAt = Timestamp.FromDateTimeOffset(CreatedAt.ToDateTimeOffset()), | ||||
|             UpdatedAt = Timestamp.FromDateTimeOffset(UpdatedAt.ToDateTimeOffset()) | ||||
|             UpdatedAt = Timestamp.FromDateTimeOffset(UpdatedAt.ToDateTimeOffset()), | ||||
|         }; | ||||
|     } | ||||
|  | ||||
| @@ -339,7 +392,7 @@ public class SnPostCategory : ModelBase | ||||
|             Slug = proto.Slug, | ||||
|             Name = proto.Name != string.Empty ? proto.Name : null, | ||||
|             CreatedAt = Instant.FromDateTimeOffset(proto.CreatedAt.ToDateTimeOffset()), | ||||
|             UpdatedAt = Instant.FromDateTimeOffset(proto.UpdatedAt.ToDateTimeOffset()) | ||||
|             UpdatedAt = Instant.FromDateTimeOffset(proto.UpdatedAt.ToDateTimeOffset()), | ||||
|         }; | ||||
|     } | ||||
| } | ||||
| @@ -358,9 +411,15 @@ public class SnPostCategorySubscription : ModelBase | ||||
| public class SnPostCollection : ModelBase | ||||
| { | ||||
|     public Guid Id { get; set; } | ||||
|     [MaxLength(128)] public string Slug { get; set; } = null!; | ||||
|     [MaxLength(256)] public string? Name { get; set; } | ||||
|     [MaxLength(4096)] public string? Description { get; set; } | ||||
|  | ||||
|     [MaxLength(128)] | ||||
|     public string Slug { get; set; } = null!; | ||||
|  | ||||
|     [MaxLength(256)] | ||||
|     public string? Name { get; set; } | ||||
|  | ||||
|     [MaxLength(4096)] | ||||
|     public string? Description { get; set; } | ||||
|  | ||||
|     public SnPublisher Publisher { get; set; } = null!; | ||||
|  | ||||
| @@ -371,7 +430,9 @@ public class SnPostFeaturedRecord : ModelBase | ||||
| { | ||||
|     public Guid Id { get; set; } | ||||
|     public Guid PostId { get; set; } | ||||
|     [JsonIgnore] public SnPost Post { get; set; } = null!; | ||||
|  | ||||
|     [JsonIgnore] | ||||
|     public SnPost Post { get; set; } = null!; | ||||
|     public Instant? FeaturedAt { get; set; } | ||||
|     public int SocialCredits { get; set; } | ||||
|  | ||||
| @@ -383,7 +444,7 @@ public class SnPostFeaturedRecord : ModelBase | ||||
|             PostId = PostId.ToString(), | ||||
|             SocialCredits = SocialCredits, | ||||
|             CreatedAt = Timestamp.FromDateTimeOffset(CreatedAt.ToDateTimeOffset()), | ||||
|             UpdatedAt = Timestamp.FromDateTimeOffset(UpdatedAt.ToDateTimeOffset()) | ||||
|             UpdatedAt = Timestamp.FromDateTimeOffset(UpdatedAt.ToDateTimeOffset()), | ||||
|         }; | ||||
|         if (FeaturedAt.HasValue) | ||||
|         { | ||||
| @@ -402,7 +463,10 @@ public class SnPostFeaturedRecord : ModelBase | ||||
|             SocialCredits = proto.SocialCredits, | ||||
|             CreatedAt = Instant.FromDateTimeOffset(proto.CreatedAt.ToDateTimeOffset()), | ||||
|             UpdatedAt = Instant.FromDateTimeOffset(proto.UpdatedAt.ToDateTimeOffset()), | ||||
|             FeaturedAt = proto.FeaturedAt != null ? Instant.FromDateTimeOffset(proto.FeaturedAt.ToDateTimeOffset()) : null | ||||
|             FeaturedAt = | ||||
|                 proto.FeaturedAt != null | ||||
|                     ? Instant.FromDateTimeOffset(proto.FeaturedAt.ToDateTimeOffset()) | ||||
|                     : null, | ||||
|         }; | ||||
|     } | ||||
| } | ||||
| @@ -417,13 +481,19 @@ public enum PostReactionAttitude | ||||
| public class SnPostReaction : ModelBase | ||||
| { | ||||
|     public Guid Id { get; set; } | ||||
|     [MaxLength(256)] public string Symbol { get; set; } = null!; | ||||
|  | ||||
|     [MaxLength(256)] | ||||
|     public string Symbol { get; set; } = null!; | ||||
|     public PostReactionAttitude Attitude { get; set; } | ||||
|  | ||||
|     public Guid PostId { get; set; } | ||||
|     [JsonIgnore] public SnPost Post { get; set; } = null!; | ||||
|  | ||||
|     [JsonIgnore] | ||||
|     public SnPost Post { get; set; } = null!; | ||||
|     public Guid AccountId { get; set; } | ||||
|     [NotMapped] public SnAccount? Account { get; set; } | ||||
|  | ||||
|     [NotMapped] | ||||
|     public SnAccount? Account { get; set; } | ||||
|  | ||||
|     public PostReaction ToProtoValue() | ||||
|     { | ||||
| @@ -435,7 +505,7 @@ public class SnPostReaction : ModelBase | ||||
|             PostId = PostId.ToString(), | ||||
|             AccountId = AccountId.ToString(), | ||||
|             CreatedAt = Timestamp.FromDateTimeOffset(CreatedAt.ToDateTimeOffset()), | ||||
|             UpdatedAt = Timestamp.FromDateTimeOffset(UpdatedAt.ToDateTimeOffset()) | ||||
|             UpdatedAt = Timestamp.FromDateTimeOffset(UpdatedAt.ToDateTimeOffset()), | ||||
|         }; | ||||
|         if (Account != null) | ||||
|         { | ||||
| @@ -456,7 +526,7 @@ public class SnPostReaction : ModelBase | ||||
|             AccountId = Guid.Parse(proto.AccountId), | ||||
|             Account = proto.Account != null ? SnAccount.FromProtoValue(proto.Account) : null, | ||||
|             CreatedAt = Instant.FromDateTimeOffset(proto.CreatedAt.ToDateTimeOffset()), | ||||
|             UpdatedAt = Instant.FromDateTimeOffset(proto.UpdatedAt.ToDateTimeOffset()) | ||||
|             UpdatedAt = Instant.FromDateTimeOffset(proto.UpdatedAt.ToDateTimeOffset()), | ||||
|         }; | ||||
|     } | ||||
| } | ||||
| @@ -466,10 +536,14 @@ public class SnPostAward : ModelBase | ||||
|     public Guid Id { get; set; } | ||||
|     public decimal Amount { get; set; } | ||||
|     public PostReactionAttitude Attitude { get; set; } | ||||
|     [MaxLength(4096)] public string? Message { get; set; } | ||||
|  | ||||
|     [MaxLength(4096)] | ||||
|     public string? Message { get; set; } | ||||
|  | ||||
|     public Guid PostId { get; set; } | ||||
|     [JsonIgnore] public SnPost Post { get; set; } = null!; | ||||
|  | ||||
|     [JsonIgnore] | ||||
|     public SnPost Post { get; set; } = null!; | ||||
|     public Guid AccountId { get; set; } | ||||
|  | ||||
|     public PostAward ToProtoValue() | ||||
| @@ -482,7 +556,7 @@ public class SnPostAward : ModelBase | ||||
|             PostId = PostId.ToString(), | ||||
|             AccountId = AccountId.ToString(), | ||||
|             CreatedAt = Timestamp.FromDateTimeOffset(CreatedAt.ToDateTimeOffset()), | ||||
|             UpdatedAt = Timestamp.FromDateTimeOffset(UpdatedAt.ToDateTimeOffset()) | ||||
|             UpdatedAt = Timestamp.FromDateTimeOffset(UpdatedAt.ToDateTimeOffset()), | ||||
|         }; | ||||
|         if (Message != null) | ||||
|             proto.Message = Message; | ||||
| @@ -506,7 +580,7 @@ public class PostEmbedView | ||||
|         var proto = new Proto.PostEmbedView | ||||
|         { | ||||
|             Uri = Uri, | ||||
|             Renderer = (Proto.PostEmbedViewRenderer)(int)Renderer | ||||
|             Renderer = (Proto.PostEmbedViewRenderer)(int)Renderer, | ||||
|         }; | ||||
|         if (AspectRatio.HasValue) | ||||
|         { | ||||
| @@ -522,12 +596,12 @@ public class PostEmbedView | ||||
|         { | ||||
|             Uri = proto.Uri, | ||||
|             AspectRatio = proto.HasAspectRatio ? proto.AspectRatio : null, | ||||
|             Renderer = (PostEmbedViewRenderer)((int)proto.Renderer - 1) | ||||
|             Renderer = (PostEmbedViewRenderer)((int)proto.Renderer - 1), | ||||
|         }; | ||||
|     } | ||||
| } | ||||
|  | ||||
| public enum PostEmbedViewRenderer | ||||
| { | ||||
|     WebView | ||||
|     WebView, | ||||
| } | ||||
|   | ||||
							
								
								
									
										41
									
								
								DysonNetwork.Shared/Models/Timeline.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								DysonNetwork.Shared/Models/Timeline.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,41 @@ | ||||
| using System.ComponentModel.DataAnnotations; | ||||
| using System.ComponentModel.DataAnnotations.Schema; | ||||
| using NodaTime; | ||||
|  | ||||
| namespace DysonNetwork.Shared.Models; | ||||
|  | ||||
| public interface ITimelineEvent | ||||
| { | ||||
|     public SnTimelineEvent ToActivity(); | ||||
| } | ||||
|  | ||||
| [NotMapped] | ||||
| public class SnTimelineEvent : ModelBase | ||||
| { | ||||
|     public Guid Id { get; set; } | ||||
|  | ||||
|     [MaxLength(1024)] | ||||
|     public string Type { get; set; } = null!; | ||||
|  | ||||
|     [MaxLength(4096)] | ||||
|     public string ResourceIdentifier { get; set; } = null!; | ||||
|  | ||||
|     [Column(TypeName = "jsonb")] | ||||
|     public Dictionary<string, object> Meta { get; set; } = new(); | ||||
|  | ||||
|     public object? Data { get; set; } | ||||
|  | ||||
|     public static SnTimelineEvent Empty() | ||||
|     { | ||||
|         var now = SystemClock.Instance.GetCurrentInstant(); | ||||
|         return new SnTimelineEvent | ||||
|         { | ||||
|             CreatedAt = now, | ||||
|             UpdatedAt = now, | ||||
|             Id = Guid.NewGuid(), | ||||
|             Type = "empty", | ||||
|             ResourceIdentifier = "none", | ||||
|         }; | ||||
|     } | ||||
| } | ||||
|  | ||||
| @@ -1,23 +1,23 @@ | ||||
| using System.Globalization; | ||||
| using DysonNetwork.Sphere.Activity; | ||||
| using DysonNetwork.Sphere.Chat; | ||||
| using DysonNetwork.Sphere.Chat.Realtime; | ||||
| using DysonNetwork.Sphere.Localization; | ||||
| using DysonNetwork.Sphere.Post; | ||||
| using DysonNetwork.Sphere.Publisher; | ||||
| using DysonNetwork.Sphere.Sticker; | ||||
| using NodaTime; | ||||
| using NodaTime.Serialization.SystemTextJson; | ||||
| using System.Text.Json; | ||||
| using System.Text.Json.Serialization; | ||||
| using DysonNetwork.Shared.Cache; | ||||
| using DysonNetwork.Shared.GeoIp; | ||||
| using DysonNetwork.Shared.Registry; | ||||
| using DysonNetwork.Sphere.Autocompletion; | ||||
| using DysonNetwork.Sphere.WebReader; | ||||
| using DysonNetwork.Sphere.Chat; | ||||
| using DysonNetwork.Sphere.Chat.Realtime; | ||||
| using DysonNetwork.Sphere.Discovery; | ||||
| using DysonNetwork.Sphere.Localization; | ||||
| using DysonNetwork.Sphere.Poll; | ||||
| using DysonNetwork.Sphere.Post; | ||||
| using DysonNetwork.Sphere.Publisher; | ||||
| using DysonNetwork.Sphere.Sticker; | ||||
| using DysonNetwork.Sphere.Timeline; | ||||
| using DysonNetwork.Sphere.Translation; | ||||
| using DysonNetwork.Sphere.WebReader; | ||||
| using NodaTime; | ||||
| using NodaTime.Serialization.SystemTextJson; | ||||
|  | ||||
| namespace DysonNetwork.Sphere.Startup; | ||||
|  | ||||
| @@ -34,34 +34,41 @@ public static class ServiceCollectionExtensions | ||||
|  | ||||
|         services.AddHttpClient(); | ||||
|  | ||||
|         services.AddControllers().AddJsonOptions(options => | ||||
|         services | ||||
|             .AddControllers() | ||||
|             .AddJsonOptions(options => | ||||
|             { | ||||
|             options.JsonSerializerOptions.NumberHandling = JsonNumberHandling.AllowNamedFloatingPointLiterals; | ||||
|             options.JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower; | ||||
|                 options.JsonSerializerOptions.NumberHandling = | ||||
|                     JsonNumberHandling.AllowNamedFloatingPointLiterals; | ||||
|                 options.JsonSerializerOptions.PropertyNamingPolicy = | ||||
|                     JsonNamingPolicy.SnakeCaseLower; | ||||
|  | ||||
|                 options.JsonSerializerOptions.ConfigureForNodaTime(DateTimeZoneProviders.Tzdb); | ||||
|         }).AddDataAnnotationsLocalization(options => | ||||
|             }) | ||||
|             .AddDataAnnotationsLocalization(options => | ||||
|             { | ||||
|                 options.DataAnnotationLocalizerProvider = (type, factory) => | ||||
|                     factory.Create(typeof(SharedResource)); | ||||
|         }).ConfigureApplicationPartManager(opts => | ||||
|             }) | ||||
|             .ConfigureApplicationPartManager(opts => | ||||
|             { | ||||
|             var mockingPart = opts.ApplicationParts.FirstOrDefault(a => a.Name == "DysonNetwork.Pass"); | ||||
|                 var mockingPart = opts.ApplicationParts.FirstOrDefault(a => | ||||
|                     a.Name == "DysonNetwork.Pass" | ||||
|                 ); | ||||
|                 if (mockingPart != null) | ||||
|                     opts.ApplicationParts.Remove(mockingPart); | ||||
|             }); | ||||
|         services.AddRazorPages(); | ||||
|  | ||||
|         services.AddGrpc(options => { options.EnableDetailedErrors = true; }); | ||||
|         services.AddGrpc(options => | ||||
|         { | ||||
|             options.EnableDetailedErrors = true; | ||||
|         }); | ||||
|         services.AddGrpcReflection(); | ||||
|  | ||||
|         services.Configure<RequestLocalizationOptions>(options => | ||||
|         { | ||||
|             var supportedCultures = new[] | ||||
|             { | ||||
|                 new CultureInfo("en-US"), | ||||
|                 new CultureInfo("zh-Hans"), | ||||
|             }; | ||||
|             var supportedCultures = new[] { new CultureInfo("en-US"), new CultureInfo("zh-Hans") }; | ||||
|  | ||||
|             options.SupportedCultures = supportedCultures; | ||||
|             options.SupportedUICultures = supportedCultures; | ||||
| @@ -86,14 +93,16 @@ public static class ServiceCollectionExtensions | ||||
|         return services; | ||||
|     } | ||||
|  | ||||
|     public static IServiceCollection AddAppBusinessServices(this IServiceCollection services, | ||||
|         IConfiguration configuration) | ||||
|     public static IServiceCollection AddAppBusinessServices( | ||||
|         this IServiceCollection services, | ||||
|         IConfiguration configuration | ||||
|     ) | ||||
|     { | ||||
|         services.Configure<GeoIpOptions>(configuration.GetSection("GeoIP")); | ||||
|         services.AddScoped<GeoIpService>(); | ||||
|         services.AddScoped<PublisherService>(); | ||||
|         services.AddScoped<PublisherSubscriptionService>(); | ||||
|         services.AddScoped<ActivityService>(); | ||||
|         services.AddScoped<TimelineService>(); | ||||
|         services.AddScoped<PostService>(); | ||||
|         services.AddScoped<ChatRoomService>(); | ||||
|         services.AddScoped<ChatService>(); | ||||
|   | ||||
| @@ -4,16 +4,14 @@ using Microsoft.AspNetCore.Mvc; | ||||
| using NodaTime; | ||||
| using NodaTime.Text; | ||||
| 
 | ||||
| namespace DysonNetwork.Sphere.Activity; | ||||
| namespace DysonNetwork.Sphere.Timeline; | ||||
| 
 | ||||
| /// <summary> | ||||
| /// Activity is a universal feed that contains multiple kinds of data. Personalized and generated dynamically. | ||||
| /// </summary> | ||||
| [ApiController] | ||||
| [Route("/api/activities")] | ||||
| public class ActivityController( | ||||
|     ActivityService acts | ||||
| ) : ControllerBase | ||||
| [Route("/api/timeline")] | ||||
| public class ActivityController(TimelineService acts) : ControllerBase | ||||
| { | ||||
|     /// <summary> | ||||
|     /// Listing the activities for the user, users may be logged in or not to use this API. | ||||
| @@ -24,7 +22,7 @@ public class ActivityController( | ||||
|     /// Besides, when users are logged in, it will also mix the other kinds of data and who're plying to them. | ||||
|     /// </summary> | ||||
|     [HttpGet] | ||||
|     public async Task<ActionResult<List<SnActivity>>> ListActivities( | ||||
|     public async Task<ActionResult<List<SnTimelineEvent>>> ListEvents( | ||||
|         [FromQuery] string? cursor, | ||||
|         [FromQuery] string? filter, | ||||
|         [FromQuery] int take = 20, | ||||
| @@ -48,7 +46,9 @@ public class ActivityController( | ||||
| 
 | ||||
|         HttpContext.Items.TryGetValue("CurrentUser", out var currentUserValue); | ||||
|         return currentUserValue is not Account currentUser | ||||
|             ? Ok(await acts.GetActivitiesForAnyone(take, cursorTimestamp, debugIncludeSet)) | ||||
|             : Ok(await acts.GetActivities(take, cursorTimestamp, currentUser, filter, debugIncludeSet)); | ||||
|             ? Ok(await acts.ListEventsForAnyone(take, cursorTimestamp, debugIncludeSet)) | ||||
|             : Ok( | ||||
|                 await acts.ListEvents(take, cursorTimestamp, currentUser, filter, debugIncludeSet) | ||||
|             ); | ||||
|     } | ||||
| } | ||||
| @@ -1,16 +1,16 @@ | ||||
| using DysonNetwork.Shared.Models; | ||||
| using NodaTime; | ||||
| 
 | ||||
| namespace DysonNetwork.Sphere.Activity; | ||||
| namespace DysonNetwork.Sphere.Timeline; | ||||
| 
 | ||||
| public class DiscoveryActivity(List<DiscoveryItem> items) : IActivity | ||||
| public class TimelineDiscoveryEvent(List<DiscoveryItem> items) : ITimelineEvent | ||||
| { | ||||
|     public List<DiscoveryItem> Items { get; set; } = items; | ||||
| 
 | ||||
|     public SnActivity ToActivity() | ||||
|     public SnTimelineEvent ToActivity() | ||||
|     { | ||||
|         var now = SystemClock.Instance.GetCurrentInstant(); | ||||
|         return new SnActivity | ||||
|         return new SnTimelineEvent | ||||
|         { | ||||
|             Id = Guid.NewGuid(), | ||||
|             Type = "discovery", | ||||
| @@ -23,3 +23,4 @@ public class DiscoveryActivity(List<DiscoveryItem> items) : IActivity | ||||
| } | ||||
| 
 | ||||
| public record DiscoveryItem(string Type, object Data); | ||||
| 
 | ||||
| @@ -7,9 +7,9 @@ using DysonNetwork.Sphere.WebReader; | ||||
| using Microsoft.EntityFrameworkCore; | ||||
| using NodaTime; | ||||
| 
 | ||||
| namespace DysonNetwork.Sphere.Activity; | ||||
| namespace DysonNetwork.Sphere.Timeline; | ||||
| 
 | ||||
| public class ActivityService( | ||||
| public class TimelineService( | ||||
|     AppDatabase db, | ||||
|     Publisher.PublisherService pub, | ||||
|     Post.PostService ps, | ||||
| @@ -20,7 +20,8 @@ public class ActivityService( | ||||
| { | ||||
|     private static double CalculateHotRank(SnPost post, Instant now) | ||||
|     { | ||||
|         var performanceScore = post.Upvotes - post.Downvotes + post.RepliesCount + (int)post.AwardedScore / 10; | ||||
|         var performanceScore = | ||||
|             post.Upvotes - post.Downvotes + post.RepliesCount + (int)post.AwardedScore / 10; | ||||
|         var postTime = post.PublishedAt ?? post.CreatedAt; | ||||
|         var timeScore = (now - postTime).TotalMinutes; | ||||
|         // Add 1 to score to prevent negative results for posts with more downvotes than upvotes | ||||
| @@ -31,12 +32,13 @@ public class ActivityService( | ||||
|         return performanceWeight / Math.Pow(normalizedTime + 1.0, 1.2); | ||||
|     } | ||||
| 
 | ||||
|     public async Task<List<SnActivity>> GetActivitiesForAnyone( | ||||
|     public async Task<List<SnTimelineEvent>> ListEventsForAnyone( | ||||
|         int take, | ||||
|         Instant? cursor, | ||||
|         HashSet<string>? debugInclude = null) | ||||
|         HashSet<string>? debugInclude = null | ||||
|     ) | ||||
|     { | ||||
|         var activities = new List<SnActivity>(); | ||||
|         var activities = new List<SnTimelineEvent>(); | ||||
|         debugInclude ??= new HashSet<string>(); | ||||
| 
 | ||||
|         // Get and process posts | ||||
| @@ -51,7 +53,7 @@ public class ActivityService( | ||||
|         await LoadPostsRealmsAsync(posts, rs); | ||||
|         posts = RankPosts(posts, take); | ||||
| 
 | ||||
|         var interleaved = new List<SnActivity>(); | ||||
|         var interleaved = new List<SnTimelineEvent>(); | ||||
|         var random = new Random(); | ||||
|         foreach (var post in posts) | ||||
|         { | ||||
| @@ -69,26 +71,26 @@ public class ActivityService( | ||||
|         activities.AddRange(interleaved); | ||||
| 
 | ||||
|         if (activities.Count == 0) | ||||
|             activities.Add(SnActivity.Empty()); | ||||
|             activities.Add(SnTimelineEvent.Empty()); | ||||
| 
 | ||||
|         return activities; | ||||
|     } | ||||
| 
 | ||||
|     public async Task<List<SnActivity>> GetActivities( | ||||
|     public async Task<List<SnTimelineEvent>> ListEvents( | ||||
|         int take, | ||||
|         Instant? cursor, | ||||
|         Account currentUser, | ||||
|         string? filter = null, | ||||
|         HashSet<string>? debugInclude = null) | ||||
|         HashSet<string>? debugInclude = null | ||||
|     ) | ||||
|     { | ||||
|         var activities = new List<SnActivity>(); | ||||
|         var activities = new List<SnTimelineEvent>(); | ||||
|         debugInclude ??= new HashSet<string>(); | ||||
| 
 | ||||
|         // Get user's friends and publishers | ||||
|         var friendsResponse = await accounts.ListFriendsAsync(new ListRelationshipSimpleRequest | ||||
|         { | ||||
|             AccountId = currentUser.Id | ||||
|         }); | ||||
|         var friendsResponse = await accounts.ListFriendsAsync( | ||||
|             new ListRelationshipSimpleRequest { AccountId = currentUser.Id } | ||||
|         ); | ||||
|         var userFriends = friendsResponse.AccountsId.Select(Guid.Parse).ToList(); | ||||
|         var userPublishers = await pub.GetUserPublishers(Guid.Parse(currentUser.Id)); | ||||
| 
 | ||||
| @@ -107,20 +109,18 @@ public class ActivityService( | ||||
|                 currentUser, | ||||
|                 userFriends, | ||||
|                 filter is null ? userPublishers : [], | ||||
|                 isListing: true) | ||||
|                 isListing: true | ||||
|             ) | ||||
|             .Take(take * 5); | ||||
| 
 | ||||
|         // Get, process and rank posts | ||||
|         var posts = await GetAndProcessPosts( | ||||
|             postsQuery, | ||||
|             currentUser, | ||||
|             trackViews: true); | ||||
|         var posts = await GetAndProcessPosts(postsQuery, currentUser, trackViews: true); | ||||
| 
 | ||||
|         await LoadPostsRealmsAsync(posts, rs); | ||||
| 
 | ||||
|         posts = RankPosts(posts, take); | ||||
| 
 | ||||
|         var interleaved = new List<SnActivity>(); | ||||
|         var interleaved = new List<SnTimelineEvent>(); | ||||
|         var random = new Random(); | ||||
|         foreach (var post in posts) | ||||
|         { | ||||
| @@ -137,15 +137,19 @@ public class ActivityService( | ||||
|         activities.AddRange(interleaved); | ||||
| 
 | ||||
|         if (activities.Count == 0) | ||||
|             activities.Add(SnActivity.Empty()); | ||||
|             activities.Add(SnTimelineEvent.Empty()); | ||||
| 
 | ||||
|         return activities; | ||||
|     } | ||||
| 
 | ||||
|     private async Task<SnActivity?> MaybeGetDiscoveryActivity(HashSet<string> debugInclude, Instant? cursor) | ||||
|     private async Task<SnTimelineEvent?> MaybeGetDiscoveryActivity( | ||||
|         HashSet<string> debugInclude, | ||||
|         Instant? cursor | ||||
|     ) | ||||
|     { | ||||
|         if (cursor != null) return null; | ||||
|         var options = new List<Func<Task<SnActivity?>>>(); | ||||
|         if (cursor != null) | ||||
|             return null; | ||||
|         var options = new List<Func<Task<SnTimelineEvent?>>>(); | ||||
|         if (debugInclude.Contains("realms") || Random.Shared.NextDouble() < 0.2) | ||||
|             options.Add(() => GetRealmDiscoveryActivity()); | ||||
|         if (debugInclude.Contains("publishers") || Random.Shared.NextDouble() < 0.2) | ||||
| @@ -154,7 +158,8 @@ public class ActivityService( | ||||
|             options.Add(() => GetArticleDiscoveryActivity()); | ||||
|         if (debugInclude.Contains("shuffledPosts") || Random.Shared.NextDouble() < 0.2) | ||||
|             options.Add(() => GetShuffledPostsActivity()); | ||||
|         if (options.Count == 0) return null; | ||||
|         if (options.Count == 0) | ||||
|             return null; | ||||
|         var random = new Random(); | ||||
|         var pick = options[random.Next(options.Count)]; | ||||
|         return await pick(); | ||||
| @@ -177,9 +182,7 @@ public class ActivityService( | ||||
|         var now = SystemClock.Instance.GetCurrentInstant(); | ||||
|         var recent = now.Minus(Duration.FromDays(7)); | ||||
| 
 | ||||
|         var posts = await db.Posts | ||||
|             .Where(p => p.PublishedAt > recent) | ||||
|             .ToListAsync(); | ||||
|         var posts = await db.Posts.Where(p => p.PublishedAt > recent).ToListAsync(); | ||||
| 
 | ||||
|         var publisherIds = posts.Select(p => p.PublisherId).Distinct().ToList(); | ||||
|         var publishers = await db.Publishers.Where(p => publisherIds.Contains(p.Id)).ToListAsync(); | ||||
| @@ -188,7 +191,7 @@ public class ActivityService( | ||||
|             .Select(p => new | ||||
|             { | ||||
|                 Publisher = p, | ||||
|                 Rank = CalculatePopularity(posts.Where(post => post.PublisherId == p.Id).ToList()) | ||||
|                 Rank = CalculatePopularity(posts.Where(post => post.PublisherId == p.Id).ToList()), | ||||
|             }) | ||||
|             .OrderByDescending(x => x.Rank) | ||||
|             .Select(x => x.Publisher) | ||||
| @@ -196,30 +199,33 @@ public class ActivityService( | ||||
|             .ToList(); | ||||
|     } | ||||
| 
 | ||||
|     private async Task<SnActivity?> GetRealmDiscoveryActivity(int count = 5) | ||||
|     private async Task<SnTimelineEvent?> GetRealmDiscoveryActivity(int count = 5) | ||||
|     { | ||||
|         var realms = await ds.GetCommunityRealmAsync(null, count, 0, true); | ||||
|         return realms.Count > 0 | ||||
|             ? new DiscoveryActivity(realms.Select(x => new DiscoveryItem("realm", x)).ToList()).ToActivity() | ||||
|             ? new TimelineDiscoveryEvent( | ||||
|                 realms.Select(x => new DiscoveryItem("realm", x)).ToList() | ||||
|             ).ToActivity() | ||||
|             : null; | ||||
|     } | ||||
| 
 | ||||
|     private async Task<SnActivity?> GetPublisherDiscoveryActivity(int count = 5) | ||||
|     private async Task<SnTimelineEvent?> GetPublisherDiscoveryActivity(int count = 5) | ||||
|     { | ||||
|         var popularPublishers = await GetPopularPublishers(count); | ||||
|         return popularPublishers.Count > 0 | ||||
|             ? new DiscoveryActivity(popularPublishers.Select(x => new DiscoveryItem("publisher", x)).ToList()) | ||||
|                 .ToActivity() | ||||
|             ? new TimelineDiscoveryEvent( | ||||
|                 popularPublishers.Select(x => new DiscoveryItem("publisher", x)).ToList() | ||||
|             ).ToActivity() | ||||
|             : null; | ||||
|     } | ||||
| 
 | ||||
|     private async Task<SnActivity?> GetShuffledPostsActivity(int count = 5) | ||||
|     private async Task<SnTimelineEvent?> GetShuffledPostsActivity(int count = 5) | ||||
|     { | ||||
|         var publicRealms = await rs.GetPublicRealms(); | ||||
|         var publicRealmIds = publicRealms.Select(r => r.Id).ToList(); | ||||
| 
 | ||||
|         var postsQuery = db.Posts | ||||
|             .Include(p => p.Categories) | ||||
|         var postsQuery = db | ||||
|             .Posts.Include(p => p.Categories) | ||||
|             .Include(p => p.Tags) | ||||
|             .Where(p => p.RepliedPostId == null) | ||||
|             .Where(p => p.RealmId == null || publicRealmIds.Contains(p.RealmId.Value)) | ||||
| @@ -231,17 +237,22 @@ public class ActivityService( | ||||
| 
 | ||||
|         return posts.Count == 0 | ||||
|             ? null | ||||
|             : new DiscoveryActivity(posts.Select(x => new DiscoveryItem("post", x)).ToList()).ToActivity(); | ||||
|             : new TimelineDiscoveryEvent( | ||||
|                 posts.Select(x => new DiscoveryItem("post", x)).ToList() | ||||
|             ).ToActivity(); | ||||
|     } | ||||
| 
 | ||||
|     private async Task<SnActivity?> GetArticleDiscoveryActivity(int count = 5, int feedSampleSize = 10) | ||||
|     private async Task<SnTimelineEvent?> GetArticleDiscoveryActivity( | ||||
|         int count = 5, | ||||
|         int feedSampleSize = 10 | ||||
|     ) | ||||
|     { | ||||
|         var now = SystemClock.Instance.GetCurrentInstant(); | ||||
|         var today = now.InZone(DateTimeZone.Utc).Date; | ||||
|         var todayBegin = today.AtStartOfDayInZone(DateTimeZone.Utc).ToInstant(); | ||||
|         var todayEnd = today.PlusDays(1).AtStartOfDayInZone(DateTimeZone.Utc).ToInstant(); | ||||
|         var recentFeedIds = await db.WebArticles | ||||
|             .Where(a => a.CreatedAt >= todayBegin && a.CreatedAt < todayEnd) | ||||
|         var recentFeedIds = await db | ||||
|             .WebArticles.Where(a => a.CreatedAt >= todayBegin && a.CreatedAt < todayEnd) | ||||
|             .GroupBy(a => a.FeedId) | ||||
|             .OrderByDescending(g => g.Max(a => a.PublishedAt)) | ||||
|             .Take(feedSampleSize) | ||||
| @@ -253,26 +264,31 @@ public class ActivityService( | ||||
| 
 | ||||
|         foreach (var feedId in recentFeedIds.OrderBy(_ => random.Next())) | ||||
|         { | ||||
|             var article = await db.WebArticles | ||||
|                 .Include(a => a.Feed) | ||||
|             var article = await db | ||||
|                 .WebArticles.Include(a => a.Feed) | ||||
|                 .Where(a => a.FeedId == feedId) | ||||
|                 .OrderBy(_ => EF.Functions.Random()) | ||||
|                 .FirstOrDefaultAsync(); | ||||
| 
 | ||||
|             if (article == null) continue; | ||||
|             if (article == null) | ||||
|                 continue; | ||||
|             recentArticles.Add(article); | ||||
|             if (recentArticles.Count >= count) break; | ||||
|             if (recentArticles.Count >= count) | ||||
|                 break; | ||||
|         } | ||||
| 
 | ||||
|         return recentArticles.Count > 0 | ||||
|             ? new DiscoveryActivity(recentArticles.Select(x => new DiscoveryItem("article", x)).ToList()).ToActivity() | ||||
|             ? new TimelineDiscoveryEvent( | ||||
|                 recentArticles.Select(x => new DiscoveryItem("article", x)).ToList() | ||||
|             ).ToActivity() | ||||
|             : null; | ||||
|     } | ||||
| 
 | ||||
|     private async Task<List<SnPost>> GetAndProcessPosts( | ||||
|         IQueryable<SnPost> baseQuery, | ||||
|         Account? currentUser = null, | ||||
|         bool trackViews = true) | ||||
|         bool trackViews = true | ||||
|     ) | ||||
|     { | ||||
|         var posts = await baseQuery.ToListAsync(); | ||||
|         posts = await ps.LoadPostInfo(posts, currentUser, true); | ||||
| @@ -282,7 +298,10 @@ public class ActivityService( | ||||
| 
 | ||||
|         foreach (var post in posts) | ||||
|         { | ||||
|             post.ReactionsCount = reactionMaps.GetValueOrDefault(post.Id, new Dictionary<string, int>()); | ||||
|             post.ReactionsCount = reactionMaps.GetValueOrDefault( | ||||
|                 post.Id, | ||||
|                 new Dictionary<string, int>() | ||||
|             ); | ||||
| 
 | ||||
|             if (trackViews && currentUser != null) | ||||
|             { | ||||
| @@ -299,8 +318,8 @@ public class ActivityService( | ||||
|         List<Guid>? userRealms = null | ||||
|     ) | ||||
|     { | ||||
|         var query = db.Posts | ||||
|             .Include(e => e.RepliedPost) | ||||
|         var query = db | ||||
|             .Posts.Include(e => e.RepliedPost) | ||||
|             .Include(e => e.ForwardedPost) | ||||
|             .Include(e => e.Categories) | ||||
|             .Include(e => e.Tags) | ||||
| @@ -319,8 +338,7 @@ public class ActivityService( | ||||
|             query = query.Where(p => p.RealmId == null); // Modify in caller | ||||
|         } | ||||
|         else | ||||
|             query = query.Where(p => | ||||
|                 p.RealmId == null || userRealms.Contains(p.RealmId.Value)); | ||||
|             query = query.Where(p => p.RealmId == null || userRealms.Contains(p.RealmId.Value)); | ||||
| 
 | ||||
|         return query; | ||||
|     } | ||||
| @@ -328,7 +346,8 @@ public class ActivityService( | ||||
|     private async Task<List<Shared.Models.SnPublisher>?> GetFilteredPublishers( | ||||
|         string? filter, | ||||
|         Account currentUser, | ||||
|         List<Guid> userFriends) | ||||
|         List<Guid> userFriends | ||||
|     ) | ||||
|     { | ||||
|         return filter?.ToLower() switch | ||||
|         { | ||||
| @@ -337,14 +356,19 @@ public class ActivityService( | ||||
|                 .SelectMany(x => x.Value) | ||||
|                 .DistinctBy(x => x.Id) | ||||
|                 .ToList(), | ||||
|             _ => null | ||||
|             _ => null, | ||||
|         }; | ||||
|     } | ||||
| 
 | ||||
|     private static async Task LoadPostsRealmsAsync(List<SnPost> posts, RemoteRealmService rs) | ||||
|     { | ||||
|         var postRealmIds = posts.Where(p => p.RealmId != null).Select(p => p.RealmId!.Value).Distinct().ToList(); | ||||
|         if (!postRealmIds.Any()) return; | ||||
|         var postRealmIds = posts | ||||
|             .Where(p => p.RealmId != null) | ||||
|             .Select(p => p.RealmId!.Value) | ||||
|             .Distinct() | ||||
|             .ToList(); | ||||
|         if (!postRealmIds.Any()) | ||||
|             return; | ||||
| 
 | ||||
|         var realms = await rs.GetRealmBatch(postRealmIds.Select(id => id.ToString()).ToList()); | ||||
|         var realmDict = realms.ToDictionary(r => r.Id, r => r); | ||||
		Reference in New Issue
	
	Block a user