From e75b64af1420df18350f6fcff221f074c6823aa7 Mon Sep 17 00:00:00 2001 From: purifetchi <0xlunaric@gmail.com> Date: Fri, 27 Jun 2025 21:06:21 +0200 Subject: [PATCH 01/23] feat: start work on profile editing frontend. --- PinkSea.Frontend/.vscode/settings.json | 5 +- PinkSea.Frontend/src/api/atproto/lexicons.ts | 15 +- .../src/components/SettingsGroup.vue | 34 +++++ PinkSea.Frontend/src/components/UserCard.vue | 98 +++++++++++++ PinkSea.Frontend/src/models/profile.ts | 11 ++ PinkSea.Frontend/src/router/index.ts | 17 ++- PinkSea.Frontend/src/views/SettingsView.vue | 50 +++---- PinkSea.Frontend/src/views/UserEditView.vue | 112 +++++++++++++++ PinkSea.Frontend/src/views/UserView.vue | 131 ++++++------------ 9 files changed, 343 insertions(+), 130 deletions(-) create mode 100644 PinkSea.Frontend/src/components/SettingsGroup.vue create mode 100644 PinkSea.Frontend/src/components/UserCard.vue create mode 100644 PinkSea.Frontend/src/models/profile.ts create mode 100644 PinkSea.Frontend/src/views/UserEditView.vue diff --git a/PinkSea.Frontend/.vscode/settings.json b/PinkSea.Frontend/.vscode/settings.json index 384d612..a6d8a68 100644 --- a/PinkSea.Frontend/.vscode/settings.json +++ b/PinkSea.Frontend/.vscode/settings.json @@ -9,5 +9,8 @@ "source.fixAll": "explicit" }, "editor.formatOnSave": true, - "editor.defaultFormatter": "esbenp.prettier-vscode" + "editor.defaultFormatter": "esbenp.prettier-vscode", + "[typescript]": { + "editor.defaultFormatter": "vscode.typescript-language-features" + } } diff --git a/PinkSea.Frontend/src/api/atproto/lexicons.ts b/PinkSea.Frontend/src/api/atproto/lexicons.ts index d470bf4..71afc54 100644 --- a/PinkSea.Frontend/src/api/atproto/lexicons.ts +++ b/PinkSea.Frontend/src/api/atproto/lexicons.ts @@ -2,6 +2,7 @@ import type { Oekaki } from '@/models/oekaki' import type { SearchType } from '@/models/search-type' import type { Author } from '@/models/author' import type { TagSearchResult } from '@/models/tag-search-result' +import type Profile from '@/models/profile' declare module '@atcute/client/lexicons' { type EmptyParams = object @@ -109,18 +110,6 @@ declare module '@atcute/client/lexicons' { interface Params { did: string } - - interface Output { - did: string, - handle: string, - nick: string, - description: string, - links: [{ - name: string, - url: string - }], - avatar: string - } } // eslint-disable-next-line @typescript-eslint/no-namespace @@ -174,7 +163,7 @@ declare module '@atcute/client/lexicons' { }, 'com.shinolabs.pinksea.unspecced.getProfile': { params: ComShinolabsPinkseaUnspeccedGetProfile.Params, - output: ComShinolabsPinkseaUnspeccedGetProfile.Output + output: Profile }, 'com.shinolabs.pinksea.getSearchResults': { params: ComShinolabsPinkseaGetSearchResults.Params, diff --git a/PinkSea.Frontend/src/components/SettingsGroup.vue b/PinkSea.Frontend/src/components/SettingsGroup.vue new file mode 100644 index 0000000..1af7ca3 --- /dev/null +++ b/PinkSea.Frontend/src/components/SettingsGroup.vue @@ -0,0 +1,34 @@ + + + + + \ No newline at end of file diff --git a/PinkSea.Frontend/src/components/UserCard.vue b/PinkSea.Frontend/src/components/UserCard.vue new file mode 100644 index 0000000..24b30c4 --- /dev/null +++ b/PinkSea.Frontend/src/components/UserCard.vue @@ -0,0 +1,98 @@ + + + + + \ No newline at end of file diff --git a/PinkSea.Frontend/src/models/profile.ts b/PinkSea.Frontend/src/models/profile.ts new file mode 100644 index 0000000..bd9451c --- /dev/null +++ b/PinkSea.Frontend/src/models/profile.ts @@ -0,0 +1,11 @@ +export default interface Profile { + did: string, + handle: string, + nick: string, + description: string, + links: [{ + name: string, + url: string + }], + avatar: string +} \ No newline at end of file diff --git a/PinkSea.Frontend/src/router/index.ts b/PinkSea.Frontend/src/router/index.ts index 561b486..5420d04 100644 --- a/PinkSea.Frontend/src/router/index.ts +++ b/PinkSea.Frontend/src/router/index.ts @@ -11,6 +11,7 @@ import SettingsView from '@/views/SettingsView.vue' import i18next from 'i18next' import { withTegakiViewBackProtection } from '@/api/tegaki/tegaki-view-helper' import SearchView from '@/views/SearchView.vue' +import UserEditView from '@/views/UserEditView.vue' const router = createRouter({ history: createWebHistory(import.meta.env.BASE_URL), @@ -46,7 +47,7 @@ const router = createRouter({ component: UserView, meta: { resolveBreadcrumb: async (route: RouteParamsGeneric) => { - const { data } = await xrpc.get("com.shinolabs.pinksea.getHandleFromDid", { params: { did: route.did as string }}); + const { data } = await xrpc.get("com.shinolabs.pinksea.getHandleFromDid", { params: { did: route.did as string } }); return { name: 'breadcrumb.user_profile', params: { handle: data.handle } }; } } @@ -57,7 +58,7 @@ const router = createRouter({ component: PostView, meta: { resolveBreadcrumb: async (route: RouteParamsGeneric) => { - const { data } = await xrpc.get("com.shinolabs.pinksea.getHandleFromDid", { params: { did: route.did as string }}); + const { data } = await xrpc.get("com.shinolabs.pinksea.getHandleFromDid", { params: { did: route.did as string } }); return { name: "breadcrumb.user_post", params: { handle: data.handle } }; } } @@ -78,7 +79,17 @@ const router = createRouter({ component: SettingsView, meta: { resolveBreadcrumb: async () => { - return { name: 'breadcrumb.settings' }; + return { name: 'breadcrumb.settings' }; + } + } + }, + { + path: '/settings/profile', + name: 'profile-settings', + component: UserEditView, + meta: { + resolveBreadcrumb: async () => { + return { name: 'breadcrumb.settings' }; } } }, diff --git a/PinkSea.Frontend/src/views/SettingsView.vue b/PinkSea.Frontend/src/views/SettingsView.vue index 866f904..669a1ce 100644 --- a/PinkSea.Frontend/src/views/SettingsView.vue +++ b/PinkSea.Frontend/src/views/SettingsView.vue @@ -18,13 +18,14 @@ watch(persistedStore, () => {
{{ $t("settings.category_general") }} {{ $t("settings.general_language") }} -
{{ $t("settings.category_sensitive") }} - {{ $t("settings.sensitive_blur_nsfw") }}
+ {{ $t("settings.sensitive_blur_nsfw") + }}
{{ $t("settings.sensitive_hide_nsfw") }}
@@ -33,26 +34,27 @@ watch(persistedStore, () => { diff --git a/PinkSea.Frontend/src/views/UserEditView.vue b/PinkSea.Frontend/src/views/UserEditView.vue new file mode 100644 index 0000000..61f8e4c --- /dev/null +++ b/PinkSea.Frontend/src/views/UserEditView.vue @@ -0,0 +1,112 @@ + + + + + \ No newline at end of file diff --git a/PinkSea.Frontend/src/views/UserView.vue b/PinkSea.Frontend/src/views/UserView.vue index 3223eff..b8f7676 100644 --- a/PinkSea.Frontend/src/views/UserView.vue +++ b/PinkSea.Frontend/src/views/UserView.vue @@ -8,6 +8,8 @@ import { useRoute } from 'vue-router' import { UserProfileTab } from '@/models/user-profile-tab' import { XRPCError } from '@atcute/client' import ErrorCard from '@/components/ErrorCard.vue' +import type Profile from '@/models/profile' +import UserCard from '@/components/UserCard.vue' const tabs = [ { @@ -20,7 +22,7 @@ const tabs = [ } ]; -const handle = ref(""); +const profile = ref(null); const route = useRoute(); const exists = ref(null); @@ -30,8 +32,8 @@ const currentTab = ref(UserProfileTab.Posts); watch(() => route.params.did, async () => { try { - const { data } = await xrpc.get("com.shinolabs.pinksea.unspecced.getProfile", { params: { did: route.params.did as string }}); - handle.value = data.handle; + const { data } = await xrpc.get("com.shinolabs.pinksea.unspecced.getProfile", { params: { did: route.params.did as string } }); + profile.value = data; exists.value = true; } catch (e) { if (e instanceof XRPCError) { @@ -46,14 +48,6 @@ watch(() => route.params.did, async () => { }, { immediate: true }); -const bskyUrl = computed(() => { - return `https://bsky.app/profile/${route.params.did}`; -}); - -const domainUrl = computed(() => { - return `http://${handle.value}`; -}); - From 6d4fe09cd3d8dea75a1326c8c6967833ed4bf190 Mon Sep 17 00:00:00 2001 From: purifetchi <0xlunaric@gmail.com> Date: Sun, 29 Jun 2025 11:34:04 +0200 Subject: [PATCH 02/23] feat: handle parsing profile record on backend and style front. --- PinkSea.AtProto/Helpers/AtLinkHelper.cs | 59 +++++++ PinkSea.Frontend/src/components/UserCard.vue | 12 +- PinkSea.Frontend/src/models/profile.ts | 4 +- PinkSea.Frontend/src/views/UserEditView.vue | 152 ++++++++++++++++-- PinkSea.Frontend/src/views/UserView.vue | 11 +- .../com/shinolabs/pinksea/profile.json | 59 +++++++ PinkSea/Lexicons/Records/Profile.cs | 34 ++++ PinkSea/Lexicons/Records/ProfileLink.cs | 21 +++ PinkSea/Program.cs | 2 +- .../Services/OekakiJetStreamEventHandler.cs | 70 +++++++- PinkSea/Services/UserService.cs | 50 ++++++ PinkSea/Xrpc/GetProfileQueryHandler.cs | 28 ++-- docker-compose.yml | 2 + 13 files changed, 464 insertions(+), 40 deletions(-) create mode 100644 PinkSea.AtProto/Helpers/AtLinkHelper.cs create mode 100644 PinkSea.Lexicons/com/shinolabs/pinksea/profile.json create mode 100644 PinkSea/Lexicons/Records/Profile.cs create mode 100644 PinkSea/Lexicons/Records/ProfileLink.cs diff --git a/PinkSea.AtProto/Helpers/AtLinkHelper.cs b/PinkSea.AtProto/Helpers/AtLinkHelper.cs new file mode 100644 index 0000000..1573a67 --- /dev/null +++ b/PinkSea.AtProto/Helpers/AtLinkHelper.cs @@ -0,0 +1,59 @@ +namespace PinkSea.AtProto.Helpers; + +/// +/// Utilities for working with at:// URIs used by the AT Protocol. +/// +public class AtLinkHelper +{ + private const string SchemePrefix = "at://"; + + /// Strongly-typed view of an AT Proto URI. + public readonly record struct AtUri(string Authority, string Collection, string RecordKey) + { + public override string ToString() => $"{SchemePrefix}{Authority}/{Collection}/{RecordKey}"; + } + + /// + /// Parse a raw at:// string and throw if it is invalid. + /// + public static AtUri Parse(string value) + { + if (!TryParse(value, out var result)) + throw new FormatException($"Invalid AT URI: \"{value}\""); + return result; + } + + /// + /// Try-parse variant that never throws. + /// + public static bool TryParse(string value, out AtUri result) + { + result = default; + + if (string.IsNullOrWhiteSpace(value) || + !value.StartsWith(SchemePrefix, StringComparison.OrdinalIgnoreCase)) + return false; + + // work with spans to avoid allocations + ReadOnlySpan span = value.AsSpan(SchemePrefix.Length); + + // authority = everything up to first '/' + int firstSlash = span.IndexOf('/'); + if (firstSlash <= 0) return false; + var authority = span[..firstSlash].ToString(); + + span = span[(firstSlash + 1)..]; // skip first '/' + int secondSlash = span.IndexOf('/'); + if (secondSlash <= 0 || secondSlash == span.Length - 1) return false; + + var collection = span[..secondSlash].ToString(); + var recordKey = span[(secondSlash + 1)..].ToString(); + + // basic sanity + if (collection.Length == 0 || recordKey.Length == 0) + return false; + + result = new AtUri(authority, collection, recordKey); + return true; + } +} \ No newline at end of file diff --git a/PinkSea.Frontend/src/components/UserCard.vue b/PinkSea.Frontend/src/components/UserCard.vue index 24b30c4..39b8535 100644 --- a/PinkSea.Frontend/src/components/UserCard.vue +++ b/PinkSea.Frontend/src/components/UserCard.vue @@ -1,16 +1,20 @@ @@ -31,6 +35,9 @@ const description = computed(() => { {{ link.name }} +
+ +
@@ -61,6 +68,7 @@ const description = computed(() => { .user-card-data-container { display: flex; flex-direction: column; + flex: 1; } .user-card-description { diff --git a/PinkSea.Frontend/src/models/profile.ts b/PinkSea.Frontend/src/models/profile.ts index bd9451c..308bf43 100644 --- a/PinkSea.Frontend/src/models/profile.ts +++ b/PinkSea.Frontend/src/models/profile.ts @@ -3,9 +3,9 @@ export default interface Profile { handle: string, nick: string, description: string, - links: [{ + links: { name: string, url: string - }], + }[], avatar: string } \ No newline at end of file diff --git a/PinkSea.Frontend/src/views/UserEditView.vue b/PinkSea.Frontend/src/views/UserEditView.vue index 61f8e4c..00a4461 100644 --- a/PinkSea.Frontend/src/views/UserEditView.vue +++ b/PinkSea.Frontend/src/views/UserEditView.vue @@ -11,6 +11,9 @@ import { onMounted, ref, watch } from 'vue'; const identityStore = useIdentityStore() const profile = ref(null); +const linkUrl = ref(""); +const linkName = ref(""); + const avatarList = ref([]); const updateProfile = async () => { @@ -47,6 +50,16 @@ const fetchPossibleAvatars = async () => { } }; +const addLink = (name: string, url: string) => { + profile.value!.links = [ + ...profile.value!.links, + { + name, + url + } + ]; +}; + watch(identityStore, updateProfile); onMounted(updateProfile); @@ -59,29 +72,92 @@ onMounted(updateProfile);
- +
+ +
-
- -
-
- -
+ + + + + + + + + + + +
+
Nickname
+
+ +
+ This is the name others will identify you by. By default, it's equal to your handle. +
+
+
Your bio
+
+ +
+ This is your bio. It's a short piece of text that's visible on your profile. By default, it's empty. +
+
+ v-on:click.prevent="profile.avatar = oekaki.image" + :class="oekaki.image === profile.avatar ? 'selected' : ''" /> +
+
+ Select an avatar from the list of oekaki you've drawn!
-
- {{ link.name }} - {{ link.url }} + + + + + + + + + + + + + + +
+
Name
+
+ +
+ This is the name of the link you're creating. +
+
+
Url
+
+ +
+ This is the url of the link you're creating. +
+
+ +
+
@@ -94,10 +170,13 @@ onMounted(updateProfile); overflow: hidden; } +.main-container > fieldset { + margin-bottom: 20px; +} + .avatar-list { - display: flex; - max-width: inherit; - overflow-x: scroll; + max-height: 200px; + overflow-y: scroll; } .avatar-list>img { @@ -107,6 +186,49 @@ onMounted(updateProfile); } .selected { - background-color: red; + background-color: #FFB6C1; +} + +.settings-row-heading { + vertical-align: top; + font-weight: bold; } + +.settings-description { + font-size: 10pt; +} + +textarea { + width: 100%; + border: 1px solid #FFB6C1; +} + +.user-card-padding { + margin: 10px; +} + +.link-display { + background-color: #FFE5EA; + padding: 10px; + margin-bottom: 5px; +} + +.link-display > button { + float: right; +} + +.link-name { + font-weight: bold; + font-size: 14pt; + margin-right: 15px; +} + +.link-link { + font-size: 10pt; +} + +.link-display > div { + display: inline-block; +} + \ No newline at end of file diff --git a/PinkSea.Frontend/src/views/UserView.vue b/PinkSea.Frontend/src/views/UserView.vue index b8f7676..8d446ea 100644 --- a/PinkSea.Frontend/src/views/UserView.vue +++ b/PinkSea.Frontend/src/views/UserView.vue @@ -4,12 +4,13 @@ import TimeLine from '@/components/TimeLine.vue' import PanelLayout from '@/layouts/PanelLayout.vue' import { computed, ref, watch } from 'vue' import { xrpc } from '@/api/atproto/client' -import { useRoute } from 'vue-router' +import { useRoute, useRouter } from 'vue-router' import { UserProfileTab } from '@/models/user-profile-tab' import { XRPCError } from '@atcute/client' import ErrorCard from '@/components/ErrorCard.vue' import type Profile from '@/models/profile' import UserCard from '@/components/UserCard.vue' +import { useIdentityStore } from '@/state/store' const tabs = [ { @@ -22,6 +23,10 @@ const tabs = [ } ]; +const ourProfile = computed(() => { + return profile !== null && identityStore.did == profile.value?.did; +}); + const profile = ref(null); const route = useRoute(); @@ -30,6 +35,8 @@ const profileError = ref(""); const currentTab = ref(UserProfileTab.Posts); +const identityStore = useIdentityStore(); + watch(() => route.params.did, async () => { try { const { data } = await xrpc.get("com.shinolabs.pinksea.unspecced.getProfile", { params: { did: route.params.did as string } }); @@ -56,7 +63,7 @@ watch(() => route.params.did, async () => { loading...
- +
{{ $t(tab.i18n) }} diff --git a/PinkSea.Lexicons/com/shinolabs/pinksea/profile.json b/PinkSea.Lexicons/com/shinolabs/pinksea/profile.json new file mode 100644 index 0000000..7d020b1 --- /dev/null +++ b/PinkSea.Lexicons/com/shinolabs/pinksea/profile.json @@ -0,0 +1,59 @@ +{ + "$schema": "https://internect.info/lexicon-schema.json", + "lexicon": 1, + "id": "com.shinolabs.pinksea.profile", + "defs": { + "main": { + "type": "record", + "description": "A profile of a PinkSea user.", + "record": { + "type": "object", + "properties": { + "avatar": { + "type": "ref", + "description": "The oekaki image that's the avatar of this profile.", + "ref": "com.atproto.repo.strongRef" + }, + "nickname": { + "type": "string", + "description": "The display name of the user.", + "maxGraphemes": 64, + "maxLength": 640 + }, + "bio": { + "type": "string", + "description": "The bio of the user.", + "maxGraphemes": 240, + "maxLength": 2400 + }, + "links": { + "type": "array", + "description": "The links to outside platforms for this user", + "maxLength": 5, + "items": { + "type": "ref", + "ref": "#profileLink" + } + } + } + } + }, + "profileLink": { + "type": "object", + "required": ["name", "link"], + "properties": { + "name": { + "type": "string", + "description": "The name of the link.", + "maxGraphemes": 50, + "maxLength": 500 + }, + "imageLink": { + "type": "string", + "description": "The URL of the link.", + "format": "uri" + } + } + } + } +} diff --git a/PinkSea/Lexicons/Records/Profile.cs b/PinkSea/Lexicons/Records/Profile.cs new file mode 100644 index 0000000..2a7a6f8 --- /dev/null +++ b/PinkSea/Lexicons/Records/Profile.cs @@ -0,0 +1,34 @@ +using System.Text.Json.Serialization; +using PinkSea.AtProto.Shared.Lexicons.Types; + +namespace PinkSea.Lexicons.Records; + +/// +/// The com.shinolabs.pinksea.profile record. +/// +public class Profile +{ + /// + /// The nickname of this profile. + /// + [JsonPropertyName("nickname")] + public string? Nickname { get; set; } + + /// + /// The bio of this profile. + /// + [JsonPropertyName("bio")] + public string? Bio { get; set; } + + /// + /// The avatar of this profile. + /// + [JsonPropertyName("avatar"), JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public StrongRef? Avatar { get; set; } + + /// + /// The links this profile has. + /// + [JsonPropertyName("links")] + public IReadOnlyList? Links { get; set; } +} \ No newline at end of file diff --git a/PinkSea/Lexicons/Records/ProfileLink.cs b/PinkSea/Lexicons/Records/ProfileLink.cs new file mode 100644 index 0000000..3788792 --- /dev/null +++ b/PinkSea/Lexicons/Records/ProfileLink.cs @@ -0,0 +1,21 @@ +using System.Text.Json.Serialization; + +namespace PinkSea.Lexicons.Records; + +/// +/// A link in a profile. +/// +public class ProfileLink +{ + /// + /// The name of this profile link. + /// + [JsonPropertyName("name")] + public required string Name { get; set; } + + /// + /// The link it is pointing to. + /// + [JsonPropertyName("link")] + public required string Link { get; set; } +} \ No newline at end of file diff --git a/PinkSea/Program.cs b/PinkSea/Program.cs index edb94e8..814b8dc 100644 --- a/PinkSea/Program.cs +++ b/PinkSea/Program.cs @@ -48,7 +48,7 @@ { o.Endpoint = builder.Configuration["AppViewConfig:JetStreamEndpoint"]; o.CursorFilePath = builder.Configuration["AppViewConfig:CursorFilePath"]; - o.WantedCollections = ["com.shinolabs.pinksea.oekaki"]; + o.WantedCollections = ["com.shinolabs.pinksea.oekaki", "com.shinolabs.pinksea.profile"]; }); builder.Services.AddScoped(); diff --git a/PinkSea/Services/OekakiJetStreamEventHandler.cs b/PinkSea/Services/OekakiJetStreamEventHandler.cs index b3ebd66..9c181b5 100644 --- a/PinkSea/Services/OekakiJetStreamEventHandler.cs +++ b/PinkSea/Services/OekakiJetStreamEventHandler.cs @@ -1,5 +1,6 @@ using System.Text.Json; using Microsoft.Extensions.Caching.Memory; +using Org.BouncyCastle.Asn1.X509; using PinkSea.AtProto.Resolvers.Did; using PinkSea.AtProto.Streaming.JetStream; using PinkSea.AtProto.Streaming.JetStream.Events; @@ -69,22 +70,50 @@ private async Task HandleAccount( /// /// The event. /// The commit. - private async Task HandleCommit( + private Task HandleCommit( JetStreamEvent @event, AtProtoCommit commit) { if (commit.Operation == "create") { - await ProcessCreatedOekaki( - commit, - @event.Did); + return commit.Collection switch + { + "com.shinolabs.pinksea.oekaki" => ProcessCreatedOekaki( + commit, + @event.Did), + "com.shinolabs.pinksea.profile" => ProcessCreatedProfile( + commit, + @event.Did), + _ => throw new ArgumentOutOfRangeException() + }; } - else if (commit.Operation == "delete") + + if (commit.Operation == "update") { - await ProcessDeletedOekaki( - commit, - @event.Did); + return commit.Collection switch + { + "com.shinolabs.pinksea.profile" => ProcessCreatedProfile( + commit, + @event.Did), + _ => throw new ArgumentOutOfRangeException() + }; } + + if (commit.Operation == "delete") + { + return commit.Collection switch + { + "com.shinolabs.pinksea.oekaki" => ProcessDeletedOekaki( + commit, + @event.Did), + "com.shinolabs.pinksea.profile" => ProcessDeletedProfile( + commit, + @event.Did), + _ => throw new ArgumentOutOfRangeException() + }; + } + + return Task.CompletedTask; } /// @@ -103,6 +132,31 @@ private async Task HandleIdentity( await userService.UpdateHandle(@event.Did, identity.Handle); } + private async Task ProcessCreatedProfile( + AtProtoCommit commit, + string authorDid) + { + if (!await userService.UserExists(authorDid)) + await userService.Create(authorDid); + + var profileRecord = commit.Record! + .Value + .Deserialize()!; + + await userService.UpdateProfile(authorDid, profileRecord); + } + + private async Task ProcessDeletedProfile( + AtProtoCommit commit, + string authorDid) + { + if (!await userService.UserExists(authorDid)) + return; + + // We just reset all the data with a blank profile. + await userService.UpdateProfile(authorDid, new Profile()); + } + /// /// Processes deleted oekaki. /// diff --git a/PinkSea/Services/UserService.cs b/PinkSea/Services/UserService.cs index af94493..9145df6 100644 --- a/PinkSea/Services/UserService.cs +++ b/PinkSea/Services/UserService.cs @@ -1,7 +1,11 @@ using Microsoft.EntityFrameworkCore; +using PinkSea.AtProto.Helpers; using PinkSea.AtProto.Resolvers.Did; +using PinkSea.AtProto.Resolvers.Domain; +using PinkSea.AtProto.Shared.Lexicons.Types; using PinkSea.Database; using PinkSea.Database.Models; +using PinkSea.Lexicons.Records; namespace PinkSea.Services; @@ -11,6 +15,7 @@ namespace PinkSea.Services; public class UserService( PinkSeaDbContext dbContext, IDidResolver didResolver, + IDomainDidResolver domainDidResolver, ILogger logger) { /// @@ -34,6 +39,7 @@ public async Task UserExists( string did) { return await dbContext.Users + .Include(u => u.Avatar) .FirstOrDefaultAsync(u => u.Did == did); } @@ -105,4 +111,48 @@ await dbContext.Users .Where(u => u.Did == did) .ExecuteUpdateAsync(sp => sp.SetProperty(u => u.Handle, newHandle)); } + + public async Task UpdateProfile( + string did, + Profile profile) + { + var user = await GetUserByDid(did); + if (user is null) + return; + + user.Nickname = profile.Nickname; + user.Description = profile.Bio; + await UpdateAvatarForProfile(user, profile.Avatar); + + dbContext.Users.Update(user); + await dbContext.SaveChangesAsync(); + } + + private async Task UpdateAvatarForProfile( + UserModel user, + StrongRef? avatarRef) + { + if (avatarRef is null) + { + user.Avatar = null; + return; + } + + // Check if the avatar being requested is one this user owns. + if (!AtLinkHelper.TryParse(avatarRef.Uri, out var atUri)) + return; + + // Get the did from the authority + var authority = atUri.Authority; + if (!atUri.Authority.StartsWith("did:")) + authority = await domainDidResolver.GetDidForDomainHandle(authority) ?? authority; + + // Get the avatar oekaki. + var oekaki = await dbContext.Oekaki + .Where(o => o.AuthorDid == authority && o.OekakiTid == atUri.RecordKey) + .FirstOrDefaultAsync(); + + user.Avatar = oekaki; + user.AvatarId = oekaki?.Key; + } } \ No newline at end of file diff --git a/PinkSea/Xrpc/GetProfileQueryHandler.cs b/PinkSea/Xrpc/GetProfileQueryHandler.cs index 76bf676..59ca3fd 100644 --- a/PinkSea/Xrpc/GetProfileQueryHandler.cs +++ b/PinkSea/Xrpc/GetProfileQueryHandler.cs @@ -1,6 +1,7 @@ using Microsoft.Extensions.Options; using PinkSea.AtProto.Resolvers.Did; using PinkSea.AtProto.Server.Xrpc; +using PinkSea.AtProto.Shared.Lexicons.Types; using PinkSea.AtProto.Shared.Xrpc; using PinkSea.Database.Models; using PinkSea.Lexicons.Objects; @@ -50,20 +51,27 @@ public async Task> Handle( ?? userModel.Handle ?? "invalid.handle"; - // TODO: This is only a mock for iOSSea before we implement profile editing for realsies. - // Don't mind me being here! + var avatarUrl = $"{opts.Value.AppUrl}/blank_avatar.png"; + if (userModel.Avatar != null) + { + avatarUrl = string.Format( + opts.Value.ImageProxyTemplate, + userModel.Did, + userModel.Avatar.BlobCid); + } + + var links = userModel.Links? + .Select(l => new Link { Url = l.Url, Name = l.Name }) + .ToList() ?? []; + var profile = new GetProfileQueryResponse { Did = request.Did, Handle = handle, - Nickname = handle, - Description = "", - Links = - [ - new Link { Name = "Bluesky", Url = $"https://bsky.app/profile/{request.Did}" }, - new Link { Name = "Website", Url = $"https://{handle}" } - ], - Avatar = $"{opts.Value.AppUrl}/blank_avatar.png" + Nickname = userModel.Nickname, + Description = userModel.Description, + Links = links, + Avatar = avatarUrl }; return XrpcErrorOr.Ok(profile); diff --git a/docker-compose.yml b/docker-compose.yml index 7ad902c..1898b8d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -52,6 +52,8 @@ services: - POSTGRES_DB=${POSTGRES_DATABASE} volumes: - pinksea_db:/var/lib/postgresql/data + ports: + - "5432:5432" networks: pinksea: From e7abebdc46b307698f369718d915eb9571bf63ca Mon Sep 17 00:00:00 2001 From: purifetchi <0xlunaric@gmail.com> Date: Mon, 7 Jul 2025 21:21:57 +0200 Subject: [PATCH 03/23] feat: implement pushing the profile to the repo. --- .../Xrpc/Client/DefaultXrpcClientFactory.cs | 9 +- .../Xrpc/Client/IXrpcClientFactory.cs | 9 ++ PinkSea.Frontend/src/api/atproto/lexicons.ts | 24 +++++ PinkSea.Frontend/src/components/UserCard.vue | 10 ++- PinkSea.Frontend/src/models/profile.ts | 8 +- PinkSea.Frontend/src/views/UserEditView.vue | 64 ++++++++++++-- PinkSea/Lexicons/Objects/Author.cs | 12 +++ PinkSea/Lexicons/Objects/HydratedOekaki.cs | 13 ++- .../Procedures/PutProfileProcedureRequest.cs | 16 ++++ PinkSea/Lexicons/Records/Profile.cs | 6 +- PinkSea/Services/FeedBuilder.cs | 12 +-- PinkSea/Services/SearchService.cs | 12 +-- PinkSea/Services/UserService.cs | 87 ++++++++++++++++++- PinkSea/Xrpc/GetOekakiQueryHandler.cs | 8 +- PinkSea/Xrpc/GetProfileQueryHandler.cs | 2 +- PinkSea/Xrpc/PutProfileProcedureHandler.cs | 34 ++++++++ PinkSea/cursorfile | 1 + 17 files changed, 278 insertions(+), 49 deletions(-) create mode 100644 PinkSea/Lexicons/Procedures/PutProfileProcedureRequest.cs create mode 100644 PinkSea/Xrpc/PutProfileProcedureHandler.cs create mode 100644 PinkSea/cursorfile diff --git a/PinkSea.AtProto/Xrpc/Client/DefaultXrpcClientFactory.cs b/PinkSea.AtProto/Xrpc/Client/DefaultXrpcClientFactory.cs index 97d0d62..e62816f 100644 --- a/PinkSea.AtProto/Xrpc/Client/DefaultXrpcClientFactory.cs +++ b/PinkSea.AtProto/Xrpc/Client/DefaultXrpcClientFactory.cs @@ -1,6 +1,7 @@ using Microsoft.Extensions.Logging; using PinkSea.AtProto.Http; using PinkSea.AtProto.Models.Authorization; +using PinkSea.AtProto.Models.OAuth; using PinkSea.AtProto.OAuth; using PinkSea.AtProto.Providers.OAuth; using PinkSea.AtProto.Providers.Storage; @@ -24,6 +25,12 @@ public class DefaultXrpcClientFactory( if (oauthState?.AuthorizationCode is null) return null; + return await GetForOAuthState(oauthState); + } + + /// + public async Task GetForOAuthState(OAuthState oauthState) + { var xrpcLogger = loggerFactory.CreateLogger(); var httpClient = httpClientFactory.CreateClient("xrpc-client"); @@ -36,7 +43,7 @@ public class DefaultXrpcClientFactory( clientDataProvider.ClientData, dpopClientLogger); - dpopClient.SetAuthorizationCode(oauthState.AuthorizationCode); + dpopClient.SetAuthorizationCode(oauthState.AuthorizationCode!); return new DPopXrpcClient(dpopClient, oauthState, xrpcLogger); } diff --git a/PinkSea.AtProto/Xrpc/Client/IXrpcClientFactory.cs b/PinkSea.AtProto/Xrpc/Client/IXrpcClientFactory.cs index a44924c..dca9285 100644 --- a/PinkSea.AtProto/Xrpc/Client/IXrpcClientFactory.cs +++ b/PinkSea.AtProto/Xrpc/Client/IXrpcClientFactory.cs @@ -1,3 +1,5 @@ +using PinkSea.AtProto.Models.OAuth; + namespace PinkSea.AtProto.Xrpc.Client; /// @@ -11,6 +13,13 @@ public interface IXrpcClientFactory /// The state id. /// The XRPC client. Task GetForOAuthStateId(string stateId); + + /// + /// Gets an XRPC client for an oauth state. + /// + /// The state. + /// The XRPC client. + Task GetForOAuthState(OAuthState oauthState); /// /// Gets an XRPC client without any kind of authentication. diff --git a/PinkSea.Frontend/src/api/atproto/lexicons.ts b/PinkSea.Frontend/src/api/atproto/lexicons.ts index 71afc54..9ee8e81 100644 --- a/PinkSea.Frontend/src/api/atproto/lexicons.ts +++ b/PinkSea.Frontend/src/api/atproto/lexicons.ts @@ -38,6 +38,26 @@ declare module '@atcute/client/lexicons' { } } + // eslint-disable-next-line @typescript-eslint/no-namespace + namespace ComShinolabsPinkseaPutProfile { + interface Input { + profile: { + nickname?: string | null, + bio?: string | null, + avatar?: { + uri: string, + cid: string + }, + links?: { + link: string, + name: string + }[] + } + } + interface Output { + } + } + // eslint-disable-next-line @typescript-eslint/no-namespace namespace ComShinolabsPinkseaDeleteOekaki { interface Input { @@ -176,6 +196,10 @@ declare module '@atcute/client/lexicons' { input: ComShinolabsPinkseaPutOekaki.Input, output: ComShinolabsPinkseaPutOekaki.Output }, + 'com.shinolabs.pinksea.putProfile': { + input: ComShinolabsPinkseaPutProfile.Input, + output: ComShinolabsPinkseaPutProfile.Output + }, 'com.shinolabs.pinksea.deleteOekaki': { input: ComShinolabsPinkseaDeleteOekaki.Input, output: EmptyParams diff --git a/PinkSea.Frontend/src/components/UserCard.vue b/PinkSea.Frontend/src/components/UserCard.vue index 39b8535..97e1ea3 100644 --- a/PinkSea.Frontend/src/components/UserCard.vue +++ b/PinkSea.Frontend/src/components/UserCard.vue @@ -11,9 +11,11 @@ const props = defineProps<{ }>(); const description = computed(() => { - return props.profile.description.length < 1 - ? "This user has no description." - : props.profile.description; + return props.profile.description ?? "This user has no description." +}); + +const nickname = computed(() => { + return props.profile.nick ?? props.profile.handle }); @@ -25,7 +27,7 @@ const description = computed(() => {
-

{{ props.profile.nick }}

+

{{ nickname }}

@{{ props.profile.handle }}

diff --git a/PinkSea.Frontend/src/models/profile.ts b/PinkSea.Frontend/src/models/profile.ts index 308bf43..6a4459d 100644 --- a/PinkSea.Frontend/src/models/profile.ts +++ b/PinkSea.Frontend/src/models/profile.ts @@ -1,11 +1,11 @@ export default interface Profile { did: string, handle: string, - nick: string, - description: string, - links: { + nick?: string, + description?: string, + links?: { name: string, url: string }[], - avatar: string + avatar?: string } \ No newline at end of file diff --git a/PinkSea.Frontend/src/views/UserEditView.vue b/PinkSea.Frontend/src/views/UserEditView.vue index 00a4461..f125531 100644 --- a/PinkSea.Frontend/src/views/UserEditView.vue +++ b/PinkSea.Frontend/src/views/UserEditView.vue @@ -5,10 +5,11 @@ import UserCard from '@/components/UserCard.vue'; import PanelLayout from '@/layouts/PanelLayout.vue'; import type { Oekaki } from '@/models/oekaki'; import type Profile from '@/models/profile'; -import { useIdentityStore } from '@/state/store'; +import { useIdentityStore, usePersistedStore } from '@/state/store'; import { onMounted, ref, watch } from 'vue'; const identityStore = useIdentityStore() +const persistedStore = usePersistedStore() const profile = ref(null); const linkUrl = ref(""); @@ -60,6 +61,50 @@ const addLink = (name: string, url: string) => { ]; }; +const buildAvatarRef = () => { + if (profile.value?.avatar === undefined) { + return undefined + } + + const avatar = avatarList.value.find(a => a.image == profile.value?.avatar) + + return { + uri: avatar!.at, + cid: avatar!.cid + } +} + +const sendChanges = async () => { + try { + await xrpc.call("com.shinolabs.pinksea.putProfile", { + data: { + profile: { + nickname: profile.value?.nick, + bio: profile.value?.description, + links: profile.value?.links.map(l => { + return { + link: l.url, + name: l.name + } + }), + avatar: buildAvatarRef() + } + }, + headers: { + "Authorization": `Bearer ${persistedStore.token}` + } + }) + + alert("Your changes have been saved!") + } catch (e) { + alert(e) + } +}; + +const removeLink = (index: number) => { + profile.value?.links.splice(index, 1) +} + watch(identityStore, updateProfile); onMounted(updateProfile); @@ -99,7 +144,8 @@ onMounted(updateProfile);
- This is your bio. It's a short piece of text that's visible on your profile. By default, it's empty. + This is your bio. It's a short piece of text that's visible on your profile. By + default, it's empty.
@@ -150,16 +196,17 @@ onMounted(updateProfile); - @@ -170,7 +217,7 @@ onMounted(updateProfile); overflow: hidden; } -.main-container > fieldset { +.main-container>fieldset { margin-bottom: 20px; } @@ -213,7 +260,7 @@ textarea { margin-bottom: 5px; } -.link-display > button { +.link-display>button { float: right; } @@ -227,8 +274,7 @@ textarea { font-size: 10pt; } -.link-display > div { +.link-display>div { display: inline-block; -} - +} \ No newline at end of file diff --git a/PinkSea/Lexicons/Objects/Author.cs b/PinkSea/Lexicons/Objects/Author.cs index a4b713f..d7afef6 100644 --- a/PinkSea/Lexicons/Objects/Author.cs +++ b/PinkSea/Lexicons/Objects/Author.cs @@ -18,4 +18,16 @@ public class Author /// [JsonPropertyName("handle")] public required string Handle { get; set; } + + /// + /// The nickname of the author. + /// + [JsonPropertyName("nickname"), JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? Nickname { get; set; } + + /// + /// The avatar of the author. + /// + [JsonPropertyName("avatar"), JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? Avatar { get; set; } } \ No newline at end of file diff --git a/PinkSea/Lexicons/Objects/HydratedOekaki.cs b/PinkSea/Lexicons/Objects/HydratedOekaki.cs index c55bf6f..524037a 100644 --- a/PinkSea/Lexicons/Objects/HydratedOekaki.cs +++ b/PinkSea/Lexicons/Objects/HydratedOekaki.cs @@ -59,12 +59,12 @@ public class HydratedOekaki /// Constructs an oekaki DTO from an oekaki model and the author's handle. /// /// The oekaki model. - /// The author's handle. + /// The author. /// The endpoint of the image proxy. /// The oekaki DTO. public static HydratedOekaki FromOekakiModel( OekakiModel oekakiModel, - string authorHandle, + UserModel authorModel, string imageProxyEndpoint) { var imageLink = string.Format( @@ -77,7 +77,14 @@ public static HydratedOekaki FromOekakiModel( Author = new Author { Did = oekakiModel.AuthorDid, - Handle = authorHandle + Handle = authorModel.Handle ?? "invalid.handle", + Avatar = authorModel.Avatar is not null ? + string.Format( + imageProxyEndpoint, + oekakiModel.AuthorDid, + authorModel.Avatar!.BlobCid) + : null, + Nickname = authorModel.Nickname }, CreationTime = oekakiModel.IndexedAt, ImageLink = imageLink, diff --git a/PinkSea/Lexicons/Procedures/PutProfileProcedureRequest.cs b/PinkSea/Lexicons/Procedures/PutProfileProcedureRequest.cs new file mode 100644 index 0000000..61c9384 --- /dev/null +++ b/PinkSea/Lexicons/Procedures/PutProfileProcedureRequest.cs @@ -0,0 +1,16 @@ +using System.Text.Json.Serialization; +using PinkSea.Lexicons.Records; + +namespace PinkSea.Lexicons.Procedures; + +/// +/// The request for the "com.shinolabs.pinksea.putProfile" xrpc call. +/// +public class PutProfileProcedureRequest +{ + /// + /// The updated profile. + /// + [JsonPropertyName("profile")] + public required Profile Profile { get; set; } +} \ No newline at end of file diff --git a/PinkSea/Lexicons/Records/Profile.cs b/PinkSea/Lexicons/Records/Profile.cs index 2a7a6f8..21eb4d8 100644 --- a/PinkSea/Lexicons/Records/Profile.cs +++ b/PinkSea/Lexicons/Records/Profile.cs @@ -11,13 +11,13 @@ public class Profile /// /// The nickname of this profile. /// - [JsonPropertyName("nickname")] + [JsonPropertyName("nickname"), JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string? Nickname { get; set; } /// /// The bio of this profile. /// - [JsonPropertyName("bio")] + [JsonPropertyName("bio"), JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string? Bio { get; set; } /// @@ -29,6 +29,6 @@ public class Profile /// /// The links this profile has. /// - [JsonPropertyName("links")] + [JsonPropertyName("links"), JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public IReadOnlyList? Links { get; set; } } \ No newline at end of file diff --git a/PinkSea/Services/FeedBuilder.cs b/PinkSea/Services/FeedBuilder.cs index 98c0a02..8abdd2e 100644 --- a/PinkSea/Services/FeedBuilder.cs +++ b/PinkSea/Services/FeedBuilder.cs @@ -16,7 +16,7 @@ namespace PinkSea.Services; /// The database context. public class FeedBuilder( PinkSeaDbContext dbContext, - IDidResolver didResolver, + UserService userService, IOptions opts) { /// @@ -105,15 +105,7 @@ public FeedBuilder Limit(int count) public async Task> FromOekakiModelList(IList list) { var dids = list.Select(o => o.AuthorDid); - var map = new ConcurrentDictionary(); - - await Parallel.ForEachAsync(dids, new ParallelOptions - { - MaxDegreeOfParallelism = 5 - }, async (did, _) => - { - map[did] = await didResolver.GetHandleFromDid(did) ?? "invalid.handle"; - }); + var map = await userService.GetMultipleUsers(dids); var oekakiDtos = list .Select(o => HydratedOekaki.FromOekakiModel(o, map[o.AuthorDid], opts.Value.ImageProxyTemplate)) diff --git a/PinkSea/Services/SearchService.cs b/PinkSea/Services/SearchService.cs index cd0f9b3..7af77a8 100644 --- a/PinkSea/Services/SearchService.cs +++ b/PinkSea/Services/SearchService.cs @@ -11,7 +11,7 @@ namespace PinkSea.Services; public class SearchService( PinkSeaDbContext dbContext, - IDidResolver didResolver, + UserService userService, IOptions opts) { public async Task> SearchPosts(string query, int limit, DateTimeOffset since) @@ -51,15 +51,7 @@ public async Task> SearchTags(string query, int limit, Dat .ToListAsync(); var dids = list.Select(o => o.Oekaki.AuthorDid); - var map = new ConcurrentDictionary(); - - await Parallel.ForEachAsync(dids, new ParallelOptions - { - MaxDegreeOfParallelism = 5 - }, async (did, _) => - { - map[did] = await didResolver.GetHandleFromDid(did) ?? "invalid.handle"; - }); + var map = await userService.GetMultipleUsers(dids); var oekakiDtos = list .Select(o => new TagSearchResult diff --git a/PinkSea/Services/UserService.cs b/PinkSea/Services/UserService.cs index 9145df6..0c0bea9 100644 --- a/PinkSea/Services/UserService.cs +++ b/PinkSea/Services/UserService.cs @@ -1,8 +1,11 @@ using Microsoft.EntityFrameworkCore; using PinkSea.AtProto.Helpers; +using PinkSea.AtProto.Models.OAuth; using PinkSea.AtProto.Resolvers.Did; using PinkSea.AtProto.Resolvers.Domain; +using PinkSea.AtProto.Shared.Lexicons.AtProto; using PinkSea.AtProto.Shared.Lexicons.Types; +using PinkSea.AtProto.Xrpc.Client; using PinkSea.Database; using PinkSea.Database.Models; using PinkSea.Lexicons.Records; @@ -16,6 +19,7 @@ public class UserService( PinkSeaDbContext dbContext, IDidResolver didResolver, IDomainDidResolver domainDidResolver, + IXrpcClientFactory xrpcClientFactory, ILogger logger) { /// @@ -42,6 +46,35 @@ public async Task UserExists( .Include(u => u.Avatar) .FirstOrDefaultAsync(u => u.Did == did); } + + /// + /// Gets a full user model by their DID. + /// + /// The DID of the user. + /// The user model, or nothing if they don't exist. + public async Task GetFullUserByDid( + string did) + { + return await dbContext.Users + .Include(u => u.Avatar) + .Include(u => u.Links) + .FirstOrDefaultAsync(u => u.Did == did); + } + + /// + /// Gets multiple users at once. + /// + /// The list of user's DIDs. + /// A dictionary mapping from a did to a user model. + public async Task> GetMultipleUsers(IEnumerable dids) + { + var list = await dbContext.Users + .Where(u => dids.Contains(u.Did)) + .Include(u => u.Avatar) + .ToListAsync(); + + return list.ToDictionary(u => u.Did, u => u); + } /// /// Gets all of the users. @@ -116,18 +149,70 @@ public async Task UpdateProfile( string did, Profile profile) { - var user = await GetUserByDid(did); + var user = await GetFullUserByDid(did); if (user is null) return; user.Nickname = profile.Nickname; user.Description = profile.Bio; await UpdateAvatarForProfile(user, profile.Avatar); + await UpdateLinksForProfile(user, profile.Links); dbContext.Users.Update(user); await dbContext.SaveChangesAsync(); } + /// + /// Publishes a profile update to the user's repo. + /// + /// The oauth state of the user. + /// The changed profile. + public async Task PublishProfileUpdateToRepo( + OAuthState oauthState, + Profile profile) + { + using var xrpcClient = await xrpcClientFactory.GetForOAuthState(oauthState); + if (xrpcClient is null) + return; + + await xrpcClient.Procedure( + "com.atproto.repo.putRecord", + new PutRecordRequest + { + Repo = oauthState.Did, + Collection = "com.shinolabs.pinksea.profile", + RecordKey = "self", + Record = profile + }); + } + + private async Task UpdateLinksForProfile( + UserModel user, + IEnumerable? links) + { + if (user.Links?.Count > 0) + { + foreach (var link in user.Links) + { + dbContext.Remove(link); + } + } + + if (links is null) + return; + + var newLinks = links.Select(link => new UserLinkModel() + { + UserDid = user.Did, + User = user, + Url = link.Link, + Name = link.Name + }) + .ToList(); + + user.Links = newLinks; + } + private async Task UpdateAvatarForProfile( UserModel user, StrongRef? avatarRef) diff --git a/PinkSea/Xrpc/GetOekakiQueryHandler.cs b/PinkSea/Xrpc/GetOekakiQueryHandler.cs index 90edbf4..abb74c7 100644 --- a/PinkSea/Xrpc/GetOekakiQueryHandler.cs +++ b/PinkSea/Xrpc/GetOekakiQueryHandler.cs @@ -18,7 +18,7 @@ namespace PinkSea.Xrpc; public class GetOekakiQueryHandler( PinkSeaDbContext dbContext, FeedBuilder feedBuilder, - IDidResolver didResolver, + UserService userService, IOptions opts) : IXrpcQuery { @@ -27,11 +27,13 @@ public async Task> Handle(GetOekakiQueryRequ { var parent = await dbContext.Oekaki .Include(o => o.TagOekakiRelations) + .Include(o => o.Author) + .ThenInclude(u => u.Avatar) .FirstOrDefaultAsync(o => o.AuthorDid == request.Did && o.OekakiTid == request.RecordKey); if (parent == null) return XrpcErrorOr.Fail("NotFound", "Could not find this record."); - + var childrenFeed = await feedBuilder .StartWithOrdering(c => c.IndexedAt) .Where(c => c.ParentId == parent.Key) @@ -43,7 +45,7 @@ public async Task> Handle(GetOekakiQueryRequ !parent.Tombstone ? HydratedOekaki.FromOekakiModel( parent, - await didResolver.GetHandleFromDid(parent.AuthorDid) ?? "invalid.handle", + parent.Author, opts.Value.ImageProxyTemplate) : TombstoneOekaki.FromOekakiModel( parent), diff --git a/PinkSea/Xrpc/GetProfileQueryHandler.cs b/PinkSea/Xrpc/GetProfileQueryHandler.cs index 59ca3fd..f73fab9 100644 --- a/PinkSea/Xrpc/GetProfileQueryHandler.cs +++ b/PinkSea/Xrpc/GetProfileQueryHandler.cs @@ -25,7 +25,7 @@ public class GetProfileQueryHandler( public async Task> Handle( GetProfileQueryRequest request) { - var userModel = await userService.GetUserByDid(request.Did); + var userModel = await userService.GetFullUserByDid(request.Did); if (userModel is null) { return XrpcErrorOr.Fail( diff --git a/PinkSea/Xrpc/PutProfileProcedureHandler.cs b/PinkSea/Xrpc/PutProfileProcedureHandler.cs new file mode 100644 index 0000000..c8bb729 --- /dev/null +++ b/PinkSea/Xrpc/PutProfileProcedureHandler.cs @@ -0,0 +1,34 @@ +using PinkSea.AtProto.Providers.Storage; +using PinkSea.AtProto.Server.Xrpc; +using PinkSea.AtProto.Shared.Xrpc; +using PinkSea.Extensions; +using PinkSea.Lexicons; +using PinkSea.Lexicons.Procedures; +using PinkSea.Services; + +namespace PinkSea.Xrpc; + +/// +/// The handler for the "com.shinolabs.pinksea.putProfile" XRPC procedure. Updates a user's profile. +/// +[Xrpc("com.shinolabs.pinksea.putProfile")] +public class PutProfileProcedureHandler(IHttpContextAccessor contextAccessor, IOAuthStateStorageProvider oauthStateStorageProvider, UserService userService) + : IXrpcProcedure +{ + /// + public async Task> Handle(PutProfileProcedureRequest request) + { + var state = contextAccessor.HttpContext?.GetStateToken(); + if (state is null) + return XrpcErrorOr.Fail("NoAuthToken", "Missing authorization token."); + + var oauthState = await oauthStateStorageProvider.GetForStateId(state); + if (oauthState is null) + return XrpcErrorOr.Fail("InvalidToken", "Invalid token."); + + await userService.UpdateProfile(oauthState.Did, request.Profile); + await userService.PublishProfileUpdateToRepo(oauthState, request.Profile); + + return XrpcErrorOr.Ok(new Empty()); + } +} \ No newline at end of file diff --git a/PinkSea/cursorfile b/PinkSea/cursorfile new file mode 100644 index 0000000..1ede254 --- /dev/null +++ b/PinkSea/cursorfile @@ -0,0 +1 @@ +1751916170599031 \ No newline at end of file From e54d58f9fb5f34da1a00c1b7796a10b56ae125fc Mon Sep 17 00:00:00 2001 From: purifetchi <0xlunaric@gmail.com> Date: Tue, 8 Jul 2025 00:35:26 +0200 Subject: [PATCH 04/23] feat: start working on showing profile pics in oekaki cards. --- .../public/assets/img}/blank_avatar.png | Bin .../src/components/TimeLineOekakiCard.vue | 40 ++------ .../components/oekaki/OekakiMetaContainer.vue | 87 ++++++++++++++++++ .../oekaki/PostViewOekakiParentCard.vue | 27 +----- .../src/components/profile/Avatar.vue | 13 +++ .../components/search/SearchProfileCard.vue | 17 +++- PinkSea.Frontend/src/models/author.ts | 4 +- PinkSea/cursorfile | 2 +- 8 files changed, 124 insertions(+), 66 deletions(-) rename {PinkSea/wwwroot => PinkSea.Frontend/public/assets/img}/blank_avatar.png (100%) create mode 100644 PinkSea.Frontend/src/components/oekaki/OekakiMetaContainer.vue create mode 100644 PinkSea.Frontend/src/components/profile/Avatar.vue diff --git a/PinkSea/wwwroot/blank_avatar.png b/PinkSea.Frontend/public/assets/img/blank_avatar.png similarity index 100% rename from PinkSea/wwwroot/blank_avatar.png rename to PinkSea.Frontend/public/assets/img/blank_avatar.png diff --git a/PinkSea.Frontend/src/components/TimeLineOekakiCard.vue b/PinkSea.Frontend/src/components/TimeLineOekakiCard.vue index e6d0c99..4109384 100644 --- a/PinkSea.Frontend/src/components/TimeLineOekakiCard.vue +++ b/PinkSea.Frontend/src/components/TimeLineOekakiCard.vue @@ -5,6 +5,7 @@ import { useRouter } from 'vue-router' import TagContainer from '@/components/TagContainer.vue' import { usePersistedStore } from '@/state/store' import { buildOekakiUrlFromOekakiObject, formatDate } from '@/api/atproto/helpers' +import OekakiMetaContainer from './oekaki/OekakiMetaContainer.vue' const router = useRouter(); const persistedStore = usePersistedStore(); @@ -14,10 +15,6 @@ const props = defineProps<{ }>() const imageLink = computed(() => `url(${props.oekaki.image})`) -const authorProfileLink = computed(() => `/${props.oekaki.author.did}`); -const creationTime = computed(() => { - return formatDate(props.oekaki.creationTime) -}) const altText = computed(() => props.oekaki.alt ?? ""); const navigateToPost = () => { @@ -29,38 +26,21 @@ const openInNewTab = () => { const url = buildOekakiUrlFromOekakiObject(props.oekaki); window.open(url, "_blank"); }; + + \ No newline at end of file diff --git a/PinkSea.Frontend/src/components/oekaki/PostViewOekakiParentCard.vue b/PinkSea.Frontend/src/components/oekaki/PostViewOekakiParentCard.vue index 3da6af2..8485e94 100644 --- a/PinkSea.Frontend/src/components/oekaki/PostViewOekakiParentCard.vue +++ b/PinkSea.Frontend/src/components/oekaki/PostViewOekakiParentCard.vue @@ -1,34 +1,20 @@ @@ -51,13 +37,4 @@ const creationTime = computed(() => { z-index: 5; position: relative; } - -.oekaki-meta { - font-size: small; - padding: 10px; - color: #2f4858; - border-top: 2px dashed #FFB6C1; - border-left: 0.525em solid #FFB6C1; - background-color: white; -} diff --git a/PinkSea.Frontend/src/components/profile/Avatar.vue b/PinkSea.Frontend/src/components/profile/Avatar.vue new file mode 100644 index 0000000..ff83bf7 --- /dev/null +++ b/PinkSea.Frontend/src/components/profile/Avatar.vue @@ -0,0 +1,13 @@ + + + + + \ No newline at end of file diff --git a/PinkSea.Frontend/src/components/search/SearchProfileCard.vue b/PinkSea.Frontend/src/components/search/SearchProfileCard.vue index 69e6b7d..25048d5 100644 --- a/PinkSea.Frontend/src/components/search/SearchProfileCard.vue +++ b/PinkSea.Frontend/src/components/search/SearchProfileCard.vue @@ -2,16 +2,25 @@ import CardWithImage from '@/components/CardWithImage.vue' import type { Author } from '@/models/author' +import { computed } from 'vue'; const props = defineProps<{ profile: Author }>(); + +const nickname = computed(() => { + return props.profile.nickname ?? props.profile.handle +}) + +const avatar = computed(() => { + return props.profile.avatar ?? "/assets/img/blank_avatar.png" +}) - + diff --git a/PinkSea.Frontend/src/models/author.ts b/PinkSea.Frontend/src/models/author.ts index fe3d2b4..bd1c871 100644 --- a/PinkSea.Frontend/src/models/author.ts +++ b/PinkSea.Frontend/src/models/author.ts @@ -1,4 +1,6 @@ export interface Author { did: string, - handle: string + handle: string, + avatar?: string, + nickname?: string } diff --git a/PinkSea/cursorfile b/PinkSea/cursorfile index 1ede254..baa5746 100644 --- a/PinkSea/cursorfile +++ b/PinkSea/cursorfile @@ -1 +1 @@ -1751916170599031 \ No newline at end of file +1751927785681364 \ No newline at end of file From 9966ad8b768cfc90ece54b5e31d0889a2a67cb4a Mon Sep 17 00:00:00 2001 From: purifetchi <0xlunaric@gmail.com> Date: Tue, 8 Jul 2025 20:56:11 +0200 Subject: [PATCH 05/23] feat: implement a better avatar component. --- PinkSea.Frontend/src/components/UserCard.vue | 7 ++--- .../components/oekaki/OekakiMetaContainer.vue | 11 ++------ .../src/components/profile/Avatar.vue | 28 +++++++++++++++++-- PinkSea.Frontend/src/views/UserEditView.vue | 14 +++++----- PinkSea/Xrpc/GetProfileQueryHandler.cs | 2 +- PinkSea/cursorfile | 2 +- 6 files changed, 39 insertions(+), 25 deletions(-) diff --git a/PinkSea.Frontend/src/components/UserCard.vue b/PinkSea.Frontend/src/components/UserCard.vue index 97e1ea3..5f15990 100644 --- a/PinkSea.Frontend/src/components/UserCard.vue +++ b/PinkSea.Frontend/src/components/UserCard.vue @@ -2,6 +2,7 @@ import type Profile from '@/models/profile'; import { computed } from 'vue'; import { useRouter } from 'vue-router'; +import Avatar from './profile/Avatar.vue'; const router = useRouter(); @@ -23,7 +24,7 @@ const nickname = computed(() => { diff --git a/PinkSea.Frontend/src/components/oekaki/PostViewOekakiChildCard.vue b/PinkSea.Frontend/src/components/oekaki/PostViewOekakiChildCard.vue index 542ba43..e036081 100644 --- a/PinkSea.Frontend/src/components/oekaki/PostViewOekakiChildCard.vue +++ b/PinkSea.Frontend/src/components/oekaki/PostViewOekakiChildCard.vue @@ -48,13 +48,13 @@ const redirectToParent = async () => {
+ {{ $t("post.response_from_before_handle") }} - responded {{ $t("post.response_from_after_handle") }}{{ $t("post.response_from_at_date") }}{{ creationTime - }} + {{ $t("post.response_from_after_handle") }}{{ $t("post.response_from_at_date") }}{{ creationTime }}
diff --git a/PinkSea.Frontend/src/intl/translations/en.json b/PinkSea.Frontend/src/intl/translations/en.json index bb5e84e..4ee7d63 100644 --- a/PinkSea.Frontend/src/intl/translations/en.json +++ b/PinkSea.Frontend/src/intl/translations/en.json @@ -41,8 +41,8 @@ "nothing_here": "Nothing here so far... (╥﹏╥)" }, "post": { - "response_from_before_handle": "Response from ", - "response_from_after_handle": "", + "response_from_before_handle": "", + "response_from_after_handle": "responded ", "response_from_at_date": " at ", "this_post_no_longer_exists": "This post no longer exists." }, @@ -84,10 +84,26 @@ "hint_confirm": "Once you're ready, click the button above to publish your image!" }, "profile": { - "bluesky_profile": "Bluesky profile", - "domain": "Website", "posts_tab": "Posts", - "replies_tab": "Replies" + "replies_tab": "Replies", + "edit_profile": "Edit profile" + }, + "profile_edit": { + "how_others_will_see_you": "This is how others will see you", + "basic_data": "Basic data", + "nickname": "Nickname", + "nickname_description": "This is the name others will identify you by. By default, it's equal to your handle.", + "your_bio": "Your bio", + "your_bio_description": "This is your bio. It's a short piece of text that's visible on your profile. By default, it's empty.", + "avatar": "Avatar", + "avatar_description": "Select an avatar from the list of oekaki you've drawn!", + "links": "Links", + "link_name": "Name", + "link_name_description": "This is the name of the link you're creating.", + "link_url": "Url", + "link_url_description": "This is the url of the link you're creating.", + "link_add": "Add link", + "save_changes": "Save your changes" }, "tegakijs": { "badDimensions": "Invalid dimensions.", diff --git a/PinkSea.Frontend/src/views/UserEditView.vue b/PinkSea.Frontend/src/views/UserEditView.vue index b9be6b3..407c1f5 100644 --- a/PinkSea.Frontend/src/views/UserEditView.vue +++ b/PinkSea.Frontend/src/views/UserEditView.vue @@ -120,36 +120,35 @@ onMounted(updateProfile); loading...
- +
- + @@ -157,45 +156,46 @@ onMounted(updateProfile);
-
Nickname
+
{{ $t("profile_edit.nickname") }}
- This is the name others will identify you by. By default, it's equal to your handle. + {{ $t("profile_edit.nickname_description") }}
-
Your bio
+
{{ $t("profile_edit.your_bio") }}
- This is your bio. It's a short piece of text that's visible on your profile. By - default, it's empty. + {{ $t("profile_edit.your_bio_description") }}
- +
- Select an avatar from the list of oekaki you've drawn! + {{ $t("profile_edit.avatar_description") }}
- + @@ -210,7 +210,7 @@ onMounted(updateProfile); - + From e45e5edbf0472ef84cbb3b3ebc66098b605b23d8 Mon Sep 17 00:00:00 2001 From: purifetchi <0xlunaric@gmail.com> Date: Fri, 25 Jul 2025 21:12:45 +0200 Subject: [PATCH 10/23] feat: allow unsetting an avatar. --- PinkSea.Frontend/src/views/UserEditView.vue | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/PinkSea.Frontend/src/views/UserEditView.vue b/PinkSea.Frontend/src/views/UserEditView.vue index 407c1f5..5f53d46 100644 --- a/PinkSea.Frontend/src/views/UserEditView.vue +++ b/PinkSea.Frontend/src/views/UserEditView.vue @@ -72,6 +72,10 @@ const buildAvatarRef = () => { const avatar = avatarList.value.find(a => a.image == profile.value?.avatar) + if (avatar === undefined) { + return undefined + } + return { uri: avatar!.at, cid: avatar!.cid @@ -158,6 +162,10 @@ onMounted(updateProfile);
+
+ +
@@ -195,7 +203,7 @@ onMounted(updateProfile);
From ea6e0fd13bcd3bb9140e0dc1cc1b7cbee0c2102a Mon Sep 17 00:00:00 2001 From: purifetchi <0xlunaric@gmail.com> Date: Sat, 26 Jul 2025 10:55:49 +0200 Subject: [PATCH 11/23] feat: use the new default pfp drawn by domatoxi. --- .../public/assets/img/blank_avatar.png | Bin 121037 -> 743 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/PinkSea.Frontend/public/assets/img/blank_avatar.png b/PinkSea.Frontend/public/assets/img/blank_avatar.png index 295f314ecc765ede50cbd9c3895edd40a36c4801..7f5f263957c6626aca0f3278fbeca5b46ecf3a28 100644 GIT binary patch literal 743 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

G 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 Date: Sat, 26 Jul 2025 11:22:42 +0200 Subject: [PATCH 12/23] fix: trim whitespace from handles. fixes #69. --- PinkSea/Xrpc/BeginLoginFlowProcedure.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/PinkSea/Xrpc/BeginLoginFlowProcedure.cs b/PinkSea/Xrpc/BeginLoginFlowProcedure.cs index 9cdd900..e8f4ecd 100644 --- a/PinkSea/Xrpc/BeginLoginFlowProcedure.cs +++ b/PinkSea/Xrpc/BeginLoginFlowProcedure.cs @@ -21,7 +21,9 @@ public async Task> Handle(BeginLogi { try { - var normalizedHandle = request.Handle.TrimStart('@') + var normalizedHandle = request.Handle + .Trim() + .TrimStart('@') .ToLower(); // If we don't have a domain, that is, we don't have a '.' in the name From b8b954f28101d4cb30935bb52a1ea60e0fde406b Mon Sep 17 00:00:00 2001 From: purifetchi <0xlunaric@gmail.com> Date: Sun, 27 Jul 2025 14:53:05 +0200 Subject: [PATCH 13/23] feat: login on enter keydown. fixes #67. --- PinkSea.Frontend/src/components/LoginBar.vue | 10 +++++----- PinkSea.Frontend/src/views/UserEditView.vue | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/PinkSea.Frontend/src/components/LoginBar.vue b/PinkSea.Frontend/src/components/LoginBar.vue index ee58794..adcbbc6 100644 --- a/PinkSea.Frontend/src/components/LoginBar.vue +++ b/PinkSea.Frontend/src/components/LoginBar.vue @@ -34,13 +34,13 @@ const beginOAuth = async () => { - + diff --git a/PinkSea.Frontend/src/views/UserEditView.vue b/PinkSea.Frontend/src/views/UserEditView.vue index 5f53d46..78c508d 100644 --- a/PinkSea.Frontend/src/views/UserEditView.vue +++ b/PinkSea.Frontend/src/views/UserEditView.vue @@ -162,7 +162,7 @@ onMounted(updateProfile);

-
+
From e9f61aa37bf3fd2bc85e1d39551fe6415cfccd78 Mon Sep 17 00:00:00 2001 From: purifetchi <0xlunaric@gmail.com> Date: Sun, 27 Jul 2025 16:30:17 +0200 Subject: [PATCH 14/23] feat: backfill profiles. --- .../Hosting/FirstTimeRunAssistantService.cs | 51 ++++++++++++++++++- .../Services/OekakiJetStreamEventHandler.cs | 10 ++++ 2 files changed, 60 insertions(+), 1 deletion(-) diff --git a/PinkSea/Services/Hosting/FirstTimeRunAssistantService.cs b/PinkSea/Services/Hosting/FirstTimeRunAssistantService.cs index 2f52ecf..498c53a 100644 --- a/PinkSea/Services/Hosting/FirstTimeRunAssistantService.cs +++ b/PinkSea/Services/Hosting/FirstTimeRunAssistantService.cs @@ -179,7 +179,15 @@ private async Task Backfill() var children = new List<(string, string, string, Oekaki)>(); foreach (var did in repos.Value!.Repos.Select(r => r.Did)) { - children.AddRange(await BackfillForDid(did)); + try + { + children.AddRange(await BackfillForDid(did)); + await BackfillProfile(did); + } + catch (Exception ex) + { + logger.LogError(ex, "Failed to backfill posts for DID {Did}. {Error}", did, ex.Message); + } } // Now add in the children. @@ -195,6 +203,47 @@ await InsertOekakiIntoDatabase( logger.LogInformation(" - Backfilling complete!"); } + /// + /// Backfills a profile for a DID. + /// + /// The DID. + private async Task BackfillProfile(string did) + { + var document = await didResolver.GetDocumentForDid(did); + if (document is null) + { + logger.LogWarning("Couldn't get the DID document for {Did}.", did); + return; + } + + using var xrpcClient = await xrpcClientFactory.GetWithoutAuthentication(document.GetPds()!); + + var response = await xrpcClient.Query>( + "com.atproto.repo.listRecords", + new ListRecordsRequest + { + Repo = did, + Collection = "com.shinolabs.pinksea.profile" + }); + + if (!response.IsSuccess) + return; + + var profile = response.Value! + .Records + .FirstOrDefault(r => r.AtUri.Contains("self")); + + if (profile is null) + return; + + logger.LogInformation("Creating a profile for {Did}", did); + + if (!await userService.UserExists(did)) + await userService.Create(did); + + await userService.UpdateProfile(did, profile.Value); + } + /// /// Backfills for a single did. /// diff --git a/PinkSea/Services/OekakiJetStreamEventHandler.cs b/PinkSea/Services/OekakiJetStreamEventHandler.cs index 40beedd..4ba9ba8 100644 --- a/PinkSea/Services/OekakiJetStreamEventHandler.cs +++ b/PinkSea/Services/OekakiJetStreamEventHandler.cs @@ -133,6 +133,11 @@ private async Task HandleIdentity( await userService.UpdateHandle(@event.Did, identity.Handle); } + /// + /// Processes a newly created or updated profile. + /// + /// The commit that created a profile. + /// The DID of the profile's author. private async Task ProcessCreatedProfile( AtProtoCommit commit, string authorDid) @@ -156,6 +161,11 @@ private async Task ProcessCreatedProfile( await userService.UpdateProfile(authorDid, profileRecord); } + /// + /// Processes a deleted profile. + /// + /// The commit that deleted this profile. + /// The DID of the profile. private async Task ProcessDeletedProfile( AtProtoCommit commit, string authorDid) From ceb3145bb552e3ab94da3d6c537397077f84e8e4 Mon Sep 17 00:00:00 2001 From: purifetchi <0xlunaric@gmail.com> Date: Sun, 27 Jul 2025 16:52:12 +0200 Subject: [PATCH 15/23] feat: have the link to a reply actually point to the parent. fixes #66 --- .../oekaki/PostViewOekakiChildCard.vue | 10 +--- PinkSea.Frontend/src/views/PostView.vue | 47 ++++++++++++++++--- 2 files changed, 42 insertions(+), 15 deletions(-) diff --git a/PinkSea.Frontend/src/components/oekaki/PostViewOekakiChildCard.vue b/PinkSea.Frontend/src/components/oekaki/PostViewOekakiChildCard.vue index e036081..d3d28b8 100644 --- a/PinkSea.Frontend/src/components/oekaki/PostViewOekakiChildCard.vue +++ b/PinkSea.Frontend/src/components/oekaki/PostViewOekakiChildCard.vue @@ -32,14 +32,8 @@ const classList = computed(() => { const redirectToParent = async () => { const rkey = getRecordKeyFromAtUri(props.oekaki.at); - const { data } = await xrpc.get("com.shinolabs.pinksea.getParentForReply", { - params: { - did: props.oekaki.author.did, - rkey: rkey! - } - }); - - await router.push(`/${data.did}/oekaki/${data.rkey}#${props.oekaki.author.did}-${rkey}`); + + await router.push(`/${props.oekaki.author.did}/oekaki/${rkey}`); }; diff --git a/PinkSea.Frontend/src/views/PostView.vue b/PinkSea.Frontend/src/views/PostView.vue index 84a2f6f..2316dff 100644 --- a/PinkSea.Frontend/src/views/PostView.vue +++ b/PinkSea.Frontend/src/views/PostView.vue @@ -12,6 +12,7 @@ import Loader from '@/components/Loader.vue' import PostViewOekakiTombstoneCard from '@/components/ErrorCard.vue' import type { OekakiTombstone } from '@/models/oekaki-tombstone' import ErrorCard from '@/components/ErrorCard.vue' +import { getRecordKeyFromAtUri } from '@/api/atproto/helpers' const route = useRoute(); @@ -22,12 +23,12 @@ const parentIsTombstone = computed(() => { return parent.value !== null && 'formerAt' in parent.value; }); -onBeforeMount(async () => { +const fetchOekaki = async (did: string, rkey: string) => { try { const { data } = await xrpc.get("com.shinolabs.pinksea.getOekaki", { params: { - did: route.params.did as string, - rkey: route.params.rkey as string + did: did, + rkey: rkey } }); @@ -40,6 +41,39 @@ onBeforeMount(async () => { children.value = []; } +}; + +onBeforeMount(async () => { + let did = route.params.did as string + let rkey = route.params.rkey as string + + let scrollId = undefined + + try { + const { data } = await xrpc.get("com.shinolabs.pinksea.getParentForReply", { + params: { + did: did, + rkey: rkey! + } + }); + + if (data.rkey !== rkey) { + scrollId = `${did}-${rkey}`; + did = data.did; + rkey = data.rkey; + } + } catch { + + } + + await fetchOekaki(did, rkey); + + if (scrollId !== undefined) { + const element = document.getElementById(scrollId); + if (element) { + element.scrollIntoView({ behavior: 'smooth' }); + } + } }); @@ -51,13 +85,12 @@ onBeforeMount(async () => {
- +
- + From b3fad3729cd5169362bbb1f6a4813d5da53d784e Mon Sep 17 00:00:00 2001 From: purifetchi <0xlunaric@gmail.com> Date: Sun, 27 Jul 2025 17:19:05 +0200 Subject: [PATCH 16/23] feat: allow linking to profiles and posts via handle. --- .../Lexicons/AtProto/ResolveHandleRequest.cs | 16 ++++++++ .../Lexicons/AtProto/ResolveHandleResponse.cs | 15 ++++++++ .../api/atproto/handle-did-route-resolver.ts | 23 ++++++++++++ PinkSea.Frontend/src/router/index.ts | 3 +- PinkSea/Xrpc/ResolveHandleQueryHandler.cs | 37 +++++++++++++++++++ 5 files changed, 93 insertions(+), 1 deletion(-) create mode 100644 PinkSea.AtProto.Shared/Lexicons/AtProto/ResolveHandleRequest.cs create mode 100644 PinkSea.AtProto.Shared/Lexicons/AtProto/ResolveHandleResponse.cs create mode 100644 PinkSea.Frontend/src/api/atproto/handle-did-route-resolver.ts create mode 100644 PinkSea/Xrpc/ResolveHandleQueryHandler.cs diff --git a/PinkSea.AtProto.Shared/Lexicons/AtProto/ResolveHandleRequest.cs b/PinkSea.AtProto.Shared/Lexicons/AtProto/ResolveHandleRequest.cs new file mode 100644 index 0000000..caa1b2d --- /dev/null +++ b/PinkSea.AtProto.Shared/Lexicons/AtProto/ResolveHandleRequest.cs @@ -0,0 +1,16 @@ +using System.Text.Json.Serialization; + +namespace PinkSea.AtProto.Shared.Lexicons.AtProto; + +/// +/// The "com.atproto.identity.resolveHandle" request. +/// Resolves an atproto handle (hostname) to a DID. Does not necessarily bi-directionally verify against the the DID document. +/// +public class ResolveHandleRequest +{ + /// + /// The handle to resolve. + /// + [JsonPropertyName("handle")] + public required string Handle { get; set; } +} \ No newline at end of file diff --git a/PinkSea.AtProto.Shared/Lexicons/AtProto/ResolveHandleResponse.cs b/PinkSea.AtProto.Shared/Lexicons/AtProto/ResolveHandleResponse.cs new file mode 100644 index 0000000..dc42ac7 --- /dev/null +++ b/PinkSea.AtProto.Shared/Lexicons/AtProto/ResolveHandleResponse.cs @@ -0,0 +1,15 @@ +using System.Text.Json.Serialization; + +namespace PinkSea.AtProto.Shared.Lexicons.AtProto; + +/// +/// The response for the "com.atproto.identity.resolveHandle" request. +/// +public class ResolveHandleResponse +{ + /// + /// The DID of the handle. + /// + [JsonPropertyName("did")] + public required string Did { get; set; } +} \ No newline at end of file diff --git a/PinkSea.Frontend/src/api/atproto/handle-did-route-resolver.ts b/PinkSea.Frontend/src/api/atproto/handle-did-route-resolver.ts new file mode 100644 index 0000000..1c95081 --- /dev/null +++ b/PinkSea.Frontend/src/api/atproto/handle-did-route-resolver.ts @@ -0,0 +1,23 @@ +import type { Router } from 'vue-router' +import { xrpc } from './client'; + +export const withHandleDidRouteResolver = (router: Router): void => { + router.beforeEach(async (to, from) => { + if (to.params.did === undefined) { + return + } + + const routeDid = to.params.did as string + if (routeDid.startsWith("did:")) { + return + } + + try { + const { data } = await xrpc.get("com.atproto.identity.resolveHandle", { params: { handle: routeDid } }) + + to.params.did = data.did + } catch { + return + } + }); +}; diff --git a/PinkSea.Frontend/src/router/index.ts b/PinkSea.Frontend/src/router/index.ts index 5420d04..d3d56f4 100644 --- a/PinkSea.Frontend/src/router/index.ts +++ b/PinkSea.Frontend/src/router/index.ts @@ -8,10 +8,10 @@ import PostView from '@/views/PostView.vue' import { xrpc } from '@/api/atproto/client' import TagView from '@/views/TagView.vue' import SettingsView from '@/views/SettingsView.vue' -import i18next from 'i18next' import { withTegakiViewBackProtection } from '@/api/tegaki/tegaki-view-helper' import SearchView from '@/views/SearchView.vue' import UserEditView from '@/views/UserEditView.vue' +import { withHandleDidRouteResolver } from '@/api/atproto/handle-did-route-resolver' const router = createRouter({ history: createWebHistory(import.meta.env.BASE_URL), @@ -107,6 +107,7 @@ const router = createRouter({ }); withBreadcrumb(router); +withHandleDidRouteResolver(router); withTegakiViewBackProtection(router); export default router diff --git a/PinkSea/Xrpc/ResolveHandleQueryHandler.cs b/PinkSea/Xrpc/ResolveHandleQueryHandler.cs new file mode 100644 index 0000000..d295b14 --- /dev/null +++ b/PinkSea/Xrpc/ResolveHandleQueryHandler.cs @@ -0,0 +1,37 @@ +using PinkSea.AtProto.Resolvers.Domain; +using PinkSea.AtProto.Server.Xrpc; +using PinkSea.AtProto.Shared.Lexicons.AtProto; +using PinkSea.AtProto.Shared.Xrpc; + +namespace PinkSea.Xrpc; + +/// +/// Handler for the "com.atproto.identity.resolveHandle" XRPC query. +/// Resolves an atproto handle (hostname) to a DID. +/// +[Xrpc("com.atproto.identity.resolveHandle")] +public class ResolveHandleQueryHandler(IDomainDidResolver domainDidResolver) + : IXrpcQuery +{ + /// + public async Task> Handle(ResolveHandleRequest request) + { + string? did; + try + { + did = await domainDidResolver.GetDidForDomainHandle(request.Handle); + } + catch + { + return XrpcErrorOr.Fail("InvalidRequest", "Unable to resolve handle"); + } + + if (string.IsNullOrEmpty(did)) + return XrpcErrorOr.Fail("InvalidRequest", "Unable to resolve handle"); + + return XrpcErrorOr.Ok(new ResolveHandleResponse + { + Did = did + }); + } +} \ No newline at end of file From fa364d6efd0520ac6f396a3d182447c163a3cda7 Mon Sep 17 00:00:00 2001 From: purifetchi <0xlunaric@gmail.com> Date: Sun, 27 Jul 2025 17:21:03 +0200 Subject: [PATCH 17/23] feat: translate the no description blurb. --- PinkSea.Frontend/src/components/UserCard.vue | 3 ++- PinkSea.Frontend/src/intl/translations/en.json | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/PinkSea.Frontend/src/components/UserCard.vue b/PinkSea.Frontend/src/components/UserCard.vue index 88464e8..65de8d8 100644 --- a/PinkSea.Frontend/src/components/UserCard.vue +++ b/PinkSea.Frontend/src/components/UserCard.vue @@ -3,6 +3,7 @@ import type Profile from '@/models/profile'; import { computed } from 'vue'; import { useRouter } from 'vue-router'; import Avatar from './profile/Avatar.vue'; +import i18next from 'i18next'; const router = useRouter(); @@ -12,7 +13,7 @@ const props = defineProps<{ }>(); const description = computed(() => { - return props.profile.description ?? "This user has no description." + return props.profile.description ?? i18next.t("profile.this_user_has_no_description") }); const nickname = computed(() => { diff --git a/PinkSea.Frontend/src/intl/translations/en.json b/PinkSea.Frontend/src/intl/translations/en.json index 4ee7d63..f6224b5 100644 --- a/PinkSea.Frontend/src/intl/translations/en.json +++ b/PinkSea.Frontend/src/intl/translations/en.json @@ -86,7 +86,8 @@ "profile": { "posts_tab": "Posts", "replies_tab": "Replies", - "edit_profile": "Edit profile" + "edit_profile": "Edit profile", + "this_user_has_no_description": "This user has no description." }, "profile_edit": { "how_others_will_see_you": "This is how others will see you", From 0b874bfac42f3794247126da6447326f97c19ea5 Mon Sep 17 00:00:00 2001 From: purifetchi <0xlunaric@gmail.com> Date: Sun, 27 Jul 2025 20:20:56 +0200 Subject: [PATCH 18/23] feat: implement going back to the editor before uploading. --- .../src/components/RespondBox.vue | 20 +- .../src/intl/translations/en.json | 8 +- PinkSea.Frontend/src/views/PainterView.vue | 268 ++++++++++-------- 3 files changed, 166 insertions(+), 130 deletions(-) diff --git a/PinkSea.Frontend/src/components/RespondBox.vue b/PinkSea.Frontend/src/components/RespondBox.vue index a920108..f5ece2f 100644 --- a/PinkSea.Frontend/src/components/RespondBox.vue +++ b/PinkSea.Frontend/src/components/RespondBox.vue @@ -38,6 +38,14 @@ const reply = () => { } }; +const edit = () => { + Tegaki.open({ + onDone: () => { + image.value = Tegaki.flatten().toDataURL("image/png"); + } + }) +} + const cancel = () => { image.value = null; @@ -90,8 +98,7 @@ const uploadImage = async () => { onBeforeMount(() => { if (imageStore.lastReplyId == props.parent.at - && imageStore.lastDoneReply !== null && imageStore.lastReplyErrored) - { + && imageStore.lastDoneReply !== null && imageStore.lastReplyErrored) { image.value = imageStore.lastDoneReply; } }) @@ -106,7 +113,7 @@ onBeforeMount(() => {
- +
@@ -114,6 +121,7 @@ onBeforeMount(() => {
+
@@ -149,6 +157,10 @@ img { } .two-buttons { - display: flex; + display: flex; +} + +input[type=checkbox] { + accent-color: #FFB6C1; } diff --git a/PinkSea.Frontend/src/intl/translations/en.json b/PinkSea.Frontend/src/intl/translations/en.json index f6224b5..4e7da62 100644 --- a/PinkSea.Frontend/src/intl/translations/en.json +++ b/PinkSea.Frontend/src/intl/translations/en.json @@ -51,7 +51,8 @@ "click_to_respond": "Click to open the drawing panel", "open_painter": "Open painter", "reply": "Reply!", - "cancel": "Cancel" + "cancel": "Cancel", + "edit": "Edit" }, "settings": { "category_general": "general", @@ -77,11 +78,14 @@ "upload_tags": "Tags", "upload_social": "Social", "upload_confirm": "Confirm", + "edit": "Edit", + "edit_go_back_to_editor": "Return to the editor", "hint_description": "Attaching a short description helps give context about your drawing. Optional.", "hint_tags": "Give your post up to five tags to help others discover it! For example: characters (koiwai_yotsuba), copyrights (yotsubato! / oc) or general information (portrait). Optional.", "hint_nsfw": "Please check if your post contains adult content such as nudity or highly suggestive themes.", "hint_xpost": "If checked, we'll automatically create a post for you on Bluesky with the image and a link to PinkSea attached.", - "hint_confirm": "Once you're ready, click the button above to publish your image!" + "hint_confirm": "Once you're ready, click the button above to publish your image!", + "hint_edit": "Need to adjust the image before uploading? Click the button to go back to the editor." }, "profile": { "posts_tab": "Posts", diff --git a/PinkSea.Frontend/src/views/PainterView.vue b/PinkSea.Frontend/src/views/PainterView.vue index c5a1e7c..e6ca8e8 100644 --- a/PinkSea.Frontend/src/views/PainterView.vue +++ b/PinkSea.Frontend/src/views/PainterView.vue @@ -1,148 +1,154 @@
-
Name
+
{{ $t("profile_edit.link_name") }}
- This is the name of the link you're creating. + {{ $t("profile_edit.link_name_description") }}
-
Url
+
{{ $t("profile_edit.link_url") }}
- This is the url of the link you're creating. + {{ $t("profile_edit.link_url_description") }}
- +
+ }}