Skip to content

Commit

Permalink
feat: Add 'Mark Read' Functionality for Comments (#2122)
Browse files Browse the repository at this point in the history
* Adds reference buttons to all inline and overall comments to indicate unread status
* Updates tooltips on references based on comment type and read status
* Enables users to mark unread inline and overall comments as read by clicking/tapping on reference buttons
* Makes comment focus styling for "active comments" consistent with solid stroke lines

Closes #2071
  • Loading branch information
gmeben authored Jul 29, 2024
1 parent 2229315 commit 3b52088
Show file tree
Hide file tree
Showing 28 changed files with 796 additions and 135 deletions.
68 changes: 68 additions & 0 deletions backend/app/GraphQL/Mutations/CommentStatusMutator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
<?php
declare(strict_types=1);

namespace App\GraphQL\Mutations;

use App\Models\Submission;

final readonly class CommentStatusMutator
{
/**
* Validate supplied arguments and return the comments to be marked as read.
*
* @param string $type
* @param string $submission_id
* @param array{int} $comment_ids
* @return \App\GraphQL\Mutations\Collection<\App\GraphQL\Mutations\InlineComment|\App\GraphQL\Mutations\OverallComment>
*/
private function validateArgs($type, $submission_id, $comment_ids)
{
if (!$submission_id) {
throw new \Exception('Submission ID required');
}
if (empty($comment_ids)) {
throw new \Exception('Comment ID(s) required');
}
if ($type === 'inline') {
$comments = Submission::find($submission_id)->inlineCommentsWithReplies;
} else {
$comments = Submission::find($submission_id)->overallCommentsWithReplies;
}
$matchingComments = $comments->whereIn('id', $comment_ids);
if ($matchingComments->isEmpty()) {
throw new \Exception('Invalid comment ID');
}

return $matchingComments;
}

/**
* @param null $_
* @param array{} $args
* @return \App\GraphQL\Mutations\Collection<\App\GraphQL\Mutations\InlineComment>
*/
public function inlineRead(null $_, array $args)
{
$comments = $this->validateArgs('inline', $args['input']['submission_id'], $args['input']['comment_ids']);
$comments->map(function ($comment) {
$comment->markRead();
});

return $comments;
}

/**
* @param null $_
* @param array{} $args
* @return \App\GraphQL\Mutations\Collection<\App\GraphQL\Mutations\OverallComment>
*/
public function overallRead(null $_, array $args)
{
$comments = $this->validateArgs('overall', $args['input']['submission_id'], $args['input']['comment_ids']);
$comments->map(function ($comment) {
$comment->markRead();
});

return $comments;
}
}
15 changes: 15 additions & 0 deletions backend/app/Models/CommentStatus.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?php
declare(strict_types=1);

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class CommentStatus extends Model
{
protected $fillable = [
'comment_id',
'user_id',
'type',
];
}
3 changes: 3 additions & 0 deletions backend/app/Models/InlineComment.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
namespace App\Models;

use App\Http\Traits\CreatedUpdatedBy;
use App\Models\Traits\ReadStatus;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
Expand All @@ -15,6 +16,7 @@ class InlineComment extends BaseModel
use HasFactory;
use CreatedUpdatedBy;
use SoftDeletes;
use ReadStatus;

protected $casts = [
'style_criteria' => 'json',
Expand All @@ -30,6 +32,7 @@ class InlineComment extends BaseModel
'content',
'style_criteria',
'parent_id',
'read_at',
'reply_to_id',
'from',
'to',
Expand Down
2 changes: 2 additions & 0 deletions backend/app/Models/OverallComment.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
namespace App\Models;

use App\Http\Traits\CreatedUpdatedBy;
use App\Models\Traits\ReadStatus;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
Expand All @@ -15,6 +16,7 @@ class OverallComment extends BaseModel
use HasFactory;
use CreatedUpdatedBy;
use SoftDeletes;
use ReadStatus;

/**
* The attributes that are mass assignable.
Expand Down
20 changes: 20 additions & 0 deletions backend/app/Models/Submission.php
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,16 @@ public function inlineComments(): HasMany
return $this->hasMany(InlineComment::class)->whereNull('parent_id');
}

/**
* Inline comments and their replies that belong to the submission
*
* @return \Illuminate\Database\Eloquent\Relations\HasMany
*/
public function inlineCommentsWithReplies(): HasMany
{
return $this->hasMany(InlineComment::class);
}

/**
* Overall comments that belong to the submission
*
Expand All @@ -175,6 +185,16 @@ public function overallComments(): HasMany
return $this->hasMany(OverallComment::class)->whereNull('parent_id');
}

/**
* Overall comments and their replies that belong to the submission
*
* @return \Illuminate\Database\Eloquent\Relations\HasMany
*/
public function overallCommentsWithReplies(): HasMany
{
return $this->hasMany(OverallComment::class);
}

/**
* Invitations that belong to the submission
*
Expand Down
62 changes: 62 additions & 0 deletions backend/app/Models/Traits/ReadStatus.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
<?php
declare(strict_types=1);

namespace App\Models\Traits;

use App\Models\CommentStatus;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Relations\HasOne;

trait ReadStatus
{
/**
* Returns the associated CommentStatus record
*
* @return \Illuminate\Database\Eloquent\Relations\HasOne
*/
public function readStatus(): HasOne
{
return $this->hasOne(CommentStatus::class, 'comment_id')
->where('type', static::class)
->where('user_id', auth()->id());
}

/**
* Set a value for the read status of a comment.
*
* @return \Illuminate\Database\Eloquent\Casts\Attribute
*/
public function readAt(): Attribute
{
return Attribute::make(
get: function () {
return $this->readStatus ? $this->readStatus->created_at : null;
},
set: function () {
$this->markRead();
}
);
}

/**
* Create a CommentStatus for this
*
* @param \App\Models\Traits\User $user
* @return void
*/
public function markRead($user = null)
{
if (!$user) {
$user = auth()->user();
}
if (!$user) {
throw new \Exception('Unable to save read status. No user logged in.');
}

CommentStatus::firstOrCreate([
'comment_id' => $this->attributes['id'],
'user_id' => $user->id,
'type' => static::class,
]);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('comment_statuses', function (Blueprint $table) {
$table->id();
$table->foreignId('comment_id');
$table->foreignId('user_id')->constrained('users')->onDelete('cascade');
$table->string('type');
$table->timestamps();
});
}

/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('comment_statuses');
}
};
50 changes: 48 additions & 2 deletions backend/graphql/submission.comments.graphql
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
extend type Submission {
inline_comments: [InlineComment!]! @hasMany(relation: "inlineComments") @softDeletes
overall_comments: [OverallComment!]! @hasMany(relation: "overallComments") @softDeletes
inline_comments: [InlineComment!]!
@hasMany(relation: "inlineComments")
@softDeletes
overall_comments: [OverallComment!]!
@hasMany(relation: "overallComments")
@softDeletes
}

"""
Expand All @@ -16,6 +20,7 @@ type InlineComment implements Comment {
deleted_at: DateTimeUtc
replies: [InlineCommentReply!] @hasMany @softDeletes
style_criteria: [InlineCommentStyleCriteria!]
read_at: DateTimeUtc @rename(attribute: "readAt")
from: Int
to: Int
}
Expand All @@ -42,6 +47,7 @@ type InlineCommentReply implements Comment {
deleted_at: DateTimeUtc
parent_id: ID!
reply_to_id: ID!
read_at: DateTimeUtc @rename(attribute: "readAt")
}

"""
Expand All @@ -56,6 +62,7 @@ type OverallComment implements Comment {
updated_at: DateTimeUtc!
deleted_at: DateTimeUtc
replies: [OverallCommentReply!] @hasMany @softDeletes
read_at: DateTimeUtc @rename(attribute: "readAt")
}

"""
Expand All @@ -71,6 +78,7 @@ type OverallCommentReply implements Comment {
deleted_at: DateTimeUtc
parent_id: ID!
reply_to_id: ID!
read_at: DateTimeUtc @rename(attribute: "readAt")
}

extend input UpdateSubmissionInput {
Expand Down Expand Up @@ -138,4 +146,42 @@ interface Comment {
created_at: DateTimeUtc!
updated_at: DateTimeUtc!
deleted_at: DateTimeUtc
read_at: DateTimeUtc @rename(attribute: "readAt")
}

extend type Mutation @guard {
markInlineCommentsRead(
input: MarkInlineCommentsReadInput!
): [InlineComment]!
@field(
resolver: "App\\GraphQL\\Mutations\\CommentStatusMutator@inlineRead"
)
markInlineCommentRepliesRead(
input: MarkInlineCommentsReadInput!
): [InlineCommentReply]!
@field(
resolver: "App\\GraphQL\\Mutations\\CommentStatusMutator@inlineRead"
)
markOverallCommentsRead(
input: MarkOverallCommentsReadInput!
): [OverallComment]!
@field(
resolver: "App\\GraphQL\\Mutations\\CommentStatusMutator@overallRead"
)
markOverallCommentRepliesRead(
input: MarkOverallCommentsReadInput!
): [OverallCommentReply]!
@field(
resolver: "App\\GraphQL\\Mutations\\CommentStatusMutator@overallRead"
)
}

input MarkInlineCommentsReadInput {
submission_id: ID!
comment_ids: [ID!]!
}

input MarkOverallCommentsReadInput {
submission_id: ID!
comment_ids: [ID!]!
}
Loading

0 comments on commit 3b52088

Please sign in to comment.