Following and feed

This commit is contained in:
Gordon Pedersen 2024-06-28 17:08:12 +10:00
parent 61c8ed5604
commit efa31a414c
12 changed files with 176 additions and 32 deletions

View file

@ -8,12 +8,6 @@
<i class="square fa-duotone fa-address-book"></i> <i class="square fa-duotone fa-address-book"></i>
<span>Address Directory</span> <span>Address Directory</span>
</a> </a>
@* @if (State.IsAuthorized) {
<a class="m s row" href="/feed">
<i class="square fa-solid fa-list-timeline"></i>
<span>Feed</span>
</a>
} *@
@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="() => State.SelectedAddress = address"> <a class="row @(address == State.SelectedAddress ? "active" : "")" @onclick="() => State.SelectedAddress = address">
<img class="tiny circle avatar" 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" />
@ -38,6 +32,13 @@
</a> </a>
} }
} }
@if (State.FeatureFollowing && State.IsAuthorized) {
<a class="m s row" href="/feed">
<i class="square fa-solid fa-list-timeline"></i>
<span>Feed</span>
</a>
<FollowingList class="m s"></FollowingList>
}
@if (State.IsAuthorized) { @if (State.IsAuthorized) {
<a class="row" @onclick='() => AuthStateProvider.Logout()'> <a class="row" @onclick='() => AuthStateProvider.Logout()'>
<i class="fa-solid fa-door-open"></i> <i class="fa-solid fa-door-open"></i>

View file

@ -0,0 +1,23 @@
@inject State State
@if (State.Following != null) {
<details class="@(@class)">
<summary class="none">
<a class="row">
<i class="square fa-solid fa-caret-down"></i>
<span>Following</span>
</a>
</summary>
@foreach (string address in State.Following) {
<a class="transparent button horizontal-padding indent row" href="/person/@address">
<img class="tiny circle avatar" src="https://profiles.cache.lol/@address/picture" />
<span class="address"><i class="fa-solid fa-fw fa-at tiny"></i>@address</span>
</a>
}
</details>
}
@code {
[Parameter]
public string @class { get; set; }
}

View file

@ -1,4 +1,5 @@
<NavLink class="nav-link" href="/statuslog/latest"> @inject State State
<NavLink class="nav-link" href="/statuslog/latest">
<i class="square fa-solid fa-message-smile"></i> <i class="square fa-solid fa-message-smile"></i>
<div class="label">Status.lol</div> <div class="label">Status.lol</div>
</NavLink> </NavLink>
@ -20,11 +21,14 @@
<i class="square fa-duotone fa-address-book"></i> <i class="square fa-duotone fa-address-book"></i>
<div class="label">Address Directory</div> <div class="label">Address Directory</div>
</NavLink> </NavLink>
@* <AuthorizeView> @if (State.FeatureFollowing) {
<Authorized> <AuthorizeView>
<NavLink class="l nav-link" href="/feed"> <Authorized>
<i class="square fa-solid fa-list-timeline"></i> <NavLink class="l nav-link" href="/feed">
<div class="label">Feed</div> <i class="square fa-solid fa-list-timeline"></i>
</NavLink> <div class="label">Feed</div>
</Authorized> </NavLink>
</AuthorizeView> *@ <FollowingList class="l"></FollowingList>
</Authorized>
</AuthorizeView>
}

View file

@ -1,8 +1,48 @@
@page "/feed" @page "/feed"
<h3>Feed</h3> @implements IDisposable
@inject IJSRuntime JS
@inject State State
WIP <RefreshButton></RefreshButton>
<PageHeading title="Feed" icon="fa-solid fa-list-timeline">
<Description>A feed of all the statuses and pics of the people you follow.</Description>
</PageHeading>
@if (feed != null) foreach (StatusOrPic item in feed) {
if (item.IsStatus) {
<StatusCard Status="@item.Status"></StatusCard>
}
else if (item.IsPic) {
<PicCard Pic="@item.Pic"></PicCard>
}
}
<LoadingCard id="feedLoading" icon="fa-solid fa-list-timeline"></LoadingCard>
@code { @code {
private IOrderedEnumerable<StatusOrPic>? feed;
protected override async Task OnInitializedAsync() {
await base.OnInitializedAsync();
if (feed == null || feed.Count() == 0) feed = await State.GetFeed();
State.PropertyChanged += StateChanged;
State.CanRefresh = true;
await InvokeAsync(StateHasChanged);
await JS.InvokeVoidAsync("removeElementById", "feedLoading");
}
private async void StateChanged(object? sender, PropertyChangedEventArgs e) {
if (e.PropertyName == nameof(State.IsRefreshing) && State.IsRefreshing) {
using (State.GetRefreshToken()) {
feed = await State.GetFeed(true);
await InvokeAsync(StateHasChanged);
}
}
}
public void Dispose() {
State.PropertyChanged -= StateChanged;
State.CanRefresh = false;
}
} }

View file

@ -5,14 +5,27 @@
<RefreshButton></RefreshButton> <RefreshButton></RefreshButton>
<div class="row center-align"> <div class="row center-align">
<h3 class="page-heading"><i class="fa-solid fa-fw fa-at"></i>@Address</h3> <h3 class="page-heading"><i class="fa-solid fa-fw fa-at"></i>@Address</h3>
</div> </div>
<div class="row center-align"> <div class="row center-align">
<img class="profile avatar" src="https://profiles.cache.lol/@Address/picture" alt="@Address" /> <img class="profile avatar" src="https://profiles.cache.lol/@Address/picture" alt="@Address" />
</div> </div>
@if (State.FeatureFollowing)
{
<div class="row center-align">
@if (State.IsFollowing(Address)) {
<button id="follow-button" @onclick="() => {State.Unfollow(Address);InvokeAsync(StateHasChanged);}">
<i class="fa-solid fa-minus"></i> Unfollow
</button>
}
else {
<button id="follow-button" @onclick="() => {State.Follow(Address);InvokeAsync(StateHasChanged);}">
<i class="fa-solid fa-plus"></i> Follow
</button>
}
</div>
}
<div class="responsive"> <div class="responsive">
<div class="tabs scroll"> <div class="tabs scroll">
<a data-ui="#profile" @onclick="ReloadProfile"> <a data-ui="#profile" @onclick="ReloadProfile">

View file

@ -19,13 +19,14 @@ namespace Neighbourhood.omg.lol.Models {
public string Description { get; set; } public string Description { get; set; }
public string DescriptionHtml { get => Description == null ? string.Empty : Utilities.MdToHtml(Description); } public string DescriptionHtml { get => Description == null ? string.Empty : Utilities.MdToHtml(Description); }
public DateTimeOffset CreatedTime { get => DateTimeOffset.UnixEpoch.AddSeconds(Created); }
[JsonPropertyName("exif")] [JsonPropertyName("exif")]
public JsonElement ExifJson { get; set; } public JsonElement ExifJson { get; set; }
public string RelativeTime { public string RelativeTime {
get { get {
DateTimeOffset createdTime = DateTimeOffset.UnixEpoch.AddSeconds(Created); TimeSpan offset = DateTimeOffset.UtcNow - CreatedTime;
TimeSpan offset = DateTimeOffset.UtcNow - createdTime;
var offsetString = string.Empty; var offsetString = string.Empty;
if (offset.TotalDays >= 1) offsetString = $"{Math.Floor(offset.TotalDays)} days ago"; if (offset.TotalDays >= 1) offsetString = $"{Math.Floor(offset.TotalDays)} days ago";

View file

@ -5,6 +5,8 @@ using System.Text.Json;
namespace Neighbourhood.omg.lol.Models { namespace Neighbourhood.omg.lol.Models {
public class State : INotifyPropertyChanged { public class State : INotifyPropertyChanged {
// Feature flags
public bool FeatureFollowing { get; } = true;
// Main data lists // Main data lists
public List<Status>? Statuses { get; set; } public List<Status>? Statuses { get; set; }
public List<Pic>? Pics { get; set; } public List<Pic>? Pics { get; set; }
@ -12,6 +14,8 @@ namespace Neighbourhood.omg.lol.Models {
public List<MarkupString>? EphemeralMessages { get; set; } public List<MarkupString>? EphemeralMessages { get; set; }
public List<string>? AddressDirectory { get; set; } public List<string>? AddressDirectory { get; set; }
public List<StatusOrPic>? Feed { get; set; }
// Account data // Account data
public AccountResponseData? AccountInfo { get; set; } public AccountResponseData? AccountInfo { get; set; }
public AddressResponseList? AddressList { get; set; } public AddressResponseList? AddressList { get; set; }
@ -42,6 +46,8 @@ namespace Neighbourhood.omg.lol.Models {
} }
} }
} }
public List<string>? Following { get; private set; }
public string? SelectedAddressName { get => SelectedAddress?.Address; } public string? SelectedAddressName { get => SelectedAddress?.Address; }
// data for selected address // data for selected address
@ -143,10 +149,12 @@ namespace Neighbourhood.omg.lol.Models {
string accountJson = Preferences.Default.Get("accountdetails", string.Empty); string accountJson = Preferences.Default.Get("accountdetails", string.Empty);
string addressJson = Preferences.Default.Get("accountaddresses", string.Empty); string addressJson = Preferences.Default.Get("accountaddresses", string.Empty);
string selectedAddressJson = Preferences.Default.Get("selectedaddress", string.Empty); string selectedAddressJson = Preferences.Default.Get("selectedaddress", string.Empty);
string followingJson = Preferences.Default.Get("following", string.Empty);
if (!string.IsNullOrEmpty(accountJson)) AccountInfo = JsonSerializer.Deserialize<AccountResponseData>(accountJson); if (!string.IsNullOrEmpty(accountJson)) AccountInfo = JsonSerializer.Deserialize<AccountResponseData>(accountJson);
if (!string.IsNullOrEmpty(addressJson)) AddressList = JsonSerializer.Deserialize<AddressResponseList>(addressJson); if (!string.IsNullOrEmpty(addressJson)) AddressList = JsonSerializer.Deserialize<AddressResponseList>(addressJson);
if (!string.IsNullOrEmpty(selectedAddressJson)) SelectedAddress = JsonSerializer.Deserialize<AddressResponseData>(selectedAddressJson); if (!string.IsNullOrEmpty(selectedAddressJson)) SelectedAddress = JsonSerializer.Deserialize<AddressResponseData>(selectedAddressJson);
if (!string.IsNullOrEmpty(followingJson)) Following = JsonSerializer.Deserialize<List<string>>(followingJson);
// if we haven't got account info, attempt to retrieve it. // if we haven't got account info, attempt to retrieve it.
if (AccountInfo == null) { if (AccountInfo == null) {
@ -174,9 +182,23 @@ namespace Neighbourhood.omg.lol.Models {
AccountInfo = null; AccountInfo = null;
AddressList = null; AddressList = null;
SelectedAddress = null; SelectedAddress = null;
Following = null;
api.RemoveToken(); api.RemoveToken();
} }
public bool IsFollowing(string address) => Following?.Contains(address) ?? false;
public void Follow(string address) {
if (Following == null) Following = new List<string>();
Following.Add(address);
Preferences.Default.Set("following", JsonSerializer.Serialize(Following));
}
public void Unfollow(string address) {
if (Following == null) Following = new List<string>();
Following.Remove(address);
Preferences.Default.Set("following", JsonSerializer.Serialize(Following));
}
public async Task<MarkupString?> GetBio(string address, bool forceRefresh = false) { public async Task<MarkupString?> GetBio(string address, bool forceRefresh = false) {
CachedAddress = address; CachedAddress = address;
if (forceRefresh || CachedAddressBio == null) { if (forceRefresh || CachedAddressBio == null) {
@ -252,5 +274,16 @@ namespace Neighbourhood.omg.lol.Models {
} }
public async Task RefreshNow() => await GetNowGarden(forceRefresh: true); public async Task RefreshNow() => await GetNowGarden(forceRefresh: true);
public async Task<IOrderedEnumerable<StatusOrPic>> GetFeed(bool forceRefresh = false) {
if(forceRefresh || Feed == null || Feed.Count == 0) {
Feed = new List<StatusOrPic>();
foreach(string address in Following ?? new List<string>()) {
Feed.AddRange((await GetStatuses(address, forceRefresh))?.Select(s => new StatusOrPic { Status = s }) ?? new List<StatusOrPic>());
Feed.AddRange((await GetPics(address, forceRefresh))?.Select(p => new StatusOrPic { Pic = p }) ?? new List<StatusOrPic>());
}
}
return Feed.OrderByDescending(s => s.CreatedTime);
}
} }
} }

View file

@ -14,6 +14,8 @@ namespace Neighbourhood.omg.lol.Models {
public string RenderedMarkdown { get; set; } public string RenderedMarkdown { get; set; }
public string ExternalUrl { get; set; } public string ExternalUrl { get; set; }
public DateTimeOffset CreatedTime { get => DateTimeOffset.UnixEpoch.AddSeconds(Convert.ToInt64(Created)); }
public string EmojiOrDefault { public string EmojiOrDefault {
get { get {
return string.IsNullOrEmpty(Emoji) ? "✨" : Emoji; return string.IsNullOrEmpty(Emoji) ? "✨" : Emoji;

17
Models/StatusOrPic.cs Normal file
View file

@ -0,0 +1,17 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Neighbourhood.omg.lol.Models {
public class StatusOrPic {
public Status? Status { get; set; }
public Pic? Pic { get; set; }
public bool IsStatus { get => Status != null; }
public bool IsPic { get => Pic != null; }
public DateTimeOffset? CreatedTime { get => Status?.CreatedTime ?? Pic?.CreatedTime; }
}
}

View file

@ -28,8 +28,8 @@
<ApplicationId>com.companyname.neighbourhood.omg.lol</ApplicationId> <ApplicationId>com.companyname.neighbourhood.omg.lol</ApplicationId>
<!-- Versions --> <!-- Versions -->
<ApplicationDisplayVersion>1.0</ApplicationDisplayVersion> <ApplicationDisplayVersion>0.9.1</ApplicationDisplayVersion>
<ApplicationVersion>1</ApplicationVersion> <ApplicationVersion>3</ApplicationVersion>
<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'ios'">14.2</SupportedOSPlatformVersion> <SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'ios'">14.2</SupportedOSPlatformVersion>
<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'maccatalyst'">14.0</SupportedOSPlatformVersion> <SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'maccatalyst'">14.0</SupportedOSPlatformVersion>
@ -44,13 +44,13 @@
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Debug|net8.0-ios|AnyCPU'"> <PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Debug|net8.0-ios|AnyCPU'">
<ApplicationId>au.death.lol.omg.neighbourhood</ApplicationId> <ApplicationId>au.death.lol.omg.neighbourhood</ApplicationId>
<ApplicationDisplayVersion>0.9.1</ApplicationDisplayVersion> <ApplicationDisplayVersion>0.9.1</ApplicationDisplayVersion>
<ApplicationVersion>2</ApplicationVersion> <ApplicationVersion>3</ApplicationVersion>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Debug|net8.0-maccatalyst|AnyCPU'"> <PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Debug|net8.0-maccatalyst|AnyCPU'">
<ApplicationId>au.death.lol.omg.neighbourhood</ApplicationId> <ApplicationId>au.death.lol.omg.neighbourhood</ApplicationId>
<ApplicationDisplayVersion>0.9.1</ApplicationDisplayVersion> <ApplicationDisplayVersion>0.9.1</ApplicationDisplayVersion>
<ApplicationVersion>2</ApplicationVersion> <ApplicationVersion>3</ApplicationVersion>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Debug|net8.0-android34.0|AnyCPU'"> <PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Debug|net8.0-android34.0|AnyCPU'">
@ -58,7 +58,7 @@
<ApplicationDisplayVersion>0.9.1</ApplicationDisplayVersion> <ApplicationDisplayVersion>0.9.1</ApplicationDisplayVersion>
<AndroidKeyStore>True</AndroidKeyStore> <AndroidKeyStore>True</AndroidKeyStore>
<AndroidSigningKeyStore>D:\Neighbourhood.omg.lol\neighbourhood.omg.lol.keystore</AndroidSigningKeyStore> <AndroidSigningKeyStore>D:\Neighbourhood.omg.lol\neighbourhood.omg.lol.keystore</AndroidSigningKeyStore>
<ApplicationVersion>2</ApplicationVersion> <ApplicationVersion>3</ApplicationVersion>
<AndroidSigningStorePass>a!zobzizl</AndroidSigningStorePass> <AndroidSigningStorePass>a!zobzizl</AndroidSigningStorePass>
<AndroidSigningKeyAlias>neighbourhood.omg.lol</AndroidSigningKeyAlias> <AndroidSigningKeyAlias>neighbourhood.omg.lol</AndroidSigningKeyAlias>
<AndroidSigningKeyPass>a!zobzizl</AndroidSigningKeyPass> <AndroidSigningKeyPass>a!zobzizl</AndroidSigningKeyPass>
@ -67,31 +67,31 @@
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Debug|net8.0-windows10.0.19041.0|AnyCPU'"> <PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Debug|net8.0-windows10.0.19041.0|AnyCPU'">
<ApplicationId>au.death.lol.omg.neighbourhood</ApplicationId> <ApplicationId>au.death.lol.omg.neighbourhood</ApplicationId>
<ApplicationDisplayVersion>0.9.1</ApplicationDisplayVersion> <ApplicationDisplayVersion>0.9.1</ApplicationDisplayVersion>
<ApplicationVersion>2</ApplicationVersion> <ApplicationVersion>3</ApplicationVersion>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Release|net8.0-ios|AnyCPU'"> <PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Release|net8.0-ios|AnyCPU'">
<ApplicationId>au.death.lol.omg.neighbourhood</ApplicationId> <ApplicationId>au.death.lol.omg.neighbourhood</ApplicationId>
<ApplicationDisplayVersion>0.9.1</ApplicationDisplayVersion> <ApplicationDisplayVersion>0.9.1</ApplicationDisplayVersion>
<ApplicationVersion>2</ApplicationVersion> <ApplicationVersion>3</ApplicationVersion>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Release|net8.0-maccatalyst|AnyCPU'"> <PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Release|net8.0-maccatalyst|AnyCPU'">
<ApplicationId>au.death.lol.omg.neighbourhood</ApplicationId> <ApplicationId>au.death.lol.omg.neighbourhood</ApplicationId>
<ApplicationDisplayVersion>0.9.1</ApplicationDisplayVersion> <ApplicationDisplayVersion>0.9.1</ApplicationDisplayVersion>
<ApplicationVersion>2</ApplicationVersion> <ApplicationVersion>3</ApplicationVersion>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Release|net8.0-android34.0|AnyCPU'"> <PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Release|net8.0-android34.0|AnyCPU'">
<ApplicationId>au.death.lol.omg.neighbourhood</ApplicationId> <ApplicationId>au.death.lol.omg.neighbourhood</ApplicationId>
<ApplicationDisplayVersion>0.9.1</ApplicationDisplayVersion> <ApplicationDisplayVersion>0.9.1</ApplicationDisplayVersion>
<ApplicationVersion>2</ApplicationVersion> <ApplicationVersion>3</ApplicationVersion>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Release|net8.0-windows10.0.19041.0|AnyCPU'"> <PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Release|net8.0-windows10.0.19041.0|AnyCPU'">
<ApplicationId>au.death.lol.omg.neighbourhood</ApplicationId> <ApplicationId>au.death.lol.omg.neighbourhood</ApplicationId>
<ApplicationDisplayVersion>0.9.1</ApplicationDisplayVersion> <ApplicationDisplayVersion>0.9.1</ApplicationDisplayVersion>
<ApplicationVersion>2</ApplicationVersion> <ApplicationVersion>3</ApplicationVersion>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>

View file

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="au.death.lol.omg.neighbourhood" android:versionCode="2" android:versionName="0.9.0"> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="au.death.lol.omg.neighbourhood" android:versionCode="3" android:versionName="0.9.1">
<application android:allowBackup="true" android:icon="@mipmap/icon_background" android:supportsRtl="true" android:label="omg.lol"></application> <application android:allowBackup="true" android:icon="@mipmap/icon_background" android:supportsRtl="true" android:label="omg.lol"></application>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />

View file

@ -464,4 +464,14 @@ article {
nav label:is(.checkbox, .radio, .switch) { nav label:is(.checkbox, .radio, .switch) {
white-space: break-spaces; white-space: break-spaces;
flex: 1 1 100%; flex: 1 1 100%;
}
menu > details .row, menu > li > details .row {
padding: .5rem 1rem;
min-block-size: 3rem;
flex: 1;
}
menu > details > a:is(:hover,:focus,.active), menu > details > summary:is(:hover,:focus,.active) {
background-color: var(--active);
} }