Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat: threads in following #653

Merged
merged 4 commits into from
Sep 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 20 additions & 5 deletions app/Queries/Feeds/QuestionsFollowingFeed.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use App\Models\Question;
use App\Models\User;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Facades\DB;

final readonly class QuestionsFollowingFeed
{
Expand All @@ -24,13 +25,27 @@ public function __construct(
*/
public function builder(): Builder
{
$followQueryClosure = function (Builder $query): void {
$query->where('to_id', $this->user->id)
->orWhereIn('to_id', $this->user->following()->select('users.id'));
};

return Question::query()
->whereHas('to', function (Builder $toQuery): void {
$toQuery->whereIn('id', $this->user->following()->select('users.id'));
})
->orderByDesc('updated_at')
->select('id', 'root_id', 'parent_id')
->withExists([
'root as showRoot' => $followQueryClosure,
'parent as showParent' => $followQueryClosure,
])
->withAggregate('to as username', 'username')
->withAggregate('parent as grand_parent_id', 'parent_id')
->whereNotNull('answer')
->where('is_reported', false)
->where('is_ignored', false);
->where('is_ignored', false)
->where($followQueryClosure)
->where(function (Builder $query): void {
$query->whereNull('parent_id')->orWhere('showParent', true)->orWhere('showRoot', true);
})
->groupBy(DB::Raw('IFNULL(root_id, id)'))
->orderByDesc(DB::raw('MAX(`updated_at`)'));
}
}
35 changes: 19 additions & 16 deletions resources/views/components/thread.blade.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,37 +6,40 @@
'username' => null,
])

<div>
<div wire:key="thread-{{ $questionId.'-'.$rootId.'-'.$parentId }}">
@if ($rootId !== null)
<livewire:questions.show
:questionId="$rootId"
:in-thread="true"
:key="$rootId"
:key="'question-'.$rootId"
/>
@endif
@if ($parentId !== null && $rootId !== $parentId)
@if ($grandParentId === $rootId)
<x-post-divider />
@else
<x-post-divider
:link="route('questions.show', ['username' => $username, 'question' => $rootId])"
:text="'View more comments...'"
/>
@if($rootId !== null)
@if ($grandParentId === $rootId)
<x-post-divider wire:key="divider-{{ $parentId }}" />
@else
<x-post-divider
:link="route('questions.show', ['username' => $username, 'question' => $questionId])"
:text="'View more comments...'"
wire:key="divider-{{ $parentId }}"
/>
@endif
@endif
<livewire:questions.show
:questionId="$parentId"
:in-thread="true"
:key="$parentId"
:in-thread="$rootId !== null"
:key="'question-'.$parentId"
/>
@endif

@if ($parentId !== null || $rootId !== null)
<x-post-divider />
<x-post-divider
wire:key="divider-{{ $questionId }}"
/>
@endif

<livewire:questions.show
:questionId="$questionId"
:in-thread="true"
:key="$questionId"
:in-thread="$rootId !== null || $parentId !== null"
:key="'question-'.$questionId"
/>
</div>
9 changes: 5 additions & 4 deletions resources/views/livewire/home/questions-following.blade.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,12 @@
@else
<section class="mb-12 min-h-screen space-y-10">
@foreach ($followingQuestions as $question)
<livewire:questions.show
<x-thread
:rootId="$question->showRoot ? $question->root_id : null"
:grandParentId="$question->grand_parent_id"
:parentId="$question->showParent ? $question->parent_id : null"
:questionId="$question->id"
:key="'question-' . $question->id"
:inIndex="true"
:pinnable="false"
:username="$question->username"
/>
@endforeach

Expand Down
33 changes: 28 additions & 5 deletions tests/Unit/Feeds/QuestionsFollowingTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,23 +12,23 @@

$userTo = User::factory()->create();

