Iceshrimp.NET/Iceshrimp.Frontend/Pages/ProfileView.razor

444 lines
17 KiB
Text

@page "/{User}"
@using System.Text.RegularExpressions
@using Iceshrimp.Frontend.Components
@using Iceshrimp.Frontend.Components.Note
@using Iceshrimp.Frontend.Core.Miscellaneous
@using Iceshrimp.Frontend.Core.Services
@using Iceshrimp.Shared.Schemas.Web
@using Microsoft.AspNetCore.Authorization
@using Iceshrimp.Assets.PhosphorIcons
@using Iceshrimp.Frontend.Localization
@using Microsoft.Extensions.Localization
@attribute [Authorize]
@inject ApiService Api
@inject GlobalComponentSvc GlobalComponentSvc;
@inject IJSRuntime Js;
@inject IStringLocalizer<Localization> Loc;
@inject MetadataService MetadataService
@inject NavigationManager Nav;
@if (_init)
{
<HeadTitle Text="@(UserResponse.DisplayName ?? UserResponse.Username)"/>
<div class="container">
@if (UserResponse.Host != null)
{
<RemoteUserBanner OriginalPageUrl="@Profile?.Url"/>
}
@if (UserResponse.BannerUrl != null)
{
<img class="banner" src="@UserResponse.BannerUrl"
alt="@(UserResponse.BannerAlt ?? Loc["{0}'s banner", UserResponse.DisplayName ?? UserResponse.Username])"
title="@(UserResponse.BannerAlt ?? Loc["{0}'s banner", UserResponse.DisplayName ?? UserResponse.Username])"/>
}
<div class="profile-card">
<div class="header">
<div class="subheader">
<div>
<UserAvatar User="@UserResponse" Size="5em" Title="@true"/>
</div>
<div class="name-section">
<div class="name">
<UserDisplayName User="@UserResponse"/>
</div>
<div class="identifier">
@@@UserResponse.Username
@if (UserResponse.Host != null)
{
var host = $"@{UserResponse.Host}";
@host
}
@if (Profile != null && Profile.Relations.HasFlag(Relations.FollowedBy))
{
<span class="follows-badge">Follows you</span>
}
</div>
</div>
</div>
<FollowButton User="UserResponse" UserProfile="Profile"/>
<button @ref="MenuButton" class="context-button" @onclick="ToggleMenu" @onclick:stopPropagation="true" aria-label="more">
<Icon Name="Icons.DotsThreeOutline" Size="1.3em"/>
<Menu @ref="ContextMenu">
@if (Profile != null && Profile.Relations.HasFlag(Relations.Self))
{
<MenuElement Icon="Icons.Pencil" OnSelect="EditProfile">
<Text>@Loc["Edit profile"]</Text>
</MenuElement>
}
else
{
<MenuElement Icon="Icons.Tooth" OnSelect="Bite">
<Text>@Loc["Bite"]</Text>
</MenuElement>
}
@if (UserResponse.Host != null)
{
<MenuElement Icon="Icons.ArrowsClockwise" OnSelect="RefetchUser">
<Text>@Loc["Refetch"]</Text>
</MenuElement>
}
<hr class="rule"/>
@if (UserResponse.Host != null && Profile?.Url != null)
{
<MenuElement Icon="Icons.ArrowSquareOut" OnSelect="OpenOriginal">
<Text>@Loc["Open original page"]</Text>
</MenuElement>
}
<MenuElement Icon="Icons.Share" OnSelect="CopyLink">
<Text>@Loc["Copy link"]</Text>
</MenuElement>
@if (UserResponse.Host != null && Profile?.Url != null)
{
<MenuElement Icon="Icons.ShareNetwork" OnSelect="CopyLinkRemote">
<Text>@Loc["Copy link (remote)"]</Text>
</MenuElement>
}
<MenuElement Icon="Icons.At" OnSelect="CopyUsername">
<Text>@Loc["Copy username"]</Text>
</MenuElement>
@if (Profile != null && !Profile.Relations.HasFlag(Relations.Self))
{
<hr class="rule"/>
@if (Profile != null && Profile.Relations.HasFlag(Relations.Muting))
{
<MenuElement Icon="Icons.Eye" OnSelect="Unmute">
<Text>@Loc["Unmute"]</Text>
</MenuElement>
}
else
{
<MenuElement Icon="Icons.EyeSlash" OnSelect="Mute">
<Text>@Loc["Mute"]</Text>
</MenuElement>
<MenuElement Icon="Icons.Timer" OnSelect="TemporaryMute">
<Text>@Loc["Temporary mute"]</Text>
</MenuElement>
}
@if (Profile != null && Profile.Relations.HasFlag(Relations.FollowedBy))
{
<MenuElement Icon="Icons.LinkBreak" OnSelect="RemoveFromFollowers" Danger>
<Text>@Loc["Remove follower"]</Text>
</MenuElement>
}
@if (Profile != null && Profile.Relations.HasFlag(Relations.Blocking))
{
<MenuElement Icon="Icons.Prohibit" OnSelect="Unblock" Danger>
<Text>@Loc["Unblock"]</Text>
</MenuElement>
}
else
{
<MenuElement Icon="Icons.Prohibit" OnSelect="Block" Danger>
<Text>@Loc["Block"]</Text>
</MenuElement>
}
}
<ClosingBackdrop OnClose="ContextMenu.Close"></ClosingBackdrop>
</Menu>
</button>
</div>
<ProfileInfo Emojis="@UserResponse.Emojis" User="UserResponse" UserProfile="Profile"/>
</div>
@if (UserNotes.Count > 0)
{
<div class="notes">
@foreach (var note in UserNotes)
{
<div class="note-container">
<Note NoteResponse="note" OpenNote="true"/>
</div>
}
</div>
<ScrollEnd IntersectionChange="AddNotes" ManualLoad="AddNotes" Class="end"/>
}
</div>
}
@if (_loading)
{
<HeadTitle Text="@Loc["Loading"]"/>
<div class="loading"><LoadingSpinner Scale="2" /></div>
}
@if (_notFound)
{
<HeadTitle Text="@Loc["User not found"]"/>
<div>User does not exist!</div>
}
@if (_error)
{
<HeadTitle Text="@Loc["Error"]"/>
<div>Failure</div>
}
@code {
[Parameter] public string? User { get; set; }
[Parameter] public string? Host { get; set; }
private UserResponse UserResponse { get; set; } = null!;
private UserProfileResponse? Profile { get; set; }
private string? MinId { get; set; }
private List<NoteResponse> UserNotes { get; set; } = [];
private ElementReference MenuButton { get; set; }
private Menu ContextMenu { get; set; } = null!;
private bool _loading = true;
private bool _init;
private bool _notFound;
private bool _error;
private bool _fetchLock;
private async Task GetNotes(string? minId)
{
var pq = new PaginationQuery { Limit = 10, MaxId = minId };
var notes = await Api.Users.GetUserNotesAsync(UserResponse.Id, pq);
if (notes is not null && notes.Count > 0)
{
MinId = notes.Last().Id;
UserNotes.AddRange(notes);
StateHasChanged();
}
}
private async Task AddNotes()
{
if (_fetchLock == false)
{
_fetchLock = true;
await GetNotes(MinId);
_fetchLock = false;
}
}
private async Task LoadProfile()
{
try
{
if (User is null)
{
_notFound = true;
}
else
{
var pattern = "^@([^@]+)@?(.+)?$";
var matches = Regex.Match(User, pattern);
var userResponse = await Api.Users.LookupUserAsync(matches.Groups[1].Value, matches.Groups[2].Value);
if (userResponse is null)
{
_notFound = true;
}
else
{
UserResponse = userResponse;
Profile = await Api.Users.GetUserProfileAsync(UserResponse.Id);
await GetNotes(null);
_init = true;
_loading = false;
}
}
}
catch (ApiException)
{
_loading = false;
_error = true;
}
}
protected override async Task OnInitializedAsync()
{
await LoadProfile();
}
protected override async Task OnParametersSetAsync()
{
_init = false;
_loading = true;
UserNotes = [];
MinId = null;
Profile = null;
StateHasChanged();
await LoadProfile();
}
private void ToggleMenu() => ContextMenu.Toggle(MenuButton);
private void EditProfile() => Nav.NavigateTo("/settings/profile");
private void Bite() => Api.Users.BiteUserAsync(UserResponse.Id);
private async Task RemoveFromFollowersAction(bool confirm)
{
if (!confirm) return;
var removed = await Api.Users.RemoveUserFromFollowersAsync(UserResponse.Id);
if (!removed) return;
await GlobalComponentSvc.NoticeDialog?.Display(Loc["Removed {0} from followers", UserResponse.DisplayName ?? UserResponse.Username])!;
Profile!.Relations &= ~Relations.FollowedBy;
StateHasChanged();
}
private void RemoveFromFollowers() =>
GlobalComponentSvc.ConfirmDialog?.Confirm(new EventCallback<bool>(this, RemoveFromFollowersAction), Loc["Remove {0} from followers?", UserResponse.DisplayName ?? UserResponse.Username], buttonText: Loc["Remove follower"]);
private void Mute() =>
GlobalComponentSvc.ConfirmDialog?.Confirm(new EventCallback<bool>(this, MuteCallback), Loc["Mute {0}?", UserResponse.DisplayName ?? UserResponse.Username], buttonText: Loc["Mute"]);
private async Task MuteCallback(bool confirm)
{
if (!confirm) return;
try
{
await Api.Users.MuteUserAsync(UserResponse.Id, null);
await GlobalComponentSvc.NoticeDialog?.Display(Loc["Muted {0}", UserResponse.DisplayName ?? UserResponse.Username])!;
Profile!.Relations += (int)Relations.Muting;
StateHasChanged();
}
catch (ApiException e)
{
await GlobalComponentSvc.NoticeDialog?.Display(e.Response.Message ?? Loc["An unknown error occurred"], NoticeDialog.NoticeType.Error)!;
}
}
private void TemporaryMute() =>
GlobalComponentSvc.SelectDialog?.Select(new EventCallback<object?>(this, TemporaryMuteCallback), Loc["Mute {0}?", UserResponse.DisplayName ?? UserResponse.Username], [(Loc["For 1 hour"], (object)DateTime.Now.AddHours(1)), (Loc["For {0} hours", 3], (object)DateTime.Now.AddHours(3)), (Loc["For {0} hours", 8], (object)DateTime.Now.AddHours(8)), (Loc["For 1 day"], (object)DateTime.Now.AddDays(1)), (Loc["For 1 week"], (object)DateTime.Now.AddDays(7))], Loc["Mute"]);
private async Task TemporaryMuteCallback(object? value)
{
if (value is not DateTime expires) return;
try
{
await Api.Users.MuteUserAsync(UserResponse.Id, expires);
await GlobalComponentSvc.NoticeDialog?.Display(Loc["Muted {0}", UserResponse.DisplayName ?? UserResponse.Username])!;
Profile!.Relations += (int)Relations.Muting;
StateHasChanged();
}
catch (ApiException e)
{
await GlobalComponentSvc.NoticeDialog?.Display(e.Response.Message ?? Loc["An unknown error occurred"], NoticeDialog.NoticeType.Error)!;
}
}
private void Unmute() =>
GlobalComponentSvc.ConfirmDialog?.Confirm(new EventCallback<bool>(this, UnmuteCallback), Loc["Unmute {0}?", UserResponse.DisplayName ?? UserResponse.Username], buttonText: Loc["Unmute"]);
private async Task UnmuteCallback(bool confirm)
{
if (!confirm) return;
try
{
await Api.Users.UnmuteUserAsync(UserResponse.Id);
await GlobalComponentSvc.NoticeDialog?.Display(Loc["Unmuted {0}", UserResponse.DisplayName ?? UserResponse.Username])!;
Profile!.Relations -= (int)Relations.Muting;
StateHasChanged();
}
catch (ApiException e)
{
await GlobalComponentSvc.NoticeDialog?.Display(e.Response.Message ?? Loc["An unknown error occurred"], NoticeDialog.NoticeType.Error)!;
}
}
private void Block() =>
GlobalComponentSvc.ConfirmDialog?.Confirm(new EventCallback<bool>(this, BlockCallback), Loc["Block {0}?", UserResponse.DisplayName ?? UserResponse.Username], buttonText: Loc["Block"]);
private async Task BlockCallback(bool confirm)
{
if (!confirm) return;
try
{
await Api.Users.BlockUserAsync(UserResponse.Id);
await GlobalComponentSvc.NoticeDialog?.Display(Loc["Blocked {0}", UserResponse.DisplayName ?? UserResponse.Username])!;
Profile!.Relations += (int)Relations.Blocking;
StateHasChanged();
}
catch (ApiException e)
{
await GlobalComponentSvc.NoticeDialog?.Display(e.Response.Message ?? Loc["An unknown error occurred"], NoticeDialog.NoticeType.Error)!;
}
}
private void Unblock() =>
GlobalComponentSvc.ConfirmDialog?.Confirm(new EventCallback<bool>(this, UnblockCallback), Loc["Unblock {0}?", UserResponse.DisplayName ?? UserResponse.Username], buttonText: Loc["Unblock"]);
private async Task UnblockCallback(bool confirm)
{
if (!confirm) return;
try
{
await Api.Users.UnblockUserAsync(UserResponse.Id);
await GlobalComponentSvc.NoticeDialog?.Display(Loc["Unblocked {0}", UserResponse.DisplayName ?? UserResponse.Username])!;
Profile!.Relations -= (int)Relations.Blocking;
StateHasChanged();
}
catch (ApiException e)
{
await GlobalComponentSvc.NoticeDialog?.Display(e.Response.Message ?? Loc["An unknown error occurred"], NoticeDialog.NoticeType.Error)!;
}
}
private void OpenOriginal()
{
if (Profile?.Url != null)
Js.InvokeVoidAsync("open", Profile.Url, "_blank");
}
private async Task CopyLink()
{
var instance = await MetadataService.Instance.Value;
await Js.InvokeVoidAsync("navigator.clipboard.writeText", $"https://{instance.WebDomain}/@{UserResponse.Username}" + (UserResponse.Host != null ? $"@{UserResponse.Host}" : ""));
}
private void CopyLinkRemote()
{
if (Profile?.Url != null)
Js.InvokeVoidAsync("navigator.clipboard.writeText", Profile.Url);
}
private void CopyUsername() => Js.InvokeVoidAsync("navigator.clipboard.writeText", $"@{UserResponse.Username}" + (UserResponse.Host != null ? $"@{UserResponse.Host}" : ""));
private async Task RefetchUser() =>
await GlobalComponentSvc.ConfirmDialog?.Confirm(new EventCallback<bool>(this, RefetchUserCallback), Loc["Refetch {0}'s profile?", UserResponse.DisplayName ?? UserResponse.Username])!;
private async Task RefetchUserCallback(bool refetch)
{
if (!refetch) return;
try
{
var res = await Api.Users.RefetchUserAsync(UserResponse.Id);
if (res != null)
{
UserResponse = res;
var newProfile = await Api.Users.GetUserProfileAsync(UserResponse.Id);
if (newProfile != null)
Profile = newProfile;
StateHasChanged();
await GlobalComponentSvc.NoticeDialog?.Display(Loc["Successfully refetched {0}'s profile", UserResponse.DisplayName ?? UserResponse.Username])!;
}
}
catch (ApiException e)
{
await GlobalComponentSvc.NoticeDialog?.Display(e.Response.Message ?? Loc["An unknown error occurred"], NoticeDialog.NoticeType.Error)!;
}
}
}