diff --git a/lib/Controller/PostController.php b/lib/Controller/PostController.php index acad3b4..8786b1e 100644 --- a/lib/Controller/PostController.php +++ b/lib/Controller/PostController.php @@ -283,10 +283,23 @@ class PostController extends OCSController { // For posts by a single author, we can optimize by fetching author data once $author = $this->userService->enrichUserData($authorId); - // Enrich posts with content, reactions, and pre-fetched author data - return new DataResponse(array_map(function ($p) use ($bbcodes, $reactionsByPostId, $currentUserId, $author) { + // Enrich posts with content, reactions, pre-fetched author data, and page number + $perPage = 20; + return new DataResponse(array_map(function ($p) use ($bbcodes, $reactionsByPostId, $currentUserId, $author, $perPage) { $postReactions = $reactionsByPostId[$p->getId()] ?? []; - return $this->postEnrichmentService->enrichPost($p, $bbcodes, $postReactions, $currentUserId, $author); + $enriched = $this->postEnrichmentService->enrichPost($p, $bbcodes, $postReactions, $currentUserId, $author); + + // Calculate the page number for direct linking + if (!$p->getIsFirstPost()) { + try { + $position = $this->postMapper->getReplyPosition($p->getThreadId(), $p->getId()); + $enriched['page'] = (int)floor($position / $perPage) + 1; + } catch (\Exception $e) { + // Fallback - page unknown + } + } + + return $enriched; }, $posts)); } catch (\Exception $e) { $this->logger->error('Error fetching posts by author: ' . $e->getMessage()); diff --git a/lib/Controller/SearchController.php b/lib/Controller/SearchController.php index 1469126..9093c51 100644 --- a/lib/Controller/SearchController.php +++ b/lib/Controller/SearchController.php @@ -8,6 +8,7 @@ declare(strict_types=1); namespace OCA\Forum\Controller; use OCA\Forum\Db\Post; +use OCA\Forum\Db\PostMapper; use OCA\Forum\Db\Thread; use OCA\Forum\Db\ThreadMapper; use OCA\Forum\Service\PostEnrichmentService; @@ -29,6 +30,7 @@ class SearchController extends OCSController { string $appName, IRequest $request, private SearchService $searchService, + private PostMapper $postMapper, private ThreadMapper $threadMapper, private PostEnrichmentService $postEnrichmentService, private ThreadEnrichmentService $threadEnrichmentService, @@ -124,7 +126,8 @@ class SearchController extends OCSController { }, $results['threads']); // Enrich posts with pre-fetched author data and thread context - $enrichedPosts = array_map(function ($post) use ($authors) { + $perPage = 20; + $enrichedPosts = array_map(function ($post) use ($authors, $perPage) { $enriched = $this->postEnrichmentService->enrichPost($post, [], [], null, $authors[$post->getAuthorId()]); // Add thread info for context try { @@ -137,6 +140,16 @@ class SearchController extends OCSController { $enriched['threadSlug'] = null; } + // Calculate the page number for direct linking + if (!$post->getIsFirstPost()) { + try { + $position = $this->postMapper->getReplyPosition($post->getThreadId(), $post->getId()); + $enriched['page'] = (int)floor($position / $perPage) + 1; + } catch (\Exception $e) { + // Fallback - page unknown + } + } + return $enriched; }, $results['posts']); diff --git a/src/components/SearchPostResult/SearchPostResult.test.ts b/src/components/SearchPostResult/SearchPostResult.test.ts index 60d7c1e..de8a3b2 100644 --- a/src/components/SearchPostResult/SearchPostResult.test.ts +++ b/src/components/SearchPostResult/SearchPostResult.test.ts @@ -157,7 +157,7 @@ describe('SearchPostResult', () => { }, }) await wrapper.find('.search-post-result').trigger('click') - expect(mockPush).toHaveBeenCalledWith('/t/my-thread#post-42') + expect(mockPush).toHaveBeenCalledWith({ path: '/t/my-thread', query: { post: '42' } }) }) it('should not navigate when thread slug is missing', async () => { diff --git a/src/components/SearchPostResult/SearchPostResult.vue b/src/components/SearchPostResult/SearchPostResult.vue index 58d6daf..b32f9dd 100644 --- a/src/components/SearchPostResult/SearchPostResult.vue +++ b/src/components/SearchPostResult/SearchPostResult.vue @@ -3,12 +3,7 @@
{{ strings.inThread }}: - + {{ post.threadTitle }} {{ strings.threadUnavailable }} @@ -74,6 +69,13 @@ export default defineComponent({ } }, computed: { + postLink(): { path: string; query: Record } { + const query: Record = { post: String(this.post.id) } + if ((this.post as any).page) { + query.page = String((this.post as any).page) + } + return { path: `/t/${this.post.threadSlug}`, query } + }, highlightedContent(): string { // Strip HTML tags first, then highlight query terms, then truncate const text = this.stripHtml(this.post.content) @@ -84,7 +86,7 @@ export default defineComponent({ methods: { navigateToPost(): void { if (this.post.threadSlug) { - this.$router.push(`/t/${this.post.threadSlug}#post-${this.post.id}`) + this.$router.push(this.postLink) } }, stripHtml(html: string): string { diff --git a/src/views/ProfileView.vue b/src/views/ProfileView.vue index b9ead1c..14912f5 100644 --- a/src/views/ProfileView.vue +++ b/src/views/ProfileView.vue @@ -364,12 +364,14 @@ export default defineComponent({ this.$router.push({ path: `/t/${thread.slug}`, query }) }, - navigateToPost(post: Post) { - if (post.threadSlug) { - this.$router.push(`/t/${post.threadSlug}#post-${post.id}`) - } else { - this.$router.push(`/thread/${post.threadId}#post-${post.id}`) + navigateToPost(post: Post & { page?: number }) { + const slug = post.threadSlug || `thread/${post.threadId}` + const path = `/t/${slug}` + const query: Record = { post: String(post.id) } + if (post.page) { + query.page = String(post.page) } + this.$router.push({ path, query }) }, }, }) diff --git a/tests/unit/Controller/SearchControllerTest.php b/tests/unit/Controller/SearchControllerTest.php index 1502b11..1f5072a 100644 --- a/tests/unit/Controller/SearchControllerTest.php +++ b/tests/unit/Controller/SearchControllerTest.php @@ -7,6 +7,7 @@ namespace OCA\Forum\Tests\Controller; use OCA\Forum\AppInfo\Application; use OCA\Forum\Controller\SearchController; use OCA\Forum\Db\Post; +use OCA\Forum\Db\PostMapper; use OCA\Forum\Db\Thread; use OCA\Forum\Db\ThreadMapper; use OCA\Forum\Service\PostEnrichmentService; @@ -26,6 +27,8 @@ class SearchControllerTest extends TestCase { private SearchController $controller; /** @var SearchService&MockObject */ private SearchService $searchService; + /** @var PostMapper&MockObject */ + private PostMapper $postMapper; /** @var ThreadMapper&MockObject */ private ThreadMapper $threadMapper; /** @var PostEnrichmentService&MockObject */ @@ -44,6 +47,7 @@ class SearchControllerTest extends TestCase { protected function setUp(): void { $this->request = $this->createMock(IRequest::class); $this->searchService = $this->createMock(SearchService::class); + $this->postMapper = $this->createMock(PostMapper::class); $this->threadMapper = $this->createMock(ThreadMapper::class); $this->postEnrichmentService = $this->createMock(PostEnrichmentService::class); $this->threadEnrichmentService = $this->createMock(ThreadEnrichmentService::class); @@ -75,6 +79,7 @@ class SearchControllerTest extends TestCase { Application::APP_ID, $this->request, $this->searchService, + $this->postMapper, $this->threadMapper, $this->postEnrichmentService, $this->threadEnrichmentService,