$questionWithLike = Question::factory()->create([
$question1 = Question::factory()->create([
'to_id' => $userTo->id,
'answer' => 'Answer',
'is_reported' => false,
]);

$followerUser->following()->attach($userTo->id);

Question::factory()->create([
$question2 = Question::factory()->create([
'to_id' => $userTo->id,
'answer' => 'Answer 2',
'is_reported' => false,
]);

$builder = (new QuestionsFollowingFeed($followerUser))->builder();

expect($builder->count())->toBe(2);
expect($builder->pluck('id')->all())->toEqual([$question2->id, $question1->id]);
});

it('do not render questions without answer', function () {
Expand All @@ -38,7 +38,7 @@

$answer = 'Answer to the question that needs to be rendered';

$questionWithLike = Question::factory()->create([
Question::factory()->create([
'to_id' => $userTo->id,
'answer' => $answer,
'is_reported' => false,
Expand Down Expand Up @@ -79,7 +79,7 @@

$userTo = User::factory()->create();

$questionWithLike = Question::factory()->create([
Question::factory()->create([
'to_id' => $userTo->id,
'answer' => 'Answer',
'is_reported' => false,
Expand All @@ -98,6 +98,29 @@
expect($builder->where('is_reported', false)->count())->toBe(1);
});

it('does not show the comments if it\'s on non following user\'s post', function () {
$followerUser = User::factory()->create();

$userTo = User::factory()->create();

$followerUser->following()->attach($userTo->id);

$question = Question::factory()->create([
'answer' => 'Answer',
]);

Question::factory()->create([
'parent_id' => $question->id,
'answer' => 'Answer 2',
'from_id' => $userTo->id,
'to_id' => $userTo->id,
]);

$builder = (new QuestionsFollowingFeed($followerUser))->builder();

expect($builder->count())->toBe(0);
});

it('builder returns Eloquent\Builder instance', function () {
$builder = (new QuestionsFollowingFeed(User::factory()->create()))->builder();

Expand Down
2 changes: 1 addition & 1 deletion tests/Unit/Livewire/Home/FeedTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@

it('renders the threads in the right order', function () {

// create 5 roots
// create 4 roots
$roots = Question::factory()
->forEachSequence(
['answer' => 'root 1'],
Expand Down
97 changes: 96 additions & 1 deletion tests/Unit/Livewire/Home/FollowingTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@

$questionContent = 'This is a question with a follow from the authenticated user';

$question = Question::factory()->create([
Question::factory()->create([
'content' => $questionContent,
'answer' => 'Cool question',
'from_id' => $user->id,
Expand Down Expand Up @@ -70,3 +70,98 @@

$component->assertSet('perPage', 100);
});

it('renders the threads in the right order', function () {

$user = User::factory()->create();
$anotherUser = User::factory()->create();
$authUser = User::factory()->create();

$authUser->following()->attach($user->id);
$authUser->following()->attach($anotherUser->id);

$answerForFollowingUser = 'following user question';
$answerForAnotherFollowingUser = 'another following user question';
$answerForAuthUser = 'auth user question';
$answerForNonFollowingUser = 'non following user post';

$questions = Question::factory()
->forEachSequence(
['answer' => $answerForFollowingUser, 'to_id' => $user->id],
['answer' => $answerForAnotherFollowingUser, 'to_id' => $anotherUser->id],
['answer' => $answerForAuthUser, 'to_id' => $authUser->id],
['answer' => $answerForNonFollowingUser],
)->create();

// create a child for each following user's question
$questions->each(function (Question $question) use ($answerForNonFollowingUser) {

$this->travel(1)->seconds();

Question::factory()->sharedUpdate()->create([
'answer' => '1st child question for '.str($question->answer)->snake(),
'root_id' => $question->id,
'parent_id' => $question->id,
'to_id' => $question->where('answer', '!=', $answerForNonFollowingUser)->inRandomOrder()->first()->to_id,
]);
});

$questions->load('children');

// create a root without descendants
$this->travel(1)->seconds();
Question::factory()->sharedUpdate()->create(['answer' => 'root without descendants', 'to_id' => $user->id]); // by following user

// create a child for each child of even roots
$questions->filter(fn (Question $question, int $key) => ($key + 1) % 2 === 0) // evens
->each(function (Question $question) use ($answerForNonFollowingUser) {
$this->travel(1)->seconds();

Question::factory()->sharedUpdate()->create([
'answer' => '2nd nested child question for '.str($question->answer)->snake(),
'parent_id' => $question->children->first()->id,
'root_id' => $question->id,
'to_id' => $question->where('answer', '!=', $answerForNonFollowingUser)->inRandomOrder()->first()->to_id, // random following user
]);
});

$this->travel(1)->seconds();

// 3rd nested child question for another following user question, it's 1st child should be missing in the feed
Question::factory()->sharedUpdate()->create([
'answer' => '3rd nested child question for '.str($answerForAnotherFollowingUser)->snake(),
'root_id' => $questions->where('answer', $answerForAnotherFollowingUser)->first()->id,
'parent_id' => Question::where('answer', '2nd nested child question for '.str($answerForAnotherFollowingUser)->snake())->first()->id,
'to_id' => $authUser->id,
]);

// 3rd nested child question for non following user question from non following user should be missing in the feed
Question::factory()->sharedUpdate()->create([
'answer' => '3rd nested child question for '.str($answerForNonFollowingUser)->snake(),
'root_id' => $questions->where('answer', $answerForNonFollowingUser)->first()->id,
'parent_id' => Question::where('answer', '2nd nested child question for '.str($answerForNonFollowingUser)->snake())->first()->id,
]);

$component = Livewire::actingAs($authUser)->test(QuestionsFollowing::class);

// final output needs to be root without descendants divided odds and evens in descending order
// 1st child question for another following user question should be missing
// answers for non following user should be missing
// 3rd nested child question for auth user should be missing
$component->assertSeeInOrder([
$answerForAnotherFollowingUser,
'2nd nested child question for '.str($answerForAnotherFollowingUser)->snake(),
'3rd nested child question for '.str($answerForAnotherFollowingUser)->snake(),
'1st child question for '.str($answerForNonFollowingUser)->snake(),
'2nd nested child question for '.str($answerForNonFollowingUser)->snake(),
'root without descendants',
$answerForAuthUser,
'1st child question for '.str($answerForAuthUser)->snake(),
$answerForFollowingUser,
'1st child question for '.str($answerForFollowingUser)->snake(),
]);

$component->assertDontSee('1st child question for '.str($answerForAnotherFollowingUser)->snake());
$component->assertDontSee($answerForNonFollowingUser);
$component->assertDontSee('3rd nested child question for '.str($answerForAuthUser)->snake());
});