Compare commits

...

44 commits
0.9.1 ... main

Author SHA1 Message Date
b77aac8c57 decided against adding api_key and settings to AccountResponseData (for now) 2025-02-11 09:49:24 +11:00
518a42b20a Setup imdone for task management 2025-02-10 20:52:51 +11:00
023c077b2b Minor dependency update to resolve vulnerability 2025-02-10 20:42:40 +11:00
4912ede42c minor styling fixes 2025-02-10 16:36:26 +11:00
cf3931f529 Fixed an issue with editing pastes getting the dialog stuck 2025-02-10 16:35:22 +11:00
d142010786 Bumped version number 2024-07-25 16:37:11 +10:00
8d3a454a6b changed the add paste icon 2024-07-25 16:36:56 +10:00
b089cb39e6 colour tweaks 2024-07-25 16:09:37 +10:00
72c6b590f1 Light theme! 2024-07-25 15:54:06 +10:00
b074b9d453 Added a "toast" message when copying a paste 2024-07-25 13:59:57 +10:00
1fe82c18af Added Pastes into the feed 2024-07-25 13:39:36 +10:00
04a5641650 fix for weird border on icon on startup 2024-07-25 13:28:17 +10:00
a25df4368e Fix for retrieving pastes while logged in 2024-07-25 13:27:48 +10:00
d14ee65af4 bumped version number 2024-07-23 17:20:39 +10:00
a02b14782b Added in pastes (from people)
might still need some work/testing
Also, should I add pastes in the feed?
2024-07-23 17:02:53 +10:00
278811c2c2 added icon back in for ios 2024-07-23 13:55:35 +10:00
85bc105ddd Package updates 2024-07-23 12:16:21 +10:00
6e5b206c20 Enable edit statuslog bio 2024-07-23 11:12:52 +10:00
909038ff79 Tidy up of the API service 2024-07-16 12:15:56 +10:00
cc779051bf Renamed RestService -> ApiService 2024-07-16 10:45:33 +10:00
cee1c3dfd2 cleaning up the global javascript 2024-07-16 10:42:43 +10:00
905257789b removed some superfluous styles from app.css 2024-07-16 10:38:57 +10:00
e017269685 More css reorg
Split out some separation of concerns:
- color
- font/typography
- icon/emoji related
- z-indexes
- the rest (mostly layout)
2024-07-16 10:35:01 +10:00
859f8348f0 Added character counter and alert for new statuses 2024-07-16 09:47:42 +10:00
1bf7e74a5a (in progress) cleaning up css 2024-07-12 17:08:22 +10:00
640cd73271 made the single animated emoji I'm using local 2024-07-12 15:16:46 +10:00
bdea7870ea Bumped version
I put the patch version in line with the single digit version for simplicity's sake.
It does mean I missed 0.9.5-0.9.6, but whatever.
2024-07-12 15:11:01 +10:00
7b462e3cfc Trimming some fat 2024-07-12 15:08:12 +10:00
2dc66abcd7 Fixed iframe resizer version @5.1.5 2024-07-12 14:44:19 +10:00
3d8047c01c Substituted the TossFace emoji font for Twemoji
Not only that, but a subset of Twemoji covering just the flags, bringing font size down dramatically.
2024-07-11 17:44:58 +10:00
f24ef392f7 Edit profile / profile pic 2024-07-11 15:57:53 +10:00
0fcda98b9f Customizing the markdown editor toolbar 2024-07-05 17:18:46 +10:00
25f362bfc5 Added a way to edit the profile page
Including custom css and head content, but not custom themes or metadata (yet)
2024-07-05 15:47:05 +10:00
b42bf2023d Fixed feed icon not appearing
+version bump
2024-07-04 10:21:25 +10:00
a5d860c916 Bumped version to 0.9.3 2024-07-02 13:02:16 +10:00
93b49f2533 Tidying up the feed feature 2024-07-02 12:55:07 +10:00
9c3f578eb6 Added Toss Face Emoji font specifically for flags 2024-07-02 12:04:52 +10:00
2274c34f07 Cleaned up more warnings 2024-07-02 10:29:50 +10:00
822bf52407 Clearing out some more warnings 2024-07-02 10:13:52 +10:00
fd0300fccb More reorg, cleaning up some warnings 2024-07-02 10:01:06 +10:00
1b0155d064 Some basic project tidy up 2024-07-02 09:19:37 +10:00
908f126987 bump version 2024-07-01 10:04:12 +10:00
b1cb4e21c4 Proper Ephemeral Integration 🥳 2024-07-01 09:44:55 +10:00
efa31a414c Following and feed 2024-06-28 17:08:12 +10:00
175 changed files with 2836 additions and 12420 deletions

View file

@ -0,0 +1,11 @@
#DONE update theme
<!--
order:-10
completed:2025-02-10T17:02:46+11:00
archived:true
archivedAt:2025-02-10T17:02:46+11:00
originalPath:Components\ThemeDialog.razor
originalLine:76
-->

View file

@ -0,0 +1,11 @@
#DONE upload the profile pic
<!--
order:0
completed:2025-02-10T16:51:21+11:00
archived:true
archivedAt:2025-02-10T16:51:21+11:00
originalPath:Components\EditProfilePicDialog.razor
originalLine:60
-->

61
.imdone/DONE/backlog.md Normal file
View file

@ -0,0 +1,61 @@
## Must Haves
- {check} View [latest statuslog entries](https://api.omg.lol/statuslog/latest)
- {check} View [all statuses of a single person](https://api.omg.lol/address/adam/statuses) (get [profile picture](https://profiles.cache.lol/adam/picture) and [statuslog bio](https://api.omg.lol/address/adam/statuses/bio)) Note: I'm calling this the profile page (even though omg.lol profile is a different thing)
- {check} [Log in](https://home.omg.lol/oauth/authorize?client_id=ea14dafd3e92cbcf93750c35cd81a031&scope=everything&redirect_uri=https://neatnik.net/adam/bucket/omgloloauth/&response_type=code) and [Authenticate](https://api.omg.lol/#token-get-oauth-exchange-an-authorization-code-for-an-access-token) (then [get all addresses](https://api.omg.lol/account/application/addresses) so we can pick one for other interactions)
- {check} Post a [new status](https://api.omg.lol/#token-post-statuslog-share-a-new-status) (checkbox for posting to mastodon)
- {check} Log out
- {check} Light/Dark themes (based on system theme)
## Should Haves
- {check} Share statuses, etc.
- {check} Have a character counter on statuses and a warning if going over length for posting to Mastodon.
- {check} Be a share target for creating statuses
- {check} View the [address directory](https://api.omg.lol/directory) (showing profile pics and linking to profile page)
- {check} Link to it via the account menu (There's not a lot of room in the nav)
- {check} View the [now garden](https://api.omg.lol/now/garden) (also, perhaps cache the now garden and link to the now page on a person's profile)
- {check} Updated profile page. Shows:
- {check} [profile picture](https://profiles.cache.lol/adam/picture)
- {check} [statuslog bio](https://api.omg.lol/address/adam/statuses/bio) text
- {check} [all statuses](https://api.omg.lol/address/adam/statuses)
- {check} Link to now page (if present in [now garden](https://api.omg.lol/now/garden))
- {check} Link to profile page (aka web page)
- {check} Link to person's some.pics
- {check} Link to person's pastebin
## Want to Haves
- {check} [Some.pics feed](https://api.omg.lol/pics) (plus seeing the some.pics of individuals, link on profile)
- {check} Be a share target for pictures
- {check} [Ephemeral feed](https://eph.emer.al/)
- {check} plus posting - ~~if/when an API becomes available~~ (Thanks Adam 😁)
- {check} Upload pics
- {check} Edit some.pics
- {check} delete pics
- {check} Edit statuses
- {check} delete statuses
- {check} Update / manage [now page](https://api.omg.lol/#now-page)
- {square} pull to refresh
- {check} Follow people (i.e. locally bookmark their statuslog profile)
- {check} A combined feed of all statuses and pics of everyone you're following
## Nice to Haves
- {check} Update profile picture
- {check} Update / manage statuslog bio
- {check} Update / manage [profile/web page](https://api.omg.lol/#web)
- {check} including [themes](https://api.omg.lol/#theme)
- {check} Update / manage [pastebin](https://api.omg.lol/#pastebin)
- {check} share and copy items
- {check} view as markup
- {check} visible in profile page
- {check} visible in feed
## Current Bugs
- {check} ~~Sharing to app multiple times throws an exception~~
- {check} ~~Need to update "Loading", "Logging in" and "nothing here" pages to match the splash screen (ish)~~
- {check} ~~Empty bio on person/statuses (just remove the div if the bio is empty)~~
- {check} ~~Need warnings on pics with no description~~
- {check} ~~respond appears on statuses with no external link~~
- {check} ~~statuses / pics don't refresh on update/delete~~
- {check} ~~own now page isn't showing properly in profile~~
- {check} ~~statuses with long words or urls won't wrap.~~
- {check} ~~Ephemeral scraping doesn't send a user agent string, so no longer works.~~

12
.imdone/DONE/test.md Normal file
View file

@ -0,0 +1,12 @@
#DONE test
<!--
created:2025-02-10T20:44:27+11:00
order:-20
completed:2025-02-10T20:44:57+11:00
archived:true
archivedAt:2025-02-10T20:44:57+11:00
originalPath:backlog.md
originalLine:64
-->

16
.imdone/actions/board.js Normal file
View file

@ -0,0 +1,16 @@
const path = require('path')
module.exports = function () {
const project = this.project
return [
{
title: "Open in vscode", // This is what displays in the main menu
keys: ['alt+o'], // This is the keyboard shortcut
icon: "code", // This is the font awesome icon that displays in the main menu
action (task) {
const url = `vscode://file/${path.join(project.path, task.path)}:${task.line}`
project.openUrl(url)
}
}
]
}

4
.imdone/actions/card.js Normal file
View file

@ -0,0 +1,4 @@
module.exports = function (task) {
const project = this.project
return []
}

101
.imdone/config.yml Normal file
View file

@ -0,0 +1,101 @@
keepEmptyPriority: false
languages:
.razor:
name: razor
symbol: "//"
block:
start: "@*"
end: "*@"
ignore: "*"
code:
include_lists:
- TODO
- DOING
- DONE
- PLANNING
- FIXME
- ARCHIVE
- HACK
- CHANGED
- XXX
- IDEA
- NOTE
- REVIEW
- WAITING
lists:
- name: NOTE
hidden: false
id: 9886o1muwm6yiizyq
- name: Past Due Reminders
hidden: true
ignore: false
filter: 'remind = /./ and remind < "${now}" and list != DONE -remind'
id: 9886o1muwm6yiizyr
- name: What's Due?
hidden: true
ignore: false
filter: 'dueDate < "${in 15 days}" AND list != DONE +dueDate +order'
id: 9886o1muwm6yiizys
- name: WAITING
hidden: false
ignore: false
id: 9886o10uwm6yovnxl
- name: TODO
hidden: false
id: 9886o1muwm6yiizyt
- name: DOING
hidden: false
id: 9886o1muwm6yiizyu
- name: DONE
hidden: false
ignore: true
id: 9886o1muwm6yiizyv
- name: Recently Completed
filter: 'completedDate > "${14 days ago}" -completed'
hidden: false
id: 9886o1muwm6yiizyw
settings:
'0': object Object
openIn: default
openCodeIn: default
journalType: Single File
journalPath: null
appendNewCardsTo: backlog.md
newCardSyntax: MARKDOWN
replaceSpacesWith: '-'
plugins: {}
journalTemplate: null
markdownOnly: false
kudosProbability: 0.33
views: []
name: Neighbourhood.omg.lol
cards:
colors:
- color: red
filter: tags = "BUG"
- color: black
filter: tags = "Someday"
- color: green
filter: tags = "WantToHave"
template: |
<!--
created:${timestamp}
-->
trackChanges: false
metaNewLine: true
addCompletedMeta: true
addCheckBoxTasks: false
doneList: DONE
tokenPrefix: '#'
taskPrefix: ''
tagPrefix: '#'
metaSep: ':'
orderMeta: true
maxLines: 6
addNewCardsToTop: true
showTagsAndMeta: false
defaultList: TODO
computed: !<tag:yaml.org,2002:js/undefined> ''
archiveCompleted: true
archiveFolder: .imdone/DONE

124
.imdone/properties/card.js Normal file
View file

@ -0,0 +1,124 @@
let updatedAt = new Date()
module.exports = function ({ line, source, totals }) {
const project = this.project
const emoji = {
due: dueEmoji(totals),
recent: recentEmoji(totals),
wip: wipEmoji(totals),
chart: EMOJI.CHART
}
// These are the properties that are available to use in your cards
// Use ${property_name} to permanently insert the value of the property
// Use {{property_name}} to insert the value of the property at runtime
return {
date: `${new Date().toISOString().substring(0, 10)}`,
sourceLink: `[${source.path}:${line}](${source.path}:${line})`,
cardTotal: cardTotal(totals),
allTopics: project.allTopics, // This is an array of all the topics in the project
topicTable: getTopicTable(project), // This is a markdown table with the count of tasks for each topic/list intersection
emoji,
icons
}
}
const icons = {
filter: `<span class="icon is-small fa-xs"><svg aria-hidden="true" focusable="false" data-prefix="fa" data-icon="search" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" class="svg-inline--fa fa-search fa-w-16"><path fill="currentColor" d="M505 442.7L405.3 343c-4.5-4.5-10.6-7-17-7H372c27.6-35.3 44-79.7 44-128C416 93.1 322.9 0 208 0S0 93.1 0 208s93.1 208 208 208c48.3 0 92.7-16.4 128-44v16.3c0 6.4 2.5 12.5 7 17l99.7 99.7c9.4 9.4 24.6 9.4 33.9 0l28.3-28.3c9.4-9.4 9.4-24.6.1-34zM208 336c-70.7 0-128-57.2-128-128 0-70.7 57.2-128 128-128 70.7 0 128 57.2 128 128 0 70.7-57.2 128-128 128z" class=""></path></svg></span><span data-v-fd981bec="" class="icon is-small fa-xs"><svg aria-hidden="true" focusable="false" data-prefix="fa" data-icon="chevron-down" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512" class="svg-inline--fa fa-chevron-down fa-w-14"><path fill="currentColor" d="M207.029 381.476L12.686 187.132c-9.373-9.373-9.373-24.569 0-33.941l22.667-22.667c9.357-9.357 24.522-9.375 33.901-.04L224 284.505l154.745-154.021c9.379-9.335 24.544-9.317 33.901.04l22.667 22.667c9.373 9.373 9.373 24.569 0 33.941L240.971 381.476c-9.373 9.372-24.569 9.372-33.942 0z" class=""></path></svg></span>`
,openFile: `<span class="icon is-medium"><svg version="1.1" width="16" height="16" viewBox="0 0 16 16" aria-hidden="true" class="octicon octicon-link"><path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg></span>`
,kebab: `<span class="icon is-medium"><svg version="1.1" width="3" height="16" viewBox="0 0 3 16" aria-hidden="true" class="octicon octicon-kebab-vertical"><path data-v-5bf4cb66="" fill-rule="evenodd" d="M0 2.5a1.5 1.5 0 1 0 3 0 1.5 1.5 0 0 0-3 0zm0 5a1.5 1.5 0 1 0 3 0 1.5 1.5 0 0 0-3 0zM1.5 14a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3z"></path></svg></span>`
,clone: `<span class="icon copy-button is-medium" style=""><svg aria-hidden="true" focusable="false" data-prefix="fa" data-icon="clone" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" class="svg-inline--fa fa-clone fa-w-16 fa-lg"><path fill="currentColor" d="M464 0c26.51 0 48 21.49 48 48v288c0 26.51-21.49 48-48 48H176c-26.51 0-48-21.49-48-48V48c0-26.51 21.49-48 48-48h288M176 416c-44.112 0-80-35.888-80-80V128H48c-26.51 0-48 21.49-48 48v288c0 26.51 21.49 48 48 48h288c26.51 0 48-21.49 48-48v-48H176z" class=""></path></svg></span>`
,editCard: `<span class="icon is-medium"><svg version="1.1" width="14" height="16" viewBox="0 0 14 16" aria-hidden="true" class="octicon octicon-pencil"><path fill-rule="evenodd" d="M0 12v3h3l8-8-3-3-8 8zm3 2H1v-2h1v1h1v1zm10.3-9.3L12 6 9 3l1.3-1.3a.996.996 0 0 1 1.41 0l1.59 1.59c.39.39.39 1.02 0 1.41z"></path></svg></span>`
}
const EMOJI = {
BAD: ':rotating_light:',
GREAT: ':rocket:',
SLEEP: ':sleeping:',
GOOD: ':2nd_place_medal:',
CHART: '<span style="font-size: 1.5em;">:chart:</span>'
}
function formatEmoji(emoji) {
return `<span style="font-size: 1.5em;">${emoji}</span>`
}
function dueEmoji(totals) {
const due = totals["What's Due?"]
let emoji = EMOJI.GOOD
if (due >= 3) {
emoji = EMOJI.BAD
} else if (due === 0) {
emoji = EMOJI.GREAT
}
return formatEmoji(emoji)
}
function recentEmoji(totals) {
const recentlyCompleted = totals['Recently Completed']
let emoji = EMOJI.GOOD
if (recentlyCompleted >= 3) {
emoji = EMOJI.GREAT
} else if (recentlyCompleted === 0) {
emoji = EMOJI.BAD
}
return formatEmoji(emoji)
}
function wipEmoji(totals) {
const doing = totals['DOING']
let emoji = EMOJI.GOOD
if (doing >= 3) {
emoji = EMOJI.BAD
} else if (doing === 0) {
emoji = EMOJI.SLEEP
} else if (doing === 1) {
emoji = EMOJI.GREAT
}
return formatEmoji(emoji)
}
function cardTotal(totals) {
let count = 0
Object.keys(totals).forEach((list) => {
count += totals[list]
})
return count
}
function getTopicTable(project) {
console.log('project.updatedAt', project.updatedAt)
console.log('updatedAt', updatedAt)
if (project.updatedAt < updatedAt) return ''
updatedAt = project.updatedAt
const lists = project.allLists.filter(list => !list.filter)
const topicTable = project.allTopics.map((topic) => {
return {
name: topic,
lists: [
...lists.map((list) => {
return {
name: list.name,
count: list.tasks.filter((task) => task.topics.includes(topic)).length
}
})
]
}
});
//convert topic table into a markdown table with topic name on the left and list names on the top and the count for each topic/list intersection
const table = `
| Topic | ${lists.map((list) => list.name).join(' | ')} |
| --- | ${lists.map(() => ' --- ').join(' | ')} |
${topicTable.map((topic) => {
const topicLink = `imdone://${project.path}?filter=topics="${encodeURIComponent(topic.name)}"`;
return `| [[${topic.name}]] | ${topic.lists.map((list) => `[${list.count}](${topicLink})`).join(' | ')} |`;
}).join('\n')}
`;
console.log(table);
return table
}

0
.imdone/style.css Normal file
View file

4
.imdone/tags.yml Normal file
View file

@ -0,0 +1,4 @@
tags:
- BUG
- Someday
- WantToHave

8
.imdoneignore Normal file
View file

@ -0,0 +1,8 @@
.vs
bin
obj
*.user
.imdone
Resources
.git
.vscode

310
Classes/ApiService.cs Normal file
View file

@ -0,0 +1,310 @@
using Microsoft.AspNetCore.Components;
using Neighbourhood.omg.lol.Models;
using System.Diagnostics;
using System.Net.Http.Json;
using System.Text;
using System.Text.Json;
namespace Neighbourhood.omg.lol
{
public class ApiService {
HttpClient _client;
JsonSerializerOptions _serializerOptions;
public const string BaseUrl = "https://api.omg.lol";
private string? apiToken = null;
public ApiService(string? token = null) {
_client = new HttpClient();
_client.BaseAddress = new Uri(BaseUrl);
_client.DefaultRequestHeaders.UserAgent.Add(new System.Net.Http.Headers.ProductInfoHeaderValue(App.Name, App.Version));
_serializerOptions = new JsonSerializerOptions {
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
#if DEBUG
WriteIndented = true
#else
WriteIndented = false
#endif
};
AddToken(token);
}
/// <summary>
/// Deserialize json convenience function with default serializer options
/// </summary>
/// <typeparam name="T">The type to deserialize</typeparam>
/// <param name="str">The string to deserialize</param>
/// <returns>The deserialized object if successful, otherwise default</returns>
public T? Deserialize<T>(string str) {
T? responseObj = default;
try {
responseObj = JsonSerializer.Deserialize<T>(str, _serializerOptions);
}
catch (JsonException ex) {
Debug.WriteLine(@"\tERROR {0}", ex.Message);
Debug.WriteLine(str);
}
return responseObj;
}
#region Base Requests
/// <summary>
/// Decode the response from an API call
/// </summary>
/// <typeparam name="TResponse">The type of response object we are trying to get</typeparam>
/// <param name="response">The raw Http Response Message</param>
/// <param name="cancellationToken">A cancellation token to cancel the operation</param>
/// <returns>The decoded object if successfull, otherwise default</returns>
private async Task<TResponse?> DecodeResponse<TResponse>(HttpResponseMessage response, CancellationToken cancellationToken = default)
where TResponse : IOmgLolResponseData
{
TResponse? responseData = default;
try {
string str = await response.Content.ReadAsStringAsync();
if (response.IsSuccessStatusCode) {
OmgLolResponse<TResponse>? responseObj = Deserialize<OmgLolResponse<TResponse>>(str);
if (responseObj?.Request == null || (responseObj?.Request?.Success ?? false)) {
responseData = responseObj!.Response;
}
}
else {
OmgLolResponse<TResponse>? responseObj = Deserialize<OmgLolResponse<TResponse>>(str);
throw responseObj == null ? new OmgLolApiException<TResponse>(str) : new OmgLolApiException<TResponse>(responseObj);
}
}
catch (Exception ex) {
Debug.WriteLine(@"\tERROR {0}", ex.Message);
}
return responseData;
}
/// <summary>
/// Performs a request for the supplied uri, with the supplied Http Method,
/// with the supplied data in the body (if present)
/// </summary>
/// <typeparam name="TResponse">The type of response we are expecting</typeparam>
/// <typeparam name="TData">The type of data we are sending</typeparam>
/// <param name="uri">The uri to request</param>
/// <param name="method">The Http Method to use for the request</param>
/// <param name="data">The data to send in the body of the request</param>
/// <param name="file">A FileResult for the file to send in the body of the request as binary data</param>
/// <param name="cancellationToken">A cancellation token</param>
/// <returns>The returned data if successful, otherwise default</returns>
private async Task<TResponse?> Request<TResponse, TData>(string uri, HttpMethod method, TData? data = default, FileResult? file = null, bool useAuthToken = true, CancellationToken cancellationToken = default)
where TResponse : IOmgLolResponseData
{
TResponse? responseData = default;
try {
HttpRequestMessage request = new HttpRequestMessage(method, uri);
Stream? fileStream = null;
if (file != null) {
// append "binary" query parameter (if not already present)
Uri url = new Uri(_client.BaseAddress?.AbsoluteUri + uri);
if (string.IsNullOrEmpty(url.Query)) uri += "?binary";
else if (!url.Query.Contains("binary")) uri += "&binary";
request = new HttpRequestMessage(method, uri);
fileStream = await file.OpenReadAsync();
HttpContent fileStreamContent = new StreamContent(fileStream);
fileStreamContent.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue(file.ContentType ?? "application/octet-stream");
fileStreamContent.Headers.ContentLength = fileStream.Length;
request.Content = fileStreamContent;
}
else if (data != null) {
string json = JsonSerializer.Serialize(data, _serializerOptions);
request.Content = new StringContent(json, Encoding.UTF8, "application/json");
}
if(useAuthToken) {
if (apiToken == null) apiToken = Task.Run(() => SecureStorage.GetAsync("accounttoken")).GetAwaiter().GetResult();
if (apiToken != null) request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", apiToken);
}
HttpResponseMessage response = await _client.SendAsync(request, cancellationToken: cancellationToken);
responseData = await DecodeResponse<TResponse>(response, cancellationToken);
fileStream?.Dispose();
}
catch (Exception ex) {
Debug.WriteLine(@"\tERROR {0}", ex.Message);
}
return responseData;
}
// GET request
private async Task<TResponse?> Get<TResponse>(string uri, bool useAuthToken = true, CancellationToken cancellationToken = default)
where TResponse : IOmgLolResponseData
=> await Request<TResponse, object>(uri, HttpMethod.Get, useAuthToken: useAuthToken, cancellationToken: cancellationToken);
// POST request
private async Task<TResponse?> Post<TResponse, TData>(string uri, TData data, CancellationToken cancellationToken = default)
where TResponse : IOmgLolResponseData
=> await Request<TResponse, TData>(uri, HttpMethod.Post, data: data, cancellationToken: cancellationToken);
// POST request, but with a file as binary data
private async Task<TResponse?> PostBinary<TResponse>(string uri, FileResult? fileResult = null, CancellationToken cancellationToken = default)
where TResponse : IOmgLolResponseData
=> await Request<TResponse, object>(uri, HttpMethod.Post, file: fileResult, cancellationToken: cancellationToken);
// PUT request
private async Task<TResponse?> Put<TResponse, TData>(string uri, TData data, CancellationToken cancellationToken = default)
where TResponse : IOmgLolResponseData
=> await Request<TResponse, TData>(uri, HttpMethod.Put, data: data, cancellationToken: cancellationToken);
// PATCH request
private async Task<TResponse?> Patch<TResponse, TData>(string uri, TData data, CancellationToken cancellationToken = default)
where TResponse : IOmgLolResponseData
=> await Request<TResponse, TData>(uri, HttpMethod.Patch, data: data, cancellationToken: cancellationToken);
// Delete request
private async Task<TResponse?> Delete<TResponse>(string uri, CancellationToken cancellationToken = default)
where TResponse : IOmgLolResponseData
=> await Request<TResponse, object>(uri, HttpMethod.Delete, cancellationToken: cancellationToken);
#endregion
#region Specific Requests
public async Task<List<Status>> StatuslogLatest() =>
(await Get<StatusResponseData>("/statuslog/latest"))?.Statuses ?? new List<Status>();
public async Task<List<Status>> Statuslog(string address) =>
(await Get<StatusResponseData>($"/address/{address}/statuses"))?.Statuses ?? new List<Status>();
public async Task<string> StatuslogBio(string address) =>
(await Get<StatusBioResponseData>($"/address/{address}/statuses/bio"))?.Bio ?? string.Empty;
public async Task<string> PostStatuslogBio(string address, string bio) =>
(await Post<StatusBioResponseData, PostStatusBio>($"/address/{address}/statuses/bio", new PostStatusBio() { Content = bio }))?.Bio ?? string.Empty;
public async Task<AccountResponseData?> AccountInfo() =>
await Get<AccountResponseData>("/account/application/info");
public async Task<AddressResponseList?> Addresses() =>
await Get<AddressResponseList>("/account/application/addresses");
public async Task<StatusPostResponseData?> StatusPost(string address, StatusPost statusPost) =>
await Post<StatusPostResponseData, StatusPost>($"/address/{address}/statuses", statusPost);
public async Task<List<Pic>> SomePics() =>
(await Get<SomePicsResponseData>("/pics"))?.Pics ?? new List<Pic>();
public async Task<List<Pic>> SomePics(string address) =>
(await Get<SomePicsResponseData>($"/address/{address}/pics"))?.Pics ?? new List<Pic>();
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, 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<BasicResponseData?> DeletePic(string address, string id) =>
(await Delete<BasicResponseData>($"/address/{address}/pics/{id}"));
public async Task<PatchStatusResponseData?> PatchStatus(string address, string id, string content, string? emoji) =>
(await Patch<PatchStatusResponseData, PatchStatus>($"/address/{address}/statuses/", new PatchStatus { Id = id, Content = content, Emoji = emoji }));
public async Task<BasicResponseData?> DeleteStatus(string address, string id) =>
(await Delete<BasicResponseData>($"/address/{address}/statuses/{id}"));
public async Task<List<NowData>?> NowGarden() =>
(await Get<NowResponseData>($"/now/garden"))?.Garden ?? new List<NowData>();
public async Task<List<string>?> Directory() =>
(await Get<DirectoryResponseData>($"/directory"))?.Directory ?? new List<string>();
public async Task<NowContentData?> GetNowPage(string address) =>
(await Get<NowPageResponseData>($"/address/{address}/now"))?.Now;
public async Task<BasicResponseData?> PostNowPage(string address, string content, bool listed) =>
await Post<BasicResponseData, NowContentData>($"/address/{address}/now", new NowContentData { Content = content, Listed = listed ? 1 : 0 });
public async Task<List<MarkupString>> Ephemeral() =>
(await Get<EphemeralResponseData>($"/ephemeral"))?.Content?.Select(s => (MarkupString)s)?.ToList() ?? new List<MarkupString>();
public async Task<BasicResponseData?> PostEphemeral(string content) =>
await Post<BasicResponseData, EphemeralData>("/ephemeral", new EphemeralData { Content = content });
public async Task<ProfileResponseData?> GetProfile(string address) =>
await Get<ProfileResponseData>($"/address/{address}/web");
public async Task<BasicResponseData?> PostProfile(string address, string content, bool publish = true) =>
await Post<BasicResponseData, PostProfile>($"/address/{address}/web", new PostProfile { Content = content, Publish = publish });
public async Task<BasicResponseData?> PostProfile(string address, PostProfile data) =>
await Post<BasicResponseData, PostProfile>($"/address/{address}/web", data);
public async Task<Dictionary<string, Theme>?> GetThemes() =>
(await Get<ThemeResponseData>($"/theme/list"))?.Themes;
public async Task<MarkupString?> GetThemePreview(string theme) =>
(MarkupString)((await Get<ThemePreviewResponseData>($"/theme/{theme}/preview"))?.Html ?? string.Empty);
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", useAuthToken: false))?.Pastebin ?? new List<Paste>();
public async Task<List<Paste>> GetMyPastes(string address) =>
(await Get<PastesResponseData>($"/address/{address}/pastebin", useAuthToken: true))?.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
/// <summary>
/// Add the api token into the default headers
/// </summary>
/// <param name="token">The api token</param>
public void AddToken(string? token = null) {
if (token == null) token = Task.Run(() => SecureStorage.GetAsync("accounttoken")).GetAwaiter().GetResult();
if (token != null) apiToken = token;
}
/// <summary>
/// Remove the api token from the default headers
/// </summary>
public void RemoveToken() {
_client.DefaultRequestHeaders.Remove("Authorization");
}
public async Task<string?> OAuth(string code, string client_id, string client_secret, string redirect_uri) {
string? token = null;
string uri = $"/oauth/?code={code}&client_id={client_id}&client_secret={client_secret}&redirect_uri={redirect_uri}&scope=everything";
try {
HttpResponseMessage response = await _client.GetAsync(uri);
if (response.IsSuccessStatusCode) {
TokenResponseData? responseObj = await response.Content.ReadFromJsonAsync<TokenResponseData>(_serializerOptions);
if (responseObj != null && !string.IsNullOrEmpty(responseObj.AccessToken)) {
token = responseObj.AccessToken;
}
}
}
catch (Exception ex) {
Debug.WriteLine(@"\tERROR {0}", ex.Message);
}
return token;
}
#endregion
public async Task<MarkupString?> GetHtml(string url) {
string? raw = null;
try {
HttpResponseMessage response = await _client.GetAsync(url);
if (response.IsSuccessStatusCode) {
raw = await response.Content.ReadAsStringAsync();
}
}
catch (Exception ex) {
Debug.WriteLine(@"\tERROR {0}", ex.Message);
}
return string.IsNullOrEmpty(raw) ? null : (MarkupString)raw;
}
}
}

View file

@ -1,8 +1,5 @@
using Microsoft.AspNetCore.Components.Authorization;
using Neighbourhood.omg.lol.Models;
using System.Security.Claims;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace Neighbourhood.omg.lol {
public class CustomAuthenticationStateProvider : AuthenticationStateProvider {

5
Classes/FeatureFlags.cs Normal file
View file

@ -0,0 +1,5 @@
namespace Neighbourhood.omg.lol {
public static class FeatureFlags {
public static bool Following { get; } = true;
}
}

View file

@ -0,0 +1,8 @@
using Microsoft.AspNetCore.Components;
namespace Neighbourhood.omg.lol {
public class NavigatorService {
internal NavigationManager? NavigationManager { get; set; }
internal Page? Page { get; set; }
}
}

View file

@ -2,9 +2,21 @@
using System.ComponentModel;
using System.Globalization;
using System.Text.Json;
using Neighbourhood.omg.lol.Models;
using System.Runtime.CompilerServices;
namespace Neighbourhood.omg.lol.Models {
namespace Neighbourhood.omg.lol {
public class State : INotifyPropertyChanged {
// Events
public event EventHandler<EventArgs>? IntentReceived;
public event PropertyChangedEventHandler? PropertyChanged;
// Create the OnPropertyChanged method to raise the event
// The calling member's name will be used as the parameter.
protected void OnPropertyChanged([CallerMemberName] string? name = null) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
protected void OnIntentRecieved() => IntentReceived?.Invoke(this, EventArgs.Empty);
// Main data lists
public List<Status>? Statuses { get; set; }
public List<Pic>? Pics { get; set; }
@ -12,8 +24,19 @@ namespace Neighbourhood.omg.lol.Models {
public List<MarkupString>? EphemeralMessages { get; set; }
public List<string>? AddressDirectory { get; set; }
public List<FeedItem>? Feed { get; set; }
public Dictionary<string, Theme>? Themes { get; set; }
// Account data
public AccountResponseData? AccountInfo { get; set; }
private AccountResponseData? _accountInfo;
public AccountResponseData? AccountInfo {
get => _accountInfo;
set {
_accountInfo = value;
OnPropertyChanged(nameof(AccountInfo));
}
}
public AddressResponseList? AddressList { get; set; }
public bool IsAuthorized { get => AccountInfo != null; }
@ -38,15 +61,19 @@ namespace Neighbourhood.omg.lol.Models {
else {
string selectedAddressJson = JsonSerializer.Serialize(_selectedAddress);
Preferences.Default.Set("selectedaddress", selectedAddressJson);
OnPropertyChanged();
}
}
}
}
public List<string>? Following { get; private set; }
public string? SelectedAddressName { get => SelectedAddress?.Address; }
// 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 {
@ -56,19 +83,19 @@ namespace Neighbourhood.omg.lol.Models {
_cachedAddress = value;
CachedAddressStatuses = new List<Status>();
CachedAddressPics = new List<Pic>();
CachedAddressPastes = new List<Paste>();
CachedAddressBio = null;
}
}
}
// share intent stuff
public event EventHandler<EventArgs>? IntentReceived;
private string? _shareString;
public string? ShareString {
get => _shareString;
set {
_shareString = value;
IntentReceived?.Invoke(this, EventArgs.Empty);
OnIntentRecieved();
}
}
public string? ShareStringSubject { get; set; }
@ -78,7 +105,7 @@ namespace Neighbourhood.omg.lol.Models {
get => _sharePhoto;
set {
_sharePhoto = value;
IntentReceived?.Invoke(this, EventArgs.Empty);
OnIntentRecieved();
}
}
public long? SharePhotoSize { get; set; }
@ -86,13 +113,12 @@ namespace Neighbourhood.omg.lol.Models {
public string? SharePhotoText { get; set; }
// refreshing
public event PropertyChangedEventHandler? PropertyChanged;
private bool _isRefreshing;
public bool IsRefreshing {
get => _isRefreshing;
private set {
_isRefreshing = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(IsRefreshing)));
OnPropertyChanged();
}
}
@ -126,14 +152,14 @@ namespace Neighbourhood.omg.lol.Models {
get => _canRefresh;
set {
_canRefresh = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(CanRefresh)));
OnPropertyChanged();
}
}
// api service
private RestService api { get; set; }
private ApiService api { get; set; }
public State(RestService restService) {
public State(ApiService restService) {
api = restService;
}
@ -143,10 +169,12 @@ namespace Neighbourhood.omg.lol.Models {
string accountJson = Preferences.Default.Get("accountdetails", string.Empty);
string addressJson = Preferences.Default.Get("accountaddresses", 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(addressJson)) AddressList = JsonSerializer.Deserialize<AddressResponseList>(addressJson);
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 (AccountInfo == null) {
@ -170,17 +198,35 @@ namespace Neighbourhood.omg.lol.Models {
}
public async Task RemoveAccountDetails() {
Preferences.Default.Clear();
AccountInfo = null;
AddressList = null;
SelectedAddress = null;
api.RemoveToken();
await Task.Run(() => {
Preferences.Default.Clear();
AccountInfo = null;
AddressList = null;
SelectedAddress = null;
Following = null;
api.RemoveToken();
});
}
public bool IsFollowing(string address) => Following?.Contains(address) ?? false;
public async Task Follow(string address) {
if (Following == null) Following = new List<string>();
Following.Add(address);
Preferences.Default.Set("following", JsonSerializer.Serialize(Following));
await GetFeed(forceRefresh: true);
}
public async Task Unfollow(string address) {
if (Following == null) Following = new List<string>();
Following.Remove(address);
Preferences.Default.Set("following", JsonSerializer.Serialize(Following));
await GetFeed(forceRefresh: true);
}
public async Task<MarkupString?> GetBio(string address, bool forceRefresh = false) {
CachedAddress = address;
if (forceRefresh || CachedAddressBio == null) {
CachedAddressBio = await api.StatuslogBio(address);
CachedAddressBio = Utilities.MdToHtmlMarkup(await api.StatuslogBio(address));
}
return CachedAddressBio;
}
@ -195,7 +241,7 @@ namespace Neighbourhood.omg.lol.Models {
public async Task<List<string>?> GetDirectory(bool forceRefresh = false) {
if (forceRefresh || this.AddressDirectory == null || this.AddressDirectory.Count == 0) {
IdnMapping idn = new IdnMapping();
this.AddressDirectory = (await api.Directory()).Select(s => {
this.AddressDirectory = (await api.Directory())?.Select(s => {
if (s.StartsWith("xn--")) return idn.GetUnicode(s);
else return s;
}).ToList();
@ -240,6 +286,19 @@ namespace Neighbourhood.omg.lol.Models {
return CachedAddressPics;
}
public async Task<List<Paste>?> GetPastes(string address, bool forceRefresh = false) {
CachedAddress = address;
if (forceRefresh || this.CachedAddressPastes == null || this.CachedAddressPastes.Count == 0) {
if (AddressNames?.Contains(address) ?? false) {
CachedAddressPastes = (await api.GetMyPastes(address)) ?? new List<Paste>();
}
else {
CachedAddressPastes = (await api.GetPastes(address)) ?? new List<Paste>();
}
}
return CachedAddressPastes;
}
public async Task RefreshStatuses() {
await GetStatuses(forceRefresh: true);
if(SelectedAddressName != null)
@ -252,5 +311,29 @@ namespace Neighbourhood.omg.lol.Models {
}
public async Task RefreshNow() => await GetNowGarden(forceRefresh: true);
public async Task RefreshPastes() {
if (SelectedAddressName != null)
await GetPastes(SelectedAddressName, forceRefresh: true);
}
public async Task<IOrderedEnumerable<FeedItem>> GetFeed(bool forceRefresh = false) {
if(forceRefresh || Feed == null || Feed.Count == 0) {
Feed = new List<FeedItem>();
foreach(string address in Following ?? new List<string>()) {
Feed.AddRange((await GetStatuses(address, forceRefresh))?.Select(s => new FeedItem { Status = s }) ?? new List<FeedItem>());
Feed.AddRange((await GetPics(address, forceRefresh))?.Select(p => new FeedItem { Pic = p }) ?? new List<FeedItem>());
Feed.AddRange((await GetPastes(address, forceRefresh))?.Select(p => new FeedItem { Paste = p }) ?? new List<FeedItem>());
}
}
return Feed.OrderByDescending(s => s.CreatedTime);
}
public async Task<Dictionary<string, Theme>?> GetThemes(bool forceRefresh = false) {
if (forceRefresh || this.Themes == null || this.Themes.Count == 0) {
this.Themes = await api.GetThemes();
}
return this.Themes;
}
}
}

51
Classes/Utilities.cs Normal file
View file

@ -0,0 +1,51 @@
using Markdig;
using Microsoft.AspNetCore.Components;
namespace Neighbourhood.omg.lol
{
public static class Utilities
{
private static MarkdownPipeline markdownPipeline { get; }
= new MarkdownPipelineBuilder().UseAutoLinks().Build();
public static string MdToHtml(string markdown) =>
Markdown.ToHtml(markdown, markdownPipeline);
public static MarkupString MdToHtmlMarkup(string markdown) =>
(MarkupString)MdToHtml(markdown);
public static async Task<long> FileSize(FileResult file)
{
using var fileStream = await file.OpenReadAsync();
return fileStream.Length;
}
public static async Task<string> Base64FromFile(FileResult file)
{
using var memoryStream = new MemoryStream();
using var fileStream = await file.OpenReadAsync();
await fileStream.CopyToAsync(memoryStream);
byte[] bytes = memoryStream.ToArray();
return Convert.ToBase64String(bytes);
}
public static string RelativeTimeFromUnix(long unix)
{
DateTimeOffset createdTime = DateTimeOffset.UnixEpoch.AddSeconds(unix);
TimeSpan offset = DateTimeOffset.UtcNow - createdTime;
var offsetString = string.Empty;
if (Math.Floor(offset.TotalDays) == 1) offsetString = $"{Math.Floor(offset.TotalDays)} day ago";
else if (offset.TotalDays > 1) offsetString = $"{Math.Floor(offset.TotalDays)} days ago";
else if (Math.Floor(offset.TotalHours) == 1) offsetString = $"{Math.Floor(offset.TotalHours)} hour ago";
else if (offset.TotalHours > 1) offsetString = $"{Math.Floor(offset.TotalHours)} hours ago";
else if (Math.Floor(offset.TotalMinutes) == 1) offsetString = $"{Math.Floor(offset.TotalMinutes)} minute ago";
else if (offset.TotalMinutes > 1) offsetString = $"{Math.Floor(offset.TotalMinutes)} minutes ago";
else if (Math.Floor(offset.TotalSeconds) == 1) offsetString = $"{Math.Floor(offset.TotalSeconds)} second ago";
else offsetString = $"{Math.Floor(offset.TotalSeconds)} seconds ago";
return offsetString;
}
}
}

View file

@ -0,0 +1,81 @@
@inject IJSRuntime JS
@inject State State
@inject ApiService api
<div class="overlay" data-ui="#@id"></div>
<dialog id="@id">
<h5>Edit your statuslog bio</h5>
<div class="row">
<div class="max markdown-editor">
@if (Bio != null) {
<MarkdownEditor @ref="Editor"
@bind-Value="@Bio"
Theme="material-darker"
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" />
<MarkdownToolbarButton Action="MarkdownAction.Heading" Icon="fa-solid fa-heading" Title="Heading" />
<MarkdownToolbarButton Action="MarkdownAction.Code" Icon="fa-solid fa-code" Title="Code" Separator="true" />
<MarkdownToolbarButton Action="MarkdownAction.Quote" Icon="fa-solid fa-quote-left" Title="Quote" />
<MarkdownToolbarButton Action="MarkdownAction.UnorderedList" Icon="fa-solid fa-list-ul" Title="Unordered List" />
<MarkdownToolbarButton Action="MarkdownAction.OrderedList" Icon="fa-solid fa-list-ol" Title="Ordered List" />
<MarkdownToolbarButton Action="MarkdownAction.Link" Icon="fa-solid fa-link" Title="Link" Separator="true" />
<MarkdownToolbarButton Action="MarkdownAction.Image" Icon="fa-solid fa-image" Title="Image" />
<MarkdownToolbarButton Action="MarkdownAction.HorizontalRule" Icon="fa-solid fa-horizontal-rule" Title="Horizontal Rule" />
<MarkdownToolbarButton Action="MarkdownAction.Guide" Icon="fa-solid fa-circle-question" Title="Guide" Separator="true" />
</Toolbar>
</MarkdownEditor>
}
</div>
</div>
<nav class="no-space">
<div class="max"></div>
<button class="transparent link" data-ui="#@id" disabled="@loading">Cancel</button>
<button @onclick="PostBio" disabled="@loading">
@if (loading) {
<span>Saving...</span>
}
else {
<i class="fa-solid fa-floppy-disk"></i> <span>Save</span>
}
</button>
</nav>
</dialog>
@code {
private MarkdownEditor? Editor;
public string? Bio { get; set; }
[Parameter]
public string? Address { get; set; }
private bool loading = true;
[Parameter]
public string? id { get; set; }
protected override async Task OnInitializedAsync() {
await base.OnInitializedAsync();
Bio = await api.StatuslogBio(Address ?? State.SelectedAddressName!);
await InvokeAsync(StateHasChanged);
await Editor!.SetValueAsync(Bio);
loading = false;
await InvokeAsync(StateHasChanged);
}
public async Task PostBio() {
loading = true;
await InvokeAsync(StateHasChanged);
// Post the bio
await api.PostStatuslogBio(Address!, Bio ?? string.Empty);
State.CachedAddressBio = Utilities.MdToHtmlMarkup(Bio ?? string.Empty);
await JS.InvokeVoidAsync("ui", "#" + id);
// reset input
await OnInitializedAsync();
loading = false;
await InvokeAsync(StateHasChanged);
State.SendRefresh();
}
}

View file

@ -0,0 +1,131 @@
@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;
loading = false;
await InvokeAsync(StateHasChanged);
}
}

View file

@ -1,6 +1,6 @@
@inject IJSRuntime JS
@inject State State
@inject RestService api
@inject ApiService api
<div class="overlay" data-ui="#@id"></div>
<dialog id="@id">
@ -52,10 +52,11 @@
public string? Description { get; set; }
private bool loading = false;
[Parameter]
public string id { get; set; }
public string? id { get; set; }
private bool confirmDelete { get; set; }
protected override async Task OnInitializedAsync() {
await base.OnInitializedAsync();
Description = Pic?.Description;
}
@ -70,7 +71,7 @@
await InvokeAsync(StateHasChanged);
if (!string.IsNullOrEmpty(Pic?.Id)) {
await api.DeletePic(State.SelectedAddressName, Pic.Id);
await api.DeletePic(State.SelectedAddressName!, Pic.Id);
await State.RefreshPics();
State.SendRefresh();
await InvokeAsync(StateHasChanged);
@ -91,7 +92,7 @@
await InvokeAsync(StateHasChanged);
if(!string.IsNullOrEmpty(Pic?.Id)) {
await api.PostPicDescription(State.SelectedAddressName, Pic.Id, Description);
await api.PostPicDescription(State.SelectedAddressName!, Pic.Id, Description);
await State.RefreshPics();
State.SendRefresh();
await InvokeAsync(StateHasChanged);

View file

@ -0,0 +1,114 @@
@inject IJSRuntime JS
@inject State State
@inject ApiService api
@inject NavigationManager navigationManager
<div class="overlay @(Active ? "active" : string.Empty)" data-ui="#@id"></div>
<dialog id="@id" class="@(Active ? "active" : string.Empty)" open="@Active">
<h5>Update your profile picture</h5>
<div class="padding center-align">
<img src="@(Base64Url ?? ExistingUrl)" class="small-height square" />
</div>
<div class="row">
<button @onclick="PicFromMedia"><i class="fa-solid fa-image"></i> Select a picture</button>
<button @onclick="PicFromPhoto"><i class="fa-solid fa-camera"></i> Take a photo</button>
</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 {
[Parameter]
public string? Address { get; set; }
public string ExistingUrl { get => $"https://profiles.cache.lol/{Address ?? ""}/picture"; }
// private IBrowserFile? File { get; set; }
[Parameter]
public string? Base64File { get; set; }
[Parameter]
public long? FileSize { get; set; }
[Parameter]
public string? FileContentType { get; set; }
[Parameter]
public string? id { get; set; }
[Parameter]
public bool Active { get; set; }
private bool loading = false;
private FileResult? File { get; set; }
private string? Base64Url {
get {
if (FileContentType == null || Base64File == null) return null;
return $"data:{FileContentType};base64,{Base64File}";
}
}
public async Task PostPic() {
loading = true;
await InvokeAsync(StateHasChanged);
if (Base64File != null && File != null)
{
BasicResponseData? response = await api.PostProfilePic(Address!, File);
if (response != null)
{
await JS.InvokeVoidAsync("ui", "#" + id);
// clear input
File = null;
Base64File = null;
FileSize = null;
FileContentType = null;
await JS.InvokeVoidAsync("cacheBust", ExistingUrl);
}
loading = false;
await InvokeAsync(StateHasChanged);
}
}
private string formatSizeUnits(long? bytes) {
if (bytes == null) return "?? 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;
}
private async Task PicFromMedia(EventArgs e) {
File = await MediaPicker.Default.PickPhotoAsync();
await PopulateFileDetails();
}
private async Task PicFromPhoto(EventArgs e) {
File = await MediaPicker.Default.CapturePhotoAsync();
await PopulateFileDetails();
}
private async Task PopulateFileDetails() {
if (File == null) {
FileContentType = null;
FileSize = null;
Base64File = null;
}
else {
FileContentType = File.ContentType;
FileSize = await Utilities.FileSize(File);
Base64File = await Utilities.Base64FromFile(File);
}
}
}

View file

@ -1,6 +1,6 @@
@inject IJSRuntime JS
@inject State State
@inject RestService api
@inject ApiService api
<div class="overlay" data-ui="#@id"></div>
<dialog id="@id">
@ -70,10 +70,11 @@
public string? Emoji { get; set; }
private bool loading = false;
[Parameter]
public string id { get; set; }
public string? id { get; set; }
private bool confirmDelete { get; set; }
protected override async Task OnInitializedAsync() {
await base.OnInitializedAsync();
Content = Status?.Content;
Emoji = Status?.Emoji;
}
@ -89,7 +90,7 @@
await InvokeAsync(StateHasChanged);
if (!string.IsNullOrEmpty(Status?.Id)) {
await api.DeleteStatus(State.SelectedAddressName, Status.Id);
await api.DeleteStatus(State.SelectedAddressName!, Status.Id);
await State.RefreshStatuses();
State.SendRefresh();
await InvokeAsync(StateHasChanged);
@ -111,7 +112,7 @@
await InvokeAsync(StateHasChanged);
if (!string.IsNullOrEmpty(Status?.Id)) {
await api.PatchStatus(State.SelectedAddressName, Status.Id, Content, Emoji);
await api.PatchStatus(State.SelectedAddressName!, Status.Id, Content ?? String.Empty, Emoji);
await State.RefreshStatuses();
State.SendRefresh();
await InvokeAsync(StateHasChanged);

View file

@ -1,15 +1,17 @@
@inject IJSRuntime JS
@inject State State
@inject RestService api
@inject ApiService api
@if(Html != null) {
<iframe id="@id" frameborder="0" scrolling="no" srcdoc="@Html" onload="() => iframeResize({ license: 'GPLv3' })"></iframe>
}
@code {
[Parameter]
public string Url { get; set; }
public string? Url { get; set; }
[Parameter]
public string id { get; set; }
public string? id { get; set; }
[Parameter]
public string? SrcString { get; set; }
public MarkupString? Html { get; set; }
protected override async Task OnAfterRenderAsync(bool firstRender) {
@ -19,13 +21,20 @@
}
public async Task Reload() {
// if (Html == null){
if (Url != null){
Html = await api.GetHtml(Url);
string? HtmlString = Html?.ToString();
HtmlString = HtmlString?.Replace("</head>", "<base target='_blank'></head>");
HtmlString = HtmlString?.Replace("</body>", "<script src='https://cdn.jsdelivr.net/npm/@iframe-resizer/child'></script></body>");
Html = (MarkupString)HtmlString;
// }
SrcString = Html?.ToString();
}
if(SrcString != null) {
SrcString = SrcString?.Replace("</head>", "<base target='_blank'></head>");
SrcString = SrcString?.Replace("</body>", "<script src='https://cdn.jsdelivr.net/npm/@iframe-resizer/child@5.1.5'></script></body>");
Html = (MarkupString)(SrcString ?? string.Empty);
}
await InvokeAsync(StateHasChanged);
await IframeResize();
}
public async Task IframeResize() {
await JS.InvokeVoidAsync("iframeResize", new { license = "GPLv3" });
}
}

View file

@ -24,7 +24,7 @@
<AvatarMenuLinks></AvatarMenuLinks>
</menu>
</button>
<small class="s m address">Omg.lol</small>
<small class="s m honey">Omg.lol</small>
</NavLink>
<div class="l">
Hey there. <br />

View file

@ -4,16 +4,14 @@
<i class="emoji medium" data-emoji="👋">👋</i>
<span>Hey, @(State.Name ?? "there").</span>
</a>
<a class="m s row" href="/directory">
<i class="square fa-duotone fa-address-book"></i>
<span>Address Directory</span>
<a class="s row" href="/now">
<i class="square fa-duotone fa-seedling"></i>
<span>Now.garden</span>
</a>
<a class="s row" href="/directory">
<i class="square fa-duotone fa-address-book"></i>
<span>Directory</span>
</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>()) {
<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" />
@ -36,8 +34,13 @@
<i class="fa-duotone fa-seedling"></i>
<span>/Now</span>
</a>
<a class="indent row" href="/person/@State.SelectedAddressName#pastebin">
<i class="fa-solid fa-clipboard"></i>
<span>Pastebin</span>
</a>
}
}
@if (State.IsAuthorized) {
<a class="row" @onclick='() => AuthStateProvider.Logout()'>
<i class="fa-solid fa-door-open"></i>

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>
<div class="label">Status.lol</div>
</NavLink>
@ -10,21 +11,38 @@
<i class="square fa-light fa-comment-dots"></i>
<div class="label">Eph.emer.al</div>
</NavLink>
<NavLink class="nav-link" href="/now">
<i class="square fa-duotone fa-seedling"></i>
<div class="label">Now.garden</div>
</NavLink>
<NavLink class="l nav-link" href="/directory">
@if (FeatureFlags.Following && State.IsAuthorized) {
<NavLink class="nav-link" href="/feed">
<i class="square fa-solid fa-list-timeline"></i>
<div class="label">Timeline</div>
</NavLink>
<NavLink class="l m nav-link" href="/now">
<i class="square fa-duotone fa-seedling"></i>
<div class="label">Now.garden</div>
</NavLink>
}
else {
<NavLink class="nav-link" href="/now">
<i class="square fa-duotone fa-seedling"></i>
<div class="label">Now.garden</div>
</NavLink>
}
<NavLink class="l m nav-link" href="/directory">
<i class="square fa-duotone fa-address-book"></i>
<div class="label">Address Directory</div>
<div class="label">Directory</div>
</NavLink>
@* <AuthorizeView>
<Authorized>
<NavLink class="l nav-link" href="/feed">
<i class="square fa-solid fa-list-timeline"></i>
<div class="label">Feed</div>
</NavLink>
</Authorized>
</AuthorizeView> *@
@code {
protected override async Task OnInitializedAsync() {
await base.OnInitializedAsync();
State.PropertyChanged += StateChanged;
}
private async void StateChanged(object? sender, PropertyChangedEventArgs e) {
if (e.PropertyName == nameof(State.AccountInfo)) await InvokeAsync(StateHasChanged);
}
public void Dispose() {
State.PropertyChanged -= StateChanged;
}
}

View file

@ -8,9 +8,9 @@
@code {
[Parameter]
public string id { get; set; }
public string? id { get; set; }
[Parameter]
public string? @class { get; set; }
[Parameter]
public string icon { get; set; }
public string? icon { get; set; }
}

View file

@ -0,0 +1,58 @@
@inject IJSRuntime JS
@inject State State
@inject ApiService api
@inject NavigationManager navigationManager
<div class="overlay @(Active ? "active" : string.Empty)" data-ui="#@id"></div>
<dialog id="@id" class="@(Active ? "active" : string.Empty)" open="@Active">
<h5>Share a fleeting thought</h5>
<div class="row">
<div class="field textarea border max">
<InputTextArea @bind-Value="Content"></InputTextArea>
</div>
</div>
<div class="row">
<p>Your anonymous post will be shared for a while, and then it will disappear forever. It cant be edited, so take care before submitting.</p>
</div>
<div class="row">
<p><strong>If you need help, dont suffer in silence. <a href="https://www.helpguide.org/find-help.htm">Talk to someone right now</a>.</strong></p>
</div>
<nav class="right-align no-space">
<button class="transparent link" data-ui="#@id" disabled="@loading">Cancel</button>
<button @onclick="PostEphemeral" disabled="@loading">
@if (loading) {
<span>Saving...</span>
}
else {
<i class="fa-solid fa-floppy-disk"></i> <span>Save</span>
}
</button>
</nav>
</dialog>
@code {
[Parameter]
public string? id { get; set; }
[Parameter]
public bool Active { get; set; }
[Parameter]
public string Content { get; set; } = string.Empty;
private bool loading = false;
public async Task PostEphemeral() {
loading = true;
await InvokeAsync(StateHasChanged);
var result = await api.PostEphemeral(Content);
if (result != null) {
await State.RefreshStatuses();
State.SendRefresh();
await InvokeAsync(StateHasChanged);
}
this.Active = false;
await JS.InvokeVoidAsync("ui", "#" + id);
Content = string.Empty;
loading = false;
}
}

View file

@ -1,6 +1,6 @@
@inject IJSRuntime JS
@inject State State
@inject RestService api
@inject ApiService api
@inject NavigationManager navigationManager
<div class="overlay @(Active ? "active" : string.Empty)" data-ui="#@id"></div>
@ -9,12 +9,6 @@
<div class="row">
<button @onclick="PicFromMedia"><i class="fa-solid fa-image"></i> Select a picture</button>
<button @onclick="PicFromPhoto"><i class="fa-solid fa-camera"></i> Take a photo</button>
@* <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> *@
</div>
<div class="row">
@if(Base64File != null && FileSize != null){
@ -55,7 +49,7 @@
[Parameter]
public string? Description { get; set; }
[Parameter]
public string id { get; set; }
public string? id { get; set; }
[Parameter]
public bool Active { get; set; }
@ -74,9 +68,9 @@
loading = true;
await InvokeAsync(StateHasChanged);
PutPicResponseData? response = await api.PutPic(State.SelectedAddressName, Base64File);
PutPicResponseData? response = await api.PutPic(State.SelectedAddressName!, Base64File!);
if(!string.IsNullOrEmpty(Description) && response != null && !string.IsNullOrEmpty(response.Id)) {
await api.PostPicDescription(State.SelectedAddressName, response.Id, Description);
await api.PostPicDescription(State.SelectedAddressName!, response.Id, Description);
await State.RefreshPics();
State.SendRefresh();
await InvokeAsync(StateHasChanged);
@ -116,8 +110,17 @@
}
private async Task PopulateFileDetails() {
FileContentType = File.ContentType;
FileSize = await Utilities.FileSize(File);
Base64File = await Utilities.Base64FromFile(File);
if (File == null)
{
FileContentType = null;
FileSize = null;
Base64File = null;
}
else
{
FileContentType = File.ContentType;
FileSize = await Utilities.FileSize(File);
Base64File = await Utilities.Base64FromFile(File);
}
}
}

View file

@ -1,7 +1,8 @@
@inject IJSRuntime JS
@inject State State
@inject RestService api
@inject ApiService api
@inject NavigationManager navigationManager
@inject NavigatorService navigatorService
<div class="overlay @(Active ? "active" : string.Empty)" data-ui="#@id"></div>
<dialog id="@id" class="@(Active ? "active" : string.Empty)" open="@Active">
@ -27,7 +28,8 @@
</button>
</div>
<div class="field textarea border max">
<InputTextArea @bind-Value="Content"></InputTextArea>
<textarea @bind="@Content" @bind:event="oninput" />
<div class="right-align"><small class="@( Content.Length >= 500 ? "red" : Content.Length >= 260 ? "yellow-text" : "")">@Content.Length / 500</small></div>
</div>
</div>
<nav class="right-align no-space">
@ -52,7 +54,7 @@
@code {
[Parameter]
public string id { get; set; }
public string? id { get; set; }
[Parameter]
public bool Active { get; set; }
[Parameter]
@ -66,16 +68,26 @@
public async Task PostStatus() {
StatusPost post = new StatusPost
{
StatusPost post = new StatusPost {
Emoji = Emoji,
Content = Content
};
if (State?.SelectedAddress?.Preferences?.Statuslog?.MastodonPosting ?? false){
if (State?.SelectedAddress?.Preferences?.Statuslog?.MastodonPosting ?? false) {
post.SkipMastodonPost = !postToMastodon;
}
if(Content.Length >= 500) {
bool answer = await navigatorService.Page!.DisplayAlert(
"Character limit reached",
"Your message is over 500 characters, which is a lot for a status.\n"
+ ((postToMastodon && !(post.SkipMastodonPost ?? true))? "If you continue, your post will not make it over to Mastodon.\n" : "")
+ "Do you wish to post it anyway?",
"Yes", "No"
);
if (!answer) return;
}
loading = true;
await InvokeAsync(StateHasChanged);
var result = await api.StatusPost(State!.SelectedAddressName!, post);

View file

@ -13,9 +13,9 @@
@code {
[Parameter]
public string icon { get; set; }
public string? icon { get; set; }
[Parameter]
public string title { get; set; }
public string? title { get; set; }
[Parameter]
public RenderFragment Description { get; set; }
public RenderFragment? Description { get; set; }
}

View file

@ -16,13 +16,13 @@
<article id="directoryIndex" class="responsive">
<nav class="wrap">
@foreach (var group in groupedAddresses) {
<a @onclick='()=>{JS.InvokeVoidAsync("scrollToId", $"index-{group.Key}");}' class="button circle transparent address">@group.Key</a>
<a @onclick='()=>{JS.InvokeVoidAsync("scrollToId", $"index-{group.Key}");}' class="button circle transparent honey">@group.Key</a>
}
</nav>
</article>
<article id="directory" class="responsive">
@foreach(var group in groupedAddresses) {
<h3 class="address" id="index-@group.Key">— @group.Key —</h3>
<h3 class="honey" id="index-@group.Key">— @group.Key —</h3>
<ul>
@foreach(string address in group) {
string displayAddress = address;
@ -35,7 +35,7 @@
catch (Exception) { }
}
<li>
<a class="chip medium no-border no-margin" href="/person/@address">
<a class="chip medium no-border tiny-margin transparent" href="/person/@address">
<img class="circle avatar responsive" src="https://profiles.cache.lol/@linkAddress/picture">
<span>@displayAddress</span>
</a>

View file

@ -1,15 +1,35 @@
@page "/editNow"
@inject NavigationManager Nav
@inject RestService api
@inject ApiService api
@inject State State
@inject IJSRuntime JS
<div class="max markdown-editor">
<MarkdownEditor @ref="Editor"
@bind-Value="@markdownValue"
Theme="material-darker"
MaxHeight="100%"
/>
@if (markdownValue != null)
{
<MarkdownEditor @ref="Editor"
@bind-Value="@markdownValue"
Theme="material-darker"
MaxHeight="100%"
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" />
<MarkdownToolbarButton Action="MarkdownAction.Heading" Icon="fa-solid fa-heading" Title="Heading" />
<MarkdownToolbarButton Action="MarkdownAction.Code" Icon="fa-solid fa-code" Title="Code" Separator="true" />
<MarkdownToolbarButton Action="MarkdownAction.Quote" Icon="fa-solid fa-quote-left" Title="Quote" />
<MarkdownToolbarButton Action="MarkdownAction.UnorderedList" Icon="fa-solid fa-list-ul" Title="Unordered List" />
<MarkdownToolbarButton Action="MarkdownAction.OrderedList" Icon="fa-solid fa-list-ol" Title="Ordered List" />
<MarkdownToolbarButton Action="MarkdownAction.Link" Icon="fa-solid fa-link" Title="Link" Separator="true" />
<MarkdownToolbarButton Action="MarkdownAction.Image" Icon="fa-solid fa-image" Title="Image" />
<MarkdownToolbarButton Action="MarkdownAction.HorizontalRule" Icon="fa-solid fa-horizontal-rule" Title="Horizontal Rule" />
<MarkdownToolbarButton Action="MarkdownAction.Guide" Icon="fa-solid fa-circle-question" Title="Guide" Separator="true" />
<MarkdownToolbarButton Action="MarkdownAction.Custom" Icon="omg-icon omg-prami" Title="Editor Information" Name="Help" />
</Toolbar>
</MarkdownEditor>
}
</div>
<nav>
@ -30,22 +50,27 @@
</nav>
@code {
private MarkdownEditor Editor;
private MarkdownEditor? Editor;
private bool listed;
private string markdownValue;
private string? markdownValue;
private bool loading = false;
private bool loading = true;
protected override async Task OnInitializedAsync() {
await base.OnInitializedAsync();
NowContentData? data = await api.GetNowPage(State.SelectedAddressName);
NowContentData? data = await api.GetNowPage(State.SelectedAddressName!);
if (data != null)
{
listed = data.Listed == 1;
markdownValue = data.Content;
await Editor.SetValueAsync(markdownValue);
loading = false;
await InvokeAsync(StateHasChanged);
await Editor!.SetValueAsync(markdownValue);
}
InvokeAsync(StateHasChanged);
loading = false;
await InvokeAsync(StateHasChanged);
}
Task OnMarkdownValueChanged(string value) {
@ -55,7 +80,7 @@
public async Task Save() {
loading = true;
await InvokeAsync(StateHasChanged);
var result = await api.PostNowPage(State.SelectedAddressName, markdownValue, listed);
var result = await api.PostNowPage(State.SelectedAddressName!, markdownValue ?? string.Empty, listed);
if (result != null) {
await State.RefreshNow();
await InvokeAsync(StateHasChanged);
@ -64,4 +89,10 @@
loading = false;
}
public async Task OnCustomButtonClicked(MarkdownButtonEventArgs eventArgs) {
if (eventArgs.Name == "Help") {
await JS.InvokeVoidAsync("open", "https://home.omg.lol/info/editor", "_blank");
}
}
}

View file

@ -0,0 +1,142 @@
@page "/editProfile"
@inject NavigationManager Nav
@inject ApiService api
@inject State State
@inject IJSRuntime JS
<div class="max markdown-editor">
@if (markdownValue != null)
{
<MarkdownEditor @ref="Editor"
@bind-Value="@markdownValue"
Theme="material-darker"
MaxHeight="100%"
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" />
<MarkdownToolbarButton Action="MarkdownAction.Heading" Icon="fa-solid fa-heading" Title="Heading" />
<MarkdownToolbarButton Action="MarkdownAction.Code" Icon="fa-solid fa-code" Title="Code" Separator="true" />
<MarkdownToolbarButton Action="MarkdownAction.Quote" Icon="fa-solid fa-quote-left" Title="Quote" />
<MarkdownToolbarButton Action="MarkdownAction.UnorderedList" Icon="fa-solid fa-list-ul" Title="Unordered List" />
<MarkdownToolbarButton Action="MarkdownAction.OrderedList" Icon="fa-solid fa-list-ol" Title="Ordered List" />
<MarkdownToolbarButton Action="MarkdownAction.Link" Icon="fa-solid fa-link" Title="Link" Separator="true" />
<MarkdownToolbarButton Action="MarkdownAction.Image" Icon="fa-solid fa-image" Title="Image" />
<MarkdownToolbarButton Action="MarkdownAction.HorizontalRule" Icon="fa-solid fa-horizontal-rule" Title="Horizontal Rule" />
<MarkdownToolbarButton Action="MarkdownAction.Guide" Icon="fa-solid fa-circle-question" Title="Guide" Separator="true" />
<MarkdownToolbarButton Action="MarkdownAction.Custom" Icon="omg-icon omg-prami" Title="Editor Information" Name="Help" />
</Toolbar>
</MarkdownEditor>
}
</div>
@if (markdownValue != null)
{
<details id="advanced">
<summary> Advanced </summary>
<h5>Theme:</h5>
<div class="row bottom-margin">
<ThemeDialog id="theme-modal" onthemechanged="ThemeChanged"></ThemeDialog>
<a data-ui="#theme-modal" class="row min" style="text-decoration:none;">
@if(selectedTheme != null) {
<ThemeCard theme="selectedTheme"></ThemeCard>
}
else {
<button>Choose a theme</button>
}
</a>
</div>
<small>Style you include here will be places in a &lt;style&gt; element in your pages &lt;head&gt;.</small>
<div class="field textarea label border max">
<InputTextArea @bind-Value="css"></InputTextArea>
<label>Custom CSS</label>
</div>
<small>Anything you put here will be included in your pages &lt;head&gt; element.</small>
<div class="field textarea label border max">
<InputTextArea @bind-Value="head"></InputTextArea>
<label>Additional &lt;head&gt; Content</label>
</div>
</details>
}
<nav>
<div class="max"></div>
<button class="transparent link" onclick="history.back();" disabled="@loading">Cancel</button>
<button @onclick="Save" disabled="@loading">
@if (loading) {
<span>Saving...</span>
}
else {
<i class="fa-solid fa-floppy-disk"></i> <span>Save & Publish</span>
}
</button>
</nav>
@code {
private MarkdownEditor? Editor;
private string? markdownValue;
private string? css;
private string? head;
private string? theme;
private Theme? selectedTheme;
private Dictionary<string, Theme>? themes;
private bool loading = true;
protected override async Task OnInitializedAsync() {
await base.OnInitializedAsync();
ProfileResponseData? data = await api.GetProfile(State.SelectedAddressName!);
if (data != null) {
markdownValue = data.Content;
css = data.Css;
head = data.Head;
theme = data.Theme;
themes = await State.GetThemes();
selectedTheme = themes?[theme];
loading = false;
await InvokeAsync(StateHasChanged);
await Editor!.SetValueAsync(markdownValue);
}
loading = false;
await InvokeAsync(StateHasChanged);
}
Task OnMarkdownValueChanged(string value) {
return Task.CompletedTask;
}
public async Task Save() {
loading = true;
await InvokeAsync(StateHasChanged);
var result = await api.PostProfile(State.SelectedAddressName!,
new PostProfile() {
Content = markdownValue ?? string.Empty,
Css = string.IsNullOrEmpty(css) ? null : css,
Head = string.IsNullOrEmpty(head) ? null : head,
Theme = string.IsNullOrEmpty(theme) ? null : theme
});
if (result != null) {
await State.RefreshNow();
await InvokeAsync(StateHasChanged);
Nav.NavigateTo($"/person/{State.SelectedAddressName}#profile");
}
loading = false;
await InvokeAsync(StateHasChanged);
}
public async Task OnCustomButtonClicked(MarkdownButtonEventArgs eventArgs) {
if (eventArgs.Name == "Help") {
await JS.InvokeVoidAsync("open", "https://home.omg.lol/info/editor", "_blank");
}
}
public void ThemeChanged(Theme? _theme) {
theme = _theme?.Id;
selectedTheme = _theme;
InvokeAsync(StateHasChanged);
}
}

View file

@ -9,11 +9,20 @@
<Description><a href="https://eph.emer.al">Eph.emer.al</a> is a place for fleeting thoughts. Everything on this page will disappear after a while.</Description>
</PageHeading>
<AuthorizeView>
<Authorized>
<button class="fab circle extra large-elevate" data-ui="#ephemeral-modal">
<i class="fa-light fa-comment-plus"></i>
</button>
<NewEphemeral id="ephemeral-modal"></NewEphemeral>
</Authorized>
</AuthorizeView>
<div id="ephemeral" class="responsive">
@if (messages != null) {
foreach (MarkupString message in messages) {
<article class="ephemeral center">
<article class="ephemeral">
@message
</article>
}

View file

@ -1,8 +1,113 @@
@page "/feed"
<h3>Feed</h3>
@implements IDisposable
@inject IJSRuntime JS
@inject State State
@inject NavigationManager Nav
WIP
<RefreshButton></RefreshButton>
<PageHeading title="Timeline" icon="fa-solid fa-list-timeline">
<Description>A feed of all the statuses and pics of the people you follow.</Description>
</PageHeading>
@if(!(State.Following?.Any() ?? false)) {
<PageHeading title="" icon="fa-light fa-face-sad-sweat">
<Description>
It looks like you're not following anyone yet.
</Description>
</PageHeading>
<p class="center-align">Check out the <a href="/directory">Directory</a> (or other parts of the app) to find awesome people to follow.</p>
}
else {
<div class="responsive">
<div class="tabs scroll">
<a data-ui="#feed" class="active">
<i class="fa-solid fa-list-timeline"></i>
<span>Timeline</span>
</a>
<a data-ui="#following">
<i class="fa-duotone fa-address-book"></i>
<span>Following</span>
</a>
</div>
</div>
<div class="responsive page-container">
<div id="feed" class="page no-padding active">
@if (feed != null){
foreach (FeedItem item in feed) {
if (item.IsStatus) {
<StatusCard Status="@item.Status"></StatusCard>
}
else if (item.IsPic) {
<PicCard Pic="@item.Pic"></PicCard>
}
else if (item.IsPaste) {
<PasteCard Paste="@item.Paste"></PasteCard>
}
}
}
<LoadingCard id="feedLoading" icon="fa-solid fa-list-timeline"></LoadingCard>
</div>
<div id="following" class="page no-padding">
<ul>
@foreach (string address in State.Following ?? new List<string>()) {
string displayAddress = address;
string linkAddress = address;
@* if (group.Key == "😀") {
try {
linkAddress = idn.GetAscii(address);
displayAddress = $"{address} {linkAddress}";
}
catch (Exception) { }
} *@
<li class="vertical-margin row padding surface-container">
<img class="round" src="https://profiles.cache.lol/@linkAddress/picture">
<div class="max">
<a href="/person/@linkAddress" class="address"><i class="fa-solid fa-fw fa-at tiny"></i>@displayAddress</a>
</div>
<button id="follow-button" @onclick="() => UnfollowClick(address)">
<i class="fa-solid fa-minus"></i> Unfollow
</button>
</li>
}
</ul>
</div>
</div>
}
@code {
private IOrderedEnumerable<FeedItem>? feed;
protected override async Task OnInitializedAsync() {
await base.OnInitializedAsync();
string fragment = new Uri(Nav.Uri).Fragment;
await JS.InvokeVoidAsync("ui", fragment);
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 async Task UnfollowClick(string address) {
await State.Unfollow(address);
feed = await State.GetFeed(forceRefresh: true);
await InvokeAsync(StateHasChanged);
}
public void Dispose() {
State.PropertyChanged -= StateChanged;
State.CanRefresh = false;
}
}

View file

@ -16,7 +16,7 @@
<article class="now">
<nav>
<a class="author" href="/person/@now.Address#now">
<h6><i class="fa-duotone fa-seedling"></i> @now.Address</h6>
<h6><i class="fa-duotone fa-seedling"></i><span>@now.Address</span></h6>
</a>
</nav>
<nav>

View file

@ -5,14 +5,32 @@
<RefreshButton></RefreshButton>
<div class="row center-align">
<h3 class="page-heading"><i class="fa-solid fa-fw fa-at"></i>@Address</h3>
</div>
<div class="row center-align">
<img class="profile avatar" src="https://profiles.cache.lol/@Address/picture" alt="@Address" />
<div class="min">
@if (IsMe) {
<EditProfilePicDialog id="profile-pic" Address="@Address"></EditProfilePicDialog>
<button data-ui="#profile-pic" class="small circle small-elevate absolute top right no-margin" style="z-index:1;"><i class="fa-solid fa-pencil"></i></button>
}
<img class="profile avatar" src="https://profiles.cache.lol/@Address/picture" alt="@Address" />
</div>
</div>
@if (FeatureFlags.Following) {
<div class="row center-align">
@if (State.IsFollowing(Address)) {
<button id="follow-button" @onclick="async() => {await State.Unfollow(Address);await InvokeAsync(StateHasChanged);}">
<i class="fa-solid fa-minus"></i> Unfollow
</button>
}
else {
<button id="follow-button" @onclick="async() => {await State.Follow(Address);await InvokeAsync(StateHasChanged);}">
<i class="fa-solid fa-plus"></i> Follow
</button>
}
</div>
}
<div class="responsive">
<div class="tabs scroll">
<a data-ui="#profile" @onclick="ReloadProfile">
@ -33,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>
@ -40,6 +62,12 @@
<div id="profile" class="page no-padding">
<a href="@ProfileUrl" target="_blank" class="hover absolute top right chip fill large-elevate">Open in browser <i class="fa-solid fa-arrow-up-right-from-square tiny"></i></a>
<ExternalPageComponent id="profile_page" @ref="ProfilePage" Url="@ProfileUrl"></ExternalPageComponent>
@if (IsMe) {
<a href="/editProfile" class="button fab circle extra large-elevate center-align middle-align">
<i class="square fa-solid fa-file-pen" style="line-height:56px;"></i>
<span>Edit</span>
</a>
}
</div>
<div id="statuses" class="page padding active">
@ -55,10 +83,16 @@
</article>
</div>
}
@if (IsMe) {
<EditBioDialog id="edit-bio" Address="@Address"></EditBioDialog>
<div class="row center-align">
<button data-ui="#edit-bio"><i class="fa-solid fa-pencil"></i> Edit Bio</button>
</div>
}
<StatusList @ref="StatusList" StatusFunc="@(async(refresh) => await State.GetStatuses(Address, refresh))" Editable="@IsMe"></StatusList>
@if(IsMe) {
<button class="fab circle extra large-elevate" data-ui="#post-modal">
<i class="fa-solid fa-pen-to-square"></i>
<i class="fa-solid fa-message-plus square"></i>
</button>
<NewStatusDialog id="post-modal"></NewStatusDialog>
}
@ -85,10 +119,19 @@
}
</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-medical"></i>
</button>
<EditPasteDialog id="paste-modal"></EditPasteDialog>
}
</div>
</div>
@code {
private string _address;
private string _address = string.Empty;
[Parameter]
public string Address {
get => _address;
@ -96,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 {
@ -109,11 +153,12 @@
public ExternalPageComponent? NowPage { get; set; }
public ExternalPageComponent? ProfilePage { get; set; }
private StatusList StatusList { get; set; }
private PicList PicList { get; set; }
private StatusList? StatusList { get; set; }
private PicList? PicList { get; set; }
private PasteList? PasteList { get; set; }
private bool IsMe {
get => Address == State.SelectedAddressName;
get => State.AddressList?.Any(a => a.Address == Address) ?? false;
}
private MarkupString? bio;

View file

@ -17,7 +17,7 @@
</AuthorizeView>
@code {
public string SharePhoto { get; set; }
public string? SharePhoto { get; set; }
public long? SharePhotoSize { get; set; }
public string? SharePhotoContentType { get; set; }
public string? SharePhotoText { get; set; }

View file

@ -10,7 +10,7 @@
<AuthorizeView>
<Authorized>
<button class="fab circle extra large-elevate" data-ui="#post-modal">
<i class="fa-solid fa-pen-to-square"></i>
<i class="fa-solid fa-message-plus square"></i>
</button>
<NewStatusDialog id="post-modal" Active="true" Content="@Text"></NewStatusDialog>
</Authorized>

View file

@ -11,7 +11,7 @@
<AuthorizeView>
<Authorized>
<button class="fab circle extra large-elevate" data-ui="#post-modal">
<i class="fa-solid fa-pen-to-square"></i>
<i class="fa-solid fa-message-plus square"></i>
</button>
<NewStatusDialog id="post-modal"></NewStatusDialog>
</Authorized>

View file

@ -0,0 +1,76 @@
@using CommunityToolkit.Maui.Alerts
@inject IJSRuntime JS
<article class="paste">
@* TODO link to paste view
* <!--
* order:-178.75
* -->
*@
<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="() => CopyPaste()"><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 chip no-border"><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
});
}
public async Task CopyPaste() {
if(Paste != null && !string.IsNullOrEmpty(Paste?.Content)) {
await Clipboard.Default.SetTextAsync(Paste?.Content);
var toast = Toast.Make("Copied to clipboard");
await toast.Show();
}
}
}

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

@ -1,56 +1,59 @@
@inject IJSRuntime JS
<article class="no-padding">
<img src="@Pic.Url" loading="lazy">
<div class="padding">
<nav>
<a class="author" href="/person/@Pic.Address#pics">
<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>
@if(!string.IsNullOrWhiteSpace(Pic.Description)){
<p>@((MarkupString)Pic.DescriptionHtml)</p>
}
else {
<div class="padding padding yellow-2-bg yellow-9-fg">
<i class="fa-solid fa-triangle-exclamation"></i>
<span>This picture needs a description in order to be shared.</span>
</div>
}
<nav>
<div class="max"></div>
@if(Editable) {
<button @onclick="EditPic"><i class="fa-solid fa-pencil"></i> Edit</button>
<article class="pic no-padding">
<img src="@Pic!.Url" loading="lazy">
<div class="padding row">
<div class="emoji" data-emoji="🖼️">🖼️</div>
<div class="max">
<nav>
<a class="author" href="/person/@Pic.Address#pics">
<i class="fa-solid fa-fw fa-at"></i>@Pic.Address
</a>
<span class="max"></span>
</nav>
@if(!string.IsNullOrWhiteSpace(Pic.Description)){
<p>@((MarkupString)Pic.DescriptionHtml)</p>
}
<button class="transparent circle" @onclick="ShareClick">
<i class="fa-solid fa-share-nodes"></i>
</button>
</nav>
else {
<div class="padding padding yellow-2-bg yellow-9-fg">
<i class="fa-solid fa-triangle-exclamation"></i>
<span>This picture needs a description in order to be shared.</span>
</div>
}
<nav>
<a class="chip transparent-border">
<i class="fa fa-clock"></i> @Pic.RelativeTime
</a>
<div class="max"></div>
@if(Editable) {
<button @onclick="EditPic"><i class="fa-solid fa-pencil"></i> Edit</button>
}
<button class="transparent circle" @onclick="ShareClick">
<i class="fa-solid fa-share-nodes"></i>
</button>
</nav>
</div>
</div>
</article>
@code {
[Parameter]
public Pic Pic {get; set;}
public Pic? Pic {get; set;}
[Parameter]
public bool Editable { get; set; } = false;
[Parameter]
public EditPicDialog? Dialog { get; set; }
private async Task EditPic(EventArgs e){
Dialog.Pic = Pic;
Dialog!.Pic = Pic;
await InvokeAsync(StateHasChanged);
await JS.InvokeVoidAsync("ui", "#" + Dialog?.id);
}
public async Task ShareClick(EventArgs e){
await Share.Default.RequestAsync(new ShareTextRequest{
Uri = Pic.Url,
Text = Pic.Description,
Uri = Pic!.Url,
Text = Pic!.Description,
Title = "I saw this on some.pics",
Subject = "I saw this on some.pics"
});

View file

@ -14,7 +14,7 @@
@code {
[Parameter]
public Func<bool, Task<List<Pic>?>> PicsFunc { get; set; }
public Func<bool, Task<List<Pic>?>>? PicsFunc { get; set; }
[Parameter]
public bool Editable { get; set; } = false;
@ -22,9 +22,14 @@
private List<Pic>? pics;
// TODO: There is a noticable rendering delay between the pics loading and the page rendering
//TODO There is a noticable rendering delay between the pics loading and the page rendering
// <!--
// order:-145
// -->
protected override async Task OnInitializedAsync() {
await base.OnInitializedAsync();
if (PicsFunc == null) return;
if (pics == null || pics.Count == 0) pics = await PicsFunc(false);
State.PropertyChanged += StateChanged;
State.CanRefresh = true;
@ -33,6 +38,8 @@
}
private async void StateChanged(object? sender, PropertyChangedEventArgs e) {
if (PicsFunc == null) return;
if (e.PropertyName == nameof(State.IsRefreshing) && State.IsRefreshing) {
using (State.GetRefreshToken()){
pics = await PicsFunc(true);

View file

@ -1,6 +1,6 @@
@inject IJSRuntime JS
<article class="status gray-9-fg" style="background-color:@(Status.Background)">
<article class="status gray-9-fg" style="background-color:@(Status!.Background)">
<div class="row">
<div class="emoji" data-emoji="@Status.EmojiOrDefault">@Status.EmojiOrDefault</div>
<div class="max">
@ -36,21 +36,21 @@
@code {
[Parameter]
public Status Status { get; set; }
public Status? Status { get; set; }
[Parameter]
public bool Editable { get; set; } = false;
[Parameter]
public EditStatusDialog? Dialog { get; set; }
private async Task EditStatus(EventArgs e) {
Dialog.Status = Status;
Dialog!.Status = Status;
await InvokeAsync(StateHasChanged);
await JS.InvokeVoidAsync("ui", "#" + Dialog?.id);
}
public async Task ShareClick(EventArgs e){
await Share.Default.RequestAsync(new ShareTextRequest{
Text = $"{Status.Content}\n- from [@{Status.Address}]({Status.Url})",
Text = $"{Status!.Content}\n- from [@{Status.Address}]({Status.Url})",
Title = "I saw this on status.lol",
Subject = "I saw this on status.lol"
});

View file

@ -1,16 +0,0 @@
<article class="status">
<div class="row">
<div class="large emoji skeleton round" />
<div class="max">
<span class="author skeleton"> </span>
<p class="skeleton"> </p>
</div>
</div>
<nav>
<span class="chip transparent-border skeleton"> </span>
</nav>
</article>
@code {
}

View file

@ -14,7 +14,7 @@
@code {
[Parameter]
public Func<bool, Task<List<Status>?>> StatusFunc { get; set; }
public Func<bool, Task<List<Status>?>>? StatusFunc { get; set; }
[Parameter]
public bool Editable { get; set; } = false;
@ -24,6 +24,8 @@
protected override async Task OnInitializedAsync() {
await base.OnInitializedAsync();
if (StatusFunc == null) return;
if (statuses == null || statuses.Count == 0) statuses = await StatusFunc(false);
State.PropertyChanged += StateChanged;
State.CanRefresh = true;
@ -32,6 +34,8 @@
}
private async void StateChanged(object? sender, PropertyChangedEventArgs e) {
if (StatusFunc == null) return;
if (e.PropertyName == nameof(State.IsRefreshing) && State.IsRefreshing) {
using (State.GetRefreshToken()) {
statuses = await StatusFunc(true);

View file

@ -0,0 +1,11 @@
<article class="theme" style="@theme?.PreviewCssData?.BackgroundCss ; @theme?.PreviewCssData?.TextCss">
<h5 class="honey">@theme?.Name</h5>
<p class="small theme-author" style="@theme?.PreviewCssData?.LinkCss">
<i class="fa-solid fa-palette" style="@theme?.PreviewCssData?.IconCss"></i> by @theme?.Author
</p>
</article>
@code {
[Parameter]
public Theme? theme { get; set; }
}

View file

@ -0,0 +1,81 @@
@inject IJSRuntime JS
@inject State State
@inject ApiService api
@inject NavigationManager navigationManager
<div class="overlay @(Active ? "active" : string.Empty)" data-ui="#@id"></div>
<dialog id="@id" class="@(Active ? "active" : string.Empty)" open="@Active" style="overflow:auto;">
<h5>Choose a theme</h5>
<nav class="wrap max">
@if(themes != null) foreach(Theme theme in themes.Values) {
<a onclick="@(() => ClickTheme(theme))" class="min" style="text-decoration:none;">
<ThemeCard theme="@theme"></ThemeCard>
</a>
}
</nav>
<nav class="right-align no-space">
<button class="transparent link" data-ui="#@id">Cancel</button>
</nav>
</dialog>
<div class="overlay" data-ui="#@previewId"></div>
<dialog id="@previewId" style="overflow:auto;">
<h5 class="honey">@activeTheme?.Name</h5>
<div class="max">
<p>@((MarkupString)(activeTheme?.Description ?? string.Empty)) A theme by <a href="@activeTheme?.AuthorUrl" target="_blank">@activeTheme?.Author</a>.</p>
@if(themePreview != null) {
<ExternalPageComponent id="profile_page" @ref="iframe" SrcString="@themePreview.ToString()"></ExternalPageComponent>
}
</div>
<nav class="right-align no-space">
<button class="transparent link" @onclick="CancelPreview">Back</button>
<button @onclick=UseTheme><i class="fa-solid fa-palette"></i> Use the @activeTheme?.Name theme</button>
</nav>
</dialog>
@code {
private Dictionary<string, Theme>? themes;
[Parameter]
public string? id { get; set; }
private string? previewId { get => $"{id}-preview"; }
[Parameter]
public bool Active { get; set; }
[Parameter]
public Action<Theme?>? onthemechanged { get; set; }
private Theme? activeTheme { get; set; }
private MarkupString? themePreview { get; set; }
private ExternalPageComponent iframe { get; set; }
protected override async Task OnInitializedAsync() {
await base.OnInitializedAsync();
activeTheme = null;
themes = await State.GetThemes();
await InvokeAsync(StateHasChanged);
}
public async Task ClickTheme(Theme theme) {
activeTheme = theme;
await InvokeAsync(StateHasChanged);
await JS.InvokeVoidAsync("ui", "#" + id);
await JS.InvokeVoidAsync("ui", "#" + previewId);
themePreview = await api.GetThemePreview(theme.Id);
await InvokeAsync(StateHasChanged);
@* iframe.SrcString = themePreview.ToString(); *@
await iframe.Reload();
}
public async Task CancelPreview() {
activeTheme = null;
await InvokeAsync(StateHasChanged);
await JS.InvokeVoidAsync("ui", "#" + previewId);
await JS.InvokeVoidAsync("ui", "#" + id);
}
public async Task UseTheme() {
onthemechanged?.Invoke(activeTheme);
activeTheme = null;
await InvokeAsync(StateHasChanged);
await JS.InvokeVoidAsync("ui", "#" + previewId);
}
}

View file

@ -14,3 +14,4 @@
@using Markdig
@using PSC.Blazor.Components.MarkdownEditor
@using PSC.Blazor.Components.MarkdownEditor.EventsArgs
@using PSC.Blazor.Components.MarkdownEditor.Enums

View file

@ -1,12 +0,0 @@
<?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>

View file

@ -1,18 +0,0 @@
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

@ -1,9 +1,7 @@
using Markdig;
using CommunityToolkit.Maui;
using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using Neighbourhood.omg.lol.Models;
using System.Reflection;
namespace Neighbourhood.omg.lol {
public static class MauiProgram {
@ -11,26 +9,18 @@ namespace Neighbourhood.omg.lol {
var builder = MauiApp.CreateBuilder();
builder
.UseMauiApp<App>()
.ConfigureFonts(fonts => {
fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
.UseMauiCommunityToolkit(options => {
options.SetShouldEnableSnackbarOnWindows(true);
});
builder.Services.AddMauiBlazorWebView();
builder.Services.AddTransient<LoginWebViewPage>();
builder.Services.AddTransient<EphemeralWebPage>();
builder.Services.AddSingleton<RestService>();
builder.Services.AddSingleton<ApiService>();
builder.Services.AddSingleton<State>();
builder.Services.AddSingleton<NavigatorService>();
//ConfigurationBuilder configurationBuilder = new ConfigurationBuilder();
//builder.Services.AddSingleton<IConfiguration>(configurationBuilder.AddUserSecrets<App>().Build());
var assembly = Assembly.GetExecutingAssembly();
var details = assembly.GetName();
App.Name = details.Name!;
App.Version = details.Version!.ToString();
var appSettings = $"{App.Name}.appsettings.json";
using var stream = assembly.GetManifestResourceStream(appSettings);
using Stream stream = App.Assembly.GetManifestResourceStream($"{App.Name}.appsettings.json")!;
var config = new ConfigurationBuilder().AddJsonStream(stream).Build();
builder.Configuration.AddConfiguration(config);

View file

@ -0,0 +1,8 @@
namespace Neighbourhood.omg.lol.Models {
public class AccountResponseData : IOmgLolResponseData {
public string Message { get; set; } = string.Empty;
public string Email { get; set; } = string.Empty;
public string Name { get; set; } = string.Empty;
public TimeData Created { get; set; } = TimeData.Empty;
}
}

View file

@ -1,15 +1,9 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Neighbourhood.omg.lol.Models {
namespace Neighbourhood.omg.lol.Models {
public class AddressResponseData : IOmgLolResponseData {
public string Address { get; set; }
public string Message { get; set; }
public RegistrationData Registration { get; set; }
public ExpirationData Expiration { get; set; }
public string Address { get; set; } = string.Empty;
public string Message { get; set; } = string.Empty;
public RegistrationData? Registration { get; set; }
public ExpirationData? Expiration { get; set; }
public PreferenceData? Preferences { get; set; }
public class RegistrationData : TimeData {

View file

@ -0,0 +1,5 @@
namespace Neighbourhood.omg.lol.Models {
public class AddressResponseList : List<AddressResponseData>, IOmgLolResponseList<AddressResponseData> {
public string Message { get; set; } = string.Empty;
}
}

View file

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

View file

@ -0,0 +1,7 @@
namespace Neighbourhood.omg.lol.Models {
public class DirectoryResponseData : IOmgLolResponseData {
public string Message { get; set; } = string.Empty;
public string Url { get; set; } = string.Empty;
public List<string> Directory { get; set; } = new List<string>();
}
}

View file

@ -0,0 +1,5 @@
namespace Neighbourhood.omg.lol.Models {
public class EphemeralData {
public string Content { get; set; } = string.Empty;
}
}

View file

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

View file

@ -1,4 +1,5 @@
namespace Neighbourhood.omg.lol.Models {
public interface IOmgLolResponseData {
public string Message { get; set; }
}
}

View file

@ -0,0 +1,4 @@
namespace Neighbourhood.omg.lol.Models {
public interface IOmgLolResponseList<T> : IList<T>, IOmgLolResponseData where T : IOmgLolResponseData {
}
}

View file

@ -1,10 +1,4 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Neighbourhood.omg.lol.Models {
namespace Neighbourhood.omg.lol.Models {
public class NowContentData {
public string? Content { get; set; }
public long? Updated { get; set; }

12
Models/API/NowData.cs Normal file
View file

@ -0,0 +1,12 @@
namespace Neighbourhood.omg.lol.Models
{
public class NowData {
public string Address { get; set; } = string.Empty;
public string Url { get; set; } = string.Empty;
public TimeData Updated { get; set; } = TimeData.Empty;
public string UpdatedRelative {
get => Utilities.RelativeTimeFromUnix(Convert.ToInt64(Updated.UnixEpochTime));
}
}
}

View file

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

View file

@ -0,0 +1,7 @@
namespace Neighbourhood.omg.lol.Models {
public class NowResponseData : IOmgLolResponseData {
public string Message { get; set; } = string.Empty;
public long Count { get; set; }
public List<NowData> Garden { get; set; } = new List<NowData>();
}
}

View file

@ -0,0 +1,11 @@
namespace Neighbourhood.omg.lol.Models {
public class OmgLolApiException<T> : Exception where T : IOmgLolResponseData {
public OmgLolResponse<T>? Response { get; set; }
public OmgLolApiException(OmgLolResponse<T>? response) : base(response?.Response?.Message) {
Response = response;
}
public OmgLolApiException(string? response) : base(response) { }
}
}

View file

@ -1,6 +1,6 @@
namespace Neighbourhood.omg.lol.Models {
public class OmgLolResponse<TResponseData> where TResponseData : IOmgLolResponseData {
public OmgLolRequestData Request { get; set; }
public TResponseData Response { get; set; }
public OmgLolRequestData? Request { get; set; }
public TResponseData? Response { get; set; }
}
}

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,7 @@
namespace Neighbourhood.omg.lol.Models {
public class PatchStatus {
public string Id { get; set; } = string.Empty;
public string Content { get; set; } = string.Empty;
public string? Emoji { get; set; }
}
}

View file

@ -0,0 +1,7 @@
namespace Neighbourhood.omg.lol.Models {
public class PatchStatusResponseData : IOmgLolResponseData {
public string Message { get; set; } = string.Empty;
public string Id { get; set; } = string.Empty;
public string Url { get; set; } = string.Empty;
}
}

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;
}
}

5
Models/API/PostPic.cs Normal file
View file

@ -0,0 +1,5 @@
namespace Neighbourhood.omg.lol.Models {
public class PostPic {
public string? Description { get; set; }
}
}

View file

@ -0,0 +1,9 @@
namespace Neighbourhood.omg.lol.Models {
public class PostProfile {
public string Content { get; set; } = string.Empty;
public bool Publish { get; set; } = true;
public string? Theme { get; set; }
public string? Css { get; set; }
public string? Head { get; set; }
}
}

View file

@ -0,0 +1,5 @@
namespace Neighbourhood.omg.lol.Models {
public class PostStatusBio {
public string Content { get; set; } = string.Empty;
}
}

View file

@ -0,0 +1,8 @@
namespace Neighbourhood.omg.lol.Models {
public class PreviewCssData {
public string BackgroundCss { get; set; } = string.Empty;
public string TextCss { get; set; } = string.Empty;
public string LinkCss { get; set; } = string.Empty;
public string IconCss { get; set; } = string.Empty;
}
}

View file

@ -0,0 +1,16 @@
namespace Neighbourhood.omg.lol.Models {
public class ProfileResponseData: IOmgLolResponseData {
public string Message { get; set; } = string.Empty;
public string Content { get; set; } = string.Empty;
public string Type { get; set; } = string.Empty;
public string Theme { get; set; } = string.Empty;
public string? Css { get; set; }
public string? Head { get; set; }
public short Verified { get; set; }
public string Pfp { get; set; } = string.Empty;
public string Metadata { get; set; } = string.Empty;
public string Branding { get; set; } = string.Empty;
public string? Modified { get; set; }
}
}

5
Models/API/PutPic.cs Normal file
View file

@ -0,0 +1,5 @@
namespace Neighbourhood.omg.lol.Models {
public class PutPic {
public string Pic { get; set; } = string.Empty;
}
}

View file

@ -0,0 +1,9 @@
namespace Neighbourhood.omg.lol.Models {
public class PutPicResponseData : IOmgLolResponseData {
public string Message { get; set; } = string.Empty;
public string Id { get; set; } = string.Empty;
public long Size { get; set; }
public string Mime { get; set; } = string.Empty;
public string Url { get; set; } = string.Empty;
}
}

View file

@ -0,0 +1,6 @@
namespace Neighbourhood.omg.lol.Models {
public class SomePicsResponseData : IOmgLolResponseData {
public string Message { get; set; } = string.Empty;
public List<Pic>? Pics { get; set; }
}
}

View file

@ -0,0 +1,8 @@
namespace Neighbourhood.omg.lol.Models {
public class StatusBioResponseData : IOmgLolResponseData {
public string Message { get; set; } = string.Empty;
public string Bio { get; set; } = string.Empty;
public string Css { get; set; } = string.Empty;
public string Head { get; set; } = string.Empty;
}
}

View file

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

View file

@ -0,0 +1,9 @@
namespace Neighbourhood.omg.lol.Models {
public class StatusPostResponseData : IOmgLolResponseData {
public string Message { get; set; } = string.Empty;
public string? Id { get; set; }
public string? Url { get; set; }
public string? ExternalUrl { get; set; }
}
}

View file

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

View file

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

View file

@ -0,0 +1,6 @@
namespace Neighbourhood.omg.lol.Models {
public class ThemeResponseData : IOmgLolResponseData {
public string Message { get; set; } = string.Empty;
public Dictionary<string, Theme> Themes { get; set; } = new Dictionary<string, Theme>();
}
}

View file

@ -0,0 +1,7 @@
namespace Neighbourhood.omg.lol.Models {
public class TokenResponseData {
public string AccessToken { get; set; } = string.Empty;
public string TokenType { get; set; } = string.Empty;
public string Scope { get; set; } = string.Empty;
}
}

View file

@ -1,14 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Neighbourhood.omg.lol.Models {
public class AccountResponseData : IOmgLolResponseData {
public string Message { get; set; }
public string Email { get; set; }
public string Name { get; set; }
// created, api_key and settings
}
}

View file

@ -1,10 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Neighbourhood.omg.lol.Models {
public class AddressResponseList : List<AddressResponseData>, IOmgLolResponseList<AddressResponseData> {
}
}

View file

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

View file

@ -1,13 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Neighbourhood.omg.lol.Models {
public class DirectoryResponseData : IOmgLolResponseData {
public string Message { get; set; }
public string Url { get; set; }
public List<string> Directory { get; set; }
}
}

13
Models/FeedItem.cs Normal file
View file

@ -0,0 +1,13 @@
namespace Neighbourhood.omg.lol.Models {
public class FeedItem {
public Status? Status { get; set; }
public Pic? Pic { get; set; }
public Paste? Paste { get; set; }
public bool IsStatus { get => Status != null; }
public bool IsPic { get => Pic != null; }
public bool IsPaste { get => Paste != null; }
public DateTimeOffset? CreatedTime { get => Status?.CreatedTime ?? Pic?.CreatedTime ?? Paste?.ModifiedTime; }
}
}

View file

@ -1,10 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Neighbourhood.omg.lol.Models {
public interface IOmgLolResponseList<T> : IList<T>, IOmgLolResponseData where T : IOmgLolResponseData {
}
}

View file

@ -1,17 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Neighbourhood.omg.lol.Models {
public class NowData {
public string Address { get; set; }
public string Url { get; set; }
public TimeData Updated { get; set; }
public string UpdatedRelative {
get => Utilities.RelativeTimeFromUnix(Convert.ToInt64(Updated.UnixEpochTime));
}
}
}

View file

@ -1,12 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Neighbourhood.omg.lol.Models {
public class NowPageResponseData : IOmgLolResponseData {
public string Message { get; set; }
public NowContentData Now { get; set; }
}
}

View file

@ -1,13 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Neighbourhood.omg.lol.Models {
public class NowResponseData : IOmgLolResponseData {
public string Message { get; set; }
public long Count { get; set; }
public List<NowData> Garden { get; set; }
}
}

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

@ -1,13 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Neighbourhood.omg.lol.Models {
public class PatchStatus {
public string Id { get; set; }
public string Content { get; set; }
public string? Emoji { get; set; }
}
}

Some files were not shown because too many files have changed in this diff Show more