[sln] Code cleanup

This commit is contained in:
Laura Hausmann 2024-09-13 21:21:52 +02:00
parent 28b57e35a3
commit afe62b0aab
No known key found for this signature in database
GPG key ID: D044E84C5BE01605
95 changed files with 580 additions and 523 deletions

View file

@ -88,8 +88,8 @@ public class ActivityPubController(
throw GracefulException.NotFound("Note not found"); throw GracefulException.NotFound("Note not found");
var replies = await db.Notes.Where(p => p.ReplyId == id) var replies = await db.Notes.Where(p => p.ReplyId == id)
.OrderByDescending(p => p.Id) .OrderByDescending(p => p.Id)
.ToListAsync(); .ToListAsync();
var rendered = replies.Select(noteRenderer.RenderLite).ToList(); var rendered = replies.Select(noteRenderer.RenderLite).ToList();
var res = new ASOrderedCollection var res = new ASOrderedCollection

View file

@ -16,7 +16,11 @@ namespace Iceshrimp.Backend.Controllers.Federation;
[Route("/nodeinfo")] [Route("/nodeinfo")]
[EnableCors("well-known")] [EnableCors("well-known")]
[Produces(MediaTypeNames.Application.Json)] [Produces(MediaTypeNames.Application.Json)]
public class NodeInfoController(IOptions<Config.InstanceSection> instanceConfig, IOptions<Config.StorageSection> storageConfig, DatabaseContext db) : ControllerBase public class NodeInfoController(
IOptions<Config.InstanceSection> instanceConfig,
IOptions<Config.StorageSection> storageConfig,
DatabaseContext db
) : ControllerBase
{ {
[HttpGet("2.1")] [HttpGet("2.1")]
[HttpGet("2.0")] [HttpGet("2.0")]
@ -36,7 +40,7 @@ public class NodeInfoController(IOptions<Config.InstanceSection> instanceConfig,
await db.Users.LongCountAsync(p => p.IsLocalUser && await db.Users.LongCountAsync(p => p.IsLocalUser &&
!Constants.SystemUsers.Contains(p.UsernameLower) && !Constants.SystemUsers.Contains(p.UsernameLower) &&
p.LastActiveDate > cutoffHalfYear); p.LastActiveDate > cutoffHalfYear);
var localPosts = await db.Notes.LongCountAsync(p => p.UserHost == null); var localPosts = await db.Notes.LongCountAsync(p => p.UserHost == null);
var maxUploadSize = storageConfig.Value.MaxUploadSizeBytes; var maxUploadSize = storageConfig.Value.MaxUploadSizeBytes;
return new NodeInfoResponse return new NodeInfoResponse
@ -92,30 +96,30 @@ public class NodeInfoController(IOptions<Config.InstanceSection> instanceConfig,
EnableGithubIntegration = false, EnableGithubIntegration = false,
EnableDiscordIntegration = false, EnableDiscordIntegration = false,
EnableEmail = false, EnableEmail = false,
PublicTimelineVisibility = new() { PublicTimelineVisibility = new NodeInfoResponse.PleromaPublicTimelineVisibility
{
Bubble = false, Bubble = false,
Federated = false, Federated = false,
Local = false, Local = false
}, },
UploadLimits = new() { // @formatter:off
UploadLimits = new NodeInfoResponse.PleromaUploadLimits
{
General = maxUploadSize, General = maxUploadSize,
Avatar = maxUploadSize, Avatar = maxUploadSize,
Background = maxUploadSize, Background = maxUploadSize,
Banner = maxUploadSize, Banner = maxUploadSize
}, },
Suggestions = new() { // @formatter:on
Enabled = false Suggestions = new NodeInfoResponse.PleromaSuggestions { Enabled = false },
}, Federation = new NodeInfoResponse.PleromaFederation { Enabled = true }
Federation = new() {
Enabled = true
}
}, },
OpenRegistrations = false OpenRegistrations = false
}; };
} }
[HttpGet("2.0.json")] [HttpGet("2.0.json")]
public IActionResult GetNodeInfoAkkoFE() public IActionResult GetNodeInfoAkkoma()
{ {
return Redirect("/nodeinfo/2.0"); return Redirect("/nodeinfo/2.0");
} }

View file

@ -96,7 +96,7 @@ public class ConversationsController(
var user = HttpContext.GetUserOrFail(); var user = HttpContext.GetUserOrFail();
var conversation = await db.Conversations(user) var conversation = await db.Conversations(user)
.IncludeCommonProperties() .IncludeCommonProperties()
.Where(p => (p.ThreadIdOrId) == id) .Where(p => p.ThreadIdOrId == id)
.Select(p => new Conversation .Select(p => new Conversation
{ {
Id = p.ThreadIdOrId, Id = p.ThreadIdOrId,

View file

@ -3,6 +3,7 @@ using System.Net.Mime;
using Iceshrimp.Backend.Controllers.Mastodon.Attributes; using Iceshrimp.Backend.Controllers.Mastodon.Attributes;
using Iceshrimp.Backend.Controllers.Mastodon.Schemas; using Iceshrimp.Backend.Controllers.Mastodon.Schemas;
using Iceshrimp.Backend.Controllers.Mastodon.Schemas.Entities; using Iceshrimp.Backend.Controllers.Mastodon.Schemas.Entities;
using Iceshrimp.Backend.Controllers.Pleroma.Schemas.Entities;
using Iceshrimp.Backend.Controllers.Shared.Attributes; using Iceshrimp.Backend.Controllers.Shared.Attributes;
using Iceshrimp.Backend.Core.Configuration; using Iceshrimp.Backend.Core.Configuration;
using Iceshrimp.Backend.Core.Database; using Iceshrimp.Backend.Core.Database;
@ -33,15 +34,14 @@ public class InstanceController(DatabaseContext db, MetaService meta) : Controll
var (instanceName, instanceDescription, adminContact) = var (instanceName, instanceDescription, adminContact) =
await meta.GetMany(MetaEntity.InstanceName, MetaEntity.InstanceDescription, MetaEntity.AdminContactEmail); await meta.GetMany(MetaEntity.InstanceName, MetaEntity.InstanceDescription, MetaEntity.AdminContactEmail);
var vapidKey = await meta.Get(MetaEntity.VapidPublicKey); // can't merge with above call since they're all nullable and this is not.
// can't merge with above call since they're all nullable and this is not.
var vapidKey = await meta.Get(MetaEntity.VapidPublicKey);
return new InstanceInfoV1Response(config.Value, instanceName, instanceDescription, adminContact) return new InstanceInfoV1Response(config.Value, instanceName, instanceDescription, adminContact)
{ {
Stats = new InstanceStats(userCount, noteCount, instanceCount), Stats = new InstanceStats(userCount, noteCount, instanceCount),
Pleroma = new() { Pleroma = new PleromaInstanceExtensions { VapidPublicKey = vapidKey, Metadata = new InstanceMetadata() }
VapidPublicKey = vapidKey,
Metadata = new(),
}
}; };
} }

View file

@ -33,7 +33,7 @@ public class NotificationController(DatabaseContext db, NotificationRenderer not
MastodonPaginationQuery query, NotificationSchemas.GetNotificationsRequest request MastodonPaginationQuery query, NotificationSchemas.GetNotificationsRequest request
) )
{ {
var user = HttpContext.GetUserOrFail(); var user = HttpContext.GetUserOrFail();
var isPleroma = HttpContext.GetOauthToken()!.IsPleroma; var isPleroma = HttpContext.GetOauthToken()!.IsPleroma;
return await db.Notifications return await db.Notifications
@ -65,7 +65,7 @@ public class NotificationController(DatabaseContext db, NotificationRenderer not
[ProducesErrors(HttpStatusCode.NotFound)] [ProducesErrors(HttpStatusCode.NotFound)]
public async Task<NotificationEntity> GetNotification(long id) public async Task<NotificationEntity> GetNotification(long id)
{ {
var user = HttpContext.GetUserOrFail(); var user = HttpContext.GetUserOrFail();
var isPleroma = HttpContext.GetOauthToken()!.IsPleroma; var isPleroma = HttpContext.GetOauthToken()!.IsPleroma;
var notification = await db.Notifications var notification = await db.Notifications
@ -76,7 +76,7 @@ public class NotificationController(DatabaseContext db, NotificationRenderer not
.FirstOrDefaultAsync() ?? .FirstOrDefaultAsync() ??
throw GracefulException.RecordNotFound(); throw GracefulException.RecordNotFound();
var res = await notificationRenderer.RenderAsync(notification.EnforceRenoteReplyVisibility(p => p.Note), user, isPleroma); return await notificationRenderer.RenderAsync(notification.EnforceRenoteReplyVisibility(p => p.Note),
return res; user, isPleroma);
} }
} }

View file

@ -1,4 +1,5 @@
using Iceshrimp.Backend.Controllers.Mastodon.Schemas.Entities; using Iceshrimp.Backend.Controllers.Mastodon.Schemas.Entities;
using Iceshrimp.Backend.Controllers.Pleroma.Schemas.Entities;
using Iceshrimp.Backend.Core.Configuration; using Iceshrimp.Backend.Core.Configuration;
using Iceshrimp.Backend.Core.Database; using Iceshrimp.Backend.Core.Database;
using Iceshrimp.Backend.Core.Database.Tables; using Iceshrimp.Backend.Core.Database.Tables;
@ -162,10 +163,7 @@ public class NoteRenderer(
Poll = poll, Poll = poll,
Reactions = reactions, Reactions = reactions,
Filtered = filterResult, Filtered = filterResult,
Pleroma = new() { Pleroma = new PleromaStatusExtensions { Reactions = reactions, ConversationId = note.ThreadIdOrId }
Reactions = reactions,
ConversationId = note.ThreadIdOrId
}
}; };
return res; return res;
@ -321,7 +319,11 @@ public class NoteRenderer(
db.NoteReactions.Any(i => i.NoteId == p.First().NoteId && db.NoteReactions.Any(i => i.NoteId == p.First().NoteId &&
i.Reaction == p.First().Reaction && i.Reaction == p.First().Reaction &&
i.User == user), i.User == user),
AccountIds = db.NoteReactions.Where(i => i.NoteId == p.First().NoteId && p.Select(r => r.Id).Contains(i.Id)).Select(i => i.UserId).ToList() AccountIds = db.NoteReactions
.Where(i => i.NoteId == p.First().NoteId &&
p.Select(r => r.Id).Contains(i.Id))
.Select(i => i.UserId)
.ToList()
}) })
.ToListAsync(); .ToListAsync();

View file

