Compare commits
	
		
			2 Commits
		
	
	
		
			db5d631049
			...
			7fc86441d1
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 7fc86441d1 | |||
| 1a05f16299 | 
							
								
								
									
										7
									
								
								DysonNetwork.Drive/Client/src/dy-prefetch.d.ts
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								DysonNetwork.Drive/Client/src/dy-prefetch.d.ts
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,7 @@ | ||||
| export {} | ||||
|  | ||||
| declare global { | ||||
|   interface Window { | ||||
|     DyPrefetch?: any | ||||
|   } | ||||
| } | ||||
| @@ -183,7 +183,7 @@ async function fetchUser() { | ||||
|  | ||||
|   console.log('[Fetch] Using the API to load user data.') | ||||
|   try { | ||||
|     const resp = await fetch(`/api/accounts/${route.params.name}`) | ||||
|     const resp = await fetch(`/api/accounts/${route.params.slug}`) | ||||
|     user.value = await resp.json() | ||||
|   } catch (err) { | ||||
|     console.error(err) | ||||
|   | ||||
| @@ -43,13 +43,11 @@ public class SubscriptionController(SubscriptionService subscriptions, AfdianPay | ||||
|     { | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser) return Unauthorized(); | ||||
|  | ||||
|         var subs = await db.WalletSubscriptions | ||||
|         var subscription = await db.WalletSubscriptions | ||||
|             .Where(s => s.AccountId == currentUser.Id && s.IsActive) | ||||
|             .Where(s => EF.Functions.ILike(s.Identifier, prefix + "%")) | ||||
|             .OrderByDescending(s => s.BegunAt) | ||||
|             .ToListAsync(); | ||||
|         if (subs.Count == 0) return NotFound(); | ||||
|         var subscription = subs.FirstOrDefault(s => s.IsAvailable); | ||||
|             .FirstOrDefaultAsync(); | ||||
|         if (subscription is null) return NotFound(); | ||||
|  | ||||
|         return Ok(subscription); | ||||
|   | ||||
| @@ -400,9 +400,9 @@ public class SubscriptionService( | ||||
|             .OrderByDescending(s => s.BegunAt) | ||||
|             .FirstOrDefaultAsync(); | ||||
|  | ||||
|         // Cache the result if found (with 30 minutes expiry) | ||||
|         // Cache the result if found (with 5 minutes expiry) | ||||
|         if (subscription != null) | ||||
|             await cache.SetAsync(cacheKey, subscription, TimeSpan.FromMinutes(30)); | ||||
|             await cache.SetAsync(cacheKey, subscription, TimeSpan.FromMinutes(5)); | ||||
|  | ||||
|         return subscription; | ||||
|     } | ||||
|   | ||||
| @@ -6,6 +6,7 @@ | ||||
|     <meta name="viewport" content="width=device-width, initial-scale=1.0" /> | ||||
|     <title>Solar Network</title> | ||||
|     <app-data /> | ||||
|     <og-data /> | ||||
|   </head> | ||||
|   <body> | ||||
|     <div id="app"></div> | ||||
|   | ||||
							
								
								
									
										7
									
								
								DysonNetwork.Sphere/Client/src/dy-prefetch.d.ts
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								DysonNetwork.Sphere/Client/src/dy-prefetch.d.ts
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,7 @@ | ||||
| export {} | ||||
|  | ||||
| declare global { | ||||
|   interface Window { | ||||
|     DyPrefetch?: any | ||||
|   } | ||||
| } | ||||
| @@ -10,6 +10,11 @@ const router = createRouter({ | ||||
|       name: 'index', | ||||
|       component: () => import('../views/index.vue'), | ||||
|     }, | ||||
|     { | ||||
|       path: '/posts/:slug', | ||||
|       name: 'postDetail', | ||||
|       component: () => import('../views/posts.vue'), | ||||
|     }, | ||||
|   ], | ||||
| }) | ||||
|  | ||||
|   | ||||
| @@ -4,7 +4,11 @@ | ||||
|       <n-gi span="3"> | ||||
|         <n-infinite-scroll style="height: calc(100vh - 57px)" :distance="10" @load="fetchActivites"> | ||||
|           <div v-for="activity in activites" :key="activity.id" class="mt-4"> | ||||
|             <post-item v-if="activity.type == 'posts.new'" :item="activity.data" /> | ||||
|             <post-item | ||||
|               v-if="activity.type.startsWith('posts')" | ||||
|               :item="activity.data" | ||||
|               @click="router.push('/posts/' + activity.id)" | ||||
|             /> | ||||
|           </div> | ||||
|         </n-infinite-scroll> | ||||
|       </n-gi> | ||||
| @@ -40,11 +44,14 @@ | ||||
| <script setup lang="ts"> | ||||
| import { NCard, NInfiniteScroll, NGrid, NGi, NAlert, NA, NHr } from 'naive-ui' | ||||
| import { computed, onMounted, ref } from 'vue' | ||||
| import { useRouter } from 'vue-router' | ||||
| import { useUserStore } from '@/stores/user' | ||||
|  | ||||
| import PostEditor from '@/components/PostEditor.vue' | ||||
| import PostItem from '@/components/PostItem.vue' | ||||
|  | ||||
| const router = useRouter() | ||||
|  | ||||
| const userStore = useUserStore() | ||||
|  | ||||
| const version = ref<any>(null) | ||||
|   | ||||
							
								
								
									
										100
									
								
								DysonNetwork.Sphere/Client/src/views/posts.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										100
									
								
								DysonNetwork.Sphere/Client/src/views/posts.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,100 @@ | ||||
| <template> | ||||
|   <div v-if="post" class="container max-w-5xl mx-auto mt-4"> | ||||
|     <n-grid cols="1 l:5" responsive="screen" :x-gap="16"> | ||||
|       <n-gi span="3"> | ||||
|         <post-item :item="post" /> | ||||
|       </n-gi> | ||||
|       <n-gi span="2"> | ||||
|         <n-card title="About the author"> | ||||
|           <div class="relative mb-7"> | ||||
|             <img | ||||
|               class="object-cover rounded-lg" | ||||
|               style="aspect-ratio: 16/7" | ||||
|               :src="publisherBackground" | ||||
|             /> | ||||
|             <div class="absolute left-3 bottom-[-24px]"> | ||||
|               <n-avatar :src="publisherAvatar" :size="64" round bordered /> | ||||
|             </div> | ||||
|           </div> | ||||
|           <div class="flex flex-col"> | ||||
|             <p class="flex gap-1 items-baseline"> | ||||
|               <span class="font-bold"> | ||||
|                 {{ post.publisher.nick }} | ||||
|               </span> | ||||
|               <span class="text-sm"> @{{ post.publisher.name }} </span> | ||||
|             </p> | ||||
|             <div class="max-h-96 overflow-y-auto"> | ||||
|               <div | ||||
|                 class="prose prose-sm dark:prose-invert prose-slate" | ||||
|                 v-if="publisherBio" | ||||
|                 v-html="publisherBio" | ||||
|               ></div> | ||||
|             </div> | ||||
|           </div> | ||||
|         </n-card> | ||||
|       </n-gi> | ||||
|     </n-grid> | ||||
|   </div> | ||||
|   <div v-else-if="notFound" class="flex justify-center items-center h-full"> | ||||
|     <n-result | ||||
|       status="404" | ||||
|       title="Post not found" | ||||
|       description="The post you are looking cannot be found, it might be deleted, or you have no permission to view it or it just never been posted." | ||||
|     /> | ||||
|   </div> | ||||
|   <div v-else class="flex justify-center items-center h-full"> | ||||
|     <n-spin /> | ||||
|   </div> | ||||
| </template> | ||||
|  | ||||
| <script setup lang="ts"> | ||||
| import { NGrid, NGi, NCard, NAvatar } from 'naive-ui' | ||||
| import { computed, onMounted, ref, watch } from 'vue' | ||||
| import { useRoute } from 'vue-router' | ||||
| import { Marked } from 'marked' | ||||
|  | ||||
| import PostItem from '@/components/PostItem.vue' | ||||
|  | ||||
| const route = useRoute() | ||||
|  | ||||
| const post = ref<any>() | ||||
| const notFound = ref(false) | ||||
|  | ||||
| async function fetchPost() { | ||||
|   if (window.DyPrefetch?.Post != null) { | ||||
|     console.log('[Fetch] Use the pre-rendered post data.') | ||||
|     post.value = window.DyPrefetch.Post | ||||
|     return | ||||
|   } | ||||
|  | ||||
|   console.log('[Fetch] Using the API to load user data.') | ||||
|   try { | ||||
|     const resp = await fetch(`/api/posts/${route.params.slug}`) | ||||
|     post.value = await resp.json() | ||||
|   } catch (err) { | ||||
|     console.error(err) | ||||
|     notFound.value = true | ||||
|   } | ||||
| } | ||||
| onMounted(() => fetchPost()) | ||||
|  | ||||
| const publisherAvatar = computed(() => | ||||
|   post.value.publisher.picture ? `/cgi/drive/files/${post.value.publisher.picture.id}` : undefined, | ||||
| ) | ||||
| const publisherBackground = computed(() => | ||||
|   post.value.publisher.background | ||||
|     ? `/cgi/drive/files/${post.value.publisher.background.id}` | ||||
|     : undefined, | ||||
| ) | ||||
|  | ||||
| const marked = new Marked() | ||||
|  | ||||
| const publisherBio = ref('') | ||||
| watch( | ||||
|   post, | ||||
|   async (value) => { | ||||
|     if (value?.publisher?.bio) publisherBio.value = await marked.parse(value.publisher.bio) | ||||
|   }, | ||||
|   { immediate: true, deep: true }, | ||||
| ) | ||||
| </script> | ||||
| @@ -41,6 +41,7 @@ | ||||
|         <PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL.Design" Version="1.1.0"/> | ||||
|         <PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL.NetTopologySuite" Version="9.0.4"/> | ||||
|         <PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL.NodaTime" Version="9.0.4"/> | ||||
|         <PackageReference Include="OpenGraph-Net" Version="4.0.1" /> | ||||
|         <PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.12.0"/> | ||||
|         <PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.12.0"/> | ||||
|         <PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.12.0"/> | ||||
|   | ||||
							
								
								
									
										75
									
								
								DysonNetwork.Sphere/PageData/PostPageData.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										75
									
								
								DysonNetwork.Sphere/PageData/PostPageData.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,75 @@ | ||||
| using System.Net; | ||||
| using DysonNetwork.Shared.PageData; | ||||
| using DysonNetwork.Shared.Proto; | ||||
| using DysonNetwork.Sphere.Post; | ||||
| using DysonNetwork.Sphere.Publisher; | ||||
| using Microsoft.EntityFrameworkCore; | ||||
| using OpenGraphNet; | ||||
|  | ||||
| namespace DysonNetwork.Sphere.PageData; | ||||
|  | ||||
| public class PostPageData( | ||||
|     AppDatabase db, | ||||
|     AccountService.AccountServiceClient accounts, | ||||
|     PublisherService pub, | ||||
|     PostService ps, | ||||
|     IConfiguration configuration | ||||
| ) | ||||
|     : IPageDataProvider | ||||
| { | ||||
|     private readonly string _siteUrl = configuration["SiteUrl"]!; | ||||
|  | ||||
|     public bool CanHandlePath(PathString path) => | ||||
|         path.StartsWithSegments("/posts"); | ||||
|  | ||||
|     public async Task<IDictionary<string, object?>> GetAppDataAsync(HttpContext context) | ||||
|     { | ||||
|         var path = context.Request.Path.Value!; | ||||
|         var startIndex = "/posts/".Length; | ||||
|         var endIndex = path.IndexOf('/', startIndex); | ||||
|         var slug = endIndex == -1 ? path[startIndex..] : path.Substring(startIndex, endIndex - startIndex); | ||||
|         slug = WebUtility.UrlDecode(slug); | ||||
|  | ||||
|         var postId = Guid.TryParse(slug, out var postIdGuid) ? postIdGuid : Guid.Empty; | ||||
|         if (postId == Guid.Empty) return new Dictionary<string, object?>(); | ||||
|  | ||||
|         context.Items.TryGetValue("CurrentUser", out var currentUserValue); | ||||
|         var currentUser = currentUserValue as Account; | ||||
|         List<Guid> userFriends = []; | ||||
|         if (currentUser != null) | ||||
|         { | ||||
|             var friendsResponse = await accounts.ListFriendsAsync(new ListRelationshipSimpleRequest | ||||
|                 { AccountId = currentUser.Id }); | ||||
|             userFriends = friendsResponse.AccountsId.Select(Guid.Parse).ToList(); | ||||
|         } | ||||
|  | ||||
|         var userPublishers = currentUser is null ? [] : await pub.GetUserPublishers(Guid.Parse(currentUser.Id)); | ||||
|  | ||||
|         var post = await db.Posts | ||||
|             .Where(e => e.Id == postId) | ||||
|             .Include(e => e.Publisher) | ||||
|             .Include(e => e.Tags) | ||||
|             .Include(e => e.Categories) | ||||
|             .FilterWithVisibility(currentUser, userFriends, userPublishers) | ||||
|             .FirstOrDefaultAsync(); | ||||
|         post = await ps.LoadPostInfo(post, currentUser); | ||||
|  | ||||
|         // Track view - use the account ID as viewer ID if user is logged in | ||||
|         await ps.IncreaseViewCount(post.Id, currentUser?.Id); | ||||
|  | ||||
|         var og = OpenGraph.MakeGraph( | ||||
|             title: post.Title ?? $"Post from {post.Publisher.Name}", | ||||
|             type: "article", | ||||
|             image: $"{_siteUrl}/cgi/drive/files/{post.Publisher.Background?.Id}?original=true", | ||||
|             url: $"{_siteUrl}/@{slug}", | ||||
|             description: post.Description ?? post.Content?[..80] ?? "Posted with some media", | ||||
|             siteName: "Solar Network" | ||||
|         ); | ||||
|  | ||||
|         return new Dictionary<string, object?>() | ||||
|         { | ||||
|             ["Post"] = post, | ||||
|             ["OpenGraph"] = og | ||||
|         }; | ||||
|     } | ||||
| } | ||||
| @@ -1,7 +1,9 @@ | ||||
| using DysonNetwork.Shared.Auth; | ||||
| using DysonNetwork.Shared.Http; | ||||
| using DysonNetwork.Shared.PageData; | ||||
| using DysonNetwork.Shared.Registry; | ||||
| using DysonNetwork.Sphere; | ||||
| using DysonNetwork.Sphere.PageData; | ||||
| using DysonNetwork.Sphere.Startup; | ||||
| using Microsoft.EntityFrameworkCore; | ||||
|  | ||||
| @@ -33,6 +35,8 @@ builder.Services.AddAppBusinessServices(builder.Configuration); | ||||
| // Add scheduled jobs | ||||
| builder.Services.AddAppScheduledJobs(); | ||||
|  | ||||
| builder.Services.AddTransient<IPageDataProvider, PostPageData>(); | ||||
|  | ||||
| var app = builder.Build(); | ||||
|  | ||||
| // Run database migrations | ||||
|   | ||||
| @@ -1,6 +1,7 @@ | ||||
| { | ||||
|   "Debug": true, | ||||
|   "BaseUrl": "http://localhost:5071", | ||||
|   "SiteUrl": "https://solian.app", | ||||
|   "Logging": { | ||||
|     "LogLevel": { | ||||
|       "Default": "Information", | ||||
|   | ||||
| @@ -54,6 +54,7 @@ | ||||
| 	<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AFieldMask_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F331aca3f6f414013b09964063341351379060_003F68_003Fc6da3cbf_003FFieldMask_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> | ||||
| 	<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AFileResult_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F0b5acdd962e549369896cece0026e556214600_003F8c_003F9f6e3f4f_003FFileResult_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> | ||||
| 	<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AForwardedHeaders_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fcfe5737f9bb84738979cbfedd11822a8ea00_003F50_003F9a335f87_003FForwardedHeaders_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> | ||||
| 	<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AHttpContext_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fc181aff8c6ec418494a7efcfec578fc154e00_003Fd0_003Fcc905531_003FHttpContext_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> | ||||
| 	<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AHttpRequestHeaders_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fb904f9896c4049fabd596decf1be9c381dc400_003F32_003F906beb77_003FHttpRequestHeaders_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> | ||||
| 	<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AHttpStatusCode_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fb3f2e07d4b3f4b42a41fbcf3137e534f3be00_003Fe2_003F215f9441_003FHttpStatusCode_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> | ||||
| 	<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AHttpTransformer_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fbf3f51607a3e4e76b5b91640cd7409195c430_003F8a_003Fd9fba048_003FHttpTransformer_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> | ||||
|   | ||||
		Reference in New Issue
	
	Block a user