✨ Post with polls
This commit is contained in:
@@ -20,29 +20,6 @@ public class Poll : ModelBase
|
|||||||
public Publisher.Publisher Publisher { get; set; } = null!;
|
public Publisher.Publisher Publisher { get; set; } = null!;
|
||||||
}
|
}
|
||||||
|
|
||||||
public class PollWithAnswer : Poll
|
|
||||||
{
|
|
||||||
public PollAnswer? UserAnswer { get; set; }
|
|
||||||
public Dictionary<Guid, Dictionary<string, int>> Stats { get; set; } = new(); // question id -> (option id -> count)
|
|
||||||
|
|
||||||
public static PollWithAnswer FromPoll(Poll poll, PollAnswer? userAnswer = null)
|
|
||||||
{
|
|
||||||
return new PollWithAnswer
|
|
||||||
{
|
|
||||||
Id = poll.Id,
|
|
||||||
Title = poll.Title,
|
|
||||||
Description = poll.Description,
|
|
||||||
EndedAt = poll.EndedAt,
|
|
||||||
PublisherId = poll.PublisherId,
|
|
||||||
Publisher = poll.Publisher,
|
|
||||||
Questions = poll.Questions,
|
|
||||||
CreatedAt = poll.CreatedAt,
|
|
||||||
UpdatedAt = poll.UpdatedAt,
|
|
||||||
UserAnswer = userAnswer
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public enum PollQuestionType
|
public enum PollQuestionType
|
||||||
{
|
{
|
||||||
SingleChoice,
|
SingleChoice,
|
||||||
|
@@ -14,13 +14,13 @@ namespace DysonNetwork.Sphere.Poll;
|
|||||||
public class PollController(AppDatabase db, PollService polls, PublisherService pub) : ControllerBase
|
public class PollController(AppDatabase db, PollService polls, PublisherService pub) : ControllerBase
|
||||||
{
|
{
|
||||||
[HttpGet("{id:guid}")]
|
[HttpGet("{id:guid}")]
|
||||||
public async Task<ActionResult<PollWithAnswer>> GetPoll(Guid id)
|
public async Task<ActionResult<PollWithStats>> GetPoll(Guid id)
|
||||||
{
|
{
|
||||||
var poll = await db.Polls
|
var poll = await db.Polls
|
||||||
.Include(p => p.Questions)
|
.Include(p => p.Questions)
|
||||||
.FirstOrDefaultAsync(p => p.Id == id);
|
.FirstOrDefaultAsync(p => p.Id == id);
|
||||||
if (poll is null) return NotFound("Poll not found");
|
if (poll is null) return NotFound("Poll not found");
|
||||||
var pollWithAnswer = PollWithAnswer.FromPoll(poll);
|
var pollWithAnswer = PollWithStats.FromPoll(poll);
|
||||||
|
|
||||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Ok(pollWithAnswer);
|
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Ok(pollWithAnswer);
|
||||||
|
|
||||||
|
39
DysonNetwork.Sphere/Poll/PollEmbed.cs
Normal file
39
DysonNetwork.Sphere/Poll/PollEmbed.cs
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
using DysonNetwork.Sphere.WebReader;
|
||||||
|
|
||||||
|
namespace DysonNetwork.Sphere.Poll;
|
||||||
|
|
||||||
|
public class PollWithStats : Poll
|
||||||
|
{
|
||||||
|
public PollAnswer? UserAnswer { get; set; }
|
||||||
|
public Dictionary<Guid, Dictionary<string, int>> Stats { get; set; } = new(); // question id -> (option id -> count)
|
||||||
|
|
||||||
|
public static PollWithStats FromPoll(Poll poll, PollAnswer? userAnswer = null)
|
||||||
|
{
|
||||||
|
return new PollWithStats
|
||||||
|
{
|
||||||
|
Id = poll.Id,
|
||||||
|
Title = poll.Title,
|
||||||
|
Description = poll.Description,
|
||||||
|
EndedAt = poll.EndedAt,
|
||||||
|
PublisherId = poll.PublisherId,
|
||||||
|
Publisher = poll.Publisher,
|
||||||
|
Questions = poll.Questions,
|
||||||
|
CreatedAt = poll.CreatedAt,
|
||||||
|
UpdatedAt = poll.UpdatedAt,
|
||||||
|
UserAnswer = userAnswer
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class PollEmbed : EmbeddableBase
|
||||||
|
{
|
||||||
|
public override string Type => "poll";
|
||||||
|
|
||||||
|
public required Guid Id { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Do not store this to the database
|
||||||
|
/// Only set this when sending the embed
|
||||||
|
/// </summary>
|
||||||
|
public PollWithStats? Poll { get; set; }
|
||||||
|
}
|
@@ -31,6 +31,28 @@ public class PollService(AppDatabase db, ICacheService cache)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private const string PollCachePrefix = "poll:";
|
||||||
|
|
||||||
|
public async Task<Poll?> GetPoll(Guid id)
|
||||||
|
{
|
||||||
|
var cacheKey = $"{PollCachePrefix}{id}";
|
||||||
|
var cachedPoll = await cache.GetAsync<Poll?>(cacheKey);
|
||||||
|
if (cachedPoll is not null)
|
||||||
|
return cachedPoll;
|
||||||
|
|
||||||
|
var poll = await db.Polls
|
||||||
|
.Where(e => e.Id == id)
|
||||||
|
.Include(e => e.Questions)
|
||||||
|
.FirstOrDefaultAsync();
|
||||||
|
|
||||||
|
if (poll is not null)
|
||||||
|
{
|
||||||
|
await cache.SetAsync(cacheKey, poll, TimeSpan.FromMinutes(30));
|
||||||
|
}
|
||||||
|
|
||||||
|
return poll;
|
||||||
|
}
|
||||||
|
|
||||||
private const string PollAnswerCachePrefix = "poll:answer:";
|
private const string PollAnswerCachePrefix = "poll:answer:";
|
||||||
|
|
||||||
public async Task<PollAnswer?> GetPollAnswer(Guid pollId, Guid accountId)
|
public async Task<PollAnswer?> GetPollAnswer(Guid pollId, Guid accountId)
|
||||||
@@ -44,10 +66,8 @@ public class PollService(AppDatabase db, ICacheService cache)
|
|||||||
.Where(e => e.PollId == pollId && e.AccountId == accountId)
|
.Where(e => e.PollId == pollId && e.AccountId == accountId)
|
||||||
.FirstOrDefaultAsync();
|
.FirstOrDefaultAsync();
|
||||||
|
|
||||||
if (answer is not null)
|
// Set the answer even it is null, which stands for unanswered
|
||||||
{
|
await cache.SetAsync(cacheKey, answer, TimeSpan.FromMinutes(30));
|
||||||
await cache.SetAsync(cacheKey, answer, TimeSpan.FromMinutes(30));
|
|
||||||
}
|
|
||||||
|
|
||||||
return answer;
|
return answer;
|
||||||
}
|
}
|
||||||
@@ -126,7 +146,7 @@ public class PollService(AppDatabase db, ICacheService cache)
|
|||||||
// Update cache for this poll answer and invalidate stats cache
|
// Update cache for this poll answer and invalidate stats cache
|
||||||
var answerCacheKey = $"poll:answer:{pollId}:{accountId}";
|
var answerCacheKey = $"poll:answer:{pollId}:{accountId}";
|
||||||
await cache.SetAsync(answerCacheKey, answerRecord, TimeSpan.FromMinutes(30));
|
await cache.SetAsync(answerCacheKey, answerRecord, TimeSpan.FromMinutes(30));
|
||||||
|
|
||||||
// Invalidate all stats cache for this poll since answers have changed
|
// Invalidate all stats cache for this poll since answers have changed
|
||||||
await cache.RemoveGroupAsync(PollCacheGroupPrefix + pollId);
|
await cache.RemoveGroupAsync(PollCacheGroupPrefix + pollId);
|
||||||
|
|
||||||
@@ -141,17 +161,17 @@ public class PollService(AppDatabase db, ICacheService cache)
|
|||||||
.ExecuteUpdateAsync(s => s.SetProperty(e => e.DeletedAt, now)) > 0;
|
.ExecuteUpdateAsync(s => s.SetProperty(e => e.DeletedAt, now)) > 0;
|
||||||
|
|
||||||
if (!result) return result;
|
if (!result) return result;
|
||||||
|
|
||||||
// Remove the cached answer if it exists
|
// Remove the cached answer if it exists
|
||||||
var answerCacheKey = $"poll:answer:{pollId}:{accountId}";
|
var answerCacheKey = $"poll:answer:{pollId}:{accountId}";
|
||||||
await cache.RemoveAsync(answerCacheKey);
|
await cache.RemoveAsync(answerCacheKey);
|
||||||
|
|
||||||
// Invalidate all stats cache for this poll since answers have changed
|
// Invalidate all stats cache for this poll since answers have changed
|
||||||
await cache.RemoveGroupAsync(PollCacheGroupPrefix + pollId);
|
await cache.RemoveGroupAsync(PollCacheGroupPrefix + pollId);
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
private const string PollStatsCachePrefix = "poll:stats:";
|
private const string PollStatsCachePrefix = "poll:stats:";
|
||||||
private const string PollCacheGroupPrefix = "poll:";
|
private const string PollCacheGroupPrefix = "poll:";
|
||||||
|
|
||||||
@@ -159,7 +179,7 @@ public class PollService(AppDatabase db, ICacheService cache)
|
|||||||
public async Task<Dictionary<string, int>> GetPollQuestionStats(Guid questionId)
|
public async Task<Dictionary<string, int>> GetPollQuestionStats(Guid questionId)
|
||||||
{
|
{
|
||||||
var cacheKey = $"{PollStatsCachePrefix}{questionId}";
|
var cacheKey = $"{PollStatsCachePrefix}{questionId}";
|
||||||
|
|
||||||
// Try to get from cache first
|
// Try to get from cache first
|
||||||
var (found, cachedStats) = await cache.GetAsyncWithStatus<Dictionary<string, int>>(cacheKey);
|
var (found, cachedStats) = await cache.GetAsyncWithStatus<Dictionary<string, int>>(cacheKey);
|
||||||
if (found && cachedStats != null)
|
if (found && cachedStats != null)
|
||||||
@@ -282,4 +302,27 @@ public class PollService(AppDatabase db, ICacheService cache)
|
|||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<PollEmbed> MakePollEmbed(Guid pollId)
|
||||||
|
{
|
||||||
|
// Do not read the cache here
|
||||||
|
var poll = await db.Polls
|
||||||
|
.Where(e => e.Id == pollId)
|
||||||
|
.FirstOrDefaultAsync();
|
||||||
|
if (poll is null)
|
||||||
|
throw new Exception("Poll not found");
|
||||||
|
return new PollEmbed { Id = poll.Id };
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<PollEmbed> LoadPollEmbed(Guid pollId, Guid? accountId)
|
||||||
|
{
|
||||||
|
var poll = await GetPoll(pollId);
|
||||||
|
if (poll is null)
|
||||||
|
throw new Exception("Poll not found");
|
||||||
|
var pollWithStats = PollWithStats.FromPoll(poll);
|
||||||
|
pollWithStats.Stats = await GetPollStats(poll.Id);
|
||||||
|
if (accountId is not null)
|
||||||
|
pollWithStats.UserAnswer = await GetPollAnswer(poll.Id, accountId.Value);
|
||||||
|
return new PollEmbed { Id = poll.Id, Poll = pollWithStats };
|
||||||
|
}
|
||||||
}
|
}
|
@@ -3,7 +3,9 @@ using DysonNetwork.Shared.Auth;
|
|||||||
using DysonNetwork.Shared.Content;
|
using DysonNetwork.Shared.Content;
|
||||||
using DysonNetwork.Shared.Data;
|
using DysonNetwork.Shared.Data;
|
||||||
using DysonNetwork.Shared.Proto;
|
using DysonNetwork.Shared.Proto;
|
||||||
|
using DysonNetwork.Sphere.Poll;
|
||||||
using DysonNetwork.Sphere.Publisher;
|
using DysonNetwork.Sphere.Publisher;
|
||||||
|
using DysonNetwork.Sphere.WebReader;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
@@ -18,7 +20,8 @@ public class PostController(
|
|||||||
PostService ps,
|
PostService ps,
|
||||||
PublisherService pub,
|
PublisherService pub,
|
||||||
AccountService.AccountServiceClient accounts,
|
AccountService.AccountServiceClient accounts,
|
||||||
ActionLogService.ActionLogServiceClient als
|
ActionLogService.ActionLogServiceClient als,
|
||||||
|
PollService polls
|
||||||
)
|
)
|
||||||
: ControllerBase
|
: ControllerBase
|
||||||
{
|
{
|
||||||
@@ -275,6 +278,8 @@ public class PostController(
|
|||||||
public Instant? PublishedAt { get; set; }
|
public Instant? PublishedAt { get; set; }
|
||||||
public Guid? RepliedPostId { get; set; }
|
public Guid? RepliedPostId { get; set; }
|
||||||
public Guid? ForwardedPostId { get; set; }
|
public Guid? ForwardedPostId { get; set; }
|
||||||
|
|
||||||
|
public Guid? PollId { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost]
|
[HttpPost]
|
||||||
@@ -336,6 +341,25 @@ public class PostController(
|
|||||||
post.ForwardedPostId = forwardedPost.Id;
|
post.ForwardedPostId = forwardedPost.Id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (request.PollId is not null)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var pollEmbed = await polls.MakePollEmbed(request.PollId.Value);
|
||||||
|
post.Meta ??= new Dictionary<string, object>();
|
||||||
|
if (!post.Meta.TryGetValue("embeds", out var existingEmbeds) ||
|
||||||
|
existingEmbeds is not List<EmbeddableBase>)
|
||||||
|
post.Meta["embeds"] = new List<Dictionary<string, object>>();
|
||||||
|
var embeds = (List<Dictionary<string, object>>)post.Meta["embeds"];
|
||||||
|
embeds.Add(pollEmbed.ToDictionary());
|
||||||
|
post.Meta["embeds"] = embeds;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return BadRequest(ex.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
post = await ps.PostAsync(
|
post = await ps.PostAsync(
|
||||||
@@ -469,6 +493,27 @@ public class PostController(
|
|||||||
if (request.Type is not null) post.Type = request.Type.Value;
|
if (request.Type is not null) post.Type = request.Type.Value;
|
||||||
if (request.Meta is not null) post.Meta = request.Meta;
|
if (request.Meta is not null) post.Meta = request.Meta;
|
||||||
|
|
||||||
|
if (request.PollId is not null)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var pollEmbed = await polls.MakePollEmbed(request.PollId.Value);
|
||||||
|
post.Meta ??= new Dictionary<string, object>();
|
||||||
|
if (!post.Meta.TryGetValue("embeds", out var existingEmbeds) ||
|
||||||
|
existingEmbeds is not List<EmbeddableBase>)
|
||||||
|
post.Meta["embeds"] = new List<Dictionary<string, object>>();
|
||||||
|
var embeds = (List<Dictionary<string, object>>)post.Meta["embeds"];
|
||||||
|
// Remove all old poll embeds
|
||||||
|
embeds.RemoveAll(e => e.TryGetValue("type", out var type) && type.ToString() == "poll");
|
||||||
|
embeds.Add(pollEmbed.ToDictionary());
|
||||||
|
post.Meta["embeds"] = embeds;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return BadRequest(ex.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
post = await ps.UpdatePostAsync(
|
post = await ps.UpdatePostAsync(
|
||||||
|
@@ -1,10 +1,12 @@
|
|||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
|
using AngleSharp.Common;
|
||||||
using DysonNetwork.Shared;
|
using DysonNetwork.Shared;
|
||||||
using DysonNetwork.Shared.Cache;
|
using DysonNetwork.Shared.Cache;
|
||||||
using DysonNetwork.Shared.Data;
|
using DysonNetwork.Shared.Data;
|
||||||
using DysonNetwork.Shared.Proto;
|
using DysonNetwork.Shared.Proto;
|
||||||
using DysonNetwork.Sphere.WebReader;
|
using DysonNetwork.Sphere.WebReader;
|
||||||
using DysonNetwork.Sphere.Localization;
|
using DysonNetwork.Sphere.Localization;
|
||||||
|
using DysonNetwork.Sphere.Poll;
|
||||||
using DysonNetwork.Sphere.Publisher;
|
using DysonNetwork.Sphere.Publisher;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.Localization;
|
using Microsoft.Extensions.Localization;
|
||||||
@@ -21,6 +23,7 @@ public partial class PostService(
|
|||||||
ILogger<PostService> logger,
|
ILogger<PostService> logger,
|
||||||
FileService.FileServiceClient files,
|
FileService.FileServiceClient files,
|
||||||
FileReferenceService.FileReferenceServiceClient fileRefs,
|
FileReferenceService.FileReferenceServiceClient fileRefs,
|
||||||
|
PollService polls,
|
||||||
WebReaderService reader
|
WebReaderService reader
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
@@ -306,10 +309,11 @@ public partial class PostService(
|
|||||||
var embeds = (List<Dictionary<string, object>>)item.Meta["embeds"];
|
var embeds = (List<Dictionary<string, object>>)item.Meta["embeds"];
|
||||||
|
|
||||||
// Process up to 3 links to avoid excessive processing
|
// Process up to 3 links to avoid excessive processing
|
||||||
|
const int maxLinks = 3;
|
||||||
var processedLinks = 0;
|
var processedLinks = 0;
|
||||||
foreach (Match match in matches)
|
foreach (Match match in matches)
|
||||||
{
|
{
|
||||||
if (processedLinks >= 3)
|
if (processedLinks >= maxLinks)
|
||||||
break;
|
break;
|
||||||
|
|
||||||
var url = match.Value;
|
var url = match.Value;
|
||||||
@@ -520,7 +524,8 @@ public partial class PostService(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<Dictionary<Guid, Dictionary<string, bool>>> GetPostReactionMadeMapBatch(List<Guid> postIds, Guid accountId)
|
public async Task<Dictionary<Guid, Dictionary<string, bool>>> GetPostReactionMadeMapBatch(List<Guid> postIds,
|
||||||
|
Guid accountId)
|
||||||
{
|
{
|
||||||
var reactions = await db.Set<PostReaction>()
|
var reactions = await db.Set<PostReaction>()
|
||||||
.Where(r => postIds.Contains(r.PostId) && r.AccountId == accountId)
|
.Where(r => postIds.Contains(r.PostId) && r.AccountId == accountId)
|
||||||
@@ -630,12 +635,12 @@ public partial class PostService(
|
|||||||
post.ReactionsMade = reactionMadeMap.TryGetValue(post.Id, out var made)
|
post.ReactionsMade = reactionMadeMap.TryGetValue(post.Id, out var made)
|
||||||
? made
|
? made
|
||||||
: [];
|
: [];
|
||||||
|
|
||||||
// Set reply count
|
// Set reply count
|
||||||
post.RepliesCount = repliesCountMap.TryGetValue(post.Id, out var repliesCount)
|
post.RepliesCount = repliesCountMap.TryGetValue(post.Id, out var repliesCount)
|
||||||
? repliesCount
|
? repliesCount
|
||||||
: 0;
|
: 0;
|
||||||
|
|
||||||
// Track view for each post in the list
|
// Track view for each post in the list
|
||||||
if (currentUser != null)
|
if (currentUser != null)
|
||||||
await IncreaseViewCount(post.Id, currentUser.Id);
|
await IncreaseViewCount(post.Id, currentUser.Id);
|
||||||
@@ -656,6 +661,46 @@ public partial class PostService(
|
|||||||
g => g.Count()
|
g => g.Count()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task LoadPollEmbed(Post post, Account? currentUser)
|
||||||
|
{
|
||||||
|
if (!post.Meta!.TryGetValue("embeds", out var existingEmbeds) ||
|
||||||
|
existingEmbeds is not List<EmbeddableBase>)
|
||||||
|
{
|
||||||
|
post.Meta["embeds"] = new List<Dictionary<string, object>>();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var embeds = (List<Dictionary<string, object>>)post.Meta["embeds"];
|
||||||
|
|
||||||
|
// Find the index of the poll embed first
|
||||||
|
var pollIndex = embeds.FindIndex(e =>
|
||||||
|
e.ContainsKey("type") && (string)e["type"] == "poll");
|
||||||
|
|
||||||
|
if (pollIndex < 0) return;
|
||||||
|
|
||||||
|
var pollEmbed = embeds[pollIndex];
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var pollId = pollEmbed["Id"] switch
|
||||||
|
{
|
||||||
|
Guid g => g,
|
||||||
|
string s when !string.IsNullOrEmpty(s) => Guid.Parse(s),
|
||||||
|
_ => default(Guid?)
|
||||||
|
};
|
||||||
|
|
||||||
|
if (pollId.HasValue)
|
||||||
|
{
|
||||||
|
var currentUserId = currentUser is not null ? Guid.Parse(currentUser.Id) : (Guid?)null;
|
||||||
|
var updatedPoll = await polls.LoadPollEmbed(pollId.Value, currentUserId);
|
||||||
|
embeds[pollIndex] = updatedPoll.ToDictionary();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
logger?.LogError(ex, "Failed to load poll embed for post {PostId}", post.Id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<List<Post>> LoadPostInfo(
|
public async Task<List<Post>> LoadPostInfo(
|
||||||
List<Post> posts,
|
List<Post> posts,
|
||||||
@@ -668,6 +713,13 @@ public partial class PostService(
|
|||||||
posts = await LoadPublishers(posts);
|
posts = await LoadPublishers(posts);
|
||||||
posts = await LoadInteractive(posts, currentUser);
|
posts = await LoadInteractive(posts, currentUser);
|
||||||
|
|
||||||
|
var postsWithEmbed = posts
|
||||||
|
.Where(e => e.Meta is not null && e.Meta.ContainsKey("embeds"))
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
foreach (var post in postsWithEmbed)
|
||||||
|
await LoadPollEmbed(post, currentUser);
|
||||||
|
|
||||||
if (truncate)
|
if (truncate)
|
||||||
posts = TruncatePostContent(posts);
|
posts = TruncatePostContent(posts);
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user