Skip to content

Commit

Permalink
Merge pull request #653 from pinkary-project/feat/threads-in-following
Browse files Browse the repository at this point in the history
Feat: threads in following
  • Loading branch information
nunomaduro authored Sep 21, 2024
2 parents f98b4f9 + d7e3913 commit 39067e6
Show file tree
Hide file tree
Showing 6 changed files with 169 additions and 32 deletions.
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());
});

0 comments on commit 39067e6

Please sign in to comment.