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