Auth, pics, ephemeral and more, oh my!

This commit is contained in:
Gordon Pedersen 2024-06-05 22:41:08 +10:00
parent 4fa8440e1f
commit a39eeff757
27 changed files with 2127 additions and 95 deletions

View file

@ -7,5 +7,6 @@ public partial class AppShell : Shell
InitializeComponent(); InitializeComponent();
Routing.RegisterRoute(nameof(LoginWebViewPage), typeof(LoginWebViewPage)); Routing.RegisterRoute(nameof(LoginWebViewPage), typeof(LoginWebViewPage));
Routing.RegisterRoute(nameof(EphemeralWebPage), typeof(EphemeralWebPage));
} }
} }

View file

@ -2,7 +2,6 @@
@inject NavigatorService NavigatorService @inject NavigatorService NavigatorService
@inject NavigationManager NavigationManager @inject NavigationManager NavigationManager
@inject State State @inject State State
<link rel="stylesheet" href="vendor/fluent-emoji/animated.css" />
<NavMenu /> <NavMenu />
<main class="responsive max"> <main class="responsive max">
@Body @Body

View file

@ -6,11 +6,11 @@
<AuthorizeView> <AuthorizeView>
<Authorized> <Authorized>
<button class="transparent circle large"> <button class="transparent circle large">
<img class="responsive" src="https://profiles.cache.lol/@State.SelectedAddressName/picture" alt="@State.SelectedAddressName"> <img class="responsive avatar" src="https://profiles.cache.lol/@State.SelectedAddressName/picture" alt="@State.SelectedAddressName">
<menu class="no-wrap"> <menu class="no-wrap">
@foreach (AddressResponseData address in State.AddressList ?? new List<AddressResponseData>()) { @foreach (AddressResponseData address in State.AddressList ?? new List<AddressResponseData>()) {
<a class="row @(address == State.SelectedAddress ? "active" : "")" @onclick="() => changeAddress(address)"> <a class="row @(address == State.SelectedAddress ? "active" : "")" @onclick="() => changeAddress(address)">
<img class="tiny circle" src="https://profiles.cache.lol/@address.Address/picture" alt="@address.Address" /> <img class="tiny circle avatar" src="https://profiles.cache.lol/@address.Address/picture" alt="@address.Address" />
<span class="address"><i class="fa-solid fa-fw fa-at tiny"></i>@address.Address</span> <span class="address"><i class="fa-solid fa-fw fa-at tiny"></i>@address.Address</span>
</a> </a>
} }
@ -22,7 +22,7 @@
</button> </button>
<div> <div>
Hey, @State.Name. <br /> Hey, @State.Name. <br />
<span class="address"><i class="fa-solid fa-fw fa-at tiny"></i>@State.SelectedAddressName</span> <a class="address" href="/person/@State.SelectedAddressName"><i class="fa-solid fa-fw fa-at tiny"></i>@State.SelectedAddressName</a>
</div> </div>
</Authorized> </Authorized>
<NotAuthorized> <NotAuthorized>
@ -43,13 +43,17 @@
</AuthorizeView> </AuthorizeView>
</nav> </nav>
</header> </header>
<NavLink class="row nav-link" href="" Match="NavLinkMatch.All">
<i class="fa-solid fa-fw fa-home"></i>
<div>Home</div>
</NavLink>
<NavLink class="row nav-link" href="/statuslog/latest"> <NavLink class="row nav-link" href="/statuslog/latest">
<i><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 500 500"><path fill="#4dabf7" d="M250 450c-38.388 0-76.775-14.646-106.066-43.934l-100-100c-58.579-58.58-58.579-153.553 0-212.132C100.534 37.336 191.105 35.421 250 88.191c58.898-52.768 149.47-50.853 206.066 5.743 58.58 58.58 58.58 153.553 0 212.132l-100 100C326.778 435.354 288.39 450 250 450" /><path fill="#228be6" d="M220.52 176.634a11.792 11.792 0 1 1-23.586 0 11.792 11.792 0 0 1 23.585 0" /><path fill="#1864ab" stroke="#1864ab" stroke-miterlimit="10" stroke-width="11.32074" d="M220.52 176.634a11.792 11.792 0 1 1-23.586 0 11.792 11.792 0 0 1 23.585 0Z" /><path fill="#228be6" d="M303.066 176.634a11.792 11.792 0 1 1-23.585 0 11.792 11.792 0 0 1 23.585 0" /><path fill="#1864ab" stroke="#1864ab" stroke-miterlimit="10" stroke-width="11.32074" d="M303.066 176.634a11.792 11.792 0 1 1-23.585 0 11.792 11.792 0 0 1 23.585 0Z" /><path fill="#228be6" stroke="#461036" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="18.8679" d="M208.75 223.817c18.875 30.802 63.626 30.802 82.501 0" /><path fill="#1971c2" d="M438.68 212.011c0-32.564-26.399-58.962-58.963-58.962s-58.962 26.398-58.962 58.962 26.398 58.963 58.962 58.963 58.962-26.399 58.962-58.963m-259.433 0c0-32.564-26.398-58.962-58.962-58.962S61.32 179.447 61.32 212.011s26.398 58.963 58.963 58.963 58.962-26.399 58.962-58.963" /><path fill="#4dabf7" stroke="#1864ab" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="18.8679" d="M208.75 223.817c18.875 30.802 63.626 30.802 82.501 0" /></svg></i> <i class="fa-solid fa-message-smile"></i>
<div>Statuslog</div> <div>Status.lol</div>
</NavLink>
<NavLink class="row nav-link" href="/pics">
<i class="fa-solid fa-images"></i>
<div>Some.pics</div>
</NavLink>
<NavLink class="row nav-link" href="/ephemeral">
<i class="fa-light fa-comment-dots"></i>
<div>Eph.emer.al</div>
</NavLink> </NavLink>
</nav> </nav>
@ -94,24 +98,32 @@
</NotAuthorized> </NotAuthorized>
</AuthorizeView> </AuthorizeView>
</header> </header>
<NavLink class="nav-link" href="" Match="NavLinkMatch.All">
<i class="fa-solid fa-fw fa-home"></i>
<small>Home</small>
</NavLink>
<NavLink class="nav-link" href="/statuslog/latest"> <NavLink class="nav-link" href="/statuslog/latest">
<i><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 500 500"><path fill="#4dabf7" d="M250 450c-38.388 0-76.775-14.646-106.066-43.934l-100-100c-58.579-58.58-58.579-153.553 0-212.132C100.534 37.336 191.105 35.421 250 88.191c58.898-52.768 149.47-50.853 206.066 5.743 58.58 58.58 58.58 153.553 0 212.132l-100 100C326.778 435.354 288.39 450 250 450" /><path fill="#228be6" d="M220.52 176.634a11.792 11.792 0 1 1-23.586 0 11.792 11.792 0 0 1 23.585 0" /><path fill="#1864ab" stroke="#1864ab" stroke-miterlimit="10" stroke-width="11.32074" d="M220.52 176.634a11.792 11.792 0 1 1-23.586 0 11.792 11.792 0 0 1 23.585 0Z" /><path fill="#228be6" d="M303.066 176.634a11.792 11.792 0 1 1-23.585 0 11.792 11.792 0 0 1 23.585 0" /><path fill="#1864ab" stroke="#1864ab" stroke-miterlimit="10" stroke-width="11.32074" d="M303.066 176.634a11.792 11.792 0 1 1-23.585 0 11.792 11.792 0 0 1 23.585 0Z" /><path fill="#228be6" stroke="#461036" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="18.8679" d="M208.75 223.817c18.875 30.802 63.626 30.802 82.501 0" /><path fill="#1971c2" d="M438.68 212.011c0-32.564-26.399-58.962-58.963-58.962s-58.962 26.398-58.962 58.962 26.398 58.963 58.962 58.963 58.962-26.399 58.962-58.963m-259.433 0c0-32.564-26.398-58.962-58.962-58.962S61.32 179.447 61.32 212.011s26.398 58.963 58.963 58.963 58.962-26.399 58.962-58.963" /><path fill="#4dabf7" stroke="#1864ab" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="18.8679" d="M208.75 223.817c18.875 30.802 63.626 30.802 82.501 0" /></svg></i> <i class="fa-solid fa-message-smile"></i>
<small>Statuslog</small> <small>Status.lol</small>
</NavLink>
<NavLink class="nav-link" href="/pics">
<i class="fa-solid fa-image"></i>
<small>Some.pics</small>
</NavLink>
<NavLink class="nav-link" href="/ephemeral">
<i class="fa-light fa-comment-dots"></i>
<small>Eph.emer.al</small>
</NavLink> </NavLink>
</nav> </nav>
<nav class="bottom s"> <nav class="bottom s">
<NavLink class="nav-link" href="" Match="NavLinkMatch.All">
<i class="fa-solid fa-fw fa-home"></i>
<small>Home</small>
</NavLink>
<NavLink class="nav-link" href="/statuslog/latest"> <NavLink class="nav-link" href="/statuslog/latest">
<i><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 500 500"><path fill="#4dabf7" d="M250 450c-38.388 0-76.775-14.646-106.066-43.934l-100-100c-58.579-58.58-58.579-153.553 0-212.132C100.534 37.336 191.105 35.421 250 88.191c58.898-52.768 149.47-50.853 206.066 5.743 58.58 58.58 58.58 153.553 0 212.132l-100 100C326.778 435.354 288.39 450 250 450" /><path fill="#228be6" d="M220.52 176.634a11.792 11.792 0 1 1-23.586 0 11.792 11.792 0 0 1 23.585 0" /><path fill="#1864ab" stroke="#1864ab" stroke-miterlimit="10" stroke-width="11.32074" d="M220.52 176.634a11.792 11.792 0 1 1-23.586 0 11.792 11.792 0 0 1 23.585 0Z" /><path fill="#228be6" d="M303.066 176.634a11.792 11.792 0 1 1-23.585 0 11.792 11.792 0 0 1 23.585 0" /><path fill="#1864ab" stroke="#1864ab" stroke-miterlimit="10" stroke-width="11.32074" d="M303.066 176.634a11.792 11.792 0 1 1-23.585 0 11.792 11.792 0 0 1 23.585 0Z" /><path fill="#228be6" stroke="#461036" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="18.8679" d="M208.75 223.817c18.875 30.802 63.626 30.802 82.501 0" /><path fill="#1971c2" d="M438.68 212.011c0-32.564-26.399-58.962-58.963-58.962s-58.962 26.398-58.962 58.962 26.398 58.963 58.962 58.963 58.962-26.399 58.962-58.963m-259.433 0c0-32.564-26.398-58.962-58.962-58.962S61.32 179.447 61.32 212.011s26.398 58.963 58.963 58.963 58.962-26.399 58.962-58.963" /><path fill="#4dabf7" stroke="#1864ab" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="18.8679" d="M208.75 223.817c18.875 30.802 63.626 30.802 82.501 0" /></svg></i> <i class="fa-solid fa-message-smile"></i>
<small>Statuslog</small> <small>Status.lol</small>
</NavLink>
<NavLink class="nav-link" href="/pics">
<i class="fa-solid fa-image"></i>
<small>Some.pics</small>
</NavLink>
<NavLink class="nav-link" href="/ephemeral">
<i class="fa-light fa-comment-dots"></i>
<small>Eph.emer.al</small>
</NavLink> </NavLink>
<AuthorizeView> <AuthorizeView>

View file

@ -0,0 +1,87 @@
@inject IJSRuntime JS
@inject State State
<div class="overlay" data-ui="#@id"></div>
<dialog id="@id">
<h5>Share your status</h5>
<div class="row">
<div class="min square extra">
<button class="transparent square extra no-margin">
<object id="status-emoji" class="large emoji @(statusEmoji == null ? "animated" : string.Empty)" data-emoji="@(statusEmoji ?? "🫥")">@(statusEmoji ?? "🫥")</object>
<menu class="no-wrap">
<script type="module" src="https://cdn.jsdelivr.net/npm/emoji-picker-element@@^1/index.js"></script>
<emoji-picker emoji-version="15.1"></emoji-picker>
<script>
document.querySelector('emoji-picker')
.addEventListener('emoji-click', event => {
document.getElementById('status-emoji').setAttribute('data-emoji', event.detail.unicode)
const input = document.getElementById('status-emoji-input')
input.value = event.detail.unicode
var event = new Event('change');
input.dispatchEvent(event);
})
</script>
</menu>
</button>
</div>
<div class="field textarea border max">
<InputTextArea @bind-Value="statusContent"></InputTextArea>
</div>
</div>
<nav class="right-align no-space">
@if (State?.SelectedAddress?.Preferences?.Statuslog?.MastodonPosting ?? false) {
<label class="checkbox">
<InputCheckbox @bind-Value="postToMastodon"></InputCheckbox>
<span>Post this to Mastodon</span>
</label>
}
<InputText id="status-emoji-input" class="invisible" @bind-Value="statusEmoji"></InputText>
<button class="transparent link" data-ui="#@id" disabled="@loading">Cancel</button>
<button @onclick="PostStatus" disabled="@loading">
@if (loading) {
<span>Saving...</span>
}
else {
<i class="fa-solid fa-floppy-disk"></i> <span>Save</span>
}
</button>
</nav>
</dialog>
@code {
private string statusContent = string.Empty;
private string? statusEmoji = null;
private bool postToMastodon = true;
private bool loading = false;
[Parameter]
public string id { get; set; }
public async Task PostStatus() {
await JS.InvokeVoidAsync("console.log", "hey from post status");
StatusPost post = new StatusPost
{
Emoji = statusEmoji,
Content = statusContent
};
if (State?.SelectedAddress?.Preferences?.Statuslog?.MastodonPosting ?? false){
post.SkipMastodonPost = !postToMastodon;
}
loading = true;
InvokeAsync(StateHasChanged);
RestService api = new RestService();
var result = await api.StatusPost(State.SelectedAddressName, post);
if(result != null){
State.RefreshStatuses().ContinueWith(t => InvokeAsync(StateHasChanged));
}
await JS.InvokeVoidAsync("ui", "#" + id);
statusContent = string.Empty;
statusEmoji = null;
postToMastodon = true;
loading = false;
}
}

View file

@ -0,0 +1,27 @@
@page "/ephemeral"
<div class="row center-align">
<h3>
Ephemeral
</h3>
</div>
<div class="row center-align">
<p><a href="https://eph.emer.al">Ephemeral</a> is a place for fleeting thoughts. Everything on this page will disappear after a while.</p>
</div>
<Virtualize Items="messages" Context="message">
<article class="ephemeral center">
@message
</article>
</Virtualize>
@code {
private List<string> messages = new List<string>();
protected override async Task OnInitializedAsync() {
//await Shell.Current.GoToAsync(nameof(EphemeralWebPage));
RestService api = new RestService();
messages = await api.Ephemeral();
}
}

View file

@ -16,35 +16,57 @@
} }
</div> </div>
@if (statuses == null) { <div class="responsive">
<p><em>Loading Statuses...</em></p> <div class="tabs">
} <a data-ui="#statuses" class="active">
else { <i class="fa-solid fa-message-smile"></i>
<div id="statuses"> <span>Status.lol</span>
@foreach (var status in statuses) { </a>
<StatusCard status="@status"></StatusCard> <a data-ui="#pics">
} <i class="fa-solid fa-images"></i>
<span>Some.pics</span>
</a>
</div> </div>
} <div id="statuses" class="page padding active">
<StatusList StatusFunc="@GetStatuses"></StatusList>
</div>
<div id="pics" class="page padding">
<PicList PicsFunc="@GetPics"></PicList>
</div>
</div>
@code { @code {
[Parameter] [Parameter]
public string Address { get; set; } public string Address { get; set; }
private Status[]? statuses; private Status[] statuses;
private MarkupString? bio; private MarkupString? bio;
protected override async Task OnInitializedAsync() { protected override async Task OnInitializedAsync() {
RestService api = new RestService(); RestService api = new RestService();
await GetBioAsync(api); await GetBioAsync(api);
// GetStatusesAsync(api);
}
private async Task GetStatusesAsync(RestService api) {
statuses = (await api.Statuslog(Address)).ToArray();
} }
private async Task GetBioAsync(RestService api) { private async Task GetBioAsync(RestService api) {
bio = await api.StatuslogBio(Address); bio = await api.StatuslogBio(Address);
} }
public async ValueTask<ItemsProviderResult<Status>> GetStatuses(ItemsProviderRequest request) {
// TODO: request.cancellationToken
RestService api = new RestService();
statuses = (await api.Statuslog(Address)).ToArray() ?? new Status[0];
var numStatuses = Math.Min(request.Count, statuses.Length - request.StartIndex);
return new ItemsProviderResult<Status>(statuses.Skip(request.StartIndex).Take(numStatuses), statuses.Length);
}
private List<Pic> pics;
private async ValueTask<ItemsProviderResult<Pic>> GetPics(ItemsProviderRequest request) {
// TODO: request.cancellationToken
RestService api = new RestService();
if (pics == null || pics.Count == 0) pics = await api.SomePics(Address);
var numPics = Math.Min(request.Count, pics.Count - request.StartIndex);
return new ItemsProviderResult<Pic>(pics.Skip(request.StartIndex).Take(numPics), pics.Count);
}
} }

View file

@ -0,0 +1,25 @@
@page "/pics"
<div class="row center-align">
<h3>
Some.pics
</h3>
</div>
<div class="row center-align">
<p>Sit back, relax, and look at <a href="https://some.pics/">some.pics</a></p>
</div>
<div id="pics" class="responsive">
<PicList PicsFunc="@GetPics"></PicList>
</div>
@code {
private List<Pic> pics;
private async ValueTask<ItemsProviderResult<Pic>> GetPics(ItemsProviderRequest request) {
// TODO: request.cancellationToken
RestService api = new RestService();
if(pics == null || pics.Count == 0) pics = await api.SomePics();
var numPics = Math.Min(request.Count, pics.Count - request.StartIndex);
return new ItemsProviderResult<Pic>(pics.Skip(request.StartIndex).Take(numPics), pics.Count);
}
}

View file

@ -1,4 +1,5 @@
@page "/statuslog/latest" @page "/statuslog/latest"
@inject State State
<div class="row center-align"> <div class="row center-align">
<h3> <h3>
@ -10,33 +11,36 @@
<p>The latest posts from everyone at <a href="https://status.lol">status.lol</a></p> <p>The latest posts from everyone at <a href="https://status.lol">status.lol</a></p>
</div> </div>
@if (statuses == null) { <AuthorizeView>
<p id="statuses" class="skeleton-container"> <Authorized>
<StatusCardSkeleton></StatusCardSkeleton> <button class="fab circle extra large-elevate" data-ui="#post-modal">
<StatusCardSkeleton></StatusCardSkeleton> <i class="fa-solid fa-pen-to-square"></i>
<StatusCardSkeleton></StatusCardSkeleton> </button>
<StatusCardSkeleton></StatusCardSkeleton> <NewStatusDialog id="post-modal"></NewStatusDialog>
<StatusCardSkeleton></StatusCardSkeleton> </Authorized>
<StatusCardSkeleton></StatusCardSkeleton> </AuthorizeView>
</p>
} <div id="statuses" class="responsive">
else <StatusList StatusFunc="@GetStatuses"></StatusList>
{ </div>
<div id="statuses" class="responsive">
@foreach (var status in statuses) {
<StatusCard status="@status"></StatusCard>
}
</div>
}
@code { @code {
private Status[]? statuses; private Status[] statuses;
private string statusContent = string.Empty;
private string? statusEmoji = null;
protected override async Task OnInitializedAsync() protected override async Task OnInitializedAsync()
{ {
RestService api = new RestService(); }
statuses = (await api.StatuslogLatest()).ToArray();
private async ValueTask<ItemsProviderResult<Status>> GetStatuses(ItemsProviderRequest request)
{
// TODO: request.cancellationToken
statuses = (await State.GetStatuses()) ?? new Status[0];
var numStatuses = Math.Min(request.Count, statuses.Length - request.StartIndex);
return new ItemsProviderResult<Status>(statuses.Skip(request.StartIndex).Take(numStatuses), statuses.Length);
} }
} }

31
Components/PicList.razor Normal file
View file

@ -0,0 +1,31 @@
<Virtualize ItemsProvider="GetPics" Context="pic" ItemSize="180">
<ItemContent>
<article class="no-padding center">
<img class="responsive" src="@pic.Url">
<div class="padding">
<nav>
<a class="author" href="/person/@pic.Address">
<i class="fa-solid fa-fw fa-at"></i>@pic.Address
</a>
<span class="max"></span>
<a class="chip transparent-border right">
<i class="fa fa-clock"></i> @pic.RelativeTime
</a>
</nav>
<p>@pic.Description</p>
</div>
</article>
</ItemContent>
<Placeholder>
<article class="no-padding center"></article>
</Placeholder>
</Virtualize>
@code {
[Parameter]
public Func<ItemsProviderRequest, ValueTask<ItemsProviderResult<Pic>>> PicsFunc { get; set; }
private async ValueTask<ItemsProviderResult<Pic>> GetPics(ItemsProviderRequest request) {
return await this.PicsFunc(request);
}
}

View file

@ -1,6 +1,6 @@
<article class="status"> <article class="status">
<div class="row"> <div class="row">
<object class="large emoji" data-emoji="@status.EmojiOrDefault">@status.EmojiOrDefault</object> <div class="large emoji" data-emoji="@status.EmojiOrDefault">@status.EmojiOrDefault</div>
<div class="max"> <div class="max">
<a class="author" href="/person/@status.Address"> <a class="author" href="/person/@status.Address">
<i class="fa-solid fa-fw fa-at"></i>@status.Address <i class="fa-solid fa-fw fa-at"></i>@status.Address

View file

@ -0,0 +1,17 @@
<Virtualize ItemsProvider="GetStatuses" Context="status" ItemSize="180">
<ItemContent>
<StatusCard status="@status"></StatusCard>
</ItemContent>
<Placeholder>
<StatusCardSkeleton></StatusCardSkeleton>
</Placeholder>
</Virtualize>
@code {
[Parameter]
public Func<ItemsProviderRequest, ValueTask<ItemsProviderResult<Status>>> StatusFunc { get; set; }
private async ValueTask<ItemsProviderResult<Status>> GetStatuses(ItemsProviderRequest request) {
return await this.StatusFunc(request);
}
}

View file

@ -18,7 +18,7 @@ namespace Neighbourhood.omg.lol {
public async Task Logout() { public async Task Logout() {
SecureStorage.Remove("accounttoken"); SecureStorage.Remove("accounttoken");
Preferences.Default.Clear(); await State.RemoveAccountDetails();
NotifyAuthenticationStateChanged(GetAuthenticationStateAsync()); NotifyAuthenticationStateChanged(GetAuthenticationStateAsync());
} }

12
EphemeralWebPage.xaml Normal file
View file

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="Neighbourhood.omg.lol.EphemeralWebPage"
BackgroundColor="{DynamicResource PageBackgroundColor}">
<Grid>
<WebView x:Name="webview"
Navigating="webview_Navigating"
VerticalOptions="Fill"
HorizontalOptions="Fill" />
</Grid>
</ContentPage>

18
EphemeralWebPage.xaml.cs Normal file
View file

@ -0,0 +1,18 @@
namespace Neighbourhood.omg.lol;
public partial class EphemeralWebPage : ContentPage
{
private NavigatorService NavigatorService { get; set; }
public EphemeralWebPage(NavigatorService navigatorService)
{
this.NavigatorService = navigatorService;
InitializeComponent();
this.webview.Source = $"https://home.omg.lol/ephemeral";
}
public async void webview_Navigating(object sender, WebNavigatingEventArgs e) {
var cookies = this.webview.Cookies;
Uri uri = new Uri(e.Url);
}
}

View file

@ -2,6 +2,8 @@ using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Authorization; using Microsoft.AspNetCore.Components.Authorization;
using System.Diagnostics; using System.Diagnostics;
using System.Web; using System.Web;
using Microsoft.Extensions.Configuration;
namespace Neighbourhood.omg.lol; namespace Neighbourhood.omg.lol;
@ -9,25 +11,42 @@ public partial class LoginWebViewPage : ContentPage
{ {
private AuthenticationStateProvider AuthStateProvider { get; set; } private AuthenticationStateProvider AuthStateProvider { get; set; }
private NavigatorService NavigatorService { get; set; } private NavigatorService NavigatorService { get; set; }
private IConfiguration Configuration { get; set; }
public LoginWebViewPage(AuthenticationStateProvider authStateProvider, NavigatorService navigatorService) private string? client_id;
private string? client_secret;
private string? redirect_uri;
public LoginWebViewPage(AuthenticationStateProvider authStateProvider, NavigatorService navigatorService, IConfiguration configuration)
{ {
this.AuthStateProvider = authStateProvider; this.AuthStateProvider = authStateProvider;
this.NavigatorService = navigatorService; this.NavigatorService = navigatorService;
this.Configuration = configuration;
InitializeComponent(); InitializeComponent();
this.loginwebview.Source = "https://home.omg.lol/oauth/authorize?client_id=ea14dafd3e92cbcf93750c35cd81a031&scope=everything&redirect_uri=https://neatnik.net/adam/bucket/omgloloauth/&response_type=code"; client_id = configuration.GetValue<string>("client_id");
client_secret = configuration.GetValue<string>("client_secret");
redirect_uri = configuration.GetValue<string>("redirect_uri");
// make sure they're all not null first.
if ( client_id != null
&& client_secret != null
&& redirect_uri != null) {
this.loginwebview.Source = $"https://home.omg.lol/oauth/authorize?client_id={client_id}&scope=everything&redirect_uri={redirect_uri}&response_type=code";
}
} }
public async void loginwebview_Navigating(object sender, WebNavigatingEventArgs e) { public async void loginwebview_Navigating(object sender, WebNavigatingEventArgs e) {
if(e.Url.StartsWith("https://neatnik.net/adam/bucket/omgloloauth/")) { if ( client_id != null
Debug.WriteLine("And here we go..."); && client_secret != null
&& redirect_uri != null
&& e.Url.StartsWith(redirect_uri))
{
Uri uri = new Uri(e.Url); Uri uri = new Uri(e.Url);
var query = HttpUtility.ParseQueryString(uri.Query); var query = HttpUtility.ParseQueryString(uri.Query);
string? code = query.Get("code"); string? code = query.Get("code");
if (!string.IsNullOrEmpty(code)) { if (!string.IsNullOrEmpty(code)) {
RestService api = new RestService(); RestService api = new RestService();
string? token = await api.OAuth(code); string? token = await api.OAuth(code, client_id, client_secret, redirect_uri);
if (!string.IsNullOrEmpty(token)) { if (!string.IsNullOrEmpty(token)) {
Debug.WriteLine($"Fuck yeah, a token! {token}"); Debug.WriteLine($"Fuck yeah, a token! {token}");
await ((CustomAuthenticationStateProvider)this.AuthStateProvider).Login(token); await ((CustomAuthenticationStateProvider)this.AuthStateProvider).Login(token);

View file

@ -1,4 +1,5 @@
using Microsoft.AspNetCore.Components.Authorization; using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Neighbourhood.omg.lol.Models; using Neighbourhood.omg.lol.Models;
@ -14,10 +15,14 @@ namespace Neighbourhood.omg.lol {
builder.Services.AddMauiBlazorWebView(); builder.Services.AddMauiBlazorWebView();
builder.Services.AddTransient<LoginWebViewPage>(); builder.Services.AddTransient<LoginWebViewPage>();
builder.Services.AddTransient<EphemeralWebPage>();
builder.Services.AddSingleton<State>(); builder.Services.AddSingleton<State>();
builder.Services.AddSingleton<NavigatorService>(); builder.Services.AddSingleton<NavigatorService>();
ConfigurationBuilder configurationBuilder = new ConfigurationBuilder();
builder.Services.AddSingleton<IConfiguration>(configurationBuilder.AddUserSecrets<App>().Build());
builder.Services.AddAuthorizationCore(); builder.Services.AddAuthorizationCore();
builder.Services.AddScoped<CustomAuthenticationStateProvider>(); builder.Services.AddScoped<CustomAuthenticationStateProvider>();
builder.Services.AddScoped<AuthenticationStateProvider>(s => s.GetRequiredService<CustomAuthenticationStateProvider>()); builder.Services.AddScoped<AuthenticationStateProvider>(s => s.GetRequiredService<CustomAuthenticationStateProvider>());

View file

@ -10,13 +10,13 @@ namespace Neighbourhood.omg.lol.Models {
public string Message { get; set; } public string Message { get; set; }
public RegistrationData Registration { get; set; } public RegistrationData Registration { get; set; }
public ExpirationData Expiration { get; set; } public ExpirationData Expiration { get; set; }
public PreferenceData? Preferences { get; set; }
public class TimeData { public class TimeData {
public long? UnixEpochTime { get; set; } public long? UnixEpochTime { get; set; }
public string? Iso8601Time { get; set; } public string? Iso8601Time { get; set; }
public string? Rfc2822Time { get; set; } public string? Rfc2822Time { get; set; }
public string? RelativeTime { get; set; } public string? RelativeTime { get; set; }
public PreferenceData? Preferences { get; set; }
} }
public class RegistrationData : TimeData { public class RegistrationData : TimeData {

36
Models/Pic.cs Normal file
View file

@ -0,0 +1,36 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
namespace Neighbourhood.omg.lol.Models {
public class Pic {
public string Id { get; set; }
public string Url { get; set; }
public string Address { get; set; }
public long Created { get; set; }
public long Size { get; set; }
public string Mime { get; set; }
public string Description { get; set; }
[JsonPropertyName("exif")]
public string ExifJson { get; set; }
public string RelativeTime {
get {
DateTimeOffset createdTime = DateTimeOffset.UnixEpoch.AddSeconds(Created);
TimeSpan offset = DateTimeOffset.UtcNow - createdTime;
var offsetString = string.Empty;
if (offset.TotalDays >= 1) offsetString = $"{Math.Floor(offset.TotalDays)} days ago";
else if (offset.TotalHours >= 1) offsetString = $"{Math.Floor(offset.TotalHours)} hours, {offset.Minutes} minutes ago";
else if (offset.TotalMinutes >= 1) offsetString = $"{Math.Floor(offset.TotalMinutes)} minutes ago";
else offsetString = $"{Math.Floor(offset.TotalSeconds)} seconds ago";
return offsetString;
}
}
}
}

View file

@ -0,0 +1,12 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Neighbourhood.omg.lol.Models {
public class SomePicsResponseData : IOmgLolResponseData {
public string Message { get; set; }
public List<Pic>? Pics { get; set; }
}
}

View file

@ -1,5 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
using System.Text.Json; using System.Text.Json;
@ -16,6 +17,8 @@ namespace Neighbourhood.omg.lol.Models {
public AddressResponseData? SelectedAddress { get; set; } public AddressResponseData? SelectedAddress { get; set; }
public string? SelectedAddressName { get => SelectedAddress?.Address; } public string? SelectedAddressName { get => SelectedAddress?.Address; }
public List<Status>? Statuses { get; set; }
public async Task PopulateAccountDetails(string token) { public async Task PopulateAccountDetails(string token) {
RestService api = new RestService(token); RestService api = new RestService(token);
@ -45,5 +48,28 @@ namespace Neighbourhood.omg.lol.Models {
} }
} }
} }
public async Task RemoveAccountDetails() {
Preferences.Default.Clear();
AccountInfo = null;
AddressList = null;
SelectedAddress = null;
}
public async Task<Status[]?> GetStatuses(bool forceRefresh = false) {
RestService api = new RestService();
if (forceRefresh || this.Statuses == null || this.Statuses.Count == 0) {
Debug.WriteLine("Downloading statuses from server");
this.Statuses = await api.StatuslogLatest();
}
//else Task.Run(async () => this.Statuses = await api.StatuslogLatest()); // not awaited on purpose
return this.Statuses.ToArray();
}
public async Task RefreshStatuses() {
RestService api = new RestService();
this.Statuses = await api.StatuslogLatest();
}
} }
} }

14
Models/StatusPost.cs Normal file
View file

@ -0,0 +1,14 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Neighbourhood.omg.lol.Models {
public class StatusPost {
public string? Emoji { get; set; }
public string? Content { get; set; }
public string? ExternalUrl { get; set; }
public bool? SkipMastodonPost { get; set; }
}
}

View file

@ -0,0 +1,15 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Neighbourhood.omg.lol.Models {
public class StatusPostResponseData : IOmgLolResponseData {
public string? Message { get; set; }
public string? Id { get; set; }
public string? Url { get; set; }
public string? ExternalUrl { get; set; }
}
}

View file

@ -37,6 +37,7 @@
<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'windows'">10.0.17763.0</SupportedOSPlatformVersion> <SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'windows'">10.0.17763.0</SupportedOSPlatformVersion>
<TargetPlatformMinVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'windows'">10.0.17763.0</TargetPlatformMinVersion> <TargetPlatformMinVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'windows'">10.0.17763.0</TargetPlatformMinVersion>
<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'tizen'">6.5</SupportedOSPlatformVersion> <SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'tizen'">6.5</SupportedOSPlatformVersion>
<UserSecretsId>9c986eaa-84a8-45d5-adff-b206367a1d08</UserSecretsId>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
@ -63,6 +64,7 @@
<PackageReference Include="Microsoft.AspNetCore.Authorization" Version="8.0.6" /> <PackageReference Include="Microsoft.AspNetCore.Authorization" Version="8.0.6" />
<PackageReference Include="Microsoft.AspNetCore.Components.Authorization" Version="8.0.6" /> <PackageReference Include="Microsoft.AspNetCore.Components.Authorization" Version="8.0.6" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Authentication" Version="8.0.6" /> <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Authentication" Version="8.0.6" />
<PackageReference Include="Microsoft.Extensions.Configuration.UserSecrets" Version="6.0.1" />
<PackageReference Include="Microsoft.Maui.Controls" Version="$(MauiVersion)" /> <PackageReference Include="Microsoft.Maui.Controls" Version="$(MauiVersion)" />
<PackageReference Include="Microsoft.Maui.Controls.Compatibility" Version="$(MauiVersion)" /> <PackageReference Include="Microsoft.Maui.Controls.Compatibility" Version="$(MauiVersion)" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebView.Maui" Version="$(MauiVersion)" /> <PackageReference Include="Microsoft.AspNetCore.Components.WebView.Maui" Version="$(MauiVersion)" />
@ -74,6 +76,9 @@
<MauiXaml Update="AppShell.xaml"> <MauiXaml Update="AppShell.xaml">
<Generator>MSBuild:Compile</Generator> <Generator>MSBuild:Compile</Generator>
</MauiXaml> </MauiXaml>
<MauiXaml Update="EphemeralWebPage.xaml">
<Generator>MSBuild:Compile</Generator>
</MauiXaml>
<MauiXaml Update="LoginWebViewPage.xaml"> <MauiXaml Update="LoginWebViewPage.xaml">
<Generator>MSBuild:Compile</Generator> <Generator>MSBuild:Compile</Generator>
</MauiXaml> </MauiXaml>

View file

@ -4,6 +4,7 @@ using Neighbourhood.omg.lol.Models;
using System.Diagnostics; using System.Diagnostics;
using System.Net.Http.Json; using System.Net.Http.Json;
using System.Text.Json; using System.Text.Json;
using System.Text.RegularExpressions;
namespace Neighbourhood.omg.lol { namespace Neighbourhood.omg.lol {
public class RestService { public class RestService {
@ -43,37 +44,73 @@ namespace Neighbourhood.omg.lol {
return responseData; return responseData;
} }
public async Task<List<Status>> StatuslogLatest() { private async Task<TResponse?> Post<TResponse, TData>(Uri uri, TData data) where TResponse : IOmgLolResponseData {
Uri uri = new Uri($"{BaseUrl}/statuslog/latest"); TResponse? responseData = default(TResponse);
return (await Get<StatusResponseData>(uri))?.Statuses ?? new List<Status>(); try {
HttpResponseMessage response = await _client.PostAsJsonAsync(uri, data);
if (response.IsSuccessStatusCode) {
OmgLolResponse<TResponse>? responseObj = await response.Content.ReadFromJsonAsync<OmgLolResponse<TResponse>>(_serializerOptions);
if (responseObj != null && responseObj.Request.Success) {
responseData = responseObj.Response;
}
}
}
catch (Exception ex) {
Debug.WriteLine(@"\tERROR {0}", ex.Message);
} }
public async Task<List<Status>> Statuslog(string address) { return responseData;
Uri uri = new Uri($"{BaseUrl}/address/{address}/statuses");
return (await Get<StatusResponseData>(uri))?.Statuses ?? new List<Status>();
} }
public async Task<List<Status>> StatuslogLatest() =>
(await Get<StatusResponseData>(new Uri($"{BaseUrl}/statuslog/latest")))?.Statuses ?? new List<Status>();
public async Task<List<Status>> Statuslog(string address) =>
(await Get<StatusResponseData>(new Uri($"{BaseUrl}/address/{address}/statuses")))?.Statuses ?? new List<Status>();
public async Task<MarkupString> StatuslogBio(string address) { public async Task<MarkupString> StatuslogBio(string address) {
Uri uri = new Uri($"{BaseUrl}/address/{address}/statuses/bio"); StatusBioResponseData? responseData = await Get<StatusBioResponseData>(new Uri($"{BaseUrl}/address/{address}/statuses/bio"));
StatusBioResponseData? responseData = await Get<StatusBioResponseData>(uri);
return (MarkupString)Markdown.ToHtml(responseData?.Bio ?? ""); return (MarkupString)Markdown.ToHtml(responseData?.Bio ?? "");
} }
public async Task<AccountResponseData?> AccountInfo() { public async Task<AccountResponseData?> AccountInfo() =>
Uri uri = new Uri($"{BaseUrl}/account/application/info"); await Get<AccountResponseData>(new Uri($"{BaseUrl}/account/application/info"));
AccountResponseData? responseData = await Get<AccountResponseData>(uri);
return responseData; public async Task<AddressResponseList?> Addresses() =>
await Get<AddressResponseList>(new Uri($"{BaseUrl}/account/application/addresses"));
public async Task<StatusPostResponseData?> StatusPost(string address, StatusPost statusPost) =>
await Post<StatusPostResponseData, StatusPost>(new Uri($"{BaseUrl}/address/{address}/statuses"), statusPost);
public async Task<List<Pic>> SomePics() =>
(await Get<SomePicsResponseData>(new Uri($"{BaseUrl}/pics")))?.Pics ?? new List<Pic>();
public async Task<List<Pic>> SomePics(string address) =>
(await Get<SomePicsResponseData>(new Uri($"{BaseUrl}/address/{address}/pics")))?.Pics ?? new List<Pic>();
public async Task<Pic?> SomePic(string address, string id) =>
(await Get<Pic>(new Uri($"{BaseUrl}/address/{address}/pics/{id}")));
public async Task<List<string>> Ephemeral() {
List<string> notes = new List<string>();
Uri Uri = new Uri($"https://eph.emer.al/");
try {
var response = await _client.GetAsync(Uri);
var str = await response.Content.ReadAsStringAsync();
string pattern = @"<p class=""post"">(.*?)<\/p>";
var matches = Regex.Matches(str, pattern, RegexOptions.IgnoreCase | RegexOptions.Singleline);
foreach (Match match in matches) {
notes.Add(match.Groups[1].Value);
}
}
catch (Exception ex) {
Debug.WriteLine(ex);
}
return notes;
} }
public async Task<AddressResponseList?> Addresses() { public async Task<string?> OAuth(string code, string client_id, string client_secret, string redirect_uri) {
Uri uri = new Uri($"{BaseUrl}/account/application/addresses");
AddressResponseList? responseData = await Get<AddressResponseList>(uri);
return responseData;
}
public async Task<string?> OAuth(string code) {
string? token = null; string? token = null;
Uri uri = new Uri($"{BaseUrl}/oauth/?code={code}&client_id=ea14dafd3e92cbcf93750c35cd81a031&client_secret=ec28b8653f1d98b4eef3f7a20858c43b&redirect_uri=https://neatnik.net/adam/bucket/omgloloauth/&scope=everything"); Uri uri = new Uri($"{BaseUrl}/oauth/?code={code}&client_id={client_id}&client_secret={client_secret}&redirect_uri={redirect_uri}&scope=everything");
try { try {
HttpResponseMessage response = await _client.GetAsync(uri); HttpResponseMessage response = await _client.GetAsync(uri);
if (response.IsSuccessStatusCode) { if (response.IsSuccessStatusCode) {

View file

@ -2,10 +2,14 @@
@import url(../vendor/beer.min.css); @import url(../vendor/beer.min.css);
@import url(../vendor/type.css); @import url(../vendor/type.css);
@import url(../vendor/fluent-emoji/SegoeUIEmoji.css); @import url(../vendor/fluent-emoji/SegoeUIEmoji.css);
/*@import url(../vendor/fluent-emoji/3d.css);*/
@import url(../vendor/fluent-emoji/animated-optional.css);
:root { :root {
--emoji-font: SegoeUIEmoji, 'Segoe UI Emoji', 'Noto Color Emoji', 'Apple Color Emoji', 'Segoe UI Symbol'; --emoji-font: SegoeUIEmoji, 'Segoe UI Emoji', 'Noto Color Emoji', 'Apple Color Emoji', 'Segoe UI Symbol';
--font: 'Lato', 'Helvetica Neue', Helvetica, Arial, sans-serif, var(--emoji-font); --font: 'Lato', 'Helvetica Neue', Helvetica, Arial, sans-serif, var(--emoji-font);
--prami-svg: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 500 500"><path fill="%23FF6BAE" stroke="none" d="M250 450C211.612 450 173.225 435.354 143.934 406.066L43.9346 306.066C-14.6446 247.487 -14.6446 152.513 43.9346 93.9341C100.533 37.3361 191.104 35.421 250 88.1907C308.898 35.4229 399.47 37.3379 456.066 93.9341C514.645 152.513 514.645 247.487 456.066 306.066L356.066 406.066C326.778 435.354 288.389 450 250 450" /><path fill="none" stroke="%23471036" stroke-width="19" stroke-linecap="round" d="M208.749 223.817C227.625 254.619 272.376 254.619 291.251 223.817" /><circle fill="%23471036" cx="291.3" cy="176.6" r="17.75" /><circle fill="%23471036" cx="208.6" cy="176.6" r="17.75" /><circle fill="%23E24097" cx="120.3" cy="212" r="59.2" /><circle fill="%23E24097" cx="379.7" cy="212" r="59.2" /></svg>');
} }
html, body { html, body {
font-family: var(--font); font-family: var(--font);
@ -47,14 +51,13 @@ img {
margin: 0 auto; margin: 0 auto;
} }
.status .emoji { .status .emoji, #status-emoji {
margin-bottom: auto; margin-bottom: auto;
inline-size: 3.5rem; inline-size: 3.5rem;
block-size: 3.5rem; block-size: 3.5rem;
font-size: 3.5rem; font-size: 3.1rem;
line-height: 3.5rem; line-height: 3.5rem;
text-indent: -10px; text-indent: -6px;
/*visibility:hidden;*/
} }
.profile.avatar { .profile.avatar {
@ -75,7 +78,7 @@ img {
@keyframes skeleton-loading { @keyframes skeleton-loading {
0% { 0% {
background-color: hsl(200, 20%, 80%); background-color: hsl(200, 20%, 70%);
} }
100% { 100% {
@ -137,3 +140,61 @@ i[class*=fa-at] {
vertical-align:unset; vertical-align:unset;
font-size: .75em; font-size: .75em;
} }
.fab {
position:fixed;
right: 2rem;
bottom: 2rem;
z-index: 1;
}
dialog {
overflow:visible;
width:80%;
}
.invisible {
max-width:0px;
max-height:0px;
border: none;
margin:0;
padding:0;
}
@media only screen and (max-width: 600px) {
.fab {
bottom: 7rem;
}
*:has(> main.responsive) {
/*flex-direction: row;*/
max-block-size: calc(100vh - 5rem);
}
}
.avatar {
position:relative;
}
.avatar::after {
content: '';
position:absolute;
left:0; right:0;
top:0; bottom:0;
background-color: var(--gray-8);
background-image: var(--prami-svg);
background-size: contain;
z-index:1;
}
article.ephemeral {
max-width: 50rem;
border: 2px dashed var(--gray-7);
}
#pics article {
max-width: 50rem;
}
.tabs a {
text-decoration:none;
}

View file

@ -1 +1,3 @@
@font-face{font-family:SegoeUIEmoji;src:url(seguiemj.ttf);} @font-face{font-family:SegoeUIEmoji;src:url(seguiemj.ttf);}
@font-face {font-family: 'EmojiMart';src:url(seguiemj.ttf) format('truetype');}
emoji-picker {--emoji-font-family: SegoeUIEmoji;}

File diff suppressed because it is too large Load diff