@ -1,4 +1,5 @@
using Iceshrimp.Backend.Controllers.Mastodon.Schemas.Entities; using Iceshrimp.Backend.Controllers.Mastodon.Schemas.Entities;
using Iceshrimp.Backend.Controllers.Pleroma.Schemas.Entities;
using Iceshrimp.Backend.Core.Database; using Iceshrimp.Backend.Core.Database;
using Iceshrimp.Backend.Core.Database.Tables; using Iceshrimp.Backend.Core.Database.Tables;
using Iceshrimp.Backend.Core.Extensions; using Iceshrimp.Backend.Core.Extensions;
@ -29,7 +30,8 @@ public class NotificationRenderer(DatabaseContext db, NoteRenderer noteRenderer,
await userRenderer.RenderAsync(dbNotifier); await userRenderer.RenderAsync(dbNotifier);
string? emojiUrl = null; string? emojiUrl = null;
if (notification.Reaction != null) { if (notification.Reaction != null)
{
// explicitly check to skip another database call if url is actually null // explicitly check to skip another database call if url is actually null
if (emojiUrls != null) if (emojiUrls != null)
{ {
@ -38,7 +40,10 @@ public class NotificationRenderer(DatabaseContext db, NoteRenderer noteRenderer,
else if (EmojiService.IsCustomEmoji(notification.Reaction)) else if (EmojiService.IsCustomEmoji(notification.Reaction))
{ {
var parts = notification.Reaction.Trim(':').Split('@'); var parts = notification.Reaction.Trim(':').Split('@');
emojiUrl = await db.Emojis.Where(e => e.Name == parts[0] && e.Host == (parts.Length > 1 ? parts[1] : null)).Select(e => e.PublicUrl).FirstOrDefaultAsync(); emojiUrl = await db.Emojis
.Where(e => e.Name == parts[0] && e.Host == (parts.Length > 1 ? parts[1] : null))
.Select(e => e.PublicUrl)
.FirstOrDefaultAsync();
} }
} }
@ -51,9 +56,7 @@ public class NotificationRenderer(DatabaseContext db, NoteRenderer noteRenderer,
CreatedAt = notification.CreatedAt.ToStringIso8601Like(), CreatedAt = notification.CreatedAt.ToStringIso8601Like(),
Emoji = notification.Reaction, Emoji = notification.Reaction,
EmojiUrl = emojiUrl, EmojiUrl = emojiUrl,
Pleroma = new() { Pleroma = new PleromaNotificationExtensions { IsSeen = notification.IsRead }
IsSeen = notification.IsRead
}
}; };
return res; return res;
@ -85,20 +88,28 @@ public class NotificationRenderer(DatabaseContext db, NoteRenderer noteRenderer,
.DistinctBy(p => p.Id), .DistinctBy(p => p.Id),
user, Filter.FilterContext.Notifications, accounts); user, Filter.FilterContext.Notifications, accounts);
var parts = notifications.Where(p => p.Reaction != null && EmojiService.IsCustomEmoji(p.Reaction)).Select(p => { var parts = notificationList.Where(p => p.Reaction != null && EmojiService.IsCustomEmoji(p.Reaction))
var parts = p.Reaction!.Trim(':').Split('@'); .Select(p =>
return new { Name = parts[0], Host = parts.Length > 1 ? parts[1] : null }; {
}); var parts = p.Reaction!.Trim(':').Split('@');
return new { Name = parts[0], Host = parts.Length > 1 ? parts[1] : null };
});
// https://github.com/dotnet/efcore/issues/31492 // https://github.com/dotnet/efcore/issues/31492
//TODO: is there a better way of expressing this using LINQ?
IQueryable<Emoji> urlQ = db.Emojis; IQueryable<Emoji> urlQ = db.Emojis;
foreach (var part in parts) foreach (var part in parts)
urlQ = urlQ.Concat(db.Emojis.Where(e => e.Name == part.Name && e.Host == part.Host)); urlQ = urlQ.Concat(db.Emojis.Where(e => e.Name == part.Name && e.Host == part.Host));
var emojiUrls = (await urlQ
.Select(e => new { Name = $":{e.Name}{(e.Host != null ? "@" + e.Host : "")}:", Url = e.PublicUrl }) //TODO: can we somehow optimize this to do the dedupe database side?
.ToArrayAsync()) var emojiUrls = await urlQ.Select(e => new
.DistinctBy(e => e.Name) {
.ToDictionary(e => e.Name, e => e.Url); Name = $":{e.Name}{(e.Host != null ? "@" + e.Host : "")}:",
Url = e.PublicUrl
})
.ToArrayAsync()
.ContinueWithResult(res => res.DistinctBy(e => e.Name)
.ToDictionary(e => e.Name, e => e.Url));
return await notificationList return await notificationList
.Select(p => RenderAsync(p, user, isPleroma, accounts, notes, emojiUrls)) .Select(p => RenderAsync(p, user, isPleroma, accounts, notes, emojiUrls))

View file

@ -1,8 +1,8 @@
using Iceshrimp.Backend.Controllers.Pleroma.Schemas.Entities;
using Iceshrimp.Backend.Core.Database; using Iceshrimp.Backend.Core.Database;
using Iceshrimp.Backend.Core.Middleware; using Iceshrimp.Backend.Core.Middleware;
using J = System.Text.Json.Serialization.JsonPropertyNameAttribute; using J = System.Text.Json.Serialization.JsonPropertyNameAttribute;
using static Iceshrimp.Backend.Core.Database.Tables.Notification; using static Iceshrimp.Backend.Core.Database.Tables.Notification;
using Iceshrimp.Backend.Controllers.Pleroma.Schemas.Entities;
namespace Iceshrimp.Backend.Controllers.Mastodon.Schemas.Entities; namespace Iceshrimp.Backend.Controllers.Mastodon.Schemas.Entities;
@ -14,8 +14,8 @@ public class NotificationEntity : IEntity
[J("status")] public required StatusEntity? Note { get; set; } [J("status")] public required StatusEntity? Note { get; set; }
[J("id")] public required string Id { get; set; } [J("id")] public required string Id { get; set; }
[J("pleroma")] public required PleromaNotificationExtensions Pleroma { get; set; } [J("pleroma")] public required PleromaNotificationExtensions Pleroma { get; set; }
[J("emoji")] public string? Emoji { get; set; } [J("emoji")] public string? Emoji { get; set; }
[J("emoji_url")] public string? EmojiUrl { get; set; } [J("emoji_url")] public string? EmojiUrl { get; set; }
public static string EncodeType(NotificationType type, bool isPleroma) public static string EncodeType(NotificationType type, bool isPleroma)
{ {

View file

@ -45,9 +45,7 @@ public abstract class StatusSchemas
[B(Name = "poll")] [J("poll")] public PollData? Poll { get; set; } [B(Name = "poll")] [J("poll")] public PollData? Poll { get; set; }
[B(Name = "preview")] [B(Name = "preview")] [J("preview")] public bool Preview { get; set; } = false;
[J("preview")]
public bool Preview { get; set; } = false;
public class PollData public class PollData
{ {

View file

@ -21,13 +21,15 @@ public class EmojiController(DatabaseContext db) : ControllerBase
[ProducesResults(HttpStatusCode.OK)] [ProducesResults(HttpStatusCode.OK)]
public async Task<Dictionary<string, PleromaEmojiEntity>> GetCustomEmojis() public async Task<Dictionary<string, PleromaEmojiEntity>> GetCustomEmojis()
{ {
var emoji = await db.Emojis.Where(p => p.Host == null) var emoji = await db.Emojis
.Select(p => KeyValuePair.Create(p.Name, new PleromaEmojiEntity .Where(p => p.Host == null)
{ .Select(p => KeyValuePair.Create(p.Name,
ImageUrl = p.PublicUrl, new PleromaEmojiEntity
Tags = new string[] { p.Category == null ? "" : p.Category } {
})) ImageUrl = p.PublicUrl,
.ToArrayAsync(); Tags = new[] { p.Category ?? "" }
}))
.ToArrayAsync();
return new Dictionary<string, PleromaEmojiEntity>(emoji); return new Dictionary<string, PleromaEmojiEntity>(emoji);
} }

View file

@ -4,10 +4,8 @@ using Iceshrimp.Backend.Controllers.Mastodon.Attributes;
using Iceshrimp.Backend.Controllers.Mastodon.Renderers; using Iceshrimp.Backend.Controllers.Mastodon.Renderers;
using Iceshrimp.Backend.Controllers.Mastodon.Schemas.Entities; using Iceshrimp.Backend.Controllers.Mastodon.Schemas.Entities;
using Iceshrimp.Backend.Controllers.Pleroma.Schemas; using Iceshrimp.Backend.Controllers.Pleroma.Schemas;
using Iceshrimp.Backend.Controllers.Pleroma.Schemas.Entities;
using Iceshrimp.Backend.Controllers.Shared.Attributes; using Iceshrimp.Backend.Controllers.Shared.Attributes;
using Iceshrimp.Backend.Core.Database; using Iceshrimp.Backend.Core.Database;
using Iceshrimp.Backend.Core.Database.Tables;
using Iceshrimp.Backend.Core.Extensions; using Iceshrimp.Backend.Core.Extensions;
using Iceshrimp.Backend.Core.Middleware; using Iceshrimp.Backend.Core.Middleware;
using Microsoft.AspNetCore.Cors; using Microsoft.AspNetCore.Cors;
@ -27,23 +25,23 @@ public class NotificationController(DatabaseContext db, NotificationRenderer not
[HttpPost("/api/v1/pleroma/notifications/read")] [HttpPost("/api/v1/pleroma/notifications/read")]
[Authorize("read:notifications")] [Authorize("read:notifications")]
[ProducesResults(HttpStatusCode.OK)] [ProducesResults(HttpStatusCode.OK)]
public async Task<List<NotificationEntity>> MarkNotificationsAsRead([FromHybrid] PleromaNotificationSchemas.ReadNotificationsRequest request) public async Task<List<NotificationEntity>> MarkNotificationsAsRead(
[FromHybrid] PleromaNotificationSchemas.ReadNotificationsRequest request
)
{ {
var user = HttpContext.GetUserOrFail(); var user = HttpContext.GetUserOrFail();
if (request.Id != null && request.MaxId != null) if (request is { Id: not null, MaxId: not null })
throw GracefulException.BadRequest("id and max_id are mutually exclusive."); throw GracefulException.BadRequest("id and max_id are mutually exclusive.");
var q = db.Notifications var q = db.Notifications
.IncludeCommonProperties() .IncludeCommonProperties()
.Include(p => p.Notifier) .Where(p => p.Notifiee == user)
.Include(p => p.Note) .Where(p => p.Notifier != null)
.Where(p => p.Notifiee == user) .Where(p => !p.IsRead)
.Where(p => p.Notifier != null) .EnsureNoteVisibilityFor(p => p.Note, user)
.Where(p => !p.IsRead) .OrderByDescending(n => n.MastoId)
.EnsureNoteVisibilityFor(p => p.Note, user) .Take(80);
.OrderByDescending(n => n.MastoId)
.Take(80);
if (request.Id != null) if (request.Id != null)
q = q.Where(n => n.MastoId == request.Id); q = q.Where(n => n.MastoId == request.Id);

View file

@ -10,8 +10,23 @@ public class PleromaInstanceExtensions
public class InstanceMetadata public class InstanceMetadata
{ {
[J("post_formats")] public string[] PostFormats => ["text/plain", "text/x.misskeymarkdown"]; [J("post_formats")] public string[] PostFormats => ["text/plain", "text/x.misskeymarkdown"];
[J("features")] public string[] Features => ["pleroma_api", "akkoma_api", "mastodon_api", "mastodon_api_streaming", "polls", "quote_posting", "editing", "pleroma_emoji_reactions", "exposable_reactions", "custom_emoji_reactions"];
[J("features")]
public string[] Features =>
[
"pleroma_api",
"akkoma_api",
"mastodon_api",
"mastodon_api_streaming",
"polls",
"quote_posting",
"editing",
"pleroma_emoji_reactions",
"exposable_reactions",
"custom_emoji_reactions"
];
[J("fields_limits")] public FieldsLimits FieldsLimits => new(); [J("fields_limits")] public FieldsLimits FieldsLimits => new();
} }

View file

@ -2,14 +2,13 @@ using J = System.Text.Json.Serialization.JsonPropertyNameAttribute;
namespace Iceshrimp.Backend.Controllers.Pleroma.Schemas; namespace Iceshrimp.Backend.Controllers.Pleroma.Schemas;
public class FrontendConfigurationsResponse() public class FrontendConfigurationsResponse
{ {
[J("pleroma_fe")] public PleromaFeConfiguration PleromaFe => new(); [J("pleroma_fe")] public PleromaFeConfiguration PleromaFe => new();
} }
public class PleromaFeConfiguration
public class PleromaFeConfiguration()
{ {
[J("loginMethod")] public string LoginMethod => "token"; [J("loginMethod")] public string LoginMethod => "token";
[J("useStreamingApi")] public bool UseStreamingApi => true; [J("useStreamingApi")] public bool UseStreamingApi => true;
} }

View file

@ -7,7 +7,7 @@ public abstract class PleromaNotificationSchemas
{ {
public class ReadNotificationsRequest public class ReadNotificationsRequest
{ {
[B(Name = "id")] [J("id")] public long? Id { get; set; } [B(Name = "id")] [J("id")] public long? Id { get; set; }
[B(Name = "max_id")] [J("max_id")] public long? MaxId { get; set; } [B(Name = "max_id")] [J("max_id")] public long? MaxId { get; set; }
} }
} }

View file

@ -2,12 +2,10 @@ using System.Net;
using System.Net.Mime; using System.Net.Mime;
using Iceshrimp.Backend.Controllers.Mastodon.Attributes; using Iceshrimp.Backend.Controllers.Mastodon.Attributes;
using Iceshrimp.Backend.Controllers.Mastodon.Renderers; using Iceshrimp.Backend.Controllers.Mastodon.Renderers;
using Iceshrimp.Backend.Controllers.Mastodon.Schemas;
using Iceshrimp.Backend.Controllers.Mastodon.Schemas.Entities; using Iceshrimp.Backend.Controllers.Mastodon.Schemas.Entities;
using Iceshrimp.Backend.Controllers.Shared.Attributes; using Iceshrimp.Backend.Controllers.Shared.Attributes;
using Iceshrimp.Backend.Core.Configuration; using Iceshrimp.Backend.Core.Configuration;
using Iceshrimp.Backend.Core.Database; using Iceshrimp.Backend.Core.Database;
using Iceshrimp.Backend.Core.Database.Tables;
using Iceshrimp.Backend.Core.Extensions; using Iceshrimp.Backend.Core.Extensions;
using Iceshrimp.Backend.Core.Middleware; using Iceshrimp.Backend.Core.Middleware;
using Iceshrimp.Backend.Core.Services; using Iceshrimp.Backend.Core.Services;

View file

@ -31,7 +31,6 @@ public class IdenticonController : ControllerBase
var gradient = new LinearGradientBrush(new Point(0, 0), new Point(Size, Size), GradientRepetitionMode.None, var gradient = new LinearGradientBrush(new Point(0, 0), new Point(Size, Size), GradientRepetitionMode.None,
new ColorStop(0, color.start), new ColorStop(1, color.end)); new ColorStop(0, color.start), new ColorStop(1, color.end));
image.Mutate(p => p.Fill(gradient)); image.Mutate(p => p.Fill(gradient));
var paint = new SolidBrush(Color.White); var paint = new SolidBrush(Color.White);

View file

@ -12,7 +12,7 @@ public static class FilterRenderer
Expiry = filter.Expiry, Expiry = filter.Expiry,
Keywords = filter.Keywords, Keywords = filter.Keywords,
Action = (FilterResponse.FilterAction)filter.Action, Action = (FilterResponse.FilterAction)filter.Action,
Contexts = filter.Contexts.Cast<FilterResponse.FilterContext>().ToList(), Contexts = filter.Contexts.Cast<FilterResponse.FilterContext>().ToList()
}; };
public static IEnumerable<FilterResponse> RenderMany(IEnumerable<Filter> filters) => filters.Select(RenderOne); public static IEnumerable<FilterResponse> RenderMany(IEnumerable<Filter> filters) => filters.Select(RenderOne);

View file

@ -30,7 +30,7 @@ public class SettingsController(DatabaseContext db) : ControllerBase
AlwaysMarkSensitive = settings.AlwaysMarkSensitive, AlwaysMarkSensitive = settings.AlwaysMarkSensitive,
AutoAcceptFollowed = settings.AutoAcceptFollowed, AutoAcceptFollowed = settings.AutoAcceptFollowed,
DefaultNoteVisibility = (NoteVisibility)settings.DefaultNoteVisibility, DefaultNoteVisibility = (NoteVisibility)settings.DefaultNoteVisibility,
DefaultRenoteVisibility = (NoteVisibility)settings.DefaultNoteVisibility, DefaultRenoteVisibility = (NoteVisibility)settings.DefaultNoteVisibility
}; };
} }

View file

@ -63,7 +63,7 @@ public class OauthApp
public virtual ICollection<OauthToken> OauthTokens { get; set; } = new List<OauthToken>(); public virtual ICollection<OauthToken> OauthTokens { get; set; } = new List<OauthToken>();
/// <summary> /// <summary>
/// The app token, returned by /oauth/token when grant_type client_credentials is requested /// The app token, returned by /oauth/token when grant_type client_credentials is requested
/// </summary> /// </summary>
[Column("appToken")] [StringLength(64)] [Column("appToken")] [StringLength(64)]
public string? Token; public string? Token;

View file

@ -104,7 +104,7 @@ public static class MvcBuilderExtensions
} }
/// <summary> /// <summary>
/// Overrides the default OutputFormatterSelector, to make sure we return a body when content negotiation errors occur. /// Overrides the default OutputFormatterSelector, to make sure we return a body when content negotiation errors occur.
/// </summary> /// </summary>
public class AcceptHeaderOutputFormatterSelector( public class AcceptHeaderOutputFormatterSelector(
IOptions<MvcOptions> options, IOptions<MvcOptions> options,

View file

@ -537,7 +537,7 @@ public static class QueryableExtensions
) )
{ {
if (request.ExcludeReplies) if (request.ExcludeReplies)
query = query.Where(p => p.Reply == null && p.ReplyUri == null || p.ReplyUserId == p.UserId); query = query.Where(p => (p.Reply == null && p.ReplyUri == null) || p.ReplyUserId == p.UserId);
if (request.ExcludeRenotes) if (request.ExcludeRenotes)
query = query.Where(p => p.Renote == null && p.RenoteUri == null); query = query.Where(p => p.Renote == null && p.RenoteUri == null);
if (request.Tagged != null) if (request.Tagged != null)

View file

@ -57,7 +57,7 @@ public static class StringBuilderExtensions
private const char NewLineLf = '\n'; private const char NewLineLf = '\n';
/// <summary> /// <summary>
/// Equivalent to .AppendLine, but always uses \n instead of Environment.NewLine /// Equivalent to .AppendLine, but always uses \n instead of Environment.NewLine
/// </summary> /// </summary>
public static StringBuilder AppendLineLf(this StringBuilder sb, string? value) public static StringBuilder AppendLineLf(this StringBuilder sb, string? value)
{ {

View file

@ -82,7 +82,7 @@ public class ActivityFetcherService(
} }
/// <summary> /// <summary>
/// This abstracts FetchActivityInternal to keep stack traces short in case of HTTP timeouts. /// This abstracts FetchActivityInternal to keep stack traces short in case of HTTP timeouts.
/// </summary> /// </summary>
/// <exception cref="TimeoutException"></exception> /// <exception cref="TimeoutException"></exception>
private async Task<(ASObject? obj, Uri finalUri)> FetchActivityInternalWrapper( private async Task<(ASObject? obj, Uri finalUri)> FetchActivityInternalWrapper(

View file

@ -111,11 +111,27 @@ public class NodeInfoResponse
[J("enableDiscordIntegration")] public bool? EnableDiscordIntegration { get; set; } [J("enableDiscordIntegration")] public bool? EnableDiscordIntegration { get; set; }
[J("enableEmail")] public bool? EnableEmail { get; set; } [J("enableEmail")] public bool? EnableEmail { get; set; }
[J("post_formats")] public string[] PostFormats => ["text/plain", "text/x.misskeymarkdown"]; [J("post_formats")] public string[] PostFormats => ["text/plain", "text/x.misskeymarkdown"];
[J("features")] public string[] Features => ["pleroma_api", "akkoma_api", "mastodon_api", "mastodon_api_streaming", "polls", "quote_posting", "editing", "pleroma_emoji_reactions", "exposable_reactions", "custom_emoji_reactions"];
[J("localBubbleInstances")] public string[] LocalBubbleInstances { get; set; } = []; [J("features")]
public string[] Features =>
[
"pleroma_api",
"akkoma_api",
"mastodon_api",
"mastodon_api_streaming",
"polls",
"quote_posting",
"editing",
"pleroma_emoji_reactions",
"exposable_reactions",
"custom_emoji_reactions"
];
[J("localBubbleInstances")] public string[] LocalBubbleInstances { get; set; } = [];
// TODO: list of ap object ids i believe? // TODO: list of ap object ids i believe?
[J("staffAccounts")] public string[] StaffAccounts { get; set; } = []; [J("staffAccounts")] public string[] StaffAccounts { get; set; } = [];
[J("publicTimelineVisibility")] public PleromaPublicTimelineVisibility? PublicTimelineVisibility { get; set; } [J("publicTimelineVisibility")] public PleromaPublicTimelineVisibility? PublicTimelineVisibility { get; set; }
[J("uploadLimits")] public PleromaUploadLimits? UploadLimits { get; set; } [J("uploadLimits")] public PleromaUploadLimits? UploadLimits { get; set; }

View file

@ -10,7 +10,7 @@ public static class BlurhashHelper
private static readonly ImmutableArray<float> PrecomputedLut = [..Enumerable.Range(0, 256).Select(SRgbToLinear)]; private static readonly ImmutableArray<float> PrecomputedLut = [..Enumerable.Range(0, 256).Select(SRgbToLinear)];
/// <summary> /// <summary>
/// Encodes a Span2D of raw pixel data into a Blurhash string /// Encodes a Span2D of raw pixel data into a Blurhash string
/// </summary> /// </summary>
/// <param name="pixels">The Span2D of raw pixel data to encode</param> /// <param name="pixels">The Span2D of raw pixel data to encode</param>
/// <param name="componentsX">The number of components used on the X-Axis for the DCT</param> /// <param name="componentsX">The number of components used on the X-Axis for the DCT</param>
@ -64,7 +64,7 @@ public static class BlurhashHelper
var dc = factors[0]; var dc = factors[0];
var acCount = componentsX * componentsY - 1; var acCount = componentsX * componentsY - 1;
var sizeFlag = (componentsX - 1) + (componentsY - 1) * 9; var sizeFlag = componentsX - 1 + (componentsY - 1) * 9;
sizeFlag.EncodeBase83(resultBuffer[..1]); sizeFlag.EncodeBase83(resultBuffer[..1]);
float maximumValue; float maximumValue;

View file

@ -1,4 +1,5 @@
using System.IO.Compression; using System.IO.Compression;
using System.Net;
using System.Text.Json; using System.Text.Json;
using Iceshrimp.Backend.Core.Middleware; using Iceshrimp.Backend.Core.Middleware;
using Microsoft.AspNetCore.StaticFiles; using Microsoft.AspNetCore.StaticFiles;
@ -7,100 +8,102 @@ namespace Iceshrimp.Backend.Core.Services;
public class EmojiZipEmoji public class EmojiZipEmoji
{ {
public string? Name { get; set; } public string? Name { get; set; }
public string? Category { get; set; } public string? Category { get; set; }
public List<string> Aliases { get; set; } = []; public List<string> Aliases { get; set; } = [];
} }
public class EmojiZipEntry public class EmojiZipEntry
{ {
public required string FileName { get; set; } public required string FileName { get; set; }
public required EmojiZipEmoji Emoji { get; set; } public required EmojiZipEmoji Emoji { get; set; }
} }
public class EmojiZipMeta public class EmojiZipMeta
{ {
public required ushort MetaVersion { get; set; } public required ushort MetaVersion { get; set; }
public required EmojiZipEntry[] Emojis { get; set; } public required EmojiZipEntry[] Emojis { get; set; }
} }
public record EmojiZip(EmojiZipMeta Metadata, ZipArchive Archive); public record EmojiZip(EmojiZipMeta Metadata, ZipArchive Archive);
public class EmojiImportService( public class EmojiImportService(
EmojiService emojiSvc, EmojiService emojiSvc,
ILogger<EmojiImportService> logger ILogger<EmojiImportService> logger
) )
{ {
public static readonly JsonSerializerOptions SerializerOptions = new(JsonSerializerDefaults.Web); public static readonly JsonSerializerOptions SerializerOptions = new(JsonSerializerDefaults.Web);
public async Task<EmojiZip> Parse(Stream zipStream) public async Task<EmojiZip> Parse(Stream zipStream)
{ {
var archive = new ZipArchive(zipStream, ZipArchiveMode.Read); var archive = new ZipArchive(zipStream, ZipArchiveMode.Read);
try try
{ {
var meta = archive.GetEntry("meta.json") var meta = archive.GetEntry("meta.json") ??
?? throw GracefulException.BadRequest("Invalid emoji zip. Only Misskey-style emoji zips are supported."); throw GracefulException
.BadRequest("Invalid emoji zip. Only Misskey-style emoji zips are supported.");
var metaJson = await JsonSerializer.DeserializeAsync<EmojiZipMeta>(meta.Open(), SerializerOptions) var metaJson = await JsonSerializer.DeserializeAsync<EmojiZipMeta>(meta.Open(), SerializerOptions) ??
?? throw GracefulException.BadRequest("Invalid emoji zip metadata"); throw GracefulException.BadRequest("Invalid emoji zip metadata");
if (metaJson.MetaVersion < 1 || metaJson.MetaVersion > 2) if (metaJson.MetaVersion < 1 || metaJson.MetaVersion > 2)
throw GracefulException.BadRequest("Unrecognized metaVersion {version}, expected 1 or 2", metaJson.MetaVersion.ToString()); throw GracefulException.BadRequest("Unrecognized metaVersion {version}, expected 1 or 2",
metaJson.MetaVersion.ToString());
return new(metaJson, archive); return new EmojiZip(metaJson, archive);
} }
catch catch
{ {
// We don't want to dispose of archive on success, as Import will do it when it's done. // We don't want to dispose of archive on success, as Import will do it when it's done.
archive.Dispose(); archive.Dispose();
throw; throw;
} }
} }
public async Task Import(EmojiZip zip) public async Task Import(EmojiZip zip)
{ {
using var archive = zip.Archive; using var archive = zip.Archive;
var contentTypeProvider = new FileExtensionContentTypeProvider(); var contentTypeProvider = new FileExtensionContentTypeProvider();
foreach (var emoji in zip.Metadata.Emojis) foreach (var emoji in zip.Metadata.Emojis)
{ {
var file = archive.GetEntry(emoji.FileName); var file = archive.GetEntry(emoji.FileName);
if (file == null) if (file == null)
{ {
logger.LogWarning("Skipping {file} as no such file was found in the zip.", emoji.FileName); logger.LogWarning("Skipping {file} as no such file was found in the zip.", emoji.FileName);
continue; continue;
} }
if (!contentTypeProvider.TryGetContentType(emoji.FileName, out var mimeType)) if (!contentTypeProvider.TryGetContentType(emoji.FileName, out var mimeType))
{ {
logger.LogWarning("Skipping {file} as the mime type could not be detemrined.", emoji.FileName); logger.LogWarning("Skipping {file} as the mime type could not be detemrined.", emoji.FileName);
continue; continue;
} }
// DriveService requires a seekable and .Length-able stream, which the DeflateStream from file.Open does not support. // DriveService requires a seekable and .Length-able stream, which the DeflateStream from file.Open does not support.
using var buffer = new MemoryStream((int)file.Length); using var buffer = new MemoryStream((int)file.Length);
await file.Open().CopyToAsync(buffer); await file.Open().CopyToAsync(buffer);
buffer.Seek(0, SeekOrigin.Begin); buffer.Seek(0, SeekOrigin.Begin);
var name = emoji.Emoji.Name ?? emoji.FileName; var name = emoji.Emoji.Name ?? emoji.FileName;
try try
{ {
await emojiSvc.CreateEmojiFromStream( await emojiSvc.CreateEmojiFromStream(
buffer, buffer,
name, name,
mimeType, mimeType,
emoji.Emoji.Aliases, emoji.Emoji.Aliases,
emoji.Emoji.Category emoji.Emoji.Category
); );
logger.LogDebug("Imported emoji {emoji}", name); logger.LogDebug("Imported emoji {emoji}", name);
} }
catch (GracefulException e) when (e.StatusCode == System.Net.HttpStatusCode.Conflict) catch (GracefulException e) when (e.StatusCode == HttpStatusCode.Conflict)
{ {
logger.LogDebug("Skipping {emoji} as it already exists.", name); logger.LogDebug("Skipping {emoji} as it already exists.", name);
} }
} }
} }
} }

View file

@ -81,6 +81,7 @@ public class ImageProcessor
_logger.LogWarning("No image processor supports the format {format}, skipping", p.Format.MimeType); _logger.LogWarning("No image processor supports the format {format}, skipping", p.Format.MimeType);
return null; return null;
} }
return async () => return async () =>
{ {
if (_concurrency is 0) if (_concurrency is 0)

View file

@ -111,7 +111,7 @@ public class ImageSharpProcessor : ImageProcessorBase, IImageProcessor
{ {
var width = Math.Min(ident.Width, targetWidth); var width = Math.Min(ident.Width, targetWidth);
var height = Math.Min(ident.Height, targetHeight ?? targetWidth); var height = Math.Min(ident.Height, targetHeight ?? targetWidth);
var size = new Size(width, height); var size = new Size(width, height);
var options = new DecoderOptions var options = new DecoderOptions
{ {
MaxFrames = 1, MaxFrames = 1,

View file

@ -11,7 +11,8 @@ using Microsoft.EntityFrameworkCore;
namespace Iceshrimp.Backend.Core.Services; namespace Iceshrimp.Backend.Core.Services;
/// <summary> /// <summary>
/// This is needed because static fields in generic classes aren't shared between instances with different generic type arguments /// This is needed because static fields in generic classes aren't shared between instances with different generic type
/// arguments
/// </summary> /// </summary>
file static class PluginStoreHelpers file static class PluginStoreHelpers
{ {

View file

@ -90,7 +90,7 @@ public class PushService(
"reaction" when rendered.Emoji is null => $"{name} reacted to your post", "reaction" when rendered.Emoji is null => $"{name} reacted to your post",
"reaction" when rendered.Emoji is not null => $"{name} reacted with {rendered.Emoji} to your post", "reaction" when rendered.Emoji is not null => $"{name} reacted with {rendered.Emoji} to your post",
_ => $"New notification from {name}" _ => $"New notification from {name}"
}; };
var body = ""; var body = "";

View file

@ -17,11 +17,11 @@ public class AuthorizeModel(DatabaseContext db) : PageModel
public List<string> Scopes = []; public List<string> Scopes = [];
public OauthToken? Token; public OauthToken? Token;
public List<User> AuthenticatedUsers = []; public List<User> AuthenticatedUsers = [];
[FromQuery(Name = "response_type")] public string ResponseType { get; set; } = null!; [FromQuery(Name = "response_type")] public string ResponseType { get; set; } = null!;
[FromQuery(Name = "client_id")] public string ClientId { get; set; } = null!; [FromQuery(Name = "client_id")] public string ClientId { get; set; } = null!;
[FromQuery(Name = "redirect_uri")] public string RedirectUri { get; set; } = null!; [FromQuery(Name = "redirect_uri")] public string RedirectUri { get; set; } = null!;
[FromQuery(Name = "force_login")] public bool ForceLogin { get; set; } = false; [FromQuery(Name = "force_login")] public bool ForceLogin { get; set; } = false;
[FromQuery(Name = "lang")] public string? Language { get; set; } [FromQuery(Name = "lang")] public string? Language { get; set; }
[FromQuery(Name = "scope")] [FromQuery(Name = "scope")]
public string Scope public string Scope
@ -91,7 +91,7 @@ public class AuthorizeModel(DatabaseContext db) : PageModel
RedirectUri = RedirectUri, RedirectUri = RedirectUri,
AutoDetectQuotes = autoDetectQuotes, AutoDetectQuotes = autoDetectQuotes,
SupportsHtmlFormatting = supportsHtmlFormatting, SupportsHtmlFormatting = supportsHtmlFormatting,
IsPleroma = isPleroma, IsPleroma = isPleroma
}; };
await db.AddAsync(token); await db.AddAsync(token);

View file

@ -13,7 +13,7 @@ public class QueueJobModel(DatabaseContext db) : PageModel
{ {
["inbox"] = "body", ["inbox"] = "body",
["deliver"] = "payload", ["deliver"] = "payload",
["pre-deliver"] = "serializedActivity", ["pre-deliver"] = "serializedActivity"
}; };
public Job Job = null!; public Job Job = null!;

View file

@ -1,6 +1,6 @@
@using Iceshrimp.Frontend.Components @using Iceshrimp.Frontend.Components
@using Microsoft.AspNetCore.Components.Authorization @using Microsoft.AspNetCore.Components.Authorization
<ErrorBoundary @ref=ErrorBoundary> <ErrorBoundary @ref="ErrorBoundary">
<ChildContent> <ChildContent>
<Router AppAssembly="@typeof(App).Assembly"> <Router AppAssembly="@typeof(App).Assembly">
<Found Context="routeData"> <Found Context="routeData">

View file

@ -1,6 +1,5 @@
@using Iceshrimp.Assets.PhosphorIcons @using Iceshrimp.Assets.PhosphorIcons
@using Iceshrimp.Shared.Schemas.Web @using Iceshrimp.Shared.Schemas.Web
@if (Attachment.ContentType.StartsWith("image")) @if (Attachment.ContentType.StartsWith("image"))
{ {
<div class="wrapper" @onclick="Open" @onclick:stopPropagation="true"> <div class="wrapper" @onclick="Open" @onclick:stopPropagation="true">

View file

@ -1,8 +1,8 @@
<div @onclick="Close" @onclick:stopPropagation="true" class="backdrop @(Darken ? "darken" : "")"></div> <div @onclick="Close" @onclick:stopPropagation="true" class="backdrop @(Darken ? "darken" : "")"></div>
@code { @code {
[Parameter][EditorRequired] public required EventCallback OnClose { get; set; } [Parameter] [EditorRequired] public required EventCallback OnClose { get; set; }
[Parameter] public bool Darken { get; set; } [Parameter] public bool Darken { get; set; }
private void Close() private void Close()
{ {

View file

@ -20,20 +20,20 @@
<Icon Name="Icons.X"/> <Icon Name="Icons.X"/>
</button> </button>
<Dropdown TBind="NoteVisibility" Elements="@DropDownCreate()" @bind-Value="NoteDraft.Visibility"/> <Dropdown TBind="NoteVisibility" Elements="@DropDownCreate()" @bind-Value="NoteDraft.Visibility"/>
<StateButton OnClick="SendNote" @ref=SendButton ExtraClasses="post-btn"> <StateButton OnClick="SendNote" @ref="SendButton" ExtraClasses="post-btn">
<Initial> <Initial>
@Loc["ComposePost"]<Icon Name="Icons.PaperPlaneRight"/> @Loc["ComposePost"]<Icon Name="Icons.PaperPlaneRight"/>
</Initial> </Initial>
<Loading> <Loading>
<Icon Name="Icons.Spinner"/> <Icon Name="Icons.Spinner"/>
</Loading> </Loading>
<Success> <Success>
<Icon Name="Icons.Check"/> <Icon Name="Icons.Check"/>
</Success> </Success>
<Failed> <Failed>
@Loc["Retry"]<Icon Name="Icons.X"/> @Loc["Retry"]<Icon Name="Icons.X"/>
</Failed> </Failed>
</StateButton> </StateButton>
</div> </div>
@if (ReplyOrQuote != null) @if (ReplyOrQuote != null)
{ {
@ -84,7 +84,12 @@
Cw = null Cw = null
}; };
private Dictionary<string, string> AvailablePlaceholders { get; set; } = new() { { "default", "What's on your mind?" }, { "reply", "Reply goes here!" }, { "quote", "Quote this post!" } }; private Dictionary<string, string> AvailablePlaceholders { get; set; } = new()
{
{ "default", "What's on your mind?" },
{ "reply", "Reply goes here!" },
{ "quote", "Quote this post!" }
};
RenderFragment DropdownIcon(NoteVisibility vis) RenderFragment DropdownIcon(NoteVisibility vis)
{ {
@ -117,7 +122,9 @@
new DropdownElement<NoteVisibility> new DropdownElement<NoteVisibility>
{ {
#pragma warning disable BL0005 // Setting this outside the component is fine until this is reworked #pragma warning disable BL0005 // Setting this outside the component is fine until this is reworked
Icon = DropdownIcon(vis), Content = DropdownContent(vis), Selection = vis Icon = DropdownIcon(vis),
Content = DropdownContent(vis),
Selection = vis
#pragma warning restore BL0005 #pragma warning restore BL0005
}) })
.ToList(); .ToList();
@ -210,9 +217,14 @@
private void ResetState() private void ResetState()
{ {
ReplyOrQuote = null; ReplyOrQuote = null;
Attachments = new List<DriveFileResponse>(); Attachments = new List<DriveFileResponse>();
NoteDraft = new NoteCreateRequest { Text = "", Visibility = NoteVisibility.Followers, Cw = null }; NoteDraft = new NoteCreateRequest
{
Text = "",
Visibility = NoteVisibility.Followers,
Cw = null
};
TextPlaceholder = AvailablePlaceholders["default"]; TextPlaceholder = AvailablePlaceholders["default"];
} }

View file

@ -18,8 +18,8 @@
@code { @code {
private EventCallback<EmojiResponse> OnEmojiSelect { get; set; } private EventCallback<EmojiResponse> OnEmojiSelect { get; set; }
private List<EmojiResponse> EmojiList { get; set; } = []; private List<EmojiResponse> EmojiList { get; set; } = [];
private ElementReference EmojiPickerRef { get; set; } private ElementReference EmojiPickerRef { get; set; }
private float _top; private float _top;
private float _left; private float _left;

View file

@ -7,7 +7,7 @@
@inject NavigationManager Navigation; @inject NavigationManager Navigation;
@inject VersionService Version; @inject VersionService Version;
@inject InMemoryLogService LogService; @inject InMemoryLogService LogService;
@inject IJSRuntime Js; @inject IJSRuntime Js;
<div class="error-ui"> <div class="error-ui">
<h3>@Loc["Unhandled Exception has occured"]</h3> <h3>@Loc["Unhandled Exception has occured"]</h3>
@ -40,8 +40,7 @@
} }
</div> </div>
@Loc["Logs"] @Loc["Logs"]
@Loc["These logs may contain sensitive information, please do not post them publicly.\n" + @Loc["These logs may contain sensitive information, please do not post them publicly.\n" + "Providing them to developers upon request may help with debugging."]
"Providing them to developers upon request may help with debugging."]
<div class="log-block"> <div class="log-block">
<pre><code> <pre><code>
@foreach (var line in _rawLog) @foreach (var line in _rawLog)
@ -60,7 +59,7 @@
[Parameter] [EditorRequired] public required Exception Exception { get; set; } [Parameter] [EditorRequired] public required Exception Exception { get; set; }
private IEnumerable<string> _logs = []; private IEnumerable<string> _logs = [];
private IJSInProcessObjectReference _module = null!; private IJSInProcessObjectReference _module = null!;
private List<string> _rawLog = null!; private List<string> _rawLog = null!;
private void Reload() private void Reload()
{ {
@ -70,14 +69,12 @@
protected override async Task OnInitializedAsync() protected override async Task OnInitializedAsync()
{ {
_rawLog = LogService.GetLogs(); _rawLog = LogService.GetLogs();
_module = (IJSInProcessObjectReference) await Js.InvokeAsync<IJSObjectReference>("import", "./Components/ErrorUi.razor.js"); _module = (IJSInProcessObjectReference)await Js.InvokeAsync<IJSObjectReference>("import", "./Components/ErrorUi.razor.js");
} }
private void DownloadLogs() private void DownloadLogs()
{ {
var logBytes = LogService.GetLogs().SelectMany(p => Encoding.UTF8.GetBytes(p)).ToArray(); var logBytes = LogService.GetLogs().SelectMany(p => Encoding.UTF8.GetBytes(p)).ToArray();
_module.InvokeVoid("DownloadFile", "log.txt", "text/plain", logBytes); _module.InvokeVoid("DownloadFile", "log.txt", "text/plain", logBytes);
} }
} }

View file

@ -1,8 +1,8 @@
@using Iceshrimp.Assets.PhosphorIcons
@using Iceshrimp.Frontend.Core.Services
@using Iceshrimp.Frontend.Localization @using Iceshrimp.Frontend.Localization
@using Iceshrimp.Shared.Schemas.Web @using Iceshrimp.Shared.Schemas.Web
@using Microsoft.Extensions.Localization @using Microsoft.Extensions.Localization
@using Iceshrimp.Assets.PhosphorIcons
@using Iceshrimp.Frontend.Core.Services
@inject IStringLocalizer<Localization> Loc; @inject IStringLocalizer<Localization> Loc;
@inject ApiService Api; @inject ApiService Api;
<span class="follow-button"> <span class="follow-button">

View file

@ -1,11 +1,11 @@
@using Iceshrimp.Assets.PhosphorIcons
@using Iceshrimp.Frontend.Core.Services
@using Iceshrimp.Frontend.Localization @using Iceshrimp.Frontend.Localization
@using Iceshrimp.Shared.Schemas.Web @using Iceshrimp.Shared.Schemas.Web
@using Microsoft.Extensions.Localization @using Microsoft.Extensions.Localization
@using Iceshrimp.Assets.PhosphorIcons
@using Iceshrimp.Frontend.Core.Services
@inject IStringLocalizer<Localization> Loc; @inject IStringLocalizer<Localization> Loc;
@inject ApiService Api; @inject ApiService Api;
@inject NavigationManager NavigationManager; @inject NavigationManager NavigationManager;
<div class="follow-request-card"> <div class="follow-request-card">
@ -33,7 +33,7 @@
protected override async Task OnInitializedAsync() protected override async Task OnInitializedAsync()
{ {
var profile = await Api.Users.GetUserProfile(FollowRequest.User.Id); var profile = await Api.Users.GetUserProfile(FollowRequest.User.Id);
if (profile != null) _followBack = profile.Relations.HasFlag(Relations.None); if (profile != null) _followBack = profile.Relations.HasFlag(Relations.None);
} }

View file

@ -1,4 +1,4 @@
<EmojiPicker></EmojiPicker> <EmojiPicker></EmojiPicker>
@code {
@code {
} }

View file

@ -1,4 +1,3 @@
@if (_display) @if (_display)
{ {
<div class="menu"> <div class="menu">

View file

@ -1,5 +1,4 @@
@using Iceshrimp.Assets.PhosphorIcons @using Iceshrimp.Assets.PhosphorIcons
<div @onclick="OnClick" class="menu-element"> <div @onclick="OnClick" class="menu-element">
@if (Icon != null) @if (Icon != null)
{ {
@ -7,6 +6,7 @@
} }
@Text @Text
</div> </div>
@code { @code {
[Parameter] public IconName? Icon { get; set; } [Parameter] public IconName? Icon { get; set; }
[Parameter] [EditorRequired] public required RenderFragment Text { get; set; } [Parameter] [EditorRequired] public required RenderFragment Text { get; set; }

View file

@ -1,53 +1,54 @@
@using Iceshrimp.Assets.PhosphorIcons @using Iceshrimp.Assets.PhosphorIcons
<CascadingValue Value="this"> <CascadingValue Value="this">
@if (NoteResponse.Renote != null) @if (NoteResponse.Renote != null)
{
<div class="renote">
<div class="renote-info">
<span class="user">
<Icon Name="Icons.Repeat" /> Boosted by
@if (NoteResponse.User.DisplayName != null)
{
@NoteResponse.User.DisplayName
}
else
{
@NoteResponse.User.Username
}
</span>
<span class="metadata">
<NoteMetadata Visibility="NoteResponse.Visibility" InstanceName="@null" CreatedAt="DateTime.Parse(NoteResponse.CreatedAt)"/>
</span>
</div>
<NoteComponent Note="NoteResponse.Renote" Quote="NoteResponse.Quote" Indented="Indented"/>
</div>
}
else
{
@if (NoteResponse.Filtered is not null && NoteResponse.Filtered.Hide == false)
{ {
<div> <div class="renote">
@Loc["This note contains the filter keyword '{0}'", NoteResponse.Filtered.Keyword] <div class="renote-info">
<button <span class="user">
@onclick="@ShowNote" <Icon Name="Icons.Repeat"/> Boosted by
@onclick:stopPropagation="true"> @if (NoteResponse.User.DisplayName != null)
@if (_overrideHide) {
{ @NoteResponse.User.DisplayName
@Loc["Hide"] }
} else
else {
{ @NoteResponse.User.Username
@Loc["Show"] }
} </span>
</button> <span class="metadata">
<NoteMetadata Visibility="NoteResponse.Visibility" InstanceName="@null" CreatedAt="DateTime.Parse(NoteResponse.CreatedAt)"/>
</span>
</div>
<NoteComponent Note="NoteResponse.Renote" Quote="NoteResponse.Quote" Indented="Indented"/>
</div> </div>
} }
@if (_overrideHide || NoteResponse.Filtered == null) else
{ {
<NoteComponent Note="NoteResponse" Quote="NoteResponse.Quote" Indented="Indented"/> @if (NoteResponse.Filtered is not null && NoteResponse.Filtered.Hide == false)
{
<div>
@Loc["This note contains the filter keyword '{0}'", NoteResponse.Filtered.Keyword]
<button
@onclick="@ShowNote"
@onclick:stopPropagation="true">
@if (_overrideHide)
{
@Loc["Hide"]
}
else
{
@Loc["Show"]
}
</button>
</div>
}
@if (_overrideHide || NoteResponse.Filtered == null)
{
<NoteComponent Note="NoteResponse" Quote="NoteResponse.Quote" Indented="Indented"/>
}
} }
}
</CascadingValue> </CascadingValue>

View file

@ -4,36 +4,36 @@
@inject NavigationManager NavigationManager @inject NavigationManager NavigationManager
@inject ComposeService ComposeService @inject ComposeService ComposeService
@inject SessionService Session; @inject SessionService Session;
<div class="note-header"> <div class="note-header">
<NoteUserInfo <NoteUserInfo
AvatarUrl="@Note.User.AvatarUrl" AvatarUrl="@Note.User.AvatarUrl"
DisplayName="@Note.User.DisplayName" DisplayName="@Note.User.DisplayName"
Username="@Note.User.Username" Username="@Note.User.Username"
Host="@Note.User.Host" Host="@Note.User.Host"
Indented="Indented"/> Indented="Indented"/>
<NoteMetadata <NoteMetadata
Visibility="@Note.Visibility" Visibility="@Note.Visibility"
InstanceName="@Note.User.InstanceName" InstanceName="@Note.User.InstanceName"
CreatedAt="DateTime.Parse(Note.CreatedAt)"> CreatedAt="DateTime.Parse(Note.CreatedAt)">
</NoteMetadata> </NoteMetadata>
</div>
<NoteBody NoteBase="Note" OverLength="@CheckLen()" Indented="Indented"/>
@if (Quote != null)
{
<div @onclick="OpenQuote" @onclick:stopPropagation="true" class="quote">
<NoteComponent Note="Quote" AsQuote="true"></NoteComponent>
</div> </div>
<NoteBody NoteBase="Note" OverLength="@CheckLen()" Indented="Indented"/> }
@if (Quote != null) @if (!AsQuote)
{ {
<div @onclick="OpenQuote" @onclick:stopPropagation="true" class="quote"> <NoteFooter
<NoteComponent Note="Quote" AsQuote="true"></NoteComponent> Reactions="Note.Reactions"
</div> Likes="Note.Likes"
} IsLiked="Note.Liked"
@if (!AsQuote) Renotes="Note.Renotes"
{ RenotePossible=
<NoteFooter "@(Note.Visibility == NoteVisibility.Public || Note.Visibility == NoteVisibility.Home || Session.Current?.Id == Note.User.Id)"/>
Reactions="Note.Reactions" }
Likes="Note.Likes"
IsLiked="Note.Liked"
Renotes="Note.Renotes"
RenotePossible=
"@(Note.Visibility == NoteVisibility.Public || Note.Visibility == NoteVisibility.Home || Session.Current?.Id == Note.User.Id)"/>
}
@code { @code {
[Parameter] [EditorRequired] public required NoteBase Note { get; set; } [Parameter] [EditorRequired] public required NoteBase Note { get; set; }

View file

@ -6,7 +6,7 @@
@inject IStringLocalizer<Localization> Loc; @inject IStringLocalizer<Localization> Loc;
@inject IJSRuntime Js; @inject IJSRuntime Js;
@inject SessionService Session; @inject SessionService Session;
@inject GlobalComponentSvc GlobalComponentSvc @inject GlobalComponentSvc GlobalComponentSvc
<div class="note-footer"> <div class="note-footer">
@if (Reactions.Count > 0) @if (Reactions.Count > 0)
@ -21,7 +21,7 @@
<button class="btn" @onclick="Reply" @onclick:stopPropagation="true"> <button class="btn" @onclick="Reply" @onclick:stopPropagation="true">
<Icon Name="Icons.ArrowUUpLeft" Size="1.3em"/> <Icon Name="Icons.ArrowUUpLeft" Size="1.3em"/>
</button> </button>
<button class="btn @(RenotePossible ? "" : "disabled") positioned" @onclick="@(RenotePossible ? ToggleRenoteMenu : () => {})" @onclick:stopPropagation="true"> <button class="btn @(RenotePossible ? "" : "disabled") positioned" @onclick="@(RenotePossible ? ToggleRenoteMenu : () => { })" @onclick:stopPropagation="true">
@if (RenotePossible) @if (RenotePossible)
{ {
<Icon Name="Icons.Repeat" Size="1.3em"/> <Icon Name="Icons.Repeat" Size="1.3em"/>
@ -103,7 +103,7 @@
[Parameter] [EditorRequired] public required int Renotes { get; set; } [Parameter] [EditorRequired] public required int Renotes { get; set; }
[Parameter] public bool RenotePossible { get; set; } [Parameter] public bool RenotePossible { get; set; }
private Menu ContextMenu { get; set; } = null!; private Menu ContextMenu { get; set; } = null!;
private Menu RenoteMenu { get; set; } = null!; private Menu RenoteMenu { get; set; } = null!;
private ElementReference EmojiButton { get; set; } private ElementReference EmojiButton { get; set; }
[CascadingParameter] Note Note { get; set; } = null!; [CascadingParameter] Note Note { get; set; } = null!;

View file

@ -16,12 +16,12 @@
</button> </button>
@code { @code {
[Parameter] [EditorRequired] public required NoteReactionSchema Reaction { get; set; } [Parameter] [EditorRequired] public required NoteReactionSchema Reaction { get; set; }
[CascadingParameter] Note Note { get; set; } = null!; [CascadingParameter] Note Note { get; set; } = null!;
private void React() private void React()
{ {
if (Reaction.Reacted) _ = Note.RemoveReact(Reaction.Name); if (Reaction.Reacted) _ = Note.RemoveReact(Reaction.Name);
else _ = Note.AddReact(Reaction.Name, Reaction.Url); else _ = Note.AddReact(Reaction.Name, Reaction.Url);
} }
} }

View file

@ -7,7 +7,7 @@
@inject NavigationManager NavigationManager @inject NavigationManager NavigationManager
<div class="notification"> <div class="notification">
@if (NotificationResponse is { User: not null, Type: "like" or "follow" or "reaction" or "followRequestReceived" or "followRequestAccepted"}) @if (NotificationResponse is { User: not null, Type: "like" or "follow" or "reaction" or "followRequestReceived" or "followRequestAccepted" })
{ {
<img class="user-avatar" src="@NotificationResponse.User.AvatarUrl"/> <img class="user-avatar" src="@NotificationResponse.User.AvatarUrl"/>
<div class="notification-body"> <div class="notification-body">
@ -85,7 +85,7 @@
private void OpenProfile() private void OpenProfile()
{ {
var username = $"@{NotificationResponse.User?.Username}"; var username = $"@{NotificationResponse.User?.Username}";
if (NotificationResponse.User?.Host != null) username += $"@{NotificationResponse.User.Host}"; if (NotificationResponse.User?.Host != null) username += $"@{NotificationResponse.User.Host}";
NavigationManager.NavigateTo($"/{username}"); NavigationManager.NavigateTo($"/{username}");
} }

View file

@ -4,7 +4,7 @@
@using Iceshrimp.Shared.Schemas.Web @using Iceshrimp.Shared.Schemas.Web
@inject NavigationManager NavigationManager @inject NavigationManager NavigationManager
@inject MessageService MessageService @inject MessageService MessageService
@inject ApiService Api @inject ApiService Api
@implements IDisposable @implements IDisposable
@if (_hidden == false) @if (_hidden == false)
@ -101,7 +101,6 @@
Note.Descendants = res; Note.Descendants = res;
StateHasChanged(); StateHasChanged();
} }
} }
private void OpenNote() private void OpenNote()

View file

@ -1,11 +1,11 @@
@using System.Diagnostics.CodeAnalysis @using System.Diagnostics.CodeAnalysis
@* ReSharper disable once RedundantUsingDirective *@
@using Iceshrimp.Frontend.Components.Note
@using Iceshrimp.Frontend.Core.Miscellaneous @using Iceshrimp.Frontend.Core.Miscellaneous
@using Iceshrimp.Frontend.Core.Services @using Iceshrimp.Frontend.Core.Services
@using Iceshrimp.Frontend.Localization @using Iceshrimp.Frontend.Localization
@using Microsoft.Extensions.Localization
@* ReSharper disable once RedundantUsingDirective *@
@using Iceshrimp.Frontend.Components.Note
@using Iceshrimp.Shared.Schemas.Web @using Iceshrimp.Shared.Schemas.Web
@using Microsoft.Extensions.Localization
@inject IStringLocalizer<Localization> Loc; @inject IStringLocalizer<Localization> Loc;
@inject ApiService Api; @inject ApiService Api;
@inject NavigationManager Navigation; @inject NavigationManager Navigation;

View file

@ -25,5 +25,4 @@
</div> </div>
@code { @code {
} }

View file

@ -10,13 +10,13 @@
</button> </button>
@code { @code {
[Parameter, EditorRequired] public required EventCallback OnClick { get; set; } [Parameter] [EditorRequired] public required EventCallback OnClick { get; set; }
[Parameter, EditorRequired] public required RenderFragment Initial { get; set; } [Parameter] [EditorRequired] public required RenderFragment Initial { get; set; }
[Parameter, EditorRequired] public required RenderFragment Loading { get; set; } [Parameter] [EditorRequired] public required RenderFragment Loading { get; set; }
[Parameter, EditorRequired] public required RenderFragment Failed { get; set; } [Parameter] [EditorRequired] public required RenderFragment Failed { get; set; }
[Parameter, EditorRequired] public required RenderFragment Success { get; set; } [Parameter] [EditorRequired] public required RenderFragment Success { get; set; }
[Parameter] public string? ExtraClasses { get; set; } [Parameter] public string? ExtraClasses { get; set; }
public StateEnum State { get; set; } public StateEnum State { get; set; }
public enum StateEnum public enum StateEnum
{ {

View file

@ -34,6 +34,7 @@ public partial class TimelineComponent : IAsyncDisposable
{ {
return false; return false;
} }
State.MaxId = res[0].Id; State.MaxId = res[0].Id;
State.MinId = res.Last().Id; State.MinId = res.Last().Id;
State.Timeline = res; State.Timeline = res;
@ -124,6 +125,7 @@ public partial class TimelineComponent : IAsyncDisposable
{ {
initResult = await Initialize(); initResult = await Initialize();
} }
ComponentState = initResult ? Core.Miscellaneous.State.Loaded : Core.Miscellaneous.State.Empty; ComponentState = initResult ? Core.Miscellaneous.State.Loaded : Core.Miscellaneous.State.Empty;
StateHasChanged(); StateHasChanged();
} }

View file

@ -2,15 +2,12 @@
@using Iceshrimp.Frontend.Components.Note @using Iceshrimp.Frontend.Components.Note
@using Iceshrimp.Shared.Schemas.Web @using Iceshrimp.Shared.Schemas.Web
@inject NavigationManager Navigation @inject NavigationManager Navigation
@if (Note.Filtered is { Hide: true }) @if (Note.Filtered is { Hide: true }) { }
else
{ {
}
else {
<div class="note-container" @onclick="OpenNote" id="@Note.Id"> <div class="note-container" @onclick="OpenNote" id="@Note.Id">
<Note NoteResponse="Note"></Note> <Note NoteResponse="Note"></Note>
</div> </div>
} }
@code { @code {

View file

@ -1,6 +1,6 @@
@inject ApiService Api;
@using Iceshrimp.Frontend.Core.Services @using Iceshrimp.Frontend.Core.Services
@using Iceshrimp.Shared.Schemas.Web @using Iceshrimp.Shared.Schemas.Web
@inject ApiService Api;
@inject NavigationManager NavigationManager; @inject NavigationManager NavigationManager;
@if (UserProfile != null) @if (UserProfile != null)
{ {

View file

@ -1,4 +1,3 @@
<div @ref="@_scroller" class="scroller"> <div @ref="@_scroller" class="scroller">
<div @ref="@_padTopRef" class="padding top" style="height: @(State.PadTop + "px")"></div> <div @ref="@_padTopRef" class="padding top" style="height: @(State.PadTop + "px")"></div>
@if (_loadingTop) @if (_loadingTop)
@ -30,5 +29,4 @@
</div> </div>
@code { @code {
} }

View file

@ -40,7 +40,7 @@ public partial class VirtualScroller : IAsyncDisposable
set => _refs.Add(value); set => _refs.Add(value);
} }
private bool _interlock = false; private bool _interlock = false;
private IJSInProcessObjectReference Module { get; set; } = null!; private IJSInProcessObjectReference Module { get; set; } = null!;
private void InitialRender(string? id) private void InitialRender(string? id)
@ -83,7 +83,7 @@ public partial class VirtualScroller : IAsyncDisposable
private void SaveState() private void SaveState()
{ {
GetScrollY(); // ^-^ grblll mrrp GetScrollY(); // ^-^ grblll mrrp
StateService.VirtualScroller.SetState("home", State); StateService.VirtualScroller.SetState("home", State);
} }
@ -277,7 +277,9 @@ public partial class VirtualScroller : IAsyncDisposable
{ {
if (firstRender) if (firstRender)
{ {
Module = (IJSInProcessObjectReference) await Js.InvokeAsync<IJSObjectReference>("import", "./Components/VirtualScroller.razor.js"); Module =
(IJSInProcessObjectReference)
await Js.InvokeAsync<IJSObjectReference>("import", "./Components/VirtualScroller.razor.js");
await SetupObservers(); await SetupObservers();
} }

View file

@ -6,6 +6,7 @@ namespace Iceshrimp.Frontend.Core.InMemoryLogger;
internal class InMemoryLogService(IOptions<InMemoryLoggerConfiguration> configuration) internal class InMemoryLogService(IOptions<InMemoryLoggerConfiguration> configuration)
{ {
private readonly LogBuffer _buffer = new(configuration.Value.BufferSize); private readonly LogBuffer _buffer = new(configuration.Value.BufferSize);
public void Add(string logline) public void Add(string logline)
{ {
_buffer.Add(logline); _buffer.Add(logline);

View file

@ -2,9 +2,12 @@ using Microsoft.Extensions.Options;
namespace Iceshrimp.Frontend.Core.InMemoryLogger; namespace Iceshrimp.Frontend.Core.InMemoryLogger;
internal class InMemoryLogger (IOptions<InMemoryLoggerConfiguration> config, InMemoryLogService logService) : ILogger internal class InMemoryLogger(IOptions<InMemoryLoggerConfiguration> config, InMemoryLogService logService) : ILogger
{ {
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func<TState, Exception?, string> formatter) public void Log<TState>(
LogLevel logLevel, EventId eventId, TState state, Exception? exception,
Func<TState, Exception?, string> formatter
)
{ {
logService.Add(formatter(state, exception)); logService.Add(formatter(state, exception));
} }

View file

@ -1,19 +1,19 @@
namespace Iceshrimp.Frontend.Core.InMemoryLogger;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Configuration; using Microsoft.Extensions.Logging.Configuration;
namespace Iceshrimp.Frontend.Core.InMemoryLogger;
internal static class InMemoryLoggerExtension internal static class InMemoryLoggerExtension
{ {
public static void AddInMemoryLogger( public static void AddInMemoryLogger(
this ILoggingBuilder builder, IConfiguration configuration) this ILoggingBuilder builder, IConfiguration configuration
)
{ {
builder.AddConfiguration(); builder.AddConfiguration();
builder.Services.AddOptionsWithValidateOnStart<InMemoryLoggerConfiguration>() builder.Services.AddOptionsWithValidateOnStart<InMemoryLoggerConfiguration>()
.Bind(configuration.GetSection("InMemoryLogger")); .Bind(configuration.GetSection("InMemoryLogger"));
LoggerProviderOptions.RegisterProviderOptions<InMemoryLoggerConfiguration, InMemoryLoggerProvider>(builder.Services); LoggerProviderOptions
.RegisterProviderOptions<InMemoryLoggerConfiguration, InMemoryLoggerProvider>(builder.Services);
builder.Services.TryAddSingleton<InMemoryLogService>(); builder.Services.TryAddSingleton<InMemoryLogService>();
builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<ILoggerProvider, InMemoryLoggerProvider>()); builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<ILoggerProvider, InMemoryLoggerProvider>());
} }
@ -28,5 +28,3 @@ internal static class InMemoryLoggerExtension
builder.Services.Configure(configure); builder.Services.Configure(configure);
} }
} }

View file

@ -9,19 +9,19 @@ namespace Iceshrimp.Frontend.Core.Miscellaneous;
// Adapted under MIT from https://raw.githubusercontent.com/dotnet/aspnetcore/3f1acb59718cadf111a0a796681e3d3509bb3381/src/Http/Http.Abstractions/src/QueryString.cs // Adapted under MIT from https://raw.githubusercontent.com/dotnet/aspnetcore/3f1acb59718cadf111a0a796681e3d3509bb3381/src/Http/Http.Abstractions/src/QueryString.cs
/// <summary> /// <summary>
/// Provides correct handling for QueryString value when needed to reconstruct a request or redirect URI string /// Provides correct handling for QueryString value when needed to reconstruct a request or redirect URI string
/// </summary> /// </summary>
[DebuggerDisplay("{Value}")] [DebuggerDisplay("{Value}")]
public readonly struct QueryString : IEquatable<QueryString> public readonly struct QueryString : IEquatable<QueryString>
{ {
/// <summary> /// <summary>
/// Represents the empty query string. This field is read-only. /// Represents the empty query string. This field is read-only.
/// </summary> /// </summary>
public static readonly QueryString Empty = new(string.Empty); public static readonly QueryString Empty = new(string.Empty);
/// <summary> /// <summary>
/// Initialize the query string with a given value. This value must be in escaped and delimited format with /// Initialize the query string with a given value. This value must be in escaped and delimited format with
/// a leading '?' character. /// a leading '?' character.
/// </summary> /// </summary>
/// <param name="value">The query string to be assigned to the Value property.</param> /// <param name="value">The query string to be assigned to the Value property.</param>
public QueryString(string? value) public QueryString(string? value)
@ -35,20 +35,20 @@ public readonly struct QueryString : IEquatable<QueryString>
} }
/// <summary> /// <summary>
/// The escaped query string with the leading '?' character /// The escaped query string with the leading '?' character
/// </summary> /// </summary>
public string? Value { get; } public string? Value { get; }
/// <summary> /// <summary>
/// True if the query string is not empty /// True if the query string is not empty
/// </summary> /// </summary>
[MemberNotNullWhen(true, nameof(Value))] [MemberNotNullWhen(true, nameof(Value))]
public bool HasValue => !string.IsNullOrEmpty(Value); public bool HasValue => !string.IsNullOrEmpty(Value);
/// <summary> /// <summary>
/// Provides the query string escaped in a way which is correct for combining into the URI representation. /// Provides the query string escaped in a way which is correct for combining into the URI representation.
/// A leading '?' character will be included unless the Value is null or empty. Characters which are potentially /// A leading '?' character will be included unless the Value is null or empty. Characters which are potentially
/// dangerous are escaped. /// dangerous are escaped.
/// </summary> /// </summary>
/// <returns>The query string value</returns> /// <returns>The query string value</returns>
public override string ToString() public override string ToString()
@ -57,9 +57,9 @@ public readonly struct QueryString : IEquatable<QueryString>
} }
/// <summary> /// <summary>
/// Provides the query string escaped in a way which is correct for combining into the URI representation. /// Provides the query string escaped in a way which is correct for combining into the URI representation.
/// A leading '?' character will be included unless the Value is null or empty. Characters which are potentially /// A leading '?' character will be included unless the Value is null or empty. Characters which are potentially
/// dangerous are escaped. /// dangerous are escaped.
/// </summary> /// </summary>
/// <returns>The query string value</returns> /// <returns>The query string value</returns>
public string ToUriComponent() public string ToUriComponent()
@ -69,8 +69,8 @@ public readonly struct QueryString : IEquatable<QueryString>
} }
/// <summary> /// <summary>
/// Returns an QueryString given the query as it is escaped in the URI format. The string MUST NOT contain any /// Returns an QueryString given the query as it is escaped in the URI format. The string MUST NOT contain any
/// value that is not a query. /// value that is not a query.
/// </summary> /// </summary>
/// <param name="uriComponent">The escaped query as it appears in the URI format.</param> /// <param name="uriComponent">The escaped query as it appears in the URI format.</param>
/// <returns>The resulting QueryString</returns> /// <returns>The resulting QueryString</returns>
@ -85,7 +85,7 @@ public readonly struct QueryString : IEquatable<QueryString>
} }
/// <summary> /// <summary>
/// Returns an QueryString given the query as from a Uri object. Relative Uri objects are not supported. /// Returns an QueryString given the query as from a Uri object. Relative Uri objects are not supported.
/// </summary> /// </summary>
/// <param name="uri">The Uri object</param> /// <param name="uri">The Uri object</param>
/// <returns>The resulting QueryString</returns> /// <returns>The resulting QueryString</returns>
@ -93,7 +93,7 @@ public readonly struct QueryString : IEquatable<QueryString>
{ {
ArgumentNullException.ThrowIfNull(uri); ArgumentNullException.ThrowIfNull(uri);
string queryValue = uri.GetComponents(UriComponents.Query, UriFormat.UriEscaped); var queryValue = uri.GetComponents(UriComponents.Query, UriFormat.UriEscaped);
if (!string.IsNullOrEmpty(queryValue)) if (!string.IsNullOrEmpty(queryValue))
{ {
queryValue = "?" + queryValue; queryValue = "?" + queryValue;
@ -103,7 +103,7 @@ public readonly struct QueryString : IEquatable<QueryString>
} }
/// <summary> /// <summary>
/// Create a query string with a single given parameter name and value. /// Create a query string with a single given parameter name and value.
/// </summary> /// </summary>
/// <param name="name">The un-encoded parameter name</param> /// <param name="name">The un-encoded parameter name</param>
/// <param name="value">The un-encoded parameter value</param> /// <param name="value">The un-encoded parameter value</param>
@ -121,7 +121,7 @@ public readonly struct QueryString : IEquatable<QueryString>
} }
/// <summary> /// <summary>
/// Creates a query string composed from the given name value pairs. /// Creates a query string composed from the given name value pairs.
/// </summary> /// </summary>
/// <param name="parameters"></param> /// <param name="parameters"></param>
/// <returns>The resulting QueryString</returns> /// <returns>The resulting QueryString</returns>
@ -139,7 +139,7 @@ public readonly struct QueryString : IEquatable<QueryString>
} }
/// <summary> /// <summary>
/// Creates a query string composed from the given name value pairs. /// Creates a query string composed from the given name value pairs.
/// </summary> /// </summary>
/// <param name="parameters"></param> /// <param name="parameters"></param>
/// <returns>The resulting QueryString</returns> /// <returns>The resulting QueryString</returns>
@ -170,10 +170,10 @@ public readonly struct QueryString : IEquatable<QueryString>
} }
/// <summary> /// <summary>
/// Concatenates <paramref name="other"/> to the current query string. /// Concatenates <paramref name="other" /> to the current query string.
/// </summary> /// </summary>
/// <param name="other">The <see cref="QueryString"/> to concatenate.</param> /// <param name="other">The <see cref="QueryString" /> to concatenate.</param>
/// <returns>The concatenated <see cref="QueryString"/>.</returns> /// <returns>The concatenated <see cref="QueryString" />.</returns>
public QueryString Add(QueryString other) public QueryString Add(QueryString other)
{ {
if (!HasValue || Value.Equals("?", StringComparison.Ordinal)) if (!HasValue || Value.Equals("?", StringComparison.Ordinal))
@ -191,12 +191,12 @@ public readonly struct QueryString : IEquatable<QueryString>
} }
/// <summary> /// <summary>
/// Concatenates a query string with <paramref name="name"/> and <paramref name="value"/> /// Concatenates a query string with <paramref name="name" /> and <paramref name="value" />
/// to the current query string. /// to the current query string.
/// </summary> /// </summary>
/// <param name="name">The name of the query string to concatenate.</param> /// <param name="name">The name of the query string to concatenate.</param>
/// <param name="value">The value of the query string to concatenate.</param> /// <param name="value">The value of the query string to concatenate.</param>
/// <returns>The concatenated <see cref="QueryString"/>.</returns> /// <returns>The concatenated <see cref="QueryString" />.</returns>
public QueryString Add(string name, string value) public QueryString Add(string name, string value)
{ {
ArgumentNullException.ThrowIfNull(name); ArgumentNullException.ThrowIfNull(name);
@ -212,10 +212,10 @@ public readonly struct QueryString : IEquatable<QueryString>
} }
/// <summary> /// <summary>
/// Evalutes if the current query string is equal to <paramref name="other"/>. /// Evalutes if the current query string is equal to <paramref name="other" />.
/// </summary> /// </summary>
/// <param name="other">The <see cref="QueryString"/> to compare.</param> /// <param name="other">The <see cref="QueryString" /> to compare.</param>
/// <returns><see langword="true"/> if the query strings are equal.</returns> /// <returns><see langword="true" /> if the query strings are equal.</returns>
public bool Equals(QueryString other) public bool Equals(QueryString other)
{ {
if (!HasValue && !other.HasValue) if (!HasValue && !other.HasValue)
@ -227,7 +227,7 @@ public readonly struct QueryString : IEquatable<QueryString>
} }
/// <summary> /// <summary>
/// Evaluates if the current query string is equal to an object <paramref name="obj"/>. /// Evaluates if the current query string is equal to an object <paramref name="obj" />.
/// </summary> /// </summary>
/// <param name="obj">An object to compare.</param> /// <param name="obj">An object to compare.</param>
/// <returns><see langword="true" /> if the query strings are equal.</returns> /// <returns><see langword="true" /> if the query strings are equal.</returns>
@ -242,19 +242,19 @@ public readonly struct QueryString : IEquatable<QueryString>
} }
/// <summary> /// <summary>
/// Gets a hash code for the value. /// Gets a hash code for the value.
/// </summary> /// </summary>
/// <returns>The hash code as an <see cref="int"/>.</returns> /// <returns>The hash code as an <see cref="int" />.</returns>
public override int GetHashCode() public override int GetHashCode()
{ {
return (HasValue ? Value.GetHashCode() : 0); return HasValue ? Value.GetHashCode() : 0;
} }
/// <summary> /// <summary>
/// Evaluates if one query string is equal to another. /// Evaluates if one query string is equal to another.
/// </summary> /// </summary>
/// <param name="left">A <see cref="QueryString"/> instance.</param> /// <param name="left">A <see cref="QueryString" /> instance.</param>
/// <param name="right">A <see cref="QueryString"/> instance.</param> /// <param name="right">A <see cref="QueryString" /> instance.</param>
/// <returns><see langword="true" /> if the query strings are equal.</returns> /// <returns><see langword="true" /> if the query strings are equal.</returns>
public static bool operator ==(QueryString left, QueryString right) public static bool operator ==(QueryString left, QueryString right)
{ {
@ -262,10 +262,10 @@ public readonly struct QueryString : IEquatable<QueryString>
} }
/// <summary> /// <summary>
/// Evaluates if one query string is not equal to another. /// Evaluates if one query string is not equal to another.
/// </summary> /// </summary>
/// <param name="left">A <see cref="QueryString"/> instance.</param> /// <param name="left">A <see cref="QueryString" /> instance.</param>
/// <param name="right">A <see cref="QueryString"/> instance.</param> /// <param name="right">A <see cref="QueryString" /> instance.</param>
/// <returns><see langword="true" /> if the query strings are not equal.</returns> /// <returns><see langword="true" /> if the query strings are not equal.</returns>
public static bool operator !=(QueryString left, QueryString right) public static bool operator !=(QueryString left, QueryString right)
{ {
@ -273,11 +273,11 @@ public readonly struct QueryString : IEquatable<QueryString>
} }
/// <summary> /// <summary>
/// Concatenates <paramref name="left"/> and <paramref name="right"/> into a single query string. /// Concatenates <paramref name="left" /> and <paramref name="right" /> into a single query string.
/// </summary> /// </summary>
/// <param name="left">A <see cref="QueryString"/> instance.</param> /// <param name="left">A <see cref="QueryString" /> instance.</param>
/// <param name="right">A <see cref="QueryString"/> instance.</param> /// <param name="right">A <see cref="QueryString" /> instance.</param>
/// <returns>The concatenated <see cref="QueryString"/>.</returns> /// <returns>The concatenated <see cref="QueryString" />.</returns>
public static QueryString operator +(QueryString left, QueryString right) public static QueryString operator +(QueryString left, QueryString right)
{ {
return left.Add(right); return left.Add(right);

View file

@ -109,7 +109,7 @@ public static class MfmRenderer
private static INode MfmCodeBlockNode(MfmNodeTypes.MfmCodeBlockNode node, IDocument document) private static INode MfmCodeBlockNode(MfmNodeTypes.MfmCodeBlockNode node, IDocument document)
{ {
var el = document.CreateElement("pre"); var el = document.CreateElement("pre");
el.ClassName = "code-pre"; el.ClassName = "code-pre";
var childEl = document.CreateElement("code"); var childEl = document.CreateElement("code");
childEl.TextContent = node.Code; childEl.TextContent = node.Code;

View file

@ -6,24 +6,22 @@ namespace Iceshrimp.Frontend.Core.Services;
internal class EmojiService(ApiService api) internal class EmojiService(ApiService api)
{ {
[Inject] private ApiService Api { get; set; } = api; [Inject] private ApiService Api { get; set; } = api;
private List<EmojiResponse>? Emojis { get; set; } private List<EmojiResponse>? Emojis { get; set; }
public async Task<List<EmojiResponse>> GetEmoji()
{
if (Emojis is not null) return Emojis;
try
{
var emoji = await Api.Emoji.GetAllEmoji();
Emojis = emoji;
return Emojis;
}
catch (ApiException)
{
// FIXME: Implement connection error handling
throw new Exception("Failed to fetch emoji");
}
}
public async Task<List<EmojiResponse>> GetEmoji()
{
if (Emojis is not null) return Emojis;
try
{
var emoji = await Api.Emoji.GetAllEmoji();
Emojis = emoji;
return Emojis;
}
catch (ApiException)
{
// FIXME: Implement connection error handling
throw new Exception("Failed to fetch emoji");
}
}
} }

View file

@ -1,4 +1,5 @@
using Iceshrimp.Frontend.Components; using Iceshrimp.Frontend.Components;
namespace Iceshrimp.Frontend.Core.Services; namespace Iceshrimp.Frontend.Core.Services;
public class GlobalComponentSvc public class GlobalComponentSvc

View file

@ -9,12 +9,12 @@ internal class MessageService
private readonly Dictionary<(string, Type), EventHandler<NoteResponse>> _noteChangedHandlers = new(); private readonly Dictionary<(string, Type), EventHandler<NoteResponse>> _noteChangedHandlers = new();
public enum Type public enum Type
{ {
Updated, Updated,
Deleted Deleted
} }
public NoteMessageHandler Register(string id, EventHandler<NoteResponse> func, Type type) public NoteMessageHandler Register(string id, EventHandler<NoteResponse> func, Type type)
{ {
var tuple = (id, type); var tuple = (id, type);
@ -49,13 +49,15 @@ internal class MessageService
{ {
private readonly EventHandler<NoteResponse> _handler; private readonly EventHandler<NoteResponse> _handler;
private readonly string _id; private readonly string _id;
private readonly Type _type; private readonly Type _type;
private readonly MessageService _messageService; private readonly MessageService _messageService;
public NoteMessageHandler(EventHandler<NoteResponse> handler, string id, Type type, MessageService messageService) public NoteMessageHandler(
EventHandler<NoteResponse> handler, string id, Type type, MessageService messageService
)
{ {
_handler = handler; _handler = handler;
_id = id; _id = id;
_type = type; _type = type;
_messageService = messageService; _messageService = messageService;
} }

View file

@ -63,13 +63,13 @@ internal class SessionService
Current = null; Current = null;
LocalStorage.RemoveItem("last_user"); LocalStorage.RemoveItem("last_user");
((IJSInProcessRuntime)Js).InvokeVoid("eval", ((IJSInProcessRuntime)Js).InvokeVoid("eval",
"document.cookie = \"admin_session=; path=/ ; Fri, 31 Dec 1000 23:59:59 GMT SameSite=Lax\""); "document.cookie = \"admin_session=; path=/ ; Fri, 31 Dec 1000 23:59:59 GMT SameSite=Lax\"");
} }
public void SetSession(string id) public void SetSession(string id)
{ {
((IJSInProcessRuntime)Js).InvokeVoid("eval", ((IJSInProcessRuntime)Js).InvokeVoid("eval",
"document.cookie = \"admin_session=; path=/; expires=Fri, 31 Dec 1000 23:59:59 GMT SameSite=Lax\""); "document.cookie = \"admin_session=; path=/; expires=Fri, 31 Dec 1000 23:59:59 GMT SameSite=Lax\"");
var user = GetUserById(id); var user = GetUserById(id);
if (user == null) throw new Exception("Did not find User in Local Storage"); if (user == null) throw new Exception("Did not find User in Local Storage");
ApiService.SetBearerToken(user.Token); ApiService.SetBearerToken(user.Token);
@ -77,11 +77,11 @@ internal class SessionService
LocalStorage.SetItem("last_user", user.Id); LocalStorage.SetItem("last_user", user.Id);
var sessionsString = Users.Aggregate("", (current, el) => current + $"{el.Value.Token},").TrimEnd(','); var sessionsString = Users.Aggregate("", (current, el) => current + $"{el.Value.Token},").TrimEnd(',');
((IJSInProcessRuntime)Js).InvokeVoid("eval", ((IJSInProcessRuntime)Js).InvokeVoid("eval",
$"document.cookie = \"sessions={sessionsString}; path=/; expires=Fri, 31 Dec 9999 23:59:59 GMT; SameSite=Lax\""); $"document.cookie = \"sessions={sessionsString}; path=/; expires=Fri, 31 Dec 9999 23:59:59 GMT; SameSite=Lax\"");
if (user.IsAdmin) if (user.IsAdmin)
{ {
((IJSInProcessRuntime)Js).InvokeVoid("eval", ((IJSInProcessRuntime)Js).InvokeVoid("eval",
$"document.cookie = \"admin_session={user.Token}; path=/; expires=Fri, 31 Dec 9999 23:59:59 GMT; SameSite=Lax\""); $"document.cookie = \"admin_session={user.Token}; path=/; expires=Fri, 31 Dec 9999 23:59:59 GMT; SameSite=Lax\"");
} }
// Security implications of this need a second pass? user.Id should never be user controllable, but still. // Security implications of this need a second pass? user.Id should never be user controllable, but still.
} }

View file

@ -14,7 +14,6 @@ public class SingleNote
States.TryGetValue(id, out var state); States.TryGetValue(id, out var state);
return state; return state;
} }
} }
public class SingleNoteState public class SingleNoteState

View file

@ -54,12 +54,13 @@ internal class TimelineState : IDisposable
private void OnNoteDeleted(object? _, NoteResponse note) private void OnNoteDeleted(object? _, NoteResponse note)
{ {
var i = Timeline.FindIndex(p => p.Id == note.Id); var i = Timeline.FindIndex(p => p.Id == note.Id);
if (Timeline.Count <= 1) if (Timeline.Count <= 1)
{ {
Timeline.RemoveAt(i); Timeline.RemoveAt(i);
return; return;
} }
if (i == 0) MaxId = Timeline[1].Id; if (i == 0) MaxId = Timeline[1].Id;
if (i == Timeline.Count - 1) MinId = Timeline[^2].Id; if (i == Timeline.Count - 1) MinId = Timeline[^2].Id;
Timeline.RemoveAt(i); Timeline.RemoveAt(i);

View file

@ -1,6 +1,5 @@
@using Microsoft.AspNetCore.Authorization @using Microsoft.AspNetCore.Components.Authorization
@using Microsoft.AspNetCore.Components.Sections @using Microsoft.AspNetCore.Components.Sections
@using Microsoft.AspNetCore.Components.Authorization
@inherits LayoutComponentBase @inherits LayoutComponentBase
<div class="page"> <div class="page">

View file

@ -1,8 +1,7 @@
@using Iceshrimp.Frontend.Components
@using Microsoft.AspNetCore.Components.Sections
@using Iceshrimp.Assets.PhosphorIcons @using Iceshrimp.Assets.PhosphorIcons
@using Iceshrimp.Frontend.Components
@using Iceshrimp.Frontend.Localization @using Iceshrimp.Frontend.Localization
@using Microsoft.AspNetCore.Authorization @using Microsoft.AspNetCore.Components.Sections
@using Microsoft.Extensions.Localization @using Microsoft.Extensions.Localization
@inherits LayoutComponentBase @inherits LayoutComponentBase
@layout MainLayout @layout MainLayout

View file

@ -1,13 +1,13 @@
@inject IStringLocalizer<Localization> Loc;
@using Iceshrimp.Assets.PhosphorIcons @using Iceshrimp.Assets.PhosphorIcons
@using Iceshrimp.Frontend.Components @using Iceshrimp.Frontend.Components
@using Iceshrimp.Frontend.Localization @using Iceshrimp.Frontend.Localization
@using Microsoft.Extensions.Localization @using Microsoft.Extensions.Localization
@inject IStringLocalizer<Localization> Loc;
@implements IDisposable @implements IDisposable
@inject NavigationManager Navigation; @inject NavigationManager Navigation;
<GlobalComponents></GlobalComponents> <GlobalComponents></GlobalComponents>
<div @ref="SidebarElementRef" class="sidebar @(_open ? "open" : "")" tabindex=0> <div @ref="SidebarElementRef" class="sidebar @(_open ? "open" : "")" tabindex="0">
<div class="header"> <div class="header">
<account-dropdown/> <account-dropdown/>
</div> </div>
@ -49,7 +49,7 @@
</button> </button>
@if (_open) @if (_open)
{ {
<ClosingBackdrop OnClose="Close" /> <ClosingBackdrop OnClose="Close"/>
} }
</div> </div>

View file

@ -1,11 +1,11 @@
@page "/follow-requests" @page "/follow-requests"
@attribute [Authorize] @using Iceshrimp.Assets.PhosphorIcons
@using Iceshrimp.Frontend.Components @using Iceshrimp.Frontend.Components
@using Iceshrimp.Frontend.Localization @using Iceshrimp.Frontend.Localization
@using Microsoft.Extensions.Localization
@using Microsoft.AspNetCore.Components.Sections
@using Iceshrimp.Assets.PhosphorIcons
@using Microsoft.AspNetCore.Authorization @using Microsoft.AspNetCore.Authorization
@using Microsoft.AspNetCore.Components.Sections
@using Microsoft.Extensions.Localization
@attribute [Authorize]
@inject IStringLocalizer<Localization> Loc; @inject IStringLocalizer<Localization> Loc;
<SectionContent SectionName="top-bar"> <SectionContent SectionName="top-bar">
@ -13,7 +13,7 @@
@Loc["Follow requests"] @Loc["Follow requests"]
</SectionContent> </SectionContent>
<FollowRequestList /> <FollowRequestList/>
@code {
@code {
} }

View file

@ -3,9 +3,9 @@
@using Iceshrimp.Frontend.Core.Schemas @using Iceshrimp.Frontend.Core.Schemas
@using Iceshrimp.Frontend.Core.Services @using Iceshrimp.Frontend.Core.Services
@using Iceshrimp.Shared.Schemas.Web @using Iceshrimp.Shared.Schemas.Web
@inject ApiService Api @inject ApiService Api
@inject SessionService SessionService @inject SessionService SessionService
@inject NavigationManager Navigation @inject NavigationManager Navigation
@layout LoginLayout @layout LoginLayout
<h3>Login</h3> <h3>Login</h3>
<div> <div>

View file

@ -1,11 +1,11 @@
@page "/notifications" @page "/notifications"
@attribute [Authorize] @using Iceshrimp.Assets.PhosphorIcons
@using Iceshrimp.Frontend.Components @using Iceshrimp.Frontend.Components
@using Iceshrimp.Frontend.Localization @using Iceshrimp.Frontend.Localization
@using Microsoft.Extensions.Localization
@using Microsoft.AspNetCore.Components.Sections
@using Iceshrimp.Assets.PhosphorIcons
@using Microsoft.AspNetCore.Authorization @using Microsoft.AspNetCore.Authorization
@using Microsoft.AspNetCore.Components.Sections
@using Microsoft.Extensions.Localization
@attribute [Authorize]
@inject IStringLocalizer<Localization> Loc; @inject IStringLocalizer<Localization> Loc;

View file

@ -1,6 +1,4 @@
@page "/{User}" @page "/{User}"
@attribute [Authorize]
@using System.Text.RegularExpressions @using System.Text.RegularExpressions
@using Iceshrimp.Frontend.Components @using Iceshrimp.Frontend.Components
@using Iceshrimp.Frontend.Components.Note @using Iceshrimp.Frontend.Components.Note
@ -8,7 +6,8 @@
@using Iceshrimp.Frontend.Core.Services @using Iceshrimp.Frontend.Core.Services
@using Iceshrimp.Shared.Schemas.Web @using Iceshrimp.Shared.Schemas.Web
@using Microsoft.AspNetCore.Authorization @using Microsoft.AspNetCore.Authorization
@inject ApiService Api @attribute [Authorize]
@inject ApiService Api
@if (_init) @if (_init)
{ {

View file

@ -1,11 +1,11 @@
@page "/search" @page "/search"
@attribute [Authorize] @using Iceshrimp.Assets.PhosphorIcons
@using Iceshrimp.Frontend.Components @using Iceshrimp.Frontend.Components
@using Iceshrimp.Frontend.Localization @using Iceshrimp.Frontend.Localization
@using Microsoft.Extensions.Localization
@using Microsoft.AspNetCore.Components.Sections
@using Iceshrimp.Assets.PhosphorIcons
@using Microsoft.AspNetCore.Authorization @using Microsoft.AspNetCore.Authorization
@using Microsoft.AspNetCore.Components.Sections
@using Microsoft.Extensions.Localization
@attribute [Authorize]
@inject IStringLocalizer<Localization> Loc; @inject IStringLocalizer<Localization> Loc;
@ -14,7 +14,7 @@
@Loc["Search"] @Loc["Search"]
</SectionContent> </SectionContent>
<SearchComponent /> <SearchComponent/>
@code {
@code {
} }

View file

@ -1,16 +1,16 @@
@page "/settings/about" @page "/settings/about"
@attribute [Authorize]
@using System.Text @using System.Text
@using Iceshrimp.Frontend.Core.InMemoryLogger @using Iceshrimp.Frontend.Core.InMemoryLogger
@using Iceshrimp.Frontend.Core.Services; @using Iceshrimp.Frontend.Core.Services
@using Iceshrimp.Frontend.Localization; @using Iceshrimp.Frontend.Localization
@using Microsoft.AspNetCore.Authorization @using Microsoft.AspNetCore.Authorization
@using Microsoft.Extensions.Localization; @using Microsoft.Extensions.Localization
@attribute [Authorize]
@layout SettingsLayout; @layout SettingsLayout;
@inject VersionService Version; @inject VersionService Version;
@inject IStringLocalizer<Localization> Loc; @inject IStringLocalizer<Localization> Loc;
@inject IJSRuntime Js; @inject IJSRuntime Js;
@inject InMemoryLogService LogService; @inject InMemoryLogService LogService;
<div class="version"> <div class="version">
<h1>@Loc["Version Information"]</h1> <h1>@Loc["Version Information"]</h1>
@ -36,17 +36,16 @@
</div> </div>
<div class="logs"> <div class="logs">
<h1>@Loc["Logs"]</h1> <h1>@Loc["Logs"]</h1>
@Loc["These logs may contain sensitive information, please do not post them publicly.\n" @Loc["These logs may contain sensitive information, please do not post them publicly.\n" + "Providing them to developers upon request may help with debugging."]
+ "Providing them to developers upon request may help with debugging."]
<button class="btn" @onclick="DownloadLogs">@Loc["Download Logs"]</button> <button class="btn" @onclick="DownloadLogs">@Loc["Download Logs"]</button>
</div> </div>
@code { @code {
private IJSInProcessObjectReference _module = null!; private IJSInProcessObjectReference _module = null!;
protected override async Task OnInitializedAsync() protected override async Task OnInitializedAsync()
{ {
_module = (IJSInProcessObjectReference) await Js.InvokeAsync<IJSObjectReference>("import", "./Components/ErrorUi.razor.js"); _module = (IJSInProcessObjectReference)await Js.InvokeAsync<IJSObjectReference>("import", "./Components/ErrorUi.razor.js");
} }
private void DownloadLogs() private void DownloadLogs()
@ -54,5 +53,4 @@
var logBytes = LogService.GetLogs().SelectMany(p => Encoding.UTF8.GetBytes(p)).ToArray(); var logBytes = LogService.GetLogs().SelectMany(p => Encoding.UTF8.GetBytes(p)).ToArray();
_module.InvokeVoid("DownloadFile", "log.txt", "text/plain", logBytes); _module.InvokeVoid("DownloadFile", "log.txt", "text/plain", logBytes);
} }
} }

View file

@ -1,14 +1,14 @@
@page "/settings/filters" @page "/settings/filters"
@attribute [Authorize] @using Iceshrimp.Assets.PhosphorIcons
@using Iceshrimp.Frontend.Core.Miscellaneous
@using Iceshrimp.Frontend.Components @using Iceshrimp.Frontend.Components
@using Iceshrimp.Frontend.Core.Miscellaneous
@using Iceshrimp.Frontend.Core.Services @using Iceshrimp.Frontend.Core.Services
@using Iceshrimp.Frontend.Localization @using Iceshrimp.Frontend.Localization
@using Iceshrimp.Shared.Schemas.Web @using Iceshrimp.Shared.Schemas.Web
@using Microsoft.Extensions.Localization
@using Iceshrimp.Assets.PhosphorIcons
@using Microsoft.AspNetCore.Authorization @using Microsoft.AspNetCore.Authorization
@using Microsoft.AspNetCore.Components.Sections @using Microsoft.AspNetCore.Components.Sections
@using Microsoft.Extensions.Localization
@attribute [Authorize]
@layout SettingsLayout @layout SettingsLayout
@inject ApiService Api; @inject ApiService Api;
@inject IStringLocalizer<Localization> Loc; @inject IStringLocalizer<Localization> Loc;
@ -177,7 +177,7 @@
private State State { get; set; } = State.Loading; private State State { get; set; } = State.Loading;
private string FilterName { get; set; } = ""; private string FilterName { get; set; } = "";
private string Keyword { get; set; } = ""; private string Keyword { get; set; } = "";
private List<String> FilterKeywords { get; } = []; private List<string> FilterKeywords { get; } = [];
private DateTime? FilterExpiry { get; set; } private DateTime? FilterExpiry { get; set; }
private FilterResponse.FilterAction FilterAction { get; set; } private FilterResponse.FilterAction FilterAction { get; set; }
private Menu MenuFilterAction { get; set; } = null!; private Menu MenuFilterAction { get; set; } = null!;
@ -217,7 +217,7 @@
private async Task TryAddFilter() private async Task TryAddFilter()
{ {
bool valid = true; var valid = true;
if (FilterName.Length < 1) if (FilterName.Length < 1)
{ {

View file

@ -1,14 +1,13 @@
@page "/settings/profile" @page "/settings/profile"
@attribute [Authorize] @using Iceshrimp.Assets.PhosphorIcons
@using Iceshrimp.Frontend.Components
@using Iceshrimp.Frontend.Core.Miscellaneous @using Iceshrimp.Frontend.Core.Miscellaneous
@using Iceshrimp.Frontend.Core.Services @using Iceshrimp.Frontend.Core.Services
@using Iceshrimp.Frontend.Localization @using Iceshrimp.Frontend.Localization
@using Iceshrimp.Shared.Schemas.Web @using Iceshrimp.Shared.Schemas.Web
@using Microsoft.Extensions.Localization
@using Iceshrimp.Assets.PhosphorIcons
@using Iceshrimp.Frontend.Components
@using Microsoft.AspNetCore.Authorization @using Microsoft.AspNetCore.Authorization
@using Microsoft.Extensions.Logging @using Microsoft.Extensions.Localization
@attribute [Authorize]
@layout SettingsLayout @layout SettingsLayout
@inject ApiService Api; @inject ApiService Api;
@inject ILogger<Profile> Logger; @inject ILogger<Profile> Logger;
@ -50,10 +49,18 @@
<div class="section"> <div class="section">
<StateButton OnClick="SaveChanges" @ref="SaveButton"> <StateButton OnClick="SaveChanges" @ref="SaveButton">
<Initial><Icon Name="Icons.FloppyDisk"/>@Loc["Save"]</Initial> <Initial>
<Loading><Icon Name="Icons.Spinner"/></Loading> <Icon Name="Icons.FloppyDisk"/>@Loc["Save"]
<Failed><Icon Name="Icons.X" />@Loc["Error"]</Failed> </Initial>
<Success><Icon Name="Icons.Check"/>@Loc["Saved"]</Success> <Loading>
<Icon Name="Icons.Spinner"/>
</Loading>
<Failed>
<Icon Name="Icons.X"/>@Loc["Error"]
</Failed>
<Success>
<Icon Name="Icons.Check"/>@Loc["Saved"]
</Success>
</StateButton> </StateButton>
</div> </div>
} }

View file

@ -1,11 +1,11 @@
@page "/Settings" @page "/Settings"
@attribute [Authorize]
@using Iceshrimp.Frontend.Components
@using Microsoft.AspNetCore.Components.Sections
@using Iceshrimp.Assets.PhosphorIcons @using Iceshrimp.Assets.PhosphorIcons
@using Iceshrimp.Frontend.Components
@using Iceshrimp.Frontend.Localization @using Iceshrimp.Frontend.Localization
@using Microsoft.AspNetCore.Authorization @using Microsoft.AspNetCore.Authorization
@using Microsoft.AspNetCore.Components.Sections
@using Microsoft.Extensions.Localization @using Microsoft.Extensions.Localization
@attribute [Authorize]
@* @inject NavigationManager Nav; *@ @* @inject NavigationManager Nav; *@
@inject IStringLocalizer<Localization> Loc; @inject IStringLocalizer<Localization> Loc;

View file

@ -1,5 +1,14 @@
@page "/notes/{NoteId}" @page "/notes/{NoteId}"
@attribute [Authorize] @attribute [Authorize]
@inject ApiService ApiService
@inject IJSRuntime Js
@inject MessageService MessageService
@inject StateService State
@inject NavigationManager Navigation
@inject IStringLocalizer<Localization> Loc
@inject ILogger<SingleNote> Logger;
@using Iceshrimp.Assets.PhosphorIcons
@using Iceshrimp.Frontend.Components @using Iceshrimp.Frontend.Components
@using Iceshrimp.Frontend.Components.Note @using Iceshrimp.Frontend.Components.Note
@using Iceshrimp.Frontend.Core.Miscellaneous @using Iceshrimp.Frontend.Core.Miscellaneous
@ -7,18 +16,9 @@
@using Iceshrimp.Frontend.Core.Services.StateServicePatterns @using Iceshrimp.Frontend.Core.Services.StateServicePatterns
@using Iceshrimp.Frontend.Localization @using Iceshrimp.Frontend.Localization
@using Iceshrimp.Shared.Schemas.Web @using Iceshrimp.Shared.Schemas.Web
@using Microsoft.Extensions.Localization
@using Microsoft.AspNetCore.Components.Sections
@using Iceshrimp.Assets.PhosphorIcons
@using Microsoft.AspNetCore.Authorization @using Microsoft.AspNetCore.Authorization
@inject ApiService ApiService @using Microsoft.AspNetCore.Components.Sections
@inject IJSRuntime Js @using Microsoft.Extensions.Localization
@inject MessageService MessageService
@inject StateService State
@inject NavigationManager Navigation
@inject IStringLocalizer<Localization> Loc
@inject ILogger<SingleNote> Logger;
@implements IDisposable @implements IDisposable
@if (_componentState == LoadState.Init) @if (_componentState == LoadState.Init)
@ -114,11 +114,13 @@
_componentState = LoadState.Error; _componentState = LoadState.Error;
return; return;
} }
if (RootNote == null) if (RootNote == null)
{ {
_componentState = LoadState.Error; _componentState = LoadState.Error;
return; return;
} }
_componentState = LoadState.Init; _componentState = LoadState.Init;
StateHasChanged(); StateHasChanged();

View file

@ -1,11 +1,11 @@
@page "/" @page "/"
@attribute [Authorize]
@using Iceshrimp.Frontend.Components
@using Microsoft.AspNetCore.Components.Sections
@using Iceshrimp.Assets.PhosphorIcons @using Iceshrimp.Assets.PhosphorIcons
@using Iceshrimp.Frontend.Components
@using Iceshrimp.Frontend.Localization @using Iceshrimp.Frontend.Localization
@using Microsoft.AspNetCore.Authorization @using Microsoft.AspNetCore.Authorization
@using Microsoft.AspNetCore.Components.Sections
@using Microsoft.Extensions.Localization @using Microsoft.Extensions.Localization
@attribute [Authorize]
@inject IStringLocalizer<Localization> Loc; @inject IStringLocalizer<Localization> Loc;
<SectionContent SectionName="top-bar"> <SectionContent SectionName="top-bar">

View file

@ -40,7 +40,7 @@ CultureInfo.DefaultThreadCurrentCulture = culture;
CultureInfo.DefaultThreadCurrentUICulture = culture; CultureInfo.DefaultThreadCurrentUICulture = culture;
var logger = host.Services.GetRequiredService<ILoggerFactory>() var logger = host.Services.GetRequiredService<ILoggerFactory>()
.CreateLogger<Program>(); .CreateLogger<Program>();
logger.LogInformation("Starting Iceshrimp.Frontend"); logger.LogInformation("Starting Iceshrimp.Frontend");
await host.RunAsync(); await host.RunAsync();

View file

@ -4,9 +4,7 @@ open System
open FParsec open FParsec
module SearchQueryFilters = module SearchQueryFilters =
type Filter() = type Filter() = class end
class
end
type WordFilter(neg: bool, value: string) = type WordFilter(neg: bool, value: string) =
inherit Filter() inherit Filter()

View file

@ -9,7 +9,7 @@ public class UserProfileEntity
[MaxLength(128)] public string? Location { get; set; } [MaxLength(128)] public string? Location { get; set; }
/// <remarks> /// <remarks>
/// Accepts YYYY-MM-DD format, empty string, or null. /// Accepts YYYY-MM-DD format, empty string, or null.
/// </remarks> /// </remarks>
public string? Birthday { get; set; } public string? Birthday { get; set; }

View file

@ -268,7 +268,7 @@ public class MfmTests
List<MfmNode> expected = List<MfmNode> expected =
[ [
new MfmTextNode("this is plain text > this is not a quote >this is also not a quote\n"), new MfmTextNode("this is plain text > this is not a quote >this is also not a quote\n"),
new MfmQuoteNode(ListModule.OfSeq<MfmInlineNode>([new MfmTextNode("this is a quote\nthis is part of the same quote\nthis too"),]), false, false), new MfmQuoteNode(ListModule.OfSeq<MfmInlineNode>([new MfmTextNode("this is a quote\nthis is part of the same quote\nthis too")]), false, false),
new MfmTextNode("this is some plain text inbetween\n"), new MfmTextNode("this is some plain text inbetween\n"),
new MfmQuoteNode(ListModule.OfSeq<MfmInlineNode>([new MfmTextNode("this is a second quote\nthis is part of the second quote")]), true, false), new MfmQuoteNode(ListModule.OfSeq<MfmInlineNode>([new MfmTextNode("this is a second quote\nthis is part of the second quote")]), true, false),
new MfmQuoteNode(ListModule.OfSeq<MfmInlineNode>([new MfmTextNode("this is a third quote")]), false, false), new MfmQuoteNode(ListModule.OfSeq<MfmInlineNode>([new MfmTextNode("this is a third quote")]), false, false),