Added in pastes (from people)

might still need some work/testing
Also, should I add pastes in the feed?
This commit is contained in:
Gordon Pedersen 2024-07-23 17:02:53 +10:00
parent 278811c2c2
commit a02b14782b
15 changed files with 342 additions and 5 deletions

View file

@ -2,6 +2,7 @@
using Microsoft.AspNetCore.Components.Forms;
using Neighbourhood.omg.lol.Models;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Net.Http.Json;
using System.Text;
@ -240,6 +241,15 @@ namespace Neighbourhood.omg.lol
public async Task<BasicResponseData?> PostProfilePic(string address, FileResult image) =>
await PostBinary<BasicResponseData>($"/address/{address}/pfp", fileResult: image);
public async Task<List<Paste>> GetPastes(string address) =>
(await Get<PastesResponseData>($"/address/{address}/pastebin"))?.Pastebin ?? new List<Paste>();
public async Task<BasicResponseData?> DeletePaste(string address, string title) =>
await Delete<BasicResponseData>($"/address/{address}/pastebin/{title}");
public async Task<PostPasteResponseData?> PostPaste(string address, string title, string content, bool listed) =>
await Post<PostPasteResponseData, Paste>($"/address/{address}/pastebin/", new Paste() { Title = title, Content = content, IsListed = listed });
#endregion
#region Auth

View file

@ -73,6 +73,7 @@ namespace Neighbourhood.omg.lol {
// data for selected address
public List<Status>? CachedAddressStatuses { get; set; }
public List<Pic>? CachedAddressPics { get; set; }
public List<Paste>? CachedAddressPastes { get; set; }
public MarkupString? CachedAddressBio { get; set; }
private string? _cachedAddress;
public string? CachedAddress {
@ -82,6 +83,7 @@ namespace Neighbourhood.omg.lol {
_cachedAddress = value;
CachedAddressStatuses = new List<Status>();
CachedAddressPics = new List<Pic>();
CachedAddressPastes = new List<Paste>();
CachedAddressBio = null;
}
}
@ -284,6 +286,14 @@ namespace Neighbourhood.omg.lol {
return CachedAddressPics;
}
public async Task<List<Paste>?> GetPastes(string address, bool forceRefresh = false) {
CachedAddress = address;
if (forceRefresh || this.CachedAddressPastes == null || this.CachedAddressPastes.Count == 0) {
CachedAddressPastes = (await api.GetPastes(address)) ?? new List<Paste>();
}
return CachedAddressPastes;
}
public async Task RefreshStatuses() {
await GetStatuses(forceRefresh: true);
if(SelectedAddressName != null)
@ -296,6 +306,11 @@ namespace Neighbourhood.omg.lol {
}
public async Task RefreshNow() => await GetNowGarden(forceRefresh: true);
public async Task RefreshPastes() {
if (SelectedAddressName != null)
await GetPastes(SelectedAddressName, forceRefresh: true);
}
public async Task<IOrderedEnumerable<StatusOrPic>> GetFeed(bool forceRefresh = false) {
if(forceRefresh || Feed == null || Feed.Count == 0) {
Feed = new List<StatusOrPic>();

View file

@ -11,7 +11,9 @@
<MarkdownEditor @ref="Editor"
@bind-Value="@Bio"
Theme="material-darker"
MaxHeight="100%">
MaxHeight="100%"
AutoDownloadFontAwesome="false"
>
<Toolbar>
<MarkdownToolbarButton Action="MarkdownAction.Bold" Icon="fa-solid fa-bold" Title="Bold" />
<MarkdownToolbarButton Action="MarkdownAction.Italic" Icon="fa-solid fa-italic" Title="Italic" />

View file

@ -0,0 +1,130 @@
@inject IJSRuntime JS
@inject State State
@inject ApiService api
<div class="overlay" data-ui="#@id"></div>
<dialog id="@id">
<div class="row">
<div class="field text label border max">
<InputText @bind-Value="Title"></InputText>
<label>Content</label>
</div>
</div>
<div class="row">
<div class="field textarea label border max">
<InputTextArea @bind-Value="Content"></InputTextArea>
<label>Content</label>
</div>
</div>
<nav class="no-space">
@if (Paste != null)
{
if (confirmDelete)
{
<button @onclick="ConfirmDeletePaste" disabled="@loading" class="red-7-bg white-fg">
<i class="fa-solid fa-exclamation-triangle"></i> <span>Are you sure?</span>
</button>
}
else
{
<button @onclick="DeletePaste" disabled="@loading" class="red-7-bg white-fg">
<i class="fa-solid fa-trash"></i> <span>Delete</span>
</button>
}
}
<div class="max"></div>
<label class="checkbox">
<InputCheckbox @bind-Value="Listed"></InputCheckbox>
<span>Listed?</span>
</label>
<button class="transparent link" data-ui="#@id" disabled="@loading">Cancel</button>
<button @onclick="PostPaste" disabled="@loading">
@if (loading) {
<span>Saving...</span>
}
else {
<i class="fa-solid fa-floppy-disk"></i> <span>Save</span>
}
</button>
</nav>
</dialog>
@code {
private Paste? _paste;
public Paste? Paste {
get => _paste;
set {
_paste = value;
Title = _paste?.Title;
Content = _paste?.Content;
Listed = _paste?.IsListed ?? false;
InvokeAsync(StateHasChanged);
}
}
public string? Title { get; set; }
public string? Content { get; set; }
public bool Listed { get; set; }
private bool loading = false;
[Parameter]
public string? id { get; set; }
private bool confirmDelete { get; set; }
protected override async Task OnInitializedAsync() {
await base.OnInitializedAsync();
Title = Paste?.Title;
Content = Paste?.Content;
Listed = Paste?.IsListed ?? false;
}
public async Task DeletePaste() {
if (!confirmDelete) confirmDelete = true;
await InvokeAsync(StateHasChanged);
}
public async Task ConfirmDeletePaste() {
if (confirmDelete) {
loading = true;
await InvokeAsync(StateHasChanged);
if (!string.IsNullOrEmpty(Paste?.Title)) {
await api.DeletePaste(State.SelectedAddressName!, Paste.Title);
await State.RefreshPastes();
State.SendRefresh();
await InvokeAsync(StateHasChanged);
}
await JS.InvokeVoidAsync("ui", "#" + id);
// clear input
Title = string.Empty;
Content = string.Empty;
Listed = false;
loading = false;
confirmDelete = false;
await InvokeAsync(StateHasChanged);
}
}
public async Task PostPaste() {
loading = true;
await InvokeAsync(StateHasChanged);
if (!string.IsNullOrEmpty(Paste?.Title)) {
await api.PostPaste(State.SelectedAddressName!, Title, Content, Listed);
await State.RefreshPastes();
State.SendRefresh();
await InvokeAsync(StateHasChanged);
}
await JS.InvokeVoidAsync("ui", "#" + id);
// clear input
Paste = null;
Title = string.Empty;
Content = string.Empty;
Listed = false;
confirmDelete = false;
await InvokeAsync(StateHasChanged);
}
}

View file

@ -11,7 +11,9 @@
@bind-Value="@markdownValue"
Theme="material-darker"
MaxHeight="100%"
CustomButtonClicked="@OnCustomButtonClicked">
CustomButtonClicked="@OnCustomButtonClicked"
AutoDownloadFontAwesome="false"
>
<Toolbar>
<MarkdownToolbarButton Action="MarkdownAction.Bold" Icon="fa-solid fa-bold" Title="Bold" />
<MarkdownToolbarButton Action="MarkdownAction.Italic" Icon="fa-solid fa-italic" Title="Italic" />

View file

@ -11,7 +11,9 @@
@bind-Value="@markdownValue"
Theme="material-darker"
MaxHeight="100%"
CustomButtonClicked="@OnCustomButtonClicked">
CustomButtonClicked="@OnCustomButtonClicked"
AutoDownloadFontAwesome="false"
>
<Toolbar>
<MarkdownToolbarButton Action="MarkdownAction.Bold" Icon="fa-solid fa-bold" Title="Bold" />
<MarkdownToolbarButton Action="MarkdownAction.Italic" Icon="fa-solid fa-italic" Title="Italic" />

View file

@ -51,6 +51,10 @@
<span>/Now</span>
</a>
}
<a data-ui="#pastebin">
<i class="fa-solid fa-clipboard"></i>
<span>Paste.lol</span>
</a>
</div>
</div>
@ -115,6 +119,15 @@
}
</div>
}
<div id="pastebin" class="page padding">
<PasteList @ref="PasteList" PastesFunc="@(async(refresh) => await State.GetPastes(Address, refresh))" Editable="@IsMe"></PasteList>
@if (IsMe) {
<button class="fab circle extra large-elevate" data-ui="#paste-modal">
<i class="fa-solid fa-clipboard"></i>
</button>
<EditPasteDialog id="paste-modal"></EditPasteDialog>
}
</div>
</div>
@code {
@ -126,6 +139,7 @@
_address = value;
if (StatusList != null) StatusList.StatusFunc = async (refresh) => await State.GetStatuses(_address, refresh);
if (PicList != null) PicList.PicsFunc = async (refresh) => await State.GetPics(_address, refresh);
if (PasteList != null) PasteList.PastesFunc = async (refresh) => await State.GetPastes(_address, refresh);
}
}
public string ProfileUrl {
@ -141,6 +155,7 @@
private StatusList? StatusList { get; set; }
private PicList? PicList { get; set; }
private PasteList? PasteList { get; set; }
private bool IsMe {
get => State.AddressList?.Any(a => a.Address == Address) ?? false;

View file

@ -0,0 +1,62 @@
@inject IJSRuntime JS
<article class="paste">
@* TODO: link to paste view *@
<nav>
<h5 class="mono"><a href="/pastes/tbc">@Paste.Title</a></h5>
<div class="max"></div>
@if (MarkupView)
{
<button class="transparent circle" title="View Original" @onclick="() => { MarkupView = false; InvokeAsync(StateHasChanged); }"><i class="fa-solid fa-code"></i></button>
}
else
{
<button class="transparent circle" title="View Markup" @onclick="() => { MarkupView = true; InvokeAsync(StateHasChanged); }"><i class="fa-solid fa-browser"></i></button>
}
<button class="transparent circle" title="Copy to Clipboard" @onclick="() => Clipboard.Default.SetTextAsync(Paste.Content)"><i class="fa-solid fa-copy"></i></button>
<button class="transparent circle" @onclick="ShareClick">
<i class="fa-solid fa-share-nodes"></i>
</button>
</nav>
<small class="nowrap gray-5-fg"><i class="fa-solid fa-clock tiny"></i> @Paste.RelativeTime</small>
@if(MarkupView){
<div class="padding">
@Utilities.MdToHtmlMarkup(Paste.Content)
</div>
}
else {
<pre><code class="padding margin">@((MarkupString)Paste.Content)</code></pre>
}
<nav>
<div class="max"></div>
@if (Editable) {
<button @onclick="EditPaste"><i class="fa-solid fa-pencil"></i> Edit</button>
}
</nav>
</article>
@code {
[Parameter]
public Paste? Paste { get; set; }
[Parameter]
public bool Editable { get; set; } = false;
[Parameter]
public EditPasteDialog? Dialog { get; set; }
private bool MarkupView = false;
private async Task EditPaste(EventArgs e) {
Dialog!.Paste = Paste;
await InvokeAsync(StateHasChanged);
await JS.InvokeVoidAsync("ui", "#" + Dialog?.id);
}
public async Task ShareClick(EventArgs e) {
await Share.Default.RequestAsync(new ShareTextRequest {
Uri = Paste!.Url,
Text = Paste!.Content,
Title = Paste!.Title,
Subject = Paste!.Title
});
}
}

View file

@ -0,0 +1,51 @@
@implements IDisposable
@inject IJSRuntime JS
@inject State State
@if (Editable) {
<EditPasteDialog @ref="Dialog" id="EditPasteModal"></EditPasteDialog>
}
@if (pastes != null) foreach (Paste paste in pastes) {
<PasteCard Paste="paste" Editable="Editable" Dialog="Dialog"></PasteCard>
}
<LoadingCard id="pastes-loading" icon="fa-solid fa-clipboard"></LoadingCard>
@code {
[Parameter]
public Func<bool, Task<List<Paste>?>>? PastesFunc { get; set; }
[Parameter]
public bool Editable { get; set; } = false;
public EditPasteDialog? Dialog { get; set; }
private List<Paste>? pastes;
protected override async Task OnInitializedAsync() {
await base.OnInitializedAsync();
if (PastesFunc == null) return;
if (pastes == null || pastes.Count == 0) pastes = await PastesFunc(false);
State.PropertyChanged += StateChanged;
State.CanRefresh = true;
await InvokeAsync(StateHasChanged);
await JS.InvokeVoidAsync("removeElementById", "pastes-loading");
}
private async void StateChanged(object? sender, PropertyChangedEventArgs e) {
if (PastesFunc == null) return;
if (e.PropertyName == nameof(State.IsRefreshing) && State.IsRefreshing) {
using (State.GetRefreshToken()) {
pastes = await PastesFunc(true);
await InvokeAsync(StateHasChanged);
}
}
}
public void Dispose() {
State.PropertyChanged -= StateChanged;
State.CanRefresh = false;
}
}

View file

@ -0,0 +1,6 @@
namespace Neighbourhood.omg.lol.Models {
public class PastesResponseData : IOmgLolResponseData {
public string Message { get; set; } = string.Empty;
public List<Paste> Pastebin { get; set; } = new List<Paste>();
}
}

View file

@ -0,0 +1,6 @@
namespace Neighbourhood.omg.lol.Models {
public class PostPasteResponseData : IOmgLolResponseData {
public string Message { get; set; } = string.Empty;
public string Title { get; set; } = string.Empty;
}
}

29
Models/Paste.cs Normal file
View file

@ -0,0 +1,29 @@
namespace Neighbourhood.omg.lol.Models {
public class Paste {
public string? Url;
public string Title { get; set; } = string.Empty;
public string Content { get; set; } = string.Empty;
public long? ModifiedOn { get; set; }
public int Listed { get; set; }
public bool IsListed {
get => Listed != 0;
set => Listed = value ? 1 : 0;
}
public DateTimeOffset ModifiedTime { get => DateTimeOffset.UnixEpoch.AddSeconds(ModifiedOn ?? 0); }
public string RelativeTime {
get {
TimeSpan offset = DateTimeOffset.UtcNow - ModifiedTime;
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

@ -35,7 +35,7 @@ menu > details > a:is(:hover,:focus,.active), menu > details > summary:is(:hover
background-color: var(--active)
}
#advanced :is(.field.textarea, textarea), .EasyMDEContainer {
#advanced :is(.field.textarea, textarea), .EasyMDEContainer, article.paste code {
background-color: #212121;
color: #eff
}

View file

@ -145,6 +145,11 @@ article.pic nav { flex-wrap: wrap }
}
}
article.paste code {
overflow-x: scroll;
display: block;
}
iframe {
width: 100%;
flex-grow: 1;

View file

@ -7,7 +7,9 @@ body { font-size: 1.2em }
.address, .author, .honey, .page-heading { font-family: "VC Honey Deck",var(--font) }
#advanced :is(.field.textarea, textarea), .EasyMDEContainer { font-family: 'MD IO 0.4', monospace, var(--emoji-font) }
.mono, #advanced :is(.field.textarea, textarea), .EasyMDEContainer, article.paste code {
font-family: 'MD IO 0.4', monospace, var(--emoji-font)
}
li, p { line-height: 160% }
.author { font-size: 1.2em }