We can now post pics!

This commit is contained in:
Gordon Pedersen 2024-06-06 15:20:09 +10:00
parent a39eeff757
commit 72a4b78afa
11 changed files with 295 additions and 61 deletions

View file

@ -0,0 +1,82 @@
@inject IJSRuntime JS
@inject State State
<div class="overlay" data-ui="#@id"></div>
<dialog id="@id">
<h5>Share a picture</h5>
<div class="row">
<div class="field label prefix border">
<i class="fa-solid fa-image"></i>
<InputFile OnChange="@ChangeFile" accept="image/gif, image/heic, image/heif, image/jpeg, image/png, image/svg+xml, image/webp"></InputFile>
<input type="text">
<label>Select a picture</label>
</div>
@if(File != null){
<small>
@File.ContentType (@formatSizeUnits(File.Size))
</small>
}
</div>
<div class="row">
<div class="field textarea label border max">
<InputTextArea @bind-Value="Description"></InputTextArea>
<label>Description</label>
</div>
</div>
<nav class="right-align no-space">
<button class="transparent link" data-ui="#@id" disabled="@loading">Cancel</button>
<button @onclick="PostPic" disabled="@loading">
@if (loading) {
<span>Uploading...</span>
}
else {
<i class="fa-solid fa-cloud-arrow-up"></i> <span>Upload</span>
}
</button>
</nav>
</dialog>
@code {
private IBrowserFile? File { get; set; }
private string Description { get; set; }
private bool loading = false;
[Parameter]
public string id { get; set; }
public async Task PostPic() {
loading = true;
await InvokeAsync(StateHasChanged);
RestService api = new RestService();
PutPicResponseData? response = await api.PutPic(State.SelectedAddressName, File);
if(!string.IsNullOrEmpty(Description) && response != null && !string.IsNullOrEmpty(response.Id)) {
await api.PostPicDescription(State.SelectedAddressName, response.Id, Description);
await State.RefreshPics();
await InvokeAsync(StateHasChanged);
}
await JS.InvokeVoidAsync("ui", "#" + id);
// clear input
File = null;
Description = string.Empty;
loading = false;
await InvokeAsync(StateHasChanged);
}
private async Task ChangeFile(InputFileChangeEventArgs e){
File = e.File;
}
private string formatSizeUnits(long bytes){
string formatted = "0 bytes";
if (bytes >= 1073741824) { formatted = $"{(bytes / 1073741824):.##} GB"; }
else if (bytes >= 1048576) { formatted = $"{(bytes / 1048576):.##} MB"; }
else if (bytes >= 1024) { formatted = $"{(bytes / 1024):.##} KB"; }
else if (bytes > 1) { formatted = $"{bytes} bytes"; }
else if (bytes == 1) { formatted = $"{bytes} byte"; }
return formatted;
}
}

View file

@ -1,4 +1,6 @@
@page "/person/{Address}" @page "/person/{Address}"
@inject State State
<div class="row center-align"> <div class="row center-align">
<h3><i class="fa-solid fa-fw fa-at"></i>@Address</h3> <h3><i class="fa-solid fa-fw fa-at"></i>@Address</h3>
</div> </div>
@ -28,11 +30,11 @@
</a> </a>
</div> </div>
<div id="statuses" class="page padding active"> <div id="statuses" class="page padding active">
<StatusList StatusFunc="@GetStatuses"></StatusList> <StatusList StatusFunc="@State.VirtualStatusesFunc(Address)"></StatusList>
</div> </div>
<div id="pics" class="page padding"> <div id="pics" class="page padding">
<PicList PicsFunc="@GetPics"></PicList> <PicList PicsFunc="@State.VirtualPicsFunc(Address)"></PicList>
</div> </div>
</div> </div>
@ -40,33 +42,9 @@
[Parameter] [Parameter]
public string Address { get; set; } public string Address { get; set; }
private Status[] statuses;
private MarkupString? bio; private MarkupString? bio;
protected override async Task OnInitializedAsync() { protected override async Task OnInitializedAsync() {
RestService api = new RestService(); bio = await State.GetBio(Address);
await GetBioAsync(api);
}
private async Task GetBioAsync(RestService api) {
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

@ -1,4 +1,5 @@
@page "/pics" @page "/pics"
@inject State State
<div class="row center-align"> <div class="row center-align">
<h3> <h3>
@ -9,8 +10,17 @@
<p>Sit back, relax, and look at <a href="https://some.pics/">some.pics</a></p> <p>Sit back, relax, and look at <a href="https://some.pics/">some.pics</a></p>
</div> </div>
<AuthorizeView>
<Authorized>
<button class="fab circle extra large-elevate" data-ui="#post-modal">
<i class="fa-solid fa-camera-retro"></i>
</button>
<NewPicDialog id="post-modal"></NewPicDialog>
</Authorized>
</AuthorizeView>
<div id="pics" class="responsive"> <div id="pics" class="responsive">
<PicList PicsFunc="@GetPics"></PicList> <PicList PicsFunc="@State.VirtualPicsFunc()"></PicList>
</div> </div>
@code { @code {

View file

@ -21,13 +21,13 @@
</AuthorizeView> </AuthorizeView>
<div id="statuses" class="responsive"> <div id="statuses" class="responsive">
<StatusList StatusFunc="@GetStatuses"></StatusList> <StatusList StatusFunc="@State.VirtualStatusesFunc()"></StatusList>
</div> </div>
@code { @code {
private Status[] statuses; private List<Status> statuses;
private string statusContent = string.Empty; private string statusContent = string.Empty;
private string? statusEmoji = null; private string? statusEmoji = null;
@ -38,9 +38,9 @@
private async ValueTask<ItemsProviderResult<Status>> GetStatuses(ItemsProviderRequest request) private async ValueTask<ItemsProviderResult<Status>> GetStatuses(ItemsProviderRequest request)
{ {
// TODO: request.cancellationToken // TODO: request.cancellationToken
statuses = (await State.GetStatuses()) ?? new Status[0]; statuses = (await State.GetStatuses()) ?? new List<Status>();
var numStatuses = Math.Min(request.Count, statuses.Length - request.StartIndex); var numStatuses = Math.Min(request.Count, statuses.Count - request.StartIndex);
return new ItemsProviderResult<Status>(statuses.Skip(request.StartIndex).Take(numStatuses), statuses.Length); return new ItemsProviderResult<Status>(statuses.Skip(request.StartIndex).Take(numStatuses), statuses.Count);
} }
} }

View file

@ -4,7 +4,7 @@
<img class="responsive" src="@pic.Url"> <img class="responsive" src="@pic.Url">
<div class="padding"> <div class="padding">
<nav> <nav>
<a class="author" href="/person/@pic.Address"> <a class="author" href="/person/@pic.Address#pics">
<i class="fa-solid fa-fw fa-at"></i>@pic.Address <i class="fa-solid fa-fw fa-at"></i>@pic.Address
</a> </a>
<span class="max"></span> <span class="max"></span>

View file

@ -2,6 +2,7 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
using System.Threading.Tasks; using System.Threading.Tasks;
@ -16,7 +17,7 @@ namespace Neighbourhood.omg.lol.Models {
public string Description { get; set; } public string Description { get; set; }
[JsonPropertyName("exif")] [JsonPropertyName("exif")]
public string ExifJson { get; set; } public JsonElement ExifJson { get; set; }
public string RelativeTime { public string RelativeTime {
get { get {

11
Models/PostPic.cs Normal file
View file

@ -0,0 +1,11 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Neighbourhood.omg.lol.Models {
public class PostPic {
public string Description { get; set; }
}
}

11
Models/PutPic.cs Normal file
View file

@ -0,0 +1,11 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Neighbourhood.omg.lol.Models {
public class PutPic {
public string Pic { 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 PutPicResponseData : IOmgLolResponseData {
public string Message { get; set; }
public string Id { get; set; }
public long Size { get; set; }
public string Mime { get; set; }
public string Url { get; set; }
}
}

View file

@ -1,7 +1,10 @@
using System; using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Web.Virtualization;
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.Linq; using System.Linq;
using System.Net;
using System.Text; using System.Text;
using System.Text.Json; using System.Text.Json;
using System.Threading.Tasks; using System.Threading.Tasks;
@ -18,6 +21,23 @@ namespace Neighbourhood.omg.lol.Models {
public string? SelectedAddressName { get => SelectedAddress?.Address; } public string? SelectedAddressName { get => SelectedAddress?.Address; }
public List<Status>? Statuses { get; set; } public List<Status>? Statuses { get; set; }
public List<Pic>? Pics { get; set; }
public List<Status>? CachedAddressStatuses { get; set; }
public List<Pic>? CachedAddressPics { get; set; }
public MarkupString? CachedAddressBio { get; set; }
private string? _cachedAddress;
public string? CachedAddress {
get => _cachedAddress;
set {
if (_cachedAddress != value) {
_cachedAddress = value;
CachedAddressStatuses = new List<Status>();
CachedAddressPics = new List<Pic>();
CachedAddressBio = null;
}
}
}
public async Task PopulateAccountDetails(string token) { public async Task PopulateAccountDetails(string token) {
RestService api = new RestService(token); RestService api = new RestService(token);
@ -54,22 +74,80 @@ namespace Neighbourhood.omg.lol.Models {
AccountInfo = null; AccountInfo = null;
AddressList = null; AddressList = null;
SelectedAddress = null; SelectedAddress = null;
} }
public async Task<Status[]?> GetStatuses(bool forceRefresh = false) { public async Task<MarkupString?> GetBio(string address, bool forceRefresh = false) {
CachedAddress = address;
if (forceRefresh || CachedAddressBio == null) {
RestService api = new RestService();
CachedAddressBio = await api.StatuslogBio(address);
}
return CachedAddressBio;
}
public async Task<List<Status>?> GetStatuses(bool forceRefresh = false) {
RestService api = new RestService(); RestService api = new RestService();
if (forceRefresh || this.Statuses == null || this.Statuses.Count == 0) { if (forceRefresh || this.Statuses == null || this.Statuses.Count == 0) {
Debug.WriteLine("Downloading statuses from server");
this.Statuses = await api.StatuslogLatest(); this.Statuses = await api.StatuslogLatest();
} }
//else Task.Run(async () => this.Statuses = await api.StatuslogLatest()); // not awaited on purpose return this.Statuses;
return this.Statuses.ToArray();
} }
public async Task RefreshStatuses() { public async ValueTask<ItemsProviderResult<Status>> VirtualStatuses(ItemsProviderRequest request) {
RestService api = new RestService(); // TODO: request.cancellationToken
this.Statuses = await api.StatuslogLatest(); var statuses = (await this.GetStatuses()) ?? new List<Status>();
var numStatuses = Math.Min(request.Count, statuses.Count - request.StartIndex);
return new ItemsProviderResult<Status>(statuses.Skip(request.StartIndex).Take(numStatuses), statuses.Count);
} }
public async ValueTask<ItemsProviderResult<Status>> VirtualStatuses(ItemsProviderRequest request, string address) {
// TODO: request.cancellationToken
RestService api = new RestService();
CachedAddressStatuses = (await api.Statuslog(address)) ?? new List<Status>();
var numStatuses = Math.Min(request.Count, CachedAddressStatuses.Count - request.StartIndex);
return new ItemsProviderResult<Status>(CachedAddressStatuses.Skip(request.StartIndex).Take(numStatuses), CachedAddressStatuses.Count);
}
public Func<ItemsProviderRequest, ValueTask<ItemsProviderResult<Status>>> VirtualStatusesFunc(string? address = null) {
if (address == null) return VirtualStatuses;
else {
CachedAddress = address;
return async (ItemsProviderRequest request) => await VirtualStatuses(request, CachedAddress);
}
}
public async Task<List<Pic>?> GetPics(bool forceRefresh = false) {
RestService api = new RestService();
if(forceRefresh || this.Pics == null || this.Pics.Count == 0) {
this.Pics = await api.SomePics();
}
return this.Pics;
}
public async ValueTask<ItemsProviderResult<Pic>> VirtualPics(ItemsProviderRequest request) {
// TODO: request.cancellationToken
var pics = (await this.GetPics()) ?? new List<Pic>();
var numPics = Math.Min(request.Count, pics.Count - request.StartIndex);
return new ItemsProviderResult<Pic>(pics.Skip(request.StartIndex).Take(numPics), pics.Count);
}
public async ValueTask<ItemsProviderResult<Pic>> VirtualPics(ItemsProviderRequest request, string address) {
// TODO: request.cancellationToken
RestService api = new RestService();
CachedAddressPics = (await api.SomePics(address)) ?? new List<Pic>();
var numPics = Math.Min(request.Count, CachedAddressPics.Count - request.StartIndex);
return new ItemsProviderResult<Pic>(CachedAddressPics.Skip(request.StartIndex).Take(numPics), CachedAddressPics.Count);
}
public Func<ItemsProviderRequest, ValueTask<ItemsProviderResult<Pic>>> VirtualPicsFunc(string? address = null) {
if (address == null) return VirtualPics;
else {
CachedAddress = address;
return async (ItemsProviderRequest request) => await VirtualPics(request, CachedAddress);
}
}
public async Task RefreshStatuses() => await GetStatuses(forceRefresh: true);
public async Task RefreshPics() => await GetPics(forceRefresh: true);
} }
} }

View file

@ -1,8 +1,10 @@
using Markdig; using Markdig;
using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Forms;
using Neighbourhood.omg.lol.Models; using Neighbourhood.omg.lol.Models;
using System.Diagnostics; using System.Diagnostics;
using System.Net.Http.Json; using System.Net.Http.Json;
using System.Text;
using System.Text.Json; using System.Text.Json;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
@ -14,6 +16,7 @@ namespace Neighbourhood.omg.lol {
public RestService(string? token = null) { public RestService(string? token = null) {
_client = new HttpClient(); _client = new HttpClient();
_client.BaseAddress = new Uri(BaseUrl);
_serializerOptions = new JsonSerializerOptions { _serializerOptions = new JsonSerializerOptions {
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower, PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
WriteIndented = true WriteIndented = true
@ -26,12 +29,38 @@ namespace Neighbourhood.omg.lol {
if (token != null) _client.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token); if (token != null) _client.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token);
} }
private async Task<T?> Get<T>(Uri uri) where T:IOmgLolResponseData { private async Task<T?> Get<T>(string uri, CancellationToken cancellationToken = default) where T:IOmgLolResponseData {
T? responseData = default(T); T? responseData = default(T);
try { try {
HttpResponseMessage response = await _client.GetAsync(uri); HttpResponseMessage response = await _client.GetAsync(uri, cancellationToken: cancellationToken);
if (response.IsSuccessStatusCode) { if (response.IsSuccessStatusCode) {
OmgLolResponse<T>? responseObj = await response.Content.ReadFromJsonAsync<OmgLolResponse<T>>(_serializerOptions); string str = await response.Content.ReadAsStringAsync();
try {
OmgLolResponse<T>? responseObj = await response.Content.ReadFromJsonAsync<OmgLolResponse<T>>(_serializerOptions, cancellationToken: cancellationToken);
if (responseObj != null && responseObj.Request.Success) {
responseData = responseObj.Response;
}
}
catch (JsonException ex) {
Debug.WriteLine(@"\tERROR {0}", ex.Message);
Debug.WriteLine(str);
}
}
}
catch (Exception ex) {
Debug.WriteLine(@"\tERROR {0}", ex.Message);
}
return responseData;
}
private async Task<TResponse?> Post<TResponse, TData>(string uri, TData data, CancellationToken cancellationToken = default) where TResponse : IOmgLolResponseData {
TResponse? responseData = default(TResponse);
try {
HttpResponseMessage response = await _client.PostAsJsonAsync(uri, data, _serializerOptions, cancellationToken: cancellationToken);
string str = await response.Content.ReadAsStringAsync();
if (response.IsSuccessStatusCode) {
OmgLolResponse<TResponse>? responseObj = await response.Content.ReadFromJsonAsync<OmgLolResponse<TResponse>>(_serializerOptions, cancellationToken: cancellationToken);
if (responseObj != null && responseObj.Request.Success) { if (responseObj != null && responseObj.Request.Success) {
responseData = responseObj.Response; responseData = responseObj.Response;
} }
@ -44,12 +73,15 @@ namespace Neighbourhood.omg.lol {
return responseData; return responseData;
} }
private async Task<TResponse?> Post<TResponse, TData>(Uri uri, TData data) where TResponse : IOmgLolResponseData { private async Task<TResponse?> Put<TResponse, TData>(string uri, TData data, CancellationToken cancellationToken = default) where TResponse : IOmgLolResponseData {
TResponse? responseData = default(TResponse); TResponse? responseData = default(TResponse);
try { try {
HttpResponseMessage response = await _client.PostAsJsonAsync(uri, data); string json = JsonSerializer.Serialize(data, _serializerOptions);
HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Put, uri);
request.Content = new StringContent(json, Encoding.UTF8, "application/json");
HttpResponseMessage response = await _client.SendAsync(request, cancellationToken: cancellationToken);
if (response.IsSuccessStatusCode) { if (response.IsSuccessStatusCode) {
OmgLolResponse<TResponse>? responseObj = await response.Content.ReadFromJsonAsync<OmgLolResponse<TResponse>>(_serializerOptions); OmgLolResponse<TResponse>? responseObj = await response.Content.ReadFromJsonAsync<OmgLolResponse<TResponse>>(_serializerOptions, cancellationToken: cancellationToken);
if (responseObj != null && responseObj.Request.Success) { if (responseObj != null && responseObj.Request.Success) {
responseData = responseObj.Response; responseData = responseObj.Response;
} }
@ -63,32 +95,48 @@ namespace Neighbourhood.omg.lol {
} }
public async Task<List<Status>> StatuslogLatest() => public async Task<List<Status>> StatuslogLatest() =>
(await Get<StatusResponseData>(new Uri($"{BaseUrl}/statuslog/latest")))?.Statuses ?? new List<Status>(); (await Get<StatusResponseData>("/statuslog/latest"))?.Statuses ?? new List<Status>();
public async Task<List<Status>> Statuslog(string address) => public async Task<List<Status>> Statuslog(string address) =>
(await Get<StatusResponseData>(new Uri($"{BaseUrl}/address/{address}/statuses")))?.Statuses ?? new List<Status>(); (await Get<StatusResponseData>($"/address/{address}/statuses"))?.Statuses ?? new List<Status>();
public async Task<MarkupString> StatuslogBio(string address) { public async Task<MarkupString> StatuslogBio(string address) {
StatusBioResponseData? responseData = await Get<StatusBioResponseData>(new Uri($"{BaseUrl}/address/{address}/statuses/bio")); StatusBioResponseData? responseData = await Get<StatusBioResponseData>($"/address/{address}/statuses/bio");
return (MarkupString)Markdown.ToHtml(responseData?.Bio ?? ""); return (MarkupString)Markdown.ToHtml(responseData?.Bio ?? "");
} }
public async Task<AccountResponseData?> AccountInfo() => public async Task<AccountResponseData?> AccountInfo() =>
await Get<AccountResponseData>(new Uri($"{BaseUrl}/account/application/info")); await Get<AccountResponseData>("/account/application/info");
public async Task<AddressResponseList?> Addresses() => public async Task<AddressResponseList?> Addresses() =>
await Get<AddressResponseList>(new Uri($"{BaseUrl}/account/application/addresses")); await Get<AddressResponseList>("/account/application/addresses");
public async Task<StatusPostResponseData?> StatusPost(string address, StatusPost statusPost) => public async Task<StatusPostResponseData?> StatusPost(string address, StatusPost statusPost) =>
await Post<StatusPostResponseData, StatusPost>(new Uri($"{BaseUrl}/address/{address}/statuses"), statusPost); await Post<StatusPostResponseData, StatusPost>($"/address/{address}/statuses", statusPost);
public async Task<List<Pic>> SomePics() => public async Task<List<Pic>> SomePics() =>
(await Get<SomePicsResponseData>(new Uri($"{BaseUrl}/pics")))?.Pics ?? new List<Pic>(); (await Get<SomePicsResponseData>("/pics"))?.Pics ?? new List<Pic>();
public async Task<List<Pic>> SomePics(string address) => public async Task<List<Pic>> SomePics(string address) =>
(await Get<SomePicsResponseData>(new Uri($"{BaseUrl}/address/{address}/pics")))?.Pics ?? new List<Pic>(); (await Get<SomePicsResponseData>($"/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<PutPicResponseData?> PutPic(string address, string base64Image) =>
(await Put<PutPicResponseData, PutPic>($"/address/{address}/pics/upload", new PutPic { Pic = base64Image }));
public async Task<PutPicResponseData?> PutPic(string address, IBrowserFile file) {
byte[] bytes;
using (var memoryStream = new MemoryStream()) {
await file.OpenReadStream().CopyToAsync(memoryStream);
bytes = memoryStream.ToArray();
}
return await PutPic(address, bytes);
}
public async Task<PutPicResponseData?> PutPic(string address, byte[] bytes) =>
await PutPic(address, Convert.ToBase64String(bytes));
public async Task<PutPicResponseData?> PostPicDescription(string address, string id, string description) =>
(await Post<PutPicResponseData, PostPic>($"/address/{address}/pics/{id}", new PostPic { Description = description }));
public async Task<List<string>> Ephemeral() { public async Task<List<string>> Ephemeral() {
List<string> notes = new List<string>(); List<string> notes = new List<string>();
@ -110,7 +158,7 @@ namespace Neighbourhood.omg.lol {
public async Task<string?> OAuth(string code, string client_id, string client_secret, string redirect_uri) { public async Task<string?> OAuth(string code, string client_id, string client_secret, string redirect_uri) {
string? token = null; string? token = null;
Uri uri = new Uri($"{BaseUrl}/oauth/?code={code}&client_id={client_id}&client_secret={client_secret}&redirect_uri={redirect_uri}&scope=everything"); string uri = $"/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) {