fix: directly link to replies from profile+search pages

This commit is contained in:
2026-03-23 00:48:56 +02:00
parent cf561cd44f
commit 94dc945857
6 changed files with 52 additions and 17 deletions

View File

@@ -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());

View File

@@ -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']);

View File

@@ -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 () => {

View File

@@ -3,12 +3,7 @@
<div class="result-header">
<div class="thread-context">
<span class="meta-label">{{ strings.inThread }}:</span>
<router-link
v-if="post.threadSlug"
:to="`/t/${post.threadSlug}#post-${post.id}`"
class="thread-link"
@click.stop
>
<router-link v-if="post.threadSlug" :to="postLink" class="thread-link" @click.stop>
{{ post.threadTitle }}
</router-link>
<span v-else class="thread-missing">{{ strings.threadUnavailable }}</span>
@@ -74,6 +69,13 @@ export default defineComponent({
}
},
computed: {
postLink(): { path: string; query: Record<string, string> } {
const query: Record<string, string> = { 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 {

View File

@@ -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<string, string> = { post: String(post.id) }
if (post.page) {
query.page = String(post.page)
}
this.$router.push({ path, query })
},
},
})

View File

@@ -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,