Compare commits
36 Commits
v1.0.0-alp
...
1.x
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8e5d25686c | ||
|
|
bbba63c57b | ||
|
|
f9f9950cde | ||
|
|
bb88583f09 | ||
|
|
4b783e437f | ||
|
|
d189743a9c | ||
|
|
541d11ab90 | ||
|
|
48fbd3c9d7 | ||
|
|
e7daa25fc2 | ||
|
|
bff68f87a3 | ||
|
|
583b49125f | ||
|
|
5e44a4051a | ||
|
|
812556cba2 | ||
|
|
20dba18e8e | ||
|
|
a5bf29d6c2 | ||
|
|
6a26396f0d | ||
|
|
2ace8bfdd4 | ||
|
|
3d745077b7 | ||
|
|
b44b4e309e | ||
|
|
ac97dcb092 | ||
|
|
6c96fb900b | ||
|
|
7f9f13b626 | ||
|
|
e173d9b4dd | ||
|
|
f119095ae5 | ||
|
|
889dc2828b | ||
|
|
82eb6a70ad | ||
|
|
2edcfa00f1 | ||
|
|
35571760d6 | ||
|
|
a4d4418963 | ||
|
|
b2ee8a1036 | ||
|
|
fd5bc5271b | ||
|
|
43b66f60f3 | ||
|
|
0c13d589d8 | ||
|
|
2c7c44ecbc | ||
|
|
12470a1d8b | ||
|
|
42e95a83f5 |
1
.github/workflows/pint.yml
vendored
1
.github/workflows/pint.yml
vendored
@@ -2,6 +2,7 @@ name: Pint
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
|
branches: [1.x]
|
||||||
paths:
|
paths:
|
||||||
- '**.php'
|
- '**.php'
|
||||||
|
|
||||||
|
|||||||
15
CHANGELOG.md
15
CHANGELOG.md
@@ -4,3 +4,18 @@ All notable changes to this project will be documented in this file.
|
|||||||
|
|
||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## v1.0.0-alpha.4 - 2026-03-31
|
||||||
|
|
||||||
|
<!-- Release notes generated using configuration in .github/release.yml at 1.x -->
|
||||||
|
### What's Changed
|
||||||
|
|
||||||
|
#### Other Changes
|
||||||
|
|
||||||
|
* fix: show reply instantly after adding without page refresh by @Ilyapashayan20 in https://github.com/relaticle/comments/pull/10
|
||||||
|
|
||||||
|
### New Contributors
|
||||||
|
|
||||||
|
* @Ilyapashayan20 made their first contribution in https://github.com/relaticle/comments/pull/10
|
||||||
|
|
||||||
|
**Full Changelog**: https://github.com/relaticle/comments/compare/v1.0.0-alpha.3...v1.0.0-alpha.4
|
||||||
|
|||||||
24
README.md
24
README.md
@@ -61,12 +61,12 @@ class Project extends Model implements Commentable
|
|||||||
Add the commenter trait to your User model:
|
Add the commenter trait to your User model:
|
||||||
|
|
||||||
```php
|
```php
|
||||||
use Relaticle\Comments\Concerns\IsCommenter;
|
use Relaticle\Comments\Concerns\CanComment;
|
||||||
use Relaticle\Comments\Contracts\Commenter;
|
use Relaticle\Comments\Contracts\Commentator;
|
||||||
|
|
||||||
class User extends Authenticatable implements Commenter
|
class User extends Authenticatable implements Commentator
|
||||||
{
|
{
|
||||||
use IsCommenter;
|
use CanComment;
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -134,20 +134,20 @@ public static function infolist(Infolist $infolist): Infolist
|
|||||||
<tr>
|
<tr>
|
||||||
<td width="50%" valign="top">
|
<td width="50%" valign="top">
|
||||||
|
|
||||||
### Custom Fields
|
### FilaForms
|
||||||
[<img src="https://github.com/Relaticle/custom-fields/raw/2.x/art/preview.png" width="100%" />](https://relaticle.github.io/custom-fields)
|
[<img src="https://filaforms.app/img/og-image.png" width="100%" />](https://filaforms.app/)
|
||||||
|
|
||||||
Let users add custom fields to any model without code changes.
|
Visual form builder for all your public-facing forms.
|
||||||
[Learn more ->](https://relaticle.github.io/custom-fields)
|
[Learn more ->](https://filaforms.app)
|
||||||
|
|
||||||
</td>
|
</td>
|
||||||
<td width="50%" valign="top">
|
<td width="50%" valign="top">
|
||||||
|
|
||||||
### Flowforge
|
### Custom Fields
|
||||||
[<img src="https://github.com/Relaticle/flowforge/raw/4.x/art/preview.png" width="100%" />](https://relaticle.github.io/flowforge)
|
[<img src="https://github.com/Relaticle/custom-fields/raw/3.x/art/preview.png" width="100%" />](https://relaticle.github.io/custom-fields)
|
||||||
|
|
||||||
Transform any Laravel model into a drag-and-drop Kanban board.
|
Let users add custom fields to any model without code changes.
|
||||||
[Learn more ->](https://relaticle.github.io/flowforge)
|
[Learn more ->](https://relaticle.github.io/custom-fields)
|
||||||
|
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
@@ -1,19 +1,29 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use Relaticle\Comments\Comment;
|
|
||||||
use Relaticle\Comments\Mentions\DefaultMentionResolver;
|
use Relaticle\Comments\Mentions\DefaultMentionResolver;
|
||||||
|
use Relaticle\Comments\Models\Comment;
|
||||||
use Relaticle\Comments\Policies\CommentPolicy;
|
use Relaticle\Comments\Policies\CommentPolicy;
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'tables' => [
|
|
||||||
'comments' => 'comments',
|
|
||||||
],
|
|
||||||
|
|
||||||
'models' => [
|
'models' => [
|
||||||
'comment' => Comment::class,
|
'comment' => Comment::class,
|
||||||
],
|
],
|
||||||
|
|
||||||
|
'table_names' => [
|
||||||
|
'comments' => 'comments',
|
||||||
|
'reactions' => 'comment_reactions',
|
||||||
|
'mentions' => 'comment_mentions',
|
||||||
|
'subscriptions' => 'comment_subscriptions',
|
||||||
|
'attachments' => 'comment_attachments',
|
||||||
|
],
|
||||||
|
|
||||||
|
'column_names' => [
|
||||||
|
'commenter_morph' => 'commenter',
|
||||||
|
],
|
||||||
|
|
||||||
'commenter' => [
|
'commenter' => [
|
||||||
'model' => User::class,
|
'model' => User::class,
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
namespace Relaticle\Comments\Database\Factories;
|
namespace Relaticle\Comments\Database\Factories;
|
||||||
|
|
||||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||||
use Relaticle\Comments\Comment;
|
use Relaticle\Comments\Models\Comment;
|
||||||
|
|
||||||
class CommentFactory extends Factory
|
class CommentFactory extends Factory
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -8,10 +8,10 @@ return new class extends Migration
|
|||||||
{
|
{
|
||||||
public function up(): void
|
public function up(): void
|
||||||
{
|
{
|
||||||
Schema::create('comment_attachments', function (Blueprint $table) {
|
Schema::create(config('comments.table_names.attachments', 'comment_attachments'), function (Blueprint $table) {
|
||||||
$table->id();
|
$table->id();
|
||||||
$table->foreignId('comment_id')
|
$table->foreignId('comment_id')
|
||||||
->constrained(config('comments.tables.comments', 'comments'))
|
->constrained(config('comments.table_names.comments', 'comments'))
|
||||||
->cascadeOnDelete();
|
->cascadeOnDelete();
|
||||||
$table->string('file_path');
|
$table->string('file_path');
|
||||||
$table->string('original_name');
|
$table->string('original_name');
|
||||||
|
|||||||
@@ -8,15 +8,15 @@ return new class extends Migration
|
|||||||
{
|
{
|
||||||
public function up(): void
|
public function up(): void
|
||||||
{
|
{
|
||||||
Schema::create('comment_mentions', function (Blueprint $table) {
|
Schema::create(config('comments.table_names.mentions', 'comment_mentions'), function (Blueprint $table) {
|
||||||
$table->id();
|
$table->id();
|
||||||
$table->foreignId('comment_id')
|
$table->foreignId('comment_id')
|
||||||
->constrained(config('comments.tables.comments', 'comments'))
|
->constrained(config('comments.table_names.comments', 'comments'))
|
||||||
->cascadeOnDelete();
|
->cascadeOnDelete();
|
||||||
$table->morphs('user');
|
$table->morphs('commenter');
|
||||||
$table->timestamps();
|
$table->timestamps();
|
||||||
|
|
||||||
$table->unique(['comment_id', 'user_id', 'user_type']);
|
$table->unique(['comment_id', 'commenter_id', 'commenter_type']);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -8,16 +8,16 @@ return new class extends Migration
|
|||||||
{
|
{
|
||||||
public function up(): void
|
public function up(): void
|
||||||
{
|
{
|
||||||
Schema::create('comment_reactions', function (Blueprint $table) {
|
Schema::create(config('comments.table_names.reactions', 'comment_reactions'), function (Blueprint $table) {
|
||||||
$table->id();
|
$table->id();
|
||||||
$table->foreignId('comment_id')
|
$table->foreignId('comment_id')
|
||||||
->constrained(config('comments.tables.comments', 'comments'))
|
->constrained(config('comments.table_names.comments', 'comments'))
|
||||||
->cascadeOnDelete();
|
->cascadeOnDelete();
|
||||||
$table->morphs('user');
|
$table->morphs('commenter');
|
||||||
$table->string('reaction');
|
$table->string('reaction');
|
||||||
$table->timestamps();
|
$table->timestamps();
|
||||||
|
|
||||||
$table->unique(['comment_id', 'user_id', 'user_type', 'reaction']);
|
$table->unique(['comment_id', 'commenter_id', 'commenter_type', 'reaction']);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -8,13 +8,13 @@ return new class extends Migration
|
|||||||
{
|
{
|
||||||
public function up(): void
|
public function up(): void
|
||||||
{
|
{
|
||||||
Schema::create('comment_subscriptions', function (Blueprint $table) {
|
Schema::create(config('comments.table_names.subscriptions', 'comment_subscriptions'), function (Blueprint $table) {
|
||||||
$table->id();
|
$table->id();
|
||||||
$table->morphs('commentable');
|
$table->morphs('commentable');
|
||||||
$table->morphs('user');
|
$table->morphs('commenter');
|
||||||
$table->timestamp('created_at')->nullable();
|
$table->timestamp('created_at')->nullable();
|
||||||
|
|
||||||
$table->unique(['commentable_type', 'commentable_id', 'user_type', 'user_id'], 'comment_subscriptions_unique');
|
$table->unique(['commentable_type', 'commentable_id', 'commenter_type', 'commenter_id'], 'comment_subscriptions_unique');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -8,13 +8,13 @@ return new class extends Migration
|
|||||||
{
|
{
|
||||||
public function up(): void
|
public function up(): void
|
||||||
{
|
{
|
||||||
Schema::create(config('comments.tables.comments', 'comments'), function (Blueprint $table) {
|
Schema::create(config('comments.table_names.comments', 'comments'), function (Blueprint $table) {
|
||||||
$table->id();
|
$table->id();
|
||||||
$table->morphs('commentable');
|
$table->morphs('commentable');
|
||||||
$table->morphs('user');
|
$table->morphs('commenter');
|
||||||
$table->foreignId('parent_id')
|
$table->foreignId('parent_id')
|
||||||
->nullable()
|
->nullable()
|
||||||
->constrained(config('comments.tables.comments', 'comments'))
|
->constrained(config('comments.table_names.comments', 'comments'))
|
||||||
->cascadeOnDelete();
|
->cascadeOnDelete();
|
||||||
$table->text('body');
|
$table->text('body');
|
||||||
$table->timestamp('edited_at')->nullable();
|
$table->timestamp('edited_at')->nullable();
|
||||||
|
|||||||
103
docs/components/AppHeader.vue
Normal file
103
docs/components/AppHeader.vue
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { useDocusI18n } from '#imports'
|
||||||
|
|
||||||
|
const appConfig = useAppConfig()
|
||||||
|
const site = useSiteConfig()
|
||||||
|
|
||||||
|
const { localePath, isEnabled, locales } = useDocusI18n()
|
||||||
|
const { currentVersion, isOldVersion, loadVersions } = useVersions()
|
||||||
|
|
||||||
|
onMounted(() => loadVersions())
|
||||||
|
|
||||||
|
const links = computed(() => appConfig.github && appConfig.github.url
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
'icon': 'i-simple-icons-github',
|
||||||
|
'to': appConfig.github.url,
|
||||||
|
'target': '_blank',
|
||||||
|
'aria-label': 'GitHub',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: [])
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="sticky top-0 z-50">
|
||||||
|
<!-- Version Warning Banner -->
|
||||||
|
<div
|
||||||
|
v-if="isOldVersion"
|
||||||
|
class="bg-amber-100 dark:bg-amber-900/50 text-amber-800 dark:text-amber-200 px-4 py-2 text-center text-sm border-b border-amber-200 dark:border-amber-800"
|
||||||
|
>
|
||||||
|
You are viewing documentation for Comments {{ currentVersion }}.
|
||||||
|
<a
|
||||||
|
href="/comments/"
|
||||||
|
class="underline font-medium hover:text-amber-900 dark:hover:text-amber-100"
|
||||||
|
>
|
||||||
|
View the latest version →
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Original Docus Header -->
|
||||||
|
<UHeader
|
||||||
|
:ui="{ center: 'flex-1' }"
|
||||||
|
:to="localePath('/')"
|
||||||
|
:title="appConfig.header?.title || site.name"
|
||||||
|
>
|
||||||
|
<AppHeaderCenter />
|
||||||
|
|
||||||
|
<template #title>
|
||||||
|
<AppHeaderLogo class="h-6 w-auto shrink-0" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #right>
|
||||||
|
<AppVersionSwitcher />
|
||||||
|
<AppHeaderCTA />
|
||||||
|
|
||||||
|
<template v-if="isEnabled && locales.length > 1">
|
||||||
|
<ClientOnly>
|
||||||
|
<LanguageSelect />
|
||||||
|
|
||||||
|
<template #fallback>
|
||||||
|
<div class="h-8 w-8 animate-pulse bg-neutral-200 dark:bg-neutral-800 rounded-md" />
|
||||||
|
</template>
|
||||||
|
</ClientOnly>
|
||||||
|
|
||||||
|
<USeparator
|
||||||
|
orientation="vertical"
|
||||||
|
class="h-8"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<UContentSearchButton class="lg:hidden" />
|
||||||
|
|
||||||
|
<ClientOnly>
|
||||||
|
<UColorModeButton />
|
||||||
|
|
||||||
|
<template #fallback>
|
||||||
|
<div class="h-8 w-8 animate-pulse bg-neutral-200 dark:bg-neutral-800 rounded-md" />
|
||||||
|
</template>
|
||||||
|
</ClientOnly>
|
||||||
|
|
||||||
|
<template v-if="links?.length">
|
||||||
|
<UButton
|
||||||
|
v-for="(link, index) of links"
|
||||||
|
:key="index"
|
||||||
|
v-bind="{ color: 'neutral', variant: 'ghost', ...link }"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #toggle="{ open, toggle }">
|
||||||
|
<IconMenuToggle
|
||||||
|
:open="open"
|
||||||
|
class="lg:hidden"
|
||||||
|
@click="toggle"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #body>
|
||||||
|
<AppHeaderBody />
|
||||||
|
</template>
|
||||||
|
</UHeader>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
16
docs/components/AppHeaderLogo.vue
Normal file
16
docs/components/AppHeaderLogo.vue
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
const appConfig = useAppConfig()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<UColorModeImage
|
||||||
|
v-if="appConfig.docus?.header?.logo?.dark || appConfig.docus?.header?.logo?.light"
|
||||||
|
:light="appConfig.docus?.header?.logo?.light || appConfig.docus?.header?.logo?.dark"
|
||||||
|
:dark="appConfig.docus?.header?.logo?.dark || appConfig.docus?.header?.logo?.light"
|
||||||
|
:alt="appConfig.docus?.header?.logo?.alt || appConfig.docus?.title"
|
||||||
|
class="h-8 w-auto shrink-0"
|
||||||
|
/>
|
||||||
|
<span v-else class="text-lg font-semibold">
|
||||||
|
{{ appConfig.docus?.title || 'Comments' }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
38
docs/components/AppVersionSwitcher.vue
Normal file
38
docs/components/AppVersionSwitcher.vue
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
const { versions, currentVersion, currentTitle, loadVersions } = useVersions()
|
||||||
|
|
||||||
|
onMounted(() => loadVersions())
|
||||||
|
|
||||||
|
function switchVersion(version: { version: string; path: string }): void {
|
||||||
|
if (version.version !== currentVersion) {
|
||||||
|
window.location.href = version.path
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div v-if="versions.length > 1" class="relative" @click.stop>
|
||||||
|
<UPopover>
|
||||||
|
<UButton
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
:label="currentTitle"
|
||||||
|
trailing-icon="i-lucide-chevron-down"
|
||||||
|
/>
|
||||||
|
<template #content>
|
||||||
|
<div class="p-1">
|
||||||
|
<button
|
||||||
|
v-for="version in versions"
|
||||||
|
:key="version.version"
|
||||||
|
class="w-full px-3 py-2 text-left text-sm rounded hover:bg-gray-100 dark:hover:bg-gray-800 flex items-center gap-2"
|
||||||
|
:class="{ 'font-medium text-primary': version.version === currentVersion }"
|
||||||
|
@click="switchVersion(version)"
|
||||||
|
>
|
||||||
|
{{ version.title }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</UPopover>
|
||||||
|
</div>
|
||||||
|
<UBadge v-else-if="currentVersion" variant="subtle" color="neutral">{{ currentVersion }}</UBadge>
|
||||||
|
</template>
|
||||||
60
docs/composables/useVersions.ts
Normal file
60
docs/composables/useVersions.ts
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
interface Version {
|
||||||
|
version: string
|
||||||
|
title: string
|
||||||
|
path: string
|
||||||
|
branch: string
|
||||||
|
isLatest: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const versions = ref<Version[]>([])
|
||||||
|
const isLoaded = ref(false)
|
||||||
|
const isLoading = ref(false)
|
||||||
|
|
||||||
|
export function useVersions() {
|
||||||
|
const config = useRuntimeConfig()
|
||||||
|
const currentVersion = config.public.docsVersion || '1.x'
|
||||||
|
|
||||||
|
async function loadVersions() {
|
||||||
|
if (isLoaded.value || isLoading.value) return
|
||||||
|
isLoading.value = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch('/comments/versions.json')
|
||||||
|
if (res.ok) {
|
||||||
|
versions.value = await res.json()
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Failed to load versions.json:', e)
|
||||||
|
} finally {
|
||||||
|
isLoaded.value = true
|
||||||
|
isLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const latestVersion = computed(() =>
|
||||||
|
versions.value.find(v => v.isLatest)
|
||||||
|
)
|
||||||
|
|
||||||
|
const currentVersionInfo = computed(() =>
|
||||||
|
versions.value.find(v => v.version === currentVersion)
|
||||||
|
)
|
||||||
|
|
||||||
|
const isOldVersion = computed(() => {
|
||||||
|
if (!isLoaded.value) return false
|
||||||
|
return currentVersionInfo.value?.isLatest === false
|
||||||
|
})
|
||||||
|
|
||||||
|
const currentTitle = computed(() =>
|
||||||
|
currentVersionInfo.value?.title || currentVersion
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
versions,
|
||||||
|
currentVersion,
|
||||||
|
currentTitle,
|
||||||
|
latestVersion,
|
||||||
|
isOldVersion,
|
||||||
|
isLoaded,
|
||||||
|
loadVersions,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -73,15 +73,15 @@ class Project extends Model implements Commentable
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Add the `IsCommenter` trait to your User model:
|
Add the `CanComment` trait to your User model:
|
||||||
|
|
||||||
```php [app/Models/User.php]
|
```php [app/Models/User.php]
|
||||||
use Relaticle\Comments\Concerns\IsCommenter;
|
use Relaticle\Comments\Concerns\CanComment;
|
||||||
use Relaticle\Comments\Contracts\Commenter;
|
use Relaticle\Comments\Contracts\Commentator;
|
||||||
|
|
||||||
class User extends Authenticatable implements Commenter
|
class User extends Authenticatable implements Commentator
|
||||||
{
|
{
|
||||||
use IsCommenter;
|
use CanComment;
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
---
|
|
||||||
title: Introduction
|
|
||||||
description: A full-featured commenting system for Filament panels.
|
|
||||||
navigation:
|
|
||||||
icon: i-lucide-home
|
|
||||||
seo:
|
|
||||||
title: Introduction
|
|
||||||
description: Learn about Comments - a full-featured commenting system for Filament panels with threaded replies, @mentions, emoji reactions, and real-time updates.
|
|
||||||
ogImage: /preview.png
|
|
||||||
---
|
|
||||||
|
|
||||||
Welcome to **Comments**, a powerful Laravel package that adds a full-featured commenting system to any Filament panel.
|
|
||||||
|
|
||||||
## What is Comments?
|
|
||||||
|
|
||||||
Comments provides polymorphic commenting on any Eloquent model with deep Filament integration. Add threaded discussions, @mentions, emoji reactions, file attachments, and real-time notifications to your admin panel with minimal setup.
|
|
||||||
|
|
||||||
## Why Choose Comments?
|
|
||||||
|
|
||||||
::card-group
|
|
||||||
:::card
|
|
||||||
---
|
|
||||||
icon: i-lucide-messages-square
|
|
||||||
title: Threaded Discussions
|
|
||||||
---
|
|
||||||
Nested replies with configurable depth limits keep conversations organized and easy to follow.
|
|
||||||
:::
|
|
||||||
|
|
||||||
:::card
|
|
||||||
---
|
|
||||||
icon: i-lucide-clock
|
|
||||||
title: Quick Setup
|
|
||||||
---
|
|
||||||
Add traits to your models, register the plugin, and you have a working comment system in minutes.
|
|
||||||
:::
|
|
||||||
|
|
||||||
:::card
|
|
||||||
---
|
|
||||||
icon: i-lucide-puzzle
|
|
||||||
title: 3 Integration Patterns
|
|
||||||
---
|
|
||||||
Use as a slide-over action, table row action, or inline infolist entry - whatever fits your resource.
|
|
||||||
:::
|
|
||||||
|
|
||||||
:::card
|
|
||||||
---
|
|
||||||
icon: i-lucide-bell
|
|
||||||
title: Built-in Notifications
|
|
||||||
---
|
|
||||||
Database and mail notifications with subscription management and auto-subscribe for authors and mentioned users.
|
|
||||||
:::
|
|
||||||
::
|
|
||||||
@@ -15,21 +15,34 @@ php artisan vendor:publish --tag=comments-config
|
|||||||
|
|
||||||
This creates `config/comments.php` with all available options.
|
This creates `config/comments.php` with all available options.
|
||||||
|
|
||||||
## Table Name
|
## Table Names
|
||||||
|
|
||||||
```php
|
```php
|
||||||
'tables' => [
|
'table_names' => [
|
||||||
'comments' => 'comments',
|
'comments' => 'comments',
|
||||||
|
'reactions' => 'comment_reactions',
|
||||||
|
'mentions' => 'comment_mentions',
|
||||||
|
'subscriptions' => 'comment_subscriptions',
|
||||||
|
'attachments' => 'comment_attachments',
|
||||||
],
|
],
|
||||||
```
|
```
|
||||||
|
|
||||||
Change the table name if it conflicts with your application.
|
Change the table names if they conflict with your application.
|
||||||
|
|
||||||
|
## Column Names
|
||||||
|
|
||||||
|
```php
|
||||||
|
'column_names' => [
|
||||||
|
'commenter_id' => 'commenter_id',
|
||||||
|
'commenter_type' => 'commenter_type',
|
||||||
|
],
|
||||||
|
```
|
||||||
|
|
||||||
## Models
|
## Models
|
||||||
|
|
||||||
```php
|
```php
|
||||||
'models' => [
|
'models' => [
|
||||||
'comment' => \Relaticle\Comments\Comment::class,
|
'comment' => \Relaticle\Comments\Models\Comment::class,
|
||||||
],
|
],
|
||||||
|
|
||||||
'commenter' => [
|
'commenter' => [
|
||||||
@@ -178,10 +191,10 @@ When broadcasting is disabled, the Livewire component polls for new comments at
|
|||||||
Override how the authenticated user is resolved:
|
Override how the authenticated user is resolved:
|
||||||
|
|
||||||
```php
|
```php
|
||||||
use Relaticle\Comments\Config;
|
use Relaticle\Comments\CommentsConfig;
|
||||||
|
|
||||||
// In AppServiceProvider::boot()
|
// In AppServiceProvider::boot()
|
||||||
Config::resolveAuthenticatedUserUsing(function () {
|
CommentsConfig::resolveAuthenticatedUserUsing(function () {
|
||||||
return auth()->user();
|
return auth()->user();
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -26,34 +26,34 @@ Create your own policy to customize authorization:
|
|||||||
```php
|
```php
|
||||||
namespace App\Policies;
|
namespace App\Policies;
|
||||||
|
|
||||||
use Relaticle\Comments\Comment;
|
use Relaticle\Comments\Models\Comment;
|
||||||
use Relaticle\Comments\Contracts\Commenter;
|
use Relaticle\Comments\Contracts\Commentator;
|
||||||
|
|
||||||
class CustomCommentPolicy
|
class CustomCommentPolicy
|
||||||
{
|
{
|
||||||
public function viewAny(Commenter $user): bool
|
public function viewAny(Commentator $user): bool
|
||||||
{
|
{
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function create(Commenter $user): bool
|
public function create(Commentator $user): bool
|
||||||
{
|
{
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function update(Commenter $user, Comment $comment): bool
|
public function update(Commentator $user, Comment $comment): bool
|
||||||
{
|
{
|
||||||
return $comment->user_id === $user->getKey()
|
return $comment->commenter_id === $user->getKey()
|
||||||
&& $comment->user_type === $user->getMorphClass();
|
&& $comment->commenter_type === $user->getMorphClass();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function delete(Commenter $user, Comment $comment): bool
|
public function delete(Commentator $user, Comment $comment): bool
|
||||||
{
|
{
|
||||||
return $comment->user_id === $user->getKey()
|
return $comment->commenter_id === $user->getKey()
|
||||||
|| $user->hasRole('admin');
|
|| $user->hasRole('admin');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function reply(Commenter $user, Comment $comment): bool
|
public function reply(Commentator $user, Comment $comment): bool
|
||||||
{
|
{
|
||||||
return $comment->canReply();
|
return $comment->canReply();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,4 +48,4 @@ Keys are stored in the database. If you change a key, existing reactions with th
|
|||||||
|
|
||||||
## Storage
|
## Storage
|
||||||
|
|
||||||
Reactions are stored in the `comment_reactions` table with a unique constraint on `(comment_id, user_id, user_type, reaction)`, ensuring one reaction of each type per user per comment.
|
Reactions are stored in the `comment_reactions` table with a unique constraint on `(comment_id, commenter_id, commenter_type, reaction)`, ensuring one reaction of each type per user per comment.
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ When a comment is deleted, its attachments are cascade deleted from the database
|
|||||||
|
|
||||||
## Helper Methods
|
## Helper Methods
|
||||||
|
|
||||||
The `CommentAttachment` model provides:
|
The `Attachment` model (`Relaticle\Comments\Models\Attachment`) provides:
|
||||||
|
|
||||||
```php
|
```php
|
||||||
$attachment->isImage(); // Check if attachment is an image
|
$attachment->isImage(); // Check if attachment is an image
|
||||||
|
|||||||
@@ -61,17 +61,17 @@ Users can toggle their subscription using the subscribe/unsubscribe button in th
|
|||||||
### Programmatic Access
|
### Programmatic Access
|
||||||
|
|
||||||
```php
|
```php
|
||||||
use Relaticle\Comments\CommentSubscription;
|
use Relaticle\Comments\Models\Subscription;
|
||||||
|
|
||||||
// Check subscription status
|
// Check subscription status
|
||||||
CommentSubscription::isSubscribed($commentable, $user);
|
Subscription::isSubscribed($commentable, $user);
|
||||||
|
|
||||||
// Subscribe/unsubscribe
|
// Subscribe/unsubscribe
|
||||||
CommentSubscription::subscribe($commentable, $user);
|
Subscription::subscribe($commentable, $user);
|
||||||
CommentSubscription::unsubscribe($commentable, $user);
|
Subscription::unsubscribe($commentable, $user);
|
||||||
|
|
||||||
// Get all subscribers for a commentable
|
// Get all subscribers for a commentable
|
||||||
$subscribers = CommentSubscription::subscribersFor($commentable);
|
$subscribers = Subscription::subscribersFor($commentable);
|
||||||
```
|
```
|
||||||
|
|
||||||
## Events
|
## Events
|
||||||
|
|||||||
@@ -20,8 +20,8 @@ The main comments table with polymorphic relationships and threading support.
|
|||||||
| `id` | bigint | Primary key |
|
| `id` | bigint | Primary key |
|
||||||
| `commentable_type` | string | Polymorphic model type |
|
| `commentable_type` | string | Polymorphic model type |
|
||||||
| `commentable_id` | bigint | Polymorphic model ID |
|
| `commentable_id` | bigint | Polymorphic model ID |
|
||||||
| `user_type` | string | Commenter model type |
|
| `commenter_type` | string | Commenter model type |
|
||||||
| `user_id` | bigint | Commenter model ID |
|
| `commenter_id` | bigint | Commenter model ID |
|
||||||
| `parent_id` | bigint (nullable) | Parent comment for replies |
|
| `parent_id` | bigint (nullable) | Parent comment for replies |
|
||||||
| `body` | text | HTML comment content |
|
| `body` | text | HTML comment content |
|
||||||
| `edited_at` | timestamp (nullable) | When the comment was last edited |
|
| `edited_at` | timestamp (nullable) | When the comment was last edited |
|
||||||
@@ -39,12 +39,12 @@ Tracks emoji reactions per user per comment.
|
|||||||
|--------|------|-------------|
|
|--------|------|-------------|
|
||||||
| `id` | bigint | Primary key |
|
| `id` | bigint | Primary key |
|
||||||
| `comment_id` | bigint | Foreign key to comments |
|
| `comment_id` | bigint | Foreign key to comments |
|
||||||
| `user_type` | string | Reactor model type |
|
| `commenter_type` | string | Reactor model type |
|
||||||
| `user_id` | bigint | Reactor model ID |
|
| `commenter_id` | bigint | Reactor model ID |
|
||||||
| `reaction` | string | Reaction key (e.g., `thumbs_up`) |
|
| `reaction` | string | Reaction key (e.g., `thumbs_up`) |
|
||||||
| `created_at` | timestamp | |
|
| `created_at` | timestamp | |
|
||||||
|
|
||||||
**Unique constraint:** `(comment_id, user_id, user_type, reaction)`
|
**Unique constraint:** `(comment_id, commenter_id, commenter_type, reaction)`
|
||||||
|
|
||||||
### comment_mentions
|
### comment_mentions
|
||||||
|
|
||||||
@@ -54,11 +54,11 @@ Tracks @mentioned users per comment.
|
|||||||
|--------|------|-------------|
|
|--------|------|-------------|
|
||||||
| `id` | bigint | Primary key |
|
| `id` | bigint | Primary key |
|
||||||
| `comment_id` | bigint | Foreign key to comments |
|
| `comment_id` | bigint | Foreign key to comments |
|
||||||
| `user_type` | string | Mentioned user model type |
|
| `commenter_type` | string | Mentioned user model type |
|
||||||
| `user_id` | bigint | Mentioned user model ID |
|
| `commenter_id` | bigint | Mentioned user model ID |
|
||||||
| `created_at` | timestamp | |
|
| `created_at` | timestamp | |
|
||||||
|
|
||||||
**Unique constraint:** `(comment_id, user_id, user_type)`
|
**Unique constraint:** `(comment_id, commenter_id, commenter_type)`
|
||||||
|
|
||||||
### comment_subscriptions
|
### comment_subscriptions
|
||||||
|
|
||||||
@@ -69,11 +69,11 @@ Tracks which users are subscribed to comment threads on specific models.
|
|||||||
| `id` | bigint | Primary key |
|
| `id` | bigint | Primary key |
|
||||||
| `commentable_type` | string | Subscribed model type |
|
| `commentable_type` | string | Subscribed model type |
|
||||||
| `commentable_id` | bigint | Subscribed model ID |
|
| `commentable_id` | bigint | Subscribed model ID |
|
||||||
| `user_type` | string | Subscriber model type |
|
| `commenter_type` | string | Subscriber model type |
|
||||||
| `user_id` | bigint | Subscriber model ID |
|
| `commenter_id` | bigint | Subscriber model ID |
|
||||||
| `created_at` | timestamp | |
|
| `created_at` | timestamp | |
|
||||||
|
|
||||||
**Unique constraint:** `(commentable_type, commentable_id, user_type, user_id)`
|
**Unique constraint:** `(commentable_type, commentable_id, commenter_type, commenter_id)`
|
||||||
|
|
||||||
### comment_attachments
|
### comment_attachments
|
||||||
|
|
||||||
@@ -96,11 +96,11 @@ Stores file attachment metadata for comments.
|
|||||||
```
|
```
|
||||||
Commentable Model (e.g., Project)
|
Commentable Model (e.g., Project)
|
||||||
└── comments (morphMany)
|
└── comments (morphMany)
|
||||||
├── user (morphTo → User)
|
├── commenter (morphTo → User)
|
||||||
├── parent (belongsTo → Comment)
|
├── parent (belongsTo → Comment)
|
||||||
├── replies (hasMany → Comment)
|
├── replies (hasMany → Comment)
|
||||||
├── reactions (hasMany → CommentReaction)
|
├── reactions (hasMany → Reaction)
|
||||||
├── attachments (hasMany → CommentAttachment)
|
├── attachments (hasMany → Attachment)
|
||||||
└── mentions (morphToMany → User)
|
└── mentions (morphToMany → User)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,10 @@ A full-featured commenting system for Filament panels with threaded replies, @me
|
|||||||
|
|
||||||
Drop-in integration with any Filament resource.
|
Drop-in integration with any Filament resource.
|
||||||
|
|
||||||
|
:::callout{icon="i-lucide-triangle-alert" color="amber"}
|
||||||
|
**Alpha Software** — Breaking changes may occur between releases. Not recommended for production use.
|
||||||
|
:::
|
||||||
|
|
||||||
#links
|
#links
|
||||||
:::u-button
|
:::u-button
|
||||||
---
|
---
|
||||||
@@ -37,6 +41,12 @@ Drop-in integration with any Filament resource.
|
|||||||
:::
|
:::
|
||||||
::
|
::
|
||||||
|
|
||||||
|
<div class="text-center max-w-5xl mx-auto">
|
||||||
|
<div class="aspect-video rounded-lg shadow-lg overflow-hidden">
|
||||||
|
<img src="/preview.png" alt="Comments - threaded discussions in Filament" class="w-full h-full object-cover object-top" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
::u-page-section
|
::u-page-section
|
||||||
#title
|
#title
|
||||||
Why choose Comments?
|
Why choose Comments?
|
||||||
@@ -125,6 +135,8 @@ Extend your Laravel applications with our ecosystem of complementary tools
|
|||||||
to: https://filaforms.app
|
to: https://filaforms.app
|
||||||
target: _blank
|
target: _blank
|
||||||
---
|
---
|
||||||
|
:img{src="https://filaforms.app/img/og-image.png" alt="FilaForms" class="mb-4 rounded-lg w-full pointer-events-none"}
|
||||||
|
|
||||||
Visual form builder for all your public-facing forms.
|
Visual form builder for all your public-facing forms.
|
||||||
:::
|
:::
|
||||||
|
|
||||||
@@ -135,17 +147,9 @@ Extend your Laravel applications with our ecosystem of complementary tools
|
|||||||
to: https://relaticle.github.io/custom-fields
|
to: https://relaticle.github.io/custom-fields
|
||||||
target: _blank
|
target: _blank
|
||||||
---
|
---
|
||||||
Let users add custom fields to any model without code changes.
|
:img{src="https://relaticle.github.io/custom-fields/og-image.png" alt="Custom Fields" class="mb-4 rounded-lg w-full pointer-events-none"}
|
||||||
:::
|
|
||||||
|
|
||||||
:::card
|
Let users add custom fields to any model without code changes.
|
||||||
---
|
|
||||||
title: Flowforge
|
|
||||||
icon: i-lucide-kanban
|
|
||||||
to: https://relaticle.github.io/flowforge
|
|
||||||
target: _blank
|
|
||||||
---
|
|
||||||
Transform any Laravel model into a drag-and-drop Kanban board.
|
|
||||||
:::
|
:::
|
||||||
::
|
::
|
||||||
::
|
::
|
||||||
|
|||||||
13
docs/public/logo-dark.svg
Normal file
13
docs/public/logo-dark.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 5.9 KiB |
13
docs/public/logo-light.svg
Normal file
13
docs/public/logo-light.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 5.9 KiB |
BIN
docs/public/preview.png
Normal file
BIN
docs/public/preview.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 330 KiB |
@@ -31,12 +31,12 @@ class Project extends Model implements Commentable
|
|||||||
```
|
```
|
||||||
|
|
||||||
```php
|
```php
|
||||||
use Relaticle\Comments\Concerns\IsCommenter;
|
use Relaticle\Comments\Concerns\CanComment;
|
||||||
use Relaticle\Comments\Contracts\Commenter;
|
use Relaticle\Comments\Contracts\Commentator;
|
||||||
|
|
||||||
class User extends Authenticatable implements Commenter
|
class User extends Authenticatable implements Commentator
|
||||||
{
|
{
|
||||||
use IsCommenter;
|
use CanComment;
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -113,7 +113,7 @@ Publish config: `php artisan vendor:publish --tag=comments-config`
|
|||||||
|
|
||||||
| Key | Default | Purpose |
|
| Key | Default | Purpose |
|
||||||
|-----|---------|---------|
|
|-----|---------|---------|
|
||||||
| `tables.comments` | `'comments'` | Main comments table name |
|
| `table_names.comments` | `'comments'` | Main comments table name |
|
||||||
| `models.comment` | `Comment::class` | Comment model class |
|
| `models.comment` | `Comment::class` | Comment model class |
|
||||||
| `commenter.model` | `User::class` | Commenter (user) model class |
|
| `commenter.model` | `User::class` | Commenter (user) model class |
|
||||||
| `policy` | `CommentPolicy::class` | Authorization policy class |
|
| `policy` | `CommentPolicy::class` | Authorization policy class |
|
||||||
@@ -183,14 +183,14 @@ Default `CommentPolicy` methods:
|
|||||||
```php
|
```php
|
||||||
namespace App\Policies;
|
namespace App\Policies;
|
||||||
|
|
||||||
use Relaticle\Comments\Comment;
|
use Relaticle\Comments\Models\Comment;
|
||||||
use Relaticle\Comments\Contracts\Commenter;
|
use Relaticle\Comments\Contracts\Commentator;
|
||||||
|
|
||||||
class CustomCommentPolicy
|
class CustomCommentPolicy
|
||||||
{
|
{
|
||||||
public function delete(Commenter $user, Comment $comment): bool
|
public function delete(Commentator $user, Comment $comment): bool
|
||||||
{
|
{
|
||||||
return $comment->user_id === $user->getKey()
|
return $comment->commenter_id === $user->getKey()
|
||||||
|| $user->hasRole('admin');
|
|| $user->hasRole('admin');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -201,10 +201,10 @@ class CustomCommentPolicy
|
|||||||
### Scoped Comments (Multi-tenancy)
|
### Scoped Comments (Multi-tenancy)
|
||||||
|
|
||||||
```php
|
```php
|
||||||
use Relaticle\Comments\Config;
|
use Relaticle\Comments\CommentsConfig;
|
||||||
|
|
||||||
// In AppServiceProvider::boot()
|
// In AppServiceProvider::boot()
|
||||||
Config::resolveAuthenticatedUserUsing(function () {
|
CommentsConfig::resolveAuthenticatedUserUsing(function () {
|
||||||
return auth()->user();
|
return auth()->user();
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
@@ -276,7 +276,7 @@ $model->commentCount(); // Total count
|
|||||||
|
|
||||||
// On Comment model
|
// On Comment model
|
||||||
$comment->commentable(); // Parent model (morphTo)
|
$comment->commentable(); // Parent model (morphTo)
|
||||||
$comment->user(); // Commenter (morphTo)
|
$comment->commenter(); // Commenter (morphTo)
|
||||||
$comment->parent(); // Parent comment (belongsTo)
|
$comment->parent(); // Parent comment (belongsTo)
|
||||||
$comment->replies(); // Child comments (hasMany)
|
$comment->replies(); // Child comments (hasMany)
|
||||||
$comment->reactions(); // Reactions (hasMany)
|
$comment->reactions(); // Reactions (hasMany)
|
||||||
|
|||||||
18
resources/css/comments.css
Normal file
18
resources/css/comments.css
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
.comment-mention {
|
||||||
|
background-color: rgb(var(--primary-50));
|
||||||
|
color: rgb(var(--primary-600));
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 0;
|
||||||
|
display: inline-block;
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
padding-left: 0.25rem;
|
||||||
|
padding-right: 0.25rem;
|
||||||
|
font-weight: 500;
|
||||||
|
white-space: nowrap;
|
||||||
|
transition: background-color 0.15s ease, color 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .comment-mention {
|
||||||
|
background-color: rgb(var(--primary-400) / 0.1);
|
||||||
|
color: rgb(var(--primary-400));
|
||||||
|
}
|
||||||
@@ -1,9 +1,58 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'deleted_comment' => 'This comment was deleted.',
|
'comments' => [
|
||||||
|
'deleted' => 'This comment was deleted.',
|
||||||
'edited' => 'edited',
|
'edited' => 'edited',
|
||||||
'load_more' => 'Load more comments',
|
|
||||||
'no_comments' => 'No comments yet.',
|
'no_comments' => 'No comments yet.',
|
||||||
'comment_placeholder' => 'Write a comment...',
|
'placeholder' => 'Write a comment...',
|
||||||
|
'load_more' => 'Load more comments',
|
||||||
|
'sort_newest' => 'Newest first',
|
||||||
|
'sort_oldest' => 'Oldest first',
|
||||||
|
],
|
||||||
|
|
||||||
|
'actions' => [
|
||||||
|
'reply' => 'Reply',
|
||||||
|
'edit' => 'Edit',
|
||||||
|
'delete' => 'Delete',
|
||||||
|
'cancel' => 'Cancel',
|
||||||
|
'save' => 'Save',
|
||||||
|
'submit' => 'Submit',
|
||||||
|
],
|
||||||
|
|
||||||
|
'reactions' => [
|
||||||
|
'thumbs_up' => 'Thumbs up',
|
||||||
|
'heart' => 'Heart',
|
||||||
|
'celebrate' => 'Celebrate',
|
||||||
|
'laugh' => 'Laugh',
|
||||||
|
'thinking' => 'Thinking',
|
||||||
|
'sad' => 'Sad',
|
||||||
|
'reacted_by' => ':names reacted with :reaction',
|
||||||
|
'and_others' => 'and :count others',
|
||||||
|
],
|
||||||
|
|
||||||
|
'subscriptions' => [
|
||||||
|
'subscribe' => 'Subscribe to replies',
|
||||||
|
'unsubscribe' => 'Unsubscribe from replies',
|
||||||
|
'subscribed' => 'You will be notified of new replies.',
|
||||||
|
'unsubscribed' => 'You will no longer be notified.',
|
||||||
|
],
|
||||||
|
|
||||||
|
'mentions' => [
|
||||||
|
'no_results' => 'No users found',
|
||||||
|
],
|
||||||
|
|
||||||
|
'attachments' => [
|
||||||
|
'add' => 'Add attachment',
|
||||||
|
'remove' => 'Remove',
|
||||||
|
'too_large' => 'File is too large. Maximum size: :max KB.',
|
||||||
|
'invalid_type' => 'File type not allowed.',
|
||||||
|
],
|
||||||
|
|
||||||
|
'notifications' => [
|
||||||
|
'reply_subject' => 'New reply to your comment',
|
||||||
|
'reply_body' => ':name replied to your comment.',
|
||||||
|
'mention_subject' => 'You were mentioned in a comment',
|
||||||
|
'mention_body' => ':name mentioned you in a comment.',
|
||||||
|
],
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -3,11 +3,11 @@
|
|||||||
<div class="shrink-0">
|
<div class="shrink-0">
|
||||||
@if ($comment->trashed())
|
@if ($comment->trashed())
|
||||||
<div class="h-8 w-8 rounded-full bg-gray-200 dark:bg-gray-700"></div>
|
<div class="h-8 w-8 rounded-full bg-gray-200 dark:bg-gray-700"></div>
|
||||||
@elseif ($comment->user?->getCommentAvatarUrl())
|
@elseif ($comment->commenter?->getCommentAvatarUrl())
|
||||||
<img src="{{ $comment->user->getCommentAvatarUrl() }}" alt="{{ $comment->user->getCommentName() }}" class="h-8 w-8 rounded-full object-cover">
|
<img src="{{ $comment->commenter->getCommentAvatarUrl() }}" alt="{{ $comment->commenter->getCommentDisplayName() }}" class="h-8 w-8 rounded-full object-cover">
|
||||||
@else
|
@else
|
||||||
<div class="flex h-8 w-8 items-center justify-center rounded-full bg-primary-100 text-sm font-medium text-primary-700 dark:bg-primary-800 dark:text-primary-300">
|
<div class="flex h-8 w-8 items-center justify-center rounded-full bg-primary-100 text-sm font-medium text-primary-700 dark:bg-primary-800 dark:text-primary-300">
|
||||||
{{ str($comment->user?->getCommentName() ?? '?')->substr(0, 1)->upper() }}
|
{{ str($comment->commenter?->getCommentDisplayName() ?? '?')->substr(0, 1)->upper() }}
|
||||||
</div>
|
</div>
|
||||||
@endif
|
@endif
|
||||||
</div>
|
</div>
|
||||||
@@ -20,7 +20,7 @@
|
|||||||
{{-- Header: name + timestamp --}}
|
{{-- Header: name + timestamp --}}
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<span class="text-sm font-medium text-gray-900 dark:text-gray-100">
|
<span class="text-sm font-medium text-gray-900 dark:text-gray-100">
|
||||||
{{ $comment->user?->getCommentName() ?? 'Unknown' }}
|
{{ $comment->commenter?->getCommentDisplayName() ?? 'Unknown' }}
|
||||||
</span>
|
</span>
|
||||||
<span class="text-xs text-gray-500 dark:text-gray-400" title="{{ $comment->created_at->format('M j, Y g:i A') }}">
|
<span class="text-xs text-gray-500 dark:text-gray-400" title="{{ $comment->created_at->format('M j, Y g:i A') }}">
|
||||||
{{ $comment->created_at->diffForHumans() }}
|
{{ $comment->created_at->diffForHumans() }}
|
||||||
@@ -32,18 +32,13 @@
|
|||||||
|
|
||||||
{{-- Body or edit form --}}
|
{{-- Body or edit form --}}
|
||||||
@if ($isEditing)
|
@if ($isEditing)
|
||||||
<form wire:submit="saveEdit" class="mt-1">
|
<div class="mt-1">
|
||||||
<textarea wire:model="editBody" rows="3"
|
{{ $this->editForm }}
|
||||||
class="block w-full rounded-lg border-gray-300 shadow-sm focus:border-primary-500 focus:ring-primary-500 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-300 sm:text-sm"
|
<div class="mt-2 flex items-center justify-between">
|
||||||
></textarea>
|
<button type="button" wire:click="cancelEdit" class="text-xs text-gray-500 hover:text-gray-700 dark:text-gray-400">Cancel</button>
|
||||||
@error('editBody')
|
<button type="button" wire:click="saveEdit" class="text-sm font-medium text-primary-600 hover:text-primary-500 dark:text-primary-400">Save</button>
|
||||||
<p class="mt-1 text-sm text-danger-600 dark:text-danger-400">{{ $message }}</p>
|
</div>
|
||||||
@enderror
|
|
||||||
<div class="mt-2 flex gap-2">
|
|
||||||
<button type="submit" class="text-sm font-medium text-primary-600 hover:text-primary-500 dark:text-primary-400">Save</button>
|
|
||||||
<button type="button" wire:click="cancelEdit" class="text-sm text-gray-500 hover:text-gray-700 dark:text-gray-400">Cancel</button>
|
|
||||||
</div>
|
</div>
|
||||||
</form>
|
|
||||||
@else
|
@else
|
||||||
<div class="fi-prose prose prose-sm mt-1 max-w-none text-gray-700 dark:prose-invert dark:text-gray-300">
|
<div class="fi-prose prose prose-sm mt-1 max-w-none text-gray-700 dark:prose-invert dark:text-gray-300">
|
||||||
{!! $comment->renderBodyWithMentions() !!}
|
{!! $comment->renderBodyWithMentions() !!}
|
||||||
@@ -109,107 +104,12 @@
|
|||||||
|
|
||||||
{{-- Reply form --}}
|
{{-- Reply form --}}
|
||||||
@if ($isReplying)
|
@if ($isReplying)
|
||||||
<form wire:submit="addReply" class="relative mt-3"
|
<div class="mt-3"
|
||||||
x-data="{
|
x-data="{ uploadError: null }"
|
||||||
showMentions: false,
|
x-on:livewire-upload-error.window="uploadError = '{{ __('File upload failed. The file may be too large or an unsupported type.') }}'"
|
||||||
mentionQuery: '',
|
x-on:livewire-upload-start.window="uploadError = null"
|
||||||
mentionResults: [],
|
>
|
||||||
selectedIndex: 0,
|
{{ $this->replyForm }}
|
||||||
mentionStart: null,
|
|
||||||
async handleInput(event) {
|
|
||||||
const textarea = event.target;
|
|
||||||
const value = textarea.value;
|
|
||||||
const cursorPos = textarea.selectionStart;
|
|
||||||
const textBeforeCursor = value.substring(0, cursorPos);
|
|
||||||
const atIndex = textBeforeCursor.lastIndexOf('@');
|
|
||||||
if (atIndex !== -1 && (atIndex === 0 || textBeforeCursor[atIndex - 1] === ' ' || textBeforeCursor[atIndex - 1] === '\n')) {
|
|
||||||
const query = textBeforeCursor.substring(atIndex + 1);
|
|
||||||
if (query.length > 0 && !query.includes(' ')) {
|
|
||||||
this.mentionStart = atIndex;
|
|
||||||
this.mentionQuery = query;
|
|
||||||
this.mentionResults = await $wire.searchUsers(query);
|
|
||||||
this.showMentions = this.mentionResults.length > 0;
|
|
||||||
this.selectedIndex = 0;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.showMentions = false;
|
|
||||||
},
|
|
||||||
handleKeydown(event) {
|
|
||||||
if (!this.showMentions) return;
|
|
||||||
if (event.key === 'ArrowDown') {
|
|
||||||
event.preventDefault();
|
|
||||||
this.selectedIndex = Math.min(this.selectedIndex + 1, this.mentionResults.length - 1);
|
|
||||||
} else if (event.key === 'ArrowUp') {
|
|
||||||
event.preventDefault();
|
|
||||||
this.selectedIndex = Math.max(this.selectedIndex - 1, 0);
|
|
||||||
} else if (event.key === 'Enter' || event.key === 'Tab') {
|
|
||||||
if (this.mentionResults.length > 0) {
|
|
||||||
event.preventDefault();
|
|
||||||
this.selectMention(this.mentionResults[this.selectedIndex]);
|
|
||||||
}
|
|
||||||
} else if (event.key === 'Escape') {
|
|
||||||
this.showMentions = false;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
selectMention(user) {
|
|
||||||
const textarea = this.$refs.replyInput;
|
|
||||||
const value = textarea.value;
|
|
||||||
const before = value.substring(0, this.mentionStart);
|
|
||||||
const after = value.substring(textarea.selectionStart);
|
|
||||||
const newValue = before + '@' + user.name + ' ' + after;
|
|
||||||
$wire.set('replyBody', newValue);
|
|
||||||
this.showMentions = false;
|
|
||||||
this.$nextTick(() => {
|
|
||||||
const pos = before.length + user.name.length + 2;
|
|
||||||
textarea.focus();
|
|
||||||
textarea.setSelectionRange(pos, pos);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}">
|
|
||||||
<textarea x-ref="replyInput"
|
|
||||||
wire:model="replyBody"
|
|
||||||
@input="handleInput($event)"
|
|
||||||
@keydown="handleKeydown($event)"
|
|
||||||
rows="2"
|
|
||||||
placeholder="Write a reply..."
|
|
||||||
class="block w-full rounded-lg border-gray-300 shadow-sm focus:border-primary-500 focus:ring-primary-500 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-300 dark:placeholder-gray-400 sm:text-sm"
|
|
||||||
></textarea>
|
|
||||||
|
|
||||||
{{-- Mention autocomplete dropdown --}}
|
|
||||||
<div x-show="showMentions" x-cloak
|
|
||||||
class="absolute z-50 mt-1 w-64 rounded-lg border border-gray-200 bg-white shadow-lg dark:border-gray-600 dark:bg-gray-800">
|
|
||||||
<template x-for="(user, index) in mentionResults" :key="user.id">
|
|
||||||
<button type="button"
|
|
||||||
@click="selectMention(user)"
|
|
||||||
:class="{ 'bg-primary-50 dark:bg-primary-900/20': index === selectedIndex }"
|
|
||||||
class="flex w-full items-center gap-2 px-3 py-2 text-left text-sm hover:bg-gray-50 dark:hover:bg-gray-700">
|
|
||||||
<template x-if="user.avatar_url">
|
|
||||||
<img :src="user.avatar_url" class="h-6 w-6 rounded-full object-cover" />
|
|
||||||
</template>
|
|
||||||
<template x-if="!user.avatar_url">
|
|
||||||
<div class="flex h-6 w-6 items-center justify-center rounded-full bg-primary-100 text-xs font-medium text-primary-700 dark:bg-primary-800 dark:text-primary-300"
|
|
||||||
x-text="user.name.charAt(0).toUpperCase()"></div>
|
|
||||||
</template>
|
|
||||||
<span x-text="user.name" class="text-gray-900 dark:text-gray-100"></span>
|
|
||||||
</button>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
@error('replyBody')
|
|
||||||
<p class="mt-1 text-sm text-danger-600 dark:text-danger-400">{{ $message }}</p>
|
|
||||||
@enderror
|
|
||||||
|
|
||||||
@if (\Relaticle\Comments\Config::areAttachmentsEnabled())
|
|
||||||
<div class="mt-2">
|
|
||||||
<label class="flex cursor-pointer items-center gap-2 text-xs text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200">
|
|
||||||
<svg class="h-4 w-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="m18.375 12.739-7.693 7.693a4.5 4.5 0 0 1-6.364-6.364l10.94-10.94A3 3 0 1 1 19.5 7.372L8.552 18.32m.009-.01-.01.01m5.699-9.941-7.81 7.81a1.5 1.5 0 0 0 2.112 2.13" />
|
|
||||||
</svg>
|
|
||||||
Attach files
|
|
||||||
<input type="file" wire:model="replyAttachments" multiple class="hidden" accept="{{ implode(',', \Relaticle\Comments\Config::getAttachmentAllowedTypes()) }}" />
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
@if (!empty($replyAttachments))
|
@if (!empty($replyAttachments))
|
||||||
<div class="mt-2 flex flex-wrap gap-2">
|
<div class="mt-2 flex flex-wrap gap-2">
|
||||||
@@ -220,22 +120,33 @@
|
|||||||
</div>
|
</div>
|
||||||
@endforeach
|
@endforeach
|
||||||
</div>
|
</div>
|
||||||
@endif
|
|
||||||
|
|
||||||
@error('replyAttachments.*')
|
@error('replyAttachments.*')
|
||||||
<p class="mt-1 text-sm text-danger-600 dark:text-danger-400">{{ $message }}</p>
|
<p class="mt-1 text-sm text-danger-600 dark:text-danger-400">{{ $message }}</p>
|
||||||
@enderror
|
@enderror
|
||||||
|
<p x-show="uploadError" x-text="uploadError" class="mt-1 text-sm text-danger-600 dark:text-danger-400"></p>
|
||||||
@endif
|
@endif
|
||||||
|
|
||||||
<div class="mt-2 flex gap-2">
|
<div class="mt-2 flex items-center justify-between">
|
||||||
<button type="submit" class="text-sm font-medium text-primary-600 hover:text-primary-500 dark:text-primary-400">Reply</button>
|
<div class="flex items-center gap-3">
|
||||||
<button type="button" wire:click="cancelReply" class="text-sm text-gray-500 hover:text-gray-700 dark:text-gray-400">Cancel</button>
|
@if (\Relaticle\Comments\CommentsConfig::areAttachmentsEnabled())
|
||||||
|
<label class="flex cursor-pointer items-center gap-1.5 text-xs text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200">
|
||||||
|
<svg class="h-4 w-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="m18.375 12.739-7.693 7.693a4.5 4.5 0 0 1-6.364-6.364l10.94-10.94A3 3 0 1 1 19.5 7.372L8.552 18.32m.009-.01-.01.01m5.699-9.941-7.81 7.81a1.5 1.5 0 0 0 2.112 2.13" />
|
||||||
|
</svg>
|
||||||
|
Attach
|
||||||
|
<input type="file" wire:model="replyAttachments" multiple class="hidden" accept="{{ implode(',', \Relaticle\Comments\CommentsConfig::getAttachmentAllowedTypes()) }}" />
|
||||||
|
</label>
|
||||||
|
@endif
|
||||||
|
<button type="button" wire:click="cancelReply" class="text-xs text-gray-500 hover:text-gray-700 dark:text-gray-400">Cancel</button>
|
||||||
|
</div>
|
||||||
|
<button type="button" wire:click="addReply" class="text-sm font-medium text-primary-600 hover:text-primary-500 dark:text-primary-400">Reply</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
|
||||||
@endif
|
@endif
|
||||||
|
|
||||||
{{-- Nested replies --}}
|
{{-- Nested replies --}}
|
||||||
@if ($comment->replies->isNotEmpty())
|
@if ($comment->relationLoaded('replies') && $comment->replies->isNotEmpty())
|
||||||
<div class="mt-3 space-y-3 border-l-2 border-gray-200 pl-4 dark:border-gray-700">
|
<div class="mt-3 space-y-3 border-l-2 border-gray-200 pl-4 dark:border-gray-700">
|
||||||
@foreach ($comment->replies as $reply)
|
@foreach ($comment->replies as $reply)
|
||||||
<livewire:comment-item :comment="$reply" :key="'comment-'.$reply->id" />
|
<livewire:comment-item :comment="$reply" :key="'comment-'.$reply->id" />
|
||||||
|
|||||||
@@ -1,12 +1,15 @@
|
|||||||
<div class="space-y-4"
|
<div class="space-y-4"
|
||||||
@if (!\Relaticle\Comments\Config::isBroadcastingEnabled())
|
@if (!\Relaticle\Comments\CommentsConfig::isBroadcastingEnabled())
|
||||||
wire:poll.{{ \Relaticle\Comments\Config::getPollingInterval() }}
|
wire:poll.{{ \Relaticle\Comments\CommentsConfig::getPollingInterval() }}
|
||||||
@endif
|
@endif
|
||||||
|
x-data="{ uploadError: null }"
|
||||||
|
x-on:livewire-upload-error.window="uploadError = '{{ __('File upload failed. The file may be too large or an unsupported type.') }}'"
|
||||||
|
x-on:livewire-upload-start.window="uploadError = null"
|
||||||
>
|
>
|
||||||
{{-- Sort toggle --}}
|
{{-- Sort toggle --}}
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<h3 class="text-sm font-medium text-gray-700 dark:text-gray-300">
|
<h3 class="text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||||
Comments ({{ $this->totalCount }})
|
Comments ({{ $this->allCommentsCount }})
|
||||||
</h3>
|
</h3>
|
||||||
@auth
|
@auth
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
@@ -57,111 +60,11 @@
|
|||||||
</div>
|
</div>
|
||||||
@endif
|
@endif
|
||||||
|
|
||||||
{{-- New comment form - only for authorized users --}}
|
{{-- New comment form - sticky at bottom of slide-over --}}
|
||||||
@auth
|
@auth
|
||||||
@can('create', \Relaticle\Comments\Config::getCommentModel())
|
@can('create', \Relaticle\Comments\CommentsConfig::getCommentModel())
|
||||||
<form wire:submit="addComment" class="relative mt-4"
|
<div class="sticky bottom-0 z-10 -mx-4 -mb-4 border-t border-gray-200 bg-white px-4 pb-4 pt-3 dark:border-gray-700 dark:bg-gray-900">
|
||||||
x-data="{
|
{{ $this->commentForm }}
|
||||||
showMentions: false,
|
|
||||||
mentionQuery: '',
|
|
||||||
mentionResults: [],
|
|
||||||
selectedIndex: 0,
|
|
||||||
mentionStart: null,
|
|
||||||
async handleInput(event) {
|
|
||||||
const textarea = event.target;
|
|
||||||
const value = textarea.value;
|
|
||||||
const cursorPos = textarea.selectionStart;
|
|
||||||
const textBeforeCursor = value.substring(0, cursorPos);
|
|
||||||
const atIndex = textBeforeCursor.lastIndexOf('@');
|
|
||||||
if (atIndex !== -1 && (atIndex === 0 || textBeforeCursor[atIndex - 1] === ' ' || textBeforeCursor[atIndex - 1] === '\n')) {
|
|
||||||
const query = textBeforeCursor.substring(atIndex + 1);
|
|
||||||
if (query.length > 0 && !query.includes(' ')) {
|
|
||||||
this.mentionStart = atIndex;
|
|
||||||
this.mentionQuery = query;
|
|
||||||
this.mentionResults = await $wire.searchUsers(query);
|
|
||||||
this.showMentions = this.mentionResults.length > 0;
|
|
||||||
this.selectedIndex = 0;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.showMentions = false;
|
|
||||||
},
|
|
||||||
handleKeydown(event) {
|
|
||||||
if (!this.showMentions) return;
|
|
||||||
if (event.key === 'ArrowDown') {
|
|
||||||
event.preventDefault();
|
|
||||||
this.selectedIndex = Math.min(this.selectedIndex + 1, this.mentionResults.length - 1);
|
|
||||||
} else if (event.key === 'ArrowUp') {
|
|
||||||
event.preventDefault();
|
|
||||||
this.selectedIndex = Math.max(this.selectedIndex - 1, 0);
|
|
||||||
} else if (event.key === 'Enter' || event.key === 'Tab') {
|
|
||||||
if (this.mentionResults.length > 0) {
|
|
||||||
event.preventDefault();
|
|
||||||
this.selectMention(this.mentionResults[this.selectedIndex]);
|
|
||||||
}
|
|
||||||
} else if (event.key === 'Escape') {
|
|
||||||
this.showMentions = false;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
selectMention(user) {
|
|
||||||
const textarea = this.$refs.commentInput;
|
|
||||||
const value = textarea.value;
|
|
||||||
const before = value.substring(0, this.mentionStart);
|
|
||||||
const after = value.substring(textarea.selectionStart);
|
|
||||||
const newValue = before + '@' + user.name + ' ' + after;
|
|
||||||
$wire.set('newComment', newValue);
|
|
||||||
this.showMentions = false;
|
|
||||||
this.$nextTick(() => {
|
|
||||||
const pos = before.length + user.name.length + 2;
|
|
||||||
textarea.focus();
|
|
||||||
textarea.setSelectionRange(pos, pos);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}">
|
|
||||||
<textarea
|
|
||||||
x-ref="commentInput"
|
|
||||||
wire:model="newComment"
|
|
||||||
@input="handleInput($event)"
|
|
||||||
@keydown="handleKeydown($event)"
|
|
||||||
rows="3"
|
|
||||||
placeholder="Write a comment..."
|
|
||||||
class="block w-full rounded-lg border-gray-300 shadow-sm focus:border-primary-500 focus:ring-primary-500 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-300 dark:placeholder-gray-400 sm:text-sm"
|
|
||||||
></textarea>
|
|
||||||
|
|
||||||
{{-- Mention autocomplete dropdown --}}
|
|
||||||
<div x-show="showMentions" x-cloak
|
|
||||||
class="absolute z-50 mt-1 w-64 rounded-lg border border-gray-200 bg-white shadow-lg dark:border-gray-600 dark:bg-gray-800">
|
|
||||||
<template x-for="(user, index) in mentionResults" :key="user.id">
|
|
||||||
<button type="button"
|
|
||||||
@click="selectMention(user)"
|
|
||||||
:class="{ 'bg-primary-50 dark:bg-primary-900/20': index === selectedIndex }"
|
|
||||||
class="flex w-full items-center gap-2 px-3 py-2 text-left text-sm hover:bg-gray-50 dark:hover:bg-gray-700">
|
|
||||||
<template x-if="user.avatar_url">
|
|
||||||
<img :src="user.avatar_url" class="h-6 w-6 rounded-full object-cover" />
|
|
||||||
</template>
|
|
||||||
<template x-if="!user.avatar_url">
|
|
||||||
<div class="flex h-6 w-6 items-center justify-center rounded-full bg-primary-100 text-xs font-medium text-primary-700 dark:bg-primary-800 dark:text-primary-300"
|
|
||||||
x-text="user.name.charAt(0).toUpperCase()"></div>
|
|
||||||
</template>
|
|
||||||
<span x-text="user.name" class="text-gray-900 dark:text-gray-100"></span>
|
|
||||||
</button>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
@error('newComment')
|
|
||||||
<p class="mt-1 text-sm text-danger-600 dark:text-danger-400">{{ $message }}</p>
|
|
||||||
@enderror
|
|
||||||
|
|
||||||
@if (\Relaticle\Comments\Config::areAttachmentsEnabled())
|
|
||||||
<div class="mt-2">
|
|
||||||
<label class="flex cursor-pointer items-center gap-2 text-xs text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200">
|
|
||||||
<svg class="h-4 w-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="m18.375 12.739-7.693 7.693a4.5 4.5 0 0 1-6.364-6.364l10.94-10.94A3 3 0 1 1 19.5 7.372L8.552 18.32m.009-.01-.01.01m5.699-9.941-7.81 7.81a1.5 1.5 0 0 0 2.112 2.13" />
|
|
||||||
</svg>
|
|
||||||
Attach files
|
|
||||||
<input type="file" wire:model="attachments" multiple class="hidden" accept="{{ implode(',', \Relaticle\Comments\Config::getAttachmentAllowedTypes()) }}" />
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
@if (!empty($attachments))
|
@if (!empty($attachments))
|
||||||
<div class="mt-2 flex flex-wrap gap-2">
|
<div class="mt-2 flex flex-wrap gap-2">
|
||||||
@@ -172,22 +75,34 @@
|
|||||||
</div>
|
</div>
|
||||||
@endforeach
|
@endforeach
|
||||||
</div>
|
</div>
|
||||||
@endif
|
|
||||||
|
|
||||||
@error('attachments.*')
|
@error('attachments.*')
|
||||||
<p class="mt-1 text-sm text-danger-600 dark:text-danger-400">{{ $message }}</p>
|
<p class="mt-1 text-sm text-danger-600 dark:text-danger-400">{{ $message }}</p>
|
||||||
@enderror
|
@enderror
|
||||||
|
<p x-show="uploadError" x-text="uploadError" class="mt-1 text-sm text-danger-600 dark:text-danger-400"></p>
|
||||||
@endif
|
@endif
|
||||||
|
|
||||||
<div class="mt-2 flex justify-end">
|
<div class="mt-2 flex items-center justify-between">
|
||||||
<button type="submit"
|
@if (\Relaticle\Comments\CommentsConfig::areAttachmentsEnabled())
|
||||||
|
<label class="flex cursor-pointer items-center gap-1.5 text-xs text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200">
|
||||||
|
<svg class="h-4 w-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="m18.375 12.739-7.693 7.693a4.5 4.5 0 0 1-6.364-6.364l10.94-10.94A3 3 0 1 1 19.5 7.372L8.552 18.32m.009-.01-.01.01m5.699-9.941-7.81 7.81a1.5 1.5 0 0 0 2.112 2.13" />
|
||||||
|
</svg>
|
||||||
|
Attach
|
||||||
|
<input type="file" wire:model="attachments" multiple class="hidden" accept="{{ implode(',', \Relaticle\Comments\CommentsConfig::getAttachmentAllowedTypes()) }}" />
|
||||||
|
</label>
|
||||||
|
@else
|
||||||
|
<div></div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
<button type="button" wire:click="addComment"
|
||||||
class="inline-flex items-center rounded-lg bg-primary-600 px-3 py-1.5 text-sm font-medium text-white hover:bg-primary-500 focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2 dark:bg-primary-500 dark:hover:bg-primary-400 dark:focus:ring-offset-gray-800"
|
class="inline-flex items-center rounded-lg bg-primary-600 px-3 py-1.5 text-sm font-medium text-white hover:bg-primary-500 focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2 dark:bg-primary-500 dark:hover:bg-primary-400 dark:focus:ring-offset-gray-800"
|
||||||
wire:loading.attr="disabled" wire:target="addComment">
|
wire:loading.attr="disabled" wire:target="addComment">
|
||||||
<span wire:loading.remove wire:target="addComment">Comment</span>
|
<span wire:loading.remove wire:target="addComment">Comment</span>
|
||||||
<span wire:loading wire:target="addComment">Posting...</span>
|
<span wire:loading wire:target="addComment">Posting...</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</div>
|
||||||
@endcan
|
@endcan
|
||||||
@endauth
|
@endauth
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -25,7 +25,7 @@
|
|||||||
{{-- Emoji picker dropdown --}}
|
{{-- Emoji picker dropdown --}}
|
||||||
<div x-show="open" x-cloak @click.outside="open = false"
|
<div x-show="open" x-cloak @click.outside="open = false"
|
||||||
class="absolute bottom-full left-0 z-50 mb-1 flex gap-1 rounded-lg border border-gray-200 bg-white p-2 shadow-lg dark:border-gray-600 dark:bg-gray-800">
|
class="absolute bottom-full left-0 z-50 mb-1 flex gap-1 rounded-lg border border-gray-200 bg-white p-2 shadow-lg dark:border-gray-600 dark:bg-gray-800">
|
||||||
@foreach (\Relaticle\Comments\Config::getReactionEmojiSet() as $key => $emoji)
|
@foreach (\Relaticle\Comments\CommentsConfig::getReactionEmojiSet() as $key => $emoji)
|
||||||
<button wire:click="toggleReaction('{{ $key }}')" type="button"
|
<button wire:click="toggleReaction('{{ $key }}')" type="button"
|
||||||
class="rounded p-1 text-base hover:bg-gray-100 dark:hover:bg-gray-700"
|
class="rounded p-1 text-base hover:bg-gray-100 dark:hover:bg-gray-700"
|
||||||
title="{{ str_replace('_', ' ', $key) }}">
|
title="{{ str_replace('_', ' ', $key) }}">
|
||||||
|
|||||||
@@ -4,10 +4,12 @@ namespace Relaticle\Comments;
|
|||||||
|
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use Closure;
|
use Closure;
|
||||||
|
use Filament\Forms\Components\RichEditor\MentionProvider;
|
||||||
use Relaticle\Comments\Mentions\DefaultMentionResolver;
|
use Relaticle\Comments\Mentions\DefaultMentionResolver;
|
||||||
|
use Relaticle\Comments\Models\Comment;
|
||||||
use Relaticle\Comments\Policies\CommentPolicy;
|
use Relaticle\Comments\Policies\CommentPolicy;
|
||||||
|
|
||||||
class Config
|
class CommentsConfig
|
||||||
{
|
{
|
||||||
protected static ?Closure $resolveAuthenticatedUser = null;
|
protected static ?Closure $resolveAuthenticatedUser = null;
|
||||||
|
|
||||||
@@ -23,7 +25,25 @@ class Config
|
|||||||
|
|
||||||
public static function getCommentTable(): string
|
public static function getCommentTable(): string
|
||||||
{
|
{
|
||||||
return config('comments.tables.comments', 'comments');
|
return static::getTableName('comments');
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getTableName(string $table): string
|
||||||
|
{
|
||||||
|
$defaults = [
|
||||||
|
'comments' => 'comments',
|
||||||
|
'reactions' => 'comment_reactions',
|
||||||
|
'mentions' => 'comment_mentions',
|
||||||
|
'subscriptions' => 'comment_subscriptions',
|
||||||
|
'attachments' => 'comment_attachments',
|
||||||
|
];
|
||||||
|
|
||||||
|
return config("comments.table_names.{$table}", $defaults[$table] ?? $table);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getCommenterMorphName(): string
|
||||||
|
{
|
||||||
|
return config('comments.column_names.commenter_morph', 'commenter');
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function getMaxDepth(): int
|
public static function getMaxDepth(): int
|
||||||
@@ -154,4 +174,19 @@ class Config
|
|||||||
{
|
{
|
||||||
static::$resolveAuthenticatedUser = $callback;
|
static::$resolveAuthenticatedUser = $callback;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static function makeMentionProvider(): MentionProvider
|
||||||
|
{
|
||||||
|
return MentionProvider::make('@')
|
||||||
|
->getSearchResultsUsing(fn (string $search): array => static::getCommenterModel()::query()
|
||||||
|
->where('name', 'like', "%{$search}%")
|
||||||
|
->orderBy('name')
|
||||||
|
->limit(static::getMentionMaxResults())
|
||||||
|
->pluck('name', 'id')
|
||||||
|
->all())
|
||||||
|
->getLabelsUsing(fn (array $ids): array => static::getCommenterModel()::query()
|
||||||
|
->whereIn('id', $ids)
|
||||||
|
->pluck('name', 'id')
|
||||||
|
->all());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
namespace Relaticle\Comments;
|
namespace Relaticle\Comments;
|
||||||
|
|
||||||
|
use Filament\Support\Assets\Css;
|
||||||
|
use Filament\Support\Facades\FilamentAsset;
|
||||||
use Illuminate\Database\Eloquent\Relations\Relation;
|
use Illuminate\Database\Eloquent\Relations\Relation;
|
||||||
use Illuminate\Support\Facades\Event;
|
use Illuminate\Support\Facades\Event;
|
||||||
use Illuminate\Support\Facades\Gate;
|
use Illuminate\Support\Facades\Gate;
|
||||||
@@ -16,6 +18,8 @@ use Relaticle\Comments\Livewire\Comments;
|
|||||||
use Relaticle\Comments\Livewire\Reactions;
|
use Relaticle\Comments\Livewire\Reactions;
|
||||||
use Spatie\LaravelPackageTools\Package;
|
use Spatie\LaravelPackageTools\Package;
|
||||||
use Spatie\LaravelPackageTools\PackageServiceProvider;
|
use Spatie\LaravelPackageTools\PackageServiceProvider;
|
||||||
|
use Symfony\Component\HtmlSanitizer\HtmlSanitizer;
|
||||||
|
use Symfony\Component\HtmlSanitizer\HtmlSanitizerConfig;
|
||||||
|
|
||||||
class CommentsServiceProvider extends PackageServiceProvider
|
class CommentsServiceProvider extends PackageServiceProvider
|
||||||
{
|
{
|
||||||
@@ -42,20 +46,41 @@ class CommentsServiceProvider extends PackageServiceProvider
|
|||||||
public function packageRegistered(): void
|
public function packageRegistered(): void
|
||||||
{
|
{
|
||||||
Relation::morphMap([
|
Relation::morphMap([
|
||||||
'comment' => Config::getCommentModel(),
|
'comment' => CommentsConfig::getCommentModel(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$this->app->bind(
|
$this->app->bind(
|
||||||
MentionResolver::class,
|
MentionResolver::class,
|
||||||
fn () => new (Config::getMentionResolver())
|
fn () => new (CommentsConfig::getMentionResolver())
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->app->scoped(
|
||||||
|
'comments.html_sanitizer',
|
||||||
|
fn (): HtmlSanitizer => new HtmlSanitizer(
|
||||||
|
(new HtmlSanitizerConfig)
|
||||||
|
->allowSafeElements()
|
||||||
|
->allowRelativeLinks()
|
||||||
|
->allowRelativeMedias()
|
||||||
|
->allowAttribute('class', allowedElements: '*')
|
||||||
|
->allowAttribute('data-color', allowedElements: '*')
|
||||||
|
->allowAttribute('data-from-breakpoint', allowedElements: '*')
|
||||||
|
->allowAttribute('data-type', allowedElements: '*')
|
||||||
|
->allowAttribute('data-id', allowedElements: 'span')
|
||||||
|
->allowAttribute('data-label', allowedElements: 'span')
|
||||||
|
->allowAttribute('data-char', allowedElements: 'span')
|
||||||
|
->allowAttribute('style', allowedElements: '*')
|
||||||
|
->allowAttribute('width', allowedElements: 'img')
|
||||||
|
->allowAttribute('height', allowedElements: 'img')
|
||||||
|
->withMaxInputLength(500000)
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function packageBooted(): void
|
public function packageBooted(): void
|
||||||
{
|
{
|
||||||
Gate::policy(
|
Gate::policy(
|
||||||
Config::getCommentModel(),
|
CommentsConfig::getCommentModel(),
|
||||||
Config::getPolicyClass(),
|
CommentsConfig::getPolicyClass(),
|
||||||
);
|
);
|
||||||
|
|
||||||
Event::listen(CommentCreated::class, SendCommentRepliedNotification::class);
|
Event::listen(CommentCreated::class, SendCommentRepliedNotification::class);
|
||||||
@@ -64,5 +89,9 @@ class CommentsServiceProvider extends PackageServiceProvider
|
|||||||
Livewire::component('comments', Comments::class);
|
Livewire::component('comments', Comments::class);
|
||||||
Livewire::component('comment-item', CommentItem::class);
|
Livewire::component('comment-item', CommentItem::class);
|
||||||
Livewire::component('reactions', Reactions::class);
|
Livewire::component('reactions', Reactions::class);
|
||||||
|
|
||||||
|
FilamentAsset::register([
|
||||||
|
Css::make('comments', __DIR__.'/../resources/css/comments.css'),
|
||||||
|
], 'relaticle/comments');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,9 +5,9 @@ namespace Relaticle\Comments\Concerns;
|
|||||||
use Filament\Models\Contracts\HasAvatar;
|
use Filament\Models\Contracts\HasAvatar;
|
||||||
use Filament\Models\Contracts\HasName;
|
use Filament\Models\Contracts\HasName;
|
||||||
|
|
||||||
trait IsCommenter
|
trait CanComment
|
||||||
{
|
{
|
||||||
public function getCommentName(): string
|
public function getCommentDisplayName(): string
|
||||||
{
|
{
|
||||||
if ($this instanceof HasName) {
|
if ($this instanceof HasName) {
|
||||||
return $this->getFilamentName();
|
return $this->getFilamentName();
|
||||||
@@ -3,13 +3,13 @@
|
|||||||
namespace Relaticle\Comments\Concerns;
|
namespace Relaticle\Comments\Concerns;
|
||||||
|
|
||||||
use Illuminate\Database\Eloquent\Relations\MorphMany;
|
use Illuminate\Database\Eloquent\Relations\MorphMany;
|
||||||
use Relaticle\Comments\Config;
|
use Relaticle\Comments\CommentsConfig;
|
||||||
|
|
||||||
trait HasComments
|
trait HasComments
|
||||||
{
|
{
|
||||||
public function comments(): MorphMany
|
public function comments(): MorphMany
|
||||||
{
|
{
|
||||||
return $this->morphMany(Config::getCommentModel(), 'commentable');
|
return $this->morphMany(CommentsConfig::getCommentModel(), 'commentable');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function topLevelComments(): MorphMany
|
public function topLevelComments(): MorphMany
|
||||||
|
|||||||
@@ -2,13 +2,13 @@
|
|||||||
|
|
||||||
namespace Relaticle\Comments\Contracts;
|
namespace Relaticle\Comments\Contracts;
|
||||||
|
|
||||||
interface Commenter
|
interface Commentator
|
||||||
{
|
{
|
||||||
public function getKey();
|
public function getKey();
|
||||||
|
|
||||||
public function getMorphClass();
|
public function getMorphClass();
|
||||||
|
|
||||||
public function getCommentName(): string;
|
public function getCommentDisplayName(): string;
|
||||||
|
|
||||||
public function getCommentAvatarUrl(): ?string;
|
public function getCommentAvatarUrl(): ?string;
|
||||||
}
|
}
|
||||||
@@ -8,8 +8,8 @@ use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
|
|||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
use Illuminate\Foundation\Events\Dispatchable;
|
use Illuminate\Foundation\Events\Dispatchable;
|
||||||
use Illuminate\Queue\SerializesModels;
|
use Illuminate\Queue\SerializesModels;
|
||||||
use Relaticle\Comments\Comment;
|
use Relaticle\Comments\CommentsConfig;
|
||||||
use Relaticle\Comments\Config;
|
use Relaticle\Comments\Models\Comment;
|
||||||
|
|
||||||
class CommentCreated implements ShouldBroadcast
|
class CommentCreated implements ShouldBroadcast
|
||||||
{
|
{
|
||||||
@@ -27,7 +27,7 @@ class CommentCreated implements ShouldBroadcast
|
|||||||
/** @return array<int, PrivateChannel> */
|
/** @return array<int, PrivateChannel> */
|
||||||
public function broadcastOn(): array
|
public function broadcastOn(): array
|
||||||
{
|
{
|
||||||
$prefix = Config::getBroadcastChannelPrefix();
|
$prefix = CommentsConfig::getBroadcastChannelPrefix();
|
||||||
|
|
||||||
return [
|
return [
|
||||||
new PrivateChannel("{$prefix}.{$this->comment->commentable_type}.{$this->comment->commentable_id}"),
|
new PrivateChannel("{$prefix}.{$this->comment->commentable_type}.{$this->comment->commentable_id}"),
|
||||||
@@ -36,7 +36,7 @@ class CommentCreated implements ShouldBroadcast
|
|||||||
|
|
||||||
public function broadcastWhen(): bool
|
public function broadcastWhen(): bool
|
||||||
{
|
{
|
||||||
return Config::isBroadcastingEnabled();
|
return CommentsConfig::isBroadcastingEnabled();
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @return array{comment_id: int|string, commentable_type: string, commentable_id: int|string} */
|
/** @return array{comment_id: int|string, commentable_type: string, commentable_id: int|string} */
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
|
|||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
use Illuminate\Foundation\Events\Dispatchable;
|
use Illuminate\Foundation\Events\Dispatchable;
|
||||||
use Illuminate\Queue\SerializesModels;
|
use Illuminate\Queue\SerializesModels;
|
||||||
use Relaticle\Comments\Comment;
|
use Relaticle\Comments\CommentsConfig;
|
||||||
use Relaticle\Comments\Config;
|
use Relaticle\Comments\Models\Comment;
|
||||||
|
|
||||||
class CommentDeleted implements ShouldBroadcast
|
class CommentDeleted implements ShouldBroadcast
|
||||||
{
|
{
|
||||||
@@ -27,7 +27,7 @@ class CommentDeleted implements ShouldBroadcast
|
|||||||
/** @return array<int, PrivateChannel> */
|
/** @return array<int, PrivateChannel> */
|
||||||
public function broadcastOn(): array
|
public function broadcastOn(): array
|
||||||
{
|
{
|
||||||
$prefix = Config::getBroadcastChannelPrefix();
|
$prefix = CommentsConfig::getBroadcastChannelPrefix();
|
||||||
|
|
||||||
return [
|
return [
|
||||||
new PrivateChannel("{$prefix}.{$this->comment->commentable_type}.{$this->comment->commentable_id}"),
|
new PrivateChannel("{$prefix}.{$this->comment->commentable_type}.{$this->comment->commentable_id}"),
|
||||||
@@ -36,7 +36,7 @@ class CommentDeleted implements ShouldBroadcast
|
|||||||
|
|
||||||
public function broadcastWhen(): bool
|
public function broadcastWhen(): bool
|
||||||
{
|
{
|
||||||
return Config::isBroadcastingEnabled();
|
return CommentsConfig::isBroadcastingEnabled();
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @return array{comment_id: int|string, commentable_type: string, commentable_id: int|string} */
|
/** @return array{comment_id: int|string, commentable_type: string, commentable_id: int|string} */
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ use Illuminate\Broadcasting\PrivateChannel;
|
|||||||
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
|
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
|
||||||
use Illuminate\Foundation\Events\Dispatchable;
|
use Illuminate\Foundation\Events\Dispatchable;
|
||||||
use Illuminate\Queue\SerializesModels;
|
use Illuminate\Queue\SerializesModels;
|
||||||
use Relaticle\Comments\Comment;
|
use Relaticle\Comments\CommentsConfig;
|
||||||
use Relaticle\Comments\Config;
|
use Relaticle\Comments\Models\Comment;
|
||||||
|
|
||||||
class CommentReacted implements ShouldBroadcast
|
class CommentReacted implements ShouldBroadcast
|
||||||
{
|
{
|
||||||
@@ -26,7 +26,7 @@ class CommentReacted implements ShouldBroadcast
|
|||||||
/** @return array<int, PrivateChannel> */
|
/** @return array<int, PrivateChannel> */
|
||||||
public function broadcastOn(): array
|
public function broadcastOn(): array
|
||||||
{
|
{
|
||||||
$prefix = Config::getBroadcastChannelPrefix();
|
$prefix = CommentsConfig::getBroadcastChannelPrefix();
|
||||||
|
|
||||||
return [
|
return [
|
||||||
new PrivateChannel("{$prefix}.{$this->comment->commentable_type}.{$this->comment->commentable_id}"),
|
new PrivateChannel("{$prefix}.{$this->comment->commentable_type}.{$this->comment->commentable_id}"),
|
||||||
@@ -35,7 +35,7 @@ class CommentReacted implements ShouldBroadcast
|
|||||||
|
|
||||||
public function broadcastWhen(): bool
|
public function broadcastWhen(): bool
|
||||||
{
|
{
|
||||||
return Config::isBroadcastingEnabled();
|
return CommentsConfig::isBroadcastingEnabled();
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @return array{comment_id: int|string, reaction: string, action: string} */
|
/** @return array{comment_id: int|string, reaction: string, action: string} */
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
|
|||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
use Illuminate\Foundation\Events\Dispatchable;
|
use Illuminate\Foundation\Events\Dispatchable;
|
||||||
use Illuminate\Queue\SerializesModels;
|
use Illuminate\Queue\SerializesModels;
|
||||||
use Relaticle\Comments\Comment;
|
use Relaticle\Comments\CommentsConfig;
|
||||||
use Relaticle\Comments\Config;
|
use Relaticle\Comments\Models\Comment;
|
||||||
|
|
||||||
class CommentUpdated implements ShouldBroadcast
|
class CommentUpdated implements ShouldBroadcast
|
||||||
{
|
{
|
||||||
@@ -27,7 +27,7 @@ class CommentUpdated implements ShouldBroadcast
|
|||||||
/** @return array<int, PrivateChannel> */
|
/** @return array<int, PrivateChannel> */
|
||||||
public function broadcastOn(): array
|
public function broadcastOn(): array
|
||||||
{
|
{
|
||||||
$prefix = Config::getBroadcastChannelPrefix();
|
$prefix = CommentsConfig::getBroadcastChannelPrefix();
|
||||||
|
|
||||||
return [
|
return [
|
||||||
new PrivateChannel("{$prefix}.{$this->comment->commentable_type}.{$this->comment->commentable_id}"),
|
new PrivateChannel("{$prefix}.{$this->comment->commentable_type}.{$this->comment->commentable_id}"),
|
||||||
@@ -36,7 +36,7 @@ class CommentUpdated implements ShouldBroadcast
|
|||||||
|
|
||||||
public function broadcastWhen(): bool
|
public function broadcastWhen(): bool
|
||||||
{
|
{
|
||||||
return Config::isBroadcastingEnabled();
|
return CommentsConfig::isBroadcastingEnabled();
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @return array{comment_id: int|string, commentable_type: string, commentable_id: int|string} */
|
/** @return array{comment_id: int|string, commentable_type: string, commentable_id: int|string} */
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ namespace Relaticle\Comments\Events;
|
|||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
use Illuminate\Foundation\Events\Dispatchable;
|
use Illuminate\Foundation\Events\Dispatchable;
|
||||||
use Illuminate\Queue\SerializesModels;
|
use Illuminate\Queue\SerializesModels;
|
||||||
use Relaticle\Comments\Comment;
|
use Relaticle\Comments\Models\Comment;
|
||||||
|
|
||||||
class UserMentioned
|
class UserMentioned
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -3,8 +3,8 @@
|
|||||||
namespace Relaticle\Comments\Filament\Actions;
|
namespace Relaticle\Comments\Filament\Actions;
|
||||||
|
|
||||||
use Filament\Actions\Action;
|
use Filament\Actions\Action;
|
||||||
use Illuminate\Contracts\View\View;
|
|
||||||
use Relaticle\Comments\Concerns\HasComments;
|
use Relaticle\Comments\Concerns\HasComments;
|
||||||
|
use Relaticle\Comments\Filament\Infolists\Components\CommentsEntry;
|
||||||
|
|
||||||
class CommentsAction extends Action
|
class CommentsAction extends Action
|
||||||
{
|
{
|
||||||
@@ -19,11 +19,9 @@ class CommentsAction extends Action
|
|||||||
->modalHeading(__('Comments'))
|
->modalHeading(__('Comments'))
|
||||||
->modalSubmitAction(false)
|
->modalSubmitAction(false)
|
||||||
->modalCancelAction(false)
|
->modalCancelAction(false)
|
||||||
->modalContent(function (): View {
|
->schema([
|
||||||
return view('comments::filament.comments-action', [
|
CommentsEntry::make('comments'),
|
||||||
'record' => $this->getRecord(),
|
])
|
||||||
]);
|
|
||||||
})
|
|
||||||
->badge(function (): ?int {
|
->badge(function (): ?int {
|
||||||
$record = $this->getRecord();
|
$record = $this->getRecord();
|
||||||
|
|
||||||
@@ -38,7 +36,8 @@ class CommentsAction extends Action
|
|||||||
$count = $record->commentCount();
|
$count = $record->commentCount();
|
||||||
|
|
||||||
return $count > 0 ? $count : null;
|
return $count > 0 ? $count : null;
|
||||||
});
|
})
|
||||||
|
->badgeColor('gray');
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function getDefaultName(): ?string
|
public static function getDefaultName(): ?string
|
||||||
|
|||||||
@@ -3,8 +3,8 @@
|
|||||||
namespace Relaticle\Comments\Filament\Actions;
|
namespace Relaticle\Comments\Filament\Actions;
|
||||||
|
|
||||||
use Filament\Actions\Action;
|
use Filament\Actions\Action;
|
||||||
use Illuminate\Contracts\View\View;
|
|
||||||
use Relaticle\Comments\Concerns\HasComments;
|
use Relaticle\Comments\Concerns\HasComments;
|
||||||
|
use Relaticle\Comments\Filament\Infolists\Components\CommentsEntry;
|
||||||
|
|
||||||
class CommentsTableAction extends Action
|
class CommentsTableAction extends Action
|
||||||
{
|
{
|
||||||
@@ -19,11 +19,9 @@ class CommentsTableAction extends Action
|
|||||||
->modalHeading(__('Comments'))
|
->modalHeading(__('Comments'))
|
||||||
->modalSubmitAction(false)
|
->modalSubmitAction(false)
|
||||||
->modalCancelAction(false)
|
->modalCancelAction(false)
|
||||||
->modalContent(function (): View {
|
->schema([
|
||||||
return view('comments::filament.comments-action', [
|
CommentsEntry::make('comments'),
|
||||||
'record' => $this->getRecord(),
|
])
|
||||||
]);
|
|
||||||
})
|
|
||||||
->badge(function (): ?int {
|
->badge(function (): ?int {
|
||||||
$record = $this->getRecord();
|
$record = $this->getRecord();
|
||||||
|
|
||||||
|
|||||||
@@ -3,35 +3,35 @@
|
|||||||
namespace Relaticle\Comments\Listeners;
|
namespace Relaticle\Comments\Listeners;
|
||||||
|
|
||||||
use Illuminate\Support\Facades\Notification;
|
use Illuminate\Support\Facades\Notification;
|
||||||
use Relaticle\Comments\CommentSubscription;
|
use Relaticle\Comments\CommentsConfig;
|
||||||
use Relaticle\Comments\Config;
|
|
||||||
use Relaticle\Comments\Events\CommentCreated;
|
use Relaticle\Comments\Events\CommentCreated;
|
||||||
|
use Relaticle\Comments\Models\Subscription;
|
||||||
use Relaticle\Comments\Notifications\CommentRepliedNotification;
|
use Relaticle\Comments\Notifications\CommentRepliedNotification;
|
||||||
|
|
||||||
class SendCommentRepliedNotification
|
class SendCommentRepliedNotification
|
||||||
{
|
{
|
||||||
public function handle(CommentCreated $event): void
|
public function handle(CommentCreated $event): void
|
||||||
{
|
{
|
||||||
if (! Config::areNotificationsEnabled()) {
|
if (! CommentsConfig::areNotificationsEnabled()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$comment = $event->comment;
|
$comment = $event->comment;
|
||||||
$commentable = $event->commentable;
|
$commentable = $event->commentable;
|
||||||
|
|
||||||
if (Config::shouldAutoSubscribe()) {
|
if (CommentsConfig::shouldAutoSubscribe()) {
|
||||||
CommentSubscription::subscribe($commentable, $comment->user);
|
Subscription::subscribe($commentable, $comment->commenter);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (! $comment->isReply()) {
|
if (! $comment->isReply()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$subscribers = CommentSubscription::subscribersFor($commentable);
|
$subscribers = Subscription::subscribersFor($commentable);
|
||||||
|
|
||||||
$recipients = $subscribers->filter(function ($user) use ($comment) {
|
$recipients = $subscribers->filter(function ($user) use ($comment) {
|
||||||
return ! ($user->getMorphClass() === $comment->user->getMorphClass()
|
return ! ($user->getMorphClass() === $comment->commenter->getMorphClass()
|
||||||
&& $user->getKey() === $comment->user->getKey());
|
&& $user->getKey() === $comment->commenter->getKey());
|
||||||
});
|
});
|
||||||
|
|
||||||
if ($recipients->isEmpty()) {
|
if ($recipients->isEmpty()) {
|
||||||
|
|||||||
@@ -2,33 +2,33 @@
|
|||||||
|
|
||||||
namespace Relaticle\Comments\Listeners;
|
namespace Relaticle\Comments\Listeners;
|
||||||
|
|
||||||
use Relaticle\Comments\CommentSubscription;
|
use Relaticle\Comments\CommentsConfig;
|
||||||
use Relaticle\Comments\Config;
|
|
||||||
use Relaticle\Comments\Events\UserMentioned;
|
use Relaticle\Comments\Events\UserMentioned;
|
||||||
|
use Relaticle\Comments\Models\Subscription;
|
||||||
use Relaticle\Comments\Notifications\UserMentionedNotification;
|
use Relaticle\Comments\Notifications\UserMentionedNotification;
|
||||||
|
|
||||||
class SendUserMentionedNotification
|
class SendUserMentionedNotification
|
||||||
{
|
{
|
||||||
public function handle(UserMentioned $event): void
|
public function handle(UserMentioned $event): void
|
||||||
{
|
{
|
||||||
if (! Config::areNotificationsEnabled()) {
|
if (! CommentsConfig::areNotificationsEnabled()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$comment = $event->comment;
|
$comment = $event->comment;
|
||||||
$mentionedUser = $event->mentionedUser;
|
$mentionedUser = $event->mentionedUser;
|
||||||
|
|
||||||
if (Config::shouldAutoSubscribe()) {
|
if (CommentsConfig::shouldAutoSubscribe()) {
|
||||||
CommentSubscription::subscribe($comment->commentable, $mentionedUser);
|
Subscription::subscribe($comment->commentable, $mentionedUser);
|
||||||
}
|
}
|
||||||
|
|
||||||
$isSelf = $mentionedUser->getMorphClass() === $comment->user->getMorphClass()
|
$isSelf = $mentionedUser->getMorphClass() === $comment->commenter->getMorphClass()
|
||||||
&& $mentionedUser->getKey() === $comment->user->getKey();
|
&& $mentionedUser->getKey() === $comment->commenter->getKey();
|
||||||
|
|
||||||
if ($isSelf) {
|
if ($isSelf) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$mentionedUser->notify(new UserMentionedNotification($comment, $comment->user));
|
$mentionedUser->notify(new UserMentionedNotification($comment, $comment->commenter));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,20 +2,27 @@
|
|||||||
|
|
||||||
namespace Relaticle\Comments\Livewire;
|
namespace Relaticle\Comments\Livewire;
|
||||||
|
|
||||||
|
use Filament\Actions\Concerns\InteractsWithActions;
|
||||||
|
use Filament\Actions\Contracts\HasActions;
|
||||||
|
use Filament\Forms\Components\RichEditor;
|
||||||
|
use Filament\Forms\Concerns\InteractsWithForms;
|
||||||
|
use Filament\Forms\Contracts\HasForms;
|
||||||
|
use Filament\Schemas\Schema;
|
||||||
use Illuminate\Contracts\View\View;
|
use Illuminate\Contracts\View\View;
|
||||||
use Livewire\Component;
|
use Livewire\Component;
|
||||||
use Livewire\Features\SupportFileUploads\TemporaryUploadedFile;
|
use Livewire\Features\SupportFileUploads\TemporaryUploadedFile;
|
||||||
use Livewire\WithFileUploads;
|
use Livewire\WithFileUploads;
|
||||||
use Relaticle\Comments\Comment;
|
use Relaticle\Comments\CommentsConfig;
|
||||||
use Relaticle\Comments\Config;
|
|
||||||
use Relaticle\Comments\Contracts\MentionResolver;
|
|
||||||
use Relaticle\Comments\Events\CommentCreated;
|
use Relaticle\Comments\Events\CommentCreated;
|
||||||
use Relaticle\Comments\Events\CommentDeleted;
|
use Relaticle\Comments\Events\CommentDeleted;
|
||||||
use Relaticle\Comments\Events\CommentUpdated;
|
use Relaticle\Comments\Events\CommentUpdated;
|
||||||
use Relaticle\Comments\Mentions\MentionParser;
|
use Relaticle\Comments\Mentions\MentionParser;
|
||||||
|
use Relaticle\Comments\Models\Comment;
|
||||||
|
|
||||||
class CommentItem extends Component
|
class CommentItem extends Component implements HasActions, HasForms
|
||||||
{
|
{
|
||||||
|
use InteractsWithActions;
|
||||||
|
use InteractsWithForms;
|
||||||
use WithFileUploads;
|
use WithFileUploads;
|
||||||
|
|
||||||
public Comment $comment;
|
public Comment $comment;
|
||||||
@@ -24,9 +31,11 @@ class CommentItem extends Component
|
|||||||
|
|
||||||
public bool $isReplying = false;
|
public bool $isReplying = false;
|
||||||
|
|
||||||
public string $editBody = '';
|
/** @var array<string, mixed> */
|
||||||
|
public ?array $editData = [];
|
||||||
|
|
||||||
public string $replyBody = '';
|
/** @var array<string, mixed> */
|
||||||
|
public ?array $replyData = [];
|
||||||
|
|
||||||
/** @var array<int, TemporaryUploadedFile> */
|
/** @var array<int, TemporaryUploadedFile> */
|
||||||
public array $replyAttachments = [];
|
public array $replyAttachments = [];
|
||||||
@@ -36,30 +45,60 @@ class CommentItem extends Component
|
|||||||
$this->comment = $comment;
|
$this->comment = $comment;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function editForm(Schema $schema): Schema
|
||||||
|
{
|
||||||
|
return $schema
|
||||||
|
->components([
|
||||||
|
RichEditor::make('body')
|
||||||
|
->hiddenLabel()
|
||||||
|
->required()
|
||||||
|
->placeholder(__('Edit your comment...'))
|
||||||
|
->toolbarButtons(CommentsConfig::getEditorToolbar())
|
||||||
|
->mentions([
|
||||||
|
CommentsConfig::makeMentionProvider(),
|
||||||
|
]),
|
||||||
|
])
|
||||||
|
->statePath('editData');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function replyForm(Schema $schema): Schema
|
||||||
|
{
|
||||||
|
return $schema
|
||||||
|
->components([
|
||||||
|
RichEditor::make('body')
|
||||||
|
->hiddenLabel()
|
||||||
|
->required()
|
||||||
|
->placeholder(__('Write a reply...'))
|
||||||
|
->toolbarButtons(CommentsConfig::getEditorToolbar())
|
||||||
|
->mentions([
|
||||||
|
CommentsConfig::makeMentionProvider(),
|
||||||
|
]),
|
||||||
|
])
|
||||||
|
->statePath('replyData');
|
||||||
|
}
|
||||||
|
|
||||||
public function startEdit(): void
|
public function startEdit(): void
|
||||||
{
|
{
|
||||||
$this->authorize('update', $this->comment);
|
$this->authorize('update', $this->comment);
|
||||||
|
|
||||||
$this->isEditing = true;
|
$this->isEditing = true;
|
||||||
$this->editBody = $this->comment->body;
|
$this->editForm->fill(['body' => $this->comment->body]);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function cancelEdit(): void
|
public function cancelEdit(): void
|
||||||
{
|
{
|
||||||
$this->isEditing = false;
|
$this->isEditing = false;
|
||||||
$this->editBody = '';
|
$this->editForm->fill();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function saveEdit(): void
|
public function saveEdit(): void
|
||||||
{
|
{
|
||||||
$this->authorize('update', $this->comment);
|
$this->authorize('update', $this->comment);
|
||||||
|
|
||||||
$this->validate([
|
$data = $this->editForm->getState();
|
||||||
'editBody' => ['required', 'string', 'min:1'],
|
|
||||||
]);
|
|
||||||
|
|
||||||
$this->comment->update([
|
$this->comment->update([
|
||||||
'body' => $this->editBody,
|
'body' => $data['body'] ?? '',
|
||||||
'edited_at' => now(),
|
'edited_at' => now(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -70,7 +109,7 @@ class CommentItem extends Component
|
|||||||
$this->dispatch('commentUpdated');
|
$this->dispatch('commentUpdated');
|
||||||
|
|
||||||
$this->isEditing = false;
|
$this->isEditing = false;
|
||||||
$this->editBody = '';
|
$this->editForm->fill();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function deleteComment(): void
|
public function deleteComment(): void
|
||||||
@@ -91,12 +130,13 @@ class CommentItem extends Component
|
|||||||
}
|
}
|
||||||
|
|
||||||
$this->isReplying = true;
|
$this->isReplying = true;
|
||||||
|
$this->replyForm->fill();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function cancelReply(): void
|
public function cancelReply(): void
|
||||||
{
|
{
|
||||||
$this->isReplying = false;
|
$this->isReplying = false;
|
||||||
$this->replyBody = '';
|
$this->replyForm->fill();
|
||||||
$this->replyAttachments = [];
|
$this->replyAttachments = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -104,27 +144,27 @@ class CommentItem extends Component
|
|||||||
{
|
{
|
||||||
$this->authorize('reply', $this->comment);
|
$this->authorize('reply', $this->comment);
|
||||||
|
|
||||||
$rules = ['replyBody' => ['required', 'string', 'min:1']];
|
$data = $this->replyForm->getState();
|
||||||
|
|
||||||
if (Config::areAttachmentsEnabled()) {
|
if (CommentsConfig::areAttachmentsEnabled()) {
|
||||||
$maxSize = Config::getAttachmentMaxSize();
|
$maxSize = CommentsConfig::getAttachmentMaxSize();
|
||||||
$allowedTypes = implode(',', Config::getAttachmentAllowedTypes());
|
$allowedTypes = implode(',', CommentsConfig::getAttachmentAllowedTypes());
|
||||||
$rules['replyAttachments.*'] = ['nullable', 'file', "max:{$maxSize}", "mimetypes:{$allowedTypes}"];
|
$this->validate([
|
||||||
|
'replyAttachments.*' => ['nullable', 'file', "max:{$maxSize}", "mimetypes:{$allowedTypes}"],
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->validate($rules);
|
$user = CommentsConfig::resolveAuthenticatedUser();
|
||||||
|
|
||||||
$user = Config::resolveAuthenticatedUser();
|
|
||||||
|
|
||||||
$reply = $this->comment->commentable->comments()->create([
|
$reply = $this->comment->commentable->comments()->create([
|
||||||
'body' => $this->replyBody,
|
'body' => $data['body'] ?? '',
|
||||||
'parent_id' => $this->comment->id,
|
'parent_id' => $this->comment->id,
|
||||||
'user_id' => $user->getKey(),
|
'commenter_id' => $user->getKey(),
|
||||||
'user_type' => $user->getMorphClass(),
|
'commenter_type' => $user->getMorphClass(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (Config::areAttachmentsEnabled() && ! empty($this->replyAttachments)) {
|
if (CommentsConfig::areAttachmentsEnabled() && ! empty($this->replyAttachments)) {
|
||||||
$disk = Config::getAttachmentDisk();
|
$disk = CommentsConfig::getAttachmentDisk();
|
||||||
|
|
||||||
foreach ($this->replyAttachments as $file) {
|
foreach ($this->replyAttachments as $file) {
|
||||||
$path = $file->store("comments/attachments/{$reply->id}", $disk);
|
$path = $file->store("comments/attachments/{$reply->id}", $disk);
|
||||||
@@ -143,10 +183,12 @@ class CommentItem extends Component
|
|||||||
|
|
||||||
app(MentionParser::class)->syncMentions($reply);
|
app(MentionParser::class)->syncMentions($reply);
|
||||||
|
|
||||||
|
$this->comment->load(['replies.commenter', 'replies.mentions', 'replies.attachments', 'replies.reactions.commenter', 'replies.replies.commenter', 'replies.replies.mentions', 'replies.replies.attachments', 'replies.replies.reactions.commenter']);
|
||||||
|
|
||||||
$this->dispatch('commentUpdated');
|
$this->dispatch('commentUpdated');
|
||||||
|
|
||||||
$this->isReplying = false;
|
$this->isReplying = false;
|
||||||
$this->replyBody = '';
|
$this->replyForm->fill();
|
||||||
$this->replyAttachments = [];
|
$this->replyAttachments = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -157,25 +199,6 @@ class CommentItem extends Component
|
|||||||
$this->replyAttachments = array_values($attachments);
|
$this->replyAttachments = array_values($attachments);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @return array<int, array{id: int, name: string, avatar_url: ?string}> */
|
|
||||||
public function searchUsers(string $query): array
|
|
||||||
{
|
|
||||||
if (mb_strlen($query) < 1) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
$resolver = app(MentionResolver::class);
|
|
||||||
|
|
||||||
return $resolver->search($query)
|
|
||||||
->map(fn ($user) => [
|
|
||||||
'id' => $user->getKey(),
|
|
||||||
'name' => $user->getCommentName(),
|
|
||||||
'avatar_url' => $user->getCommentAvatarUrl(),
|
|
||||||
])
|
|
||||||
->values()
|
|
||||||
->all();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function render(): View
|
public function render(): View
|
||||||
{
|
{
|
||||||
return view('comments::livewire.comment-item');
|
return view('comments::livewire.comment-item');
|
||||||
|
|||||||
@@ -2,6 +2,12 @@
|
|||||||
|
|
||||||
namespace Relaticle\Comments\Livewire;
|
namespace Relaticle\Comments\Livewire;
|
||||||
|
|
||||||
|
use Filament\Actions\Concerns\InteractsWithActions;
|
||||||
|
use Filament\Actions\Contracts\HasActions;
|
||||||
|
use Filament\Forms\Components\RichEditor;
|
||||||
|
use Filament\Forms\Concerns\InteractsWithForms;
|
||||||
|
use Filament\Forms\Contracts\HasForms;
|
||||||
|
use Filament\Schemas\Schema;
|
||||||
use Illuminate\Contracts\View\View;
|
use Illuminate\Contracts\View\View;
|
||||||
use Illuminate\Database\Eloquent\Collection;
|
use Illuminate\Database\Eloquent\Collection;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
@@ -9,20 +15,22 @@ use Livewire\Attributes\Computed;
|
|||||||
use Livewire\Component;
|
use Livewire\Component;
|
||||||
use Livewire\Features\SupportFileUploads\TemporaryUploadedFile;
|
use Livewire\Features\SupportFileUploads\TemporaryUploadedFile;
|
||||||
use Livewire\WithFileUploads;
|
use Livewire\WithFileUploads;
|
||||||
use Relaticle\Comments\Comment;
|
use Relaticle\Comments\CommentsConfig;
|
||||||
use Relaticle\Comments\CommentSubscription;
|
|
||||||
use Relaticle\Comments\Config;
|
|
||||||
use Relaticle\Comments\Contracts\MentionResolver;
|
|
||||||
use Relaticle\Comments\Events\CommentCreated;
|
use Relaticle\Comments\Events\CommentCreated;
|
||||||
use Relaticle\Comments\Mentions\MentionParser;
|
use Relaticle\Comments\Mentions\MentionParser;
|
||||||
|
use Relaticle\Comments\Models\Comment;
|
||||||
|
use Relaticle\Comments\Models\Subscription;
|
||||||
|
|
||||||
class Comments extends Component
|
class Comments extends Component implements HasActions, HasForms
|
||||||
{
|
{
|
||||||
|
use InteractsWithActions;
|
||||||
|
use InteractsWithForms;
|
||||||
use WithFileUploads;
|
use WithFileUploads;
|
||||||
|
|
||||||
public Model $model;
|
public Model $model;
|
||||||
|
|
||||||
public string $newComment = '';
|
/** @var array<string, mixed> */
|
||||||
|
public ?array $commentData = [];
|
||||||
|
|
||||||
public string $sortDirection = 'asc';
|
public string $sortDirection = 'asc';
|
||||||
|
|
||||||
@@ -36,8 +44,25 @@ class Comments extends Component
|
|||||||
public function mount(Model $model): void
|
public function mount(Model $model): void
|
||||||
{
|
{
|
||||||
$this->model = $model;
|
$this->model = $model;
|
||||||
$this->perPage = Config::getPerPage();
|
$this->perPage = CommentsConfig::getPerPage();
|
||||||
$this->loadedCount = $this->perPage;
|
$this->loadedCount = $this->perPage;
|
||||||
|
$this->commentForm->fill();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function commentForm(Schema $schema): Schema
|
||||||
|
{
|
||||||
|
return $schema
|
||||||
|
->components([
|
||||||
|
RichEditor::make('body')
|
||||||
|
->hiddenLabel()
|
||||||
|
->required()
|
||||||
|
->placeholder(__('Write a comment...'))
|
||||||
|
->toolbarButtons(CommentsConfig::getEditorToolbar())
|
||||||
|
->mentions([
|
||||||
|
CommentsConfig::makeMentionProvider(),
|
||||||
|
]),
|
||||||
|
])
|
||||||
|
->statePath('commentData');
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @return Collection<int, Comment> */
|
/** @return Collection<int, Comment> */
|
||||||
@@ -46,7 +71,7 @@ class Comments extends Component
|
|||||||
{
|
{
|
||||||
return $this->model
|
return $this->model
|
||||||
->topLevelComments()
|
->topLevelComments()
|
||||||
->with(['user', 'mentions', 'attachments', 'reactions.user', 'replies.user', 'replies.mentions', 'replies.attachments', 'replies.reactions.user'])
|
->with(['commenter', 'mentions', 'attachments', 'reactions.commenter', 'replies.commenter', 'replies.mentions', 'replies.attachments', 'replies.reactions.commenter', 'replies.replies.commenter', 'replies.replies.mentions', 'replies.replies.attachments', 'replies.replies.reactions.commenter'])
|
||||||
->orderBy('created_at', $this->sortDirection)
|
->orderBy('created_at', $this->sortDirection)
|
||||||
->take($this->loadedCount)
|
->take($this->loadedCount)
|
||||||
->get();
|
->get();
|
||||||
@@ -58,6 +83,12 @@ class Comments extends Component
|
|||||||
return $this->model->topLevelComments()->count();
|
return $this->model->topLevelComments()->count();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[Computed]
|
||||||
|
public function allCommentsCount(): int
|
||||||
|
{
|
||||||
|
return $this->model->commentCount();
|
||||||
|
}
|
||||||
|
|
||||||
#[Computed]
|
#[Computed]
|
||||||
public function hasMore(): bool
|
public function hasMore(): bool
|
||||||
{
|
{
|
||||||
@@ -67,27 +98,27 @@ class Comments extends Component
|
|||||||
#[Computed]
|
#[Computed]
|
||||||
public function isSubscribed(): bool
|
public function isSubscribed(): bool
|
||||||
{
|
{
|
||||||
$user = Config::resolveAuthenticatedUser();
|
$user = CommentsConfig::resolveAuthenticatedUser();
|
||||||
|
|
||||||
if (! $user) {
|
if (! $user) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return CommentSubscription::isSubscribed($this->model, $user);
|
return Subscription::isSubscribed($this->model, $user);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function toggleSubscription(): void
|
public function toggleSubscription(): void
|
||||||
{
|
{
|
||||||
$user = Config::resolveAuthenticatedUser();
|
$user = CommentsConfig::resolveAuthenticatedUser();
|
||||||
|
|
||||||
if (! $user) {
|
if (! $user) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($this->isSubscribed) {
|
if ($this->isSubscribed) {
|
||||||
CommentSubscription::unsubscribe($this->model, $user);
|
Subscription::unsubscribe($this->model, $user);
|
||||||
} else {
|
} else {
|
||||||
CommentSubscription::subscribe($this->model, $user);
|
Subscription::subscribe($this->model, $user);
|
||||||
}
|
}
|
||||||
|
|
||||||
unset($this->isSubscribed);
|
unset($this->isSubscribed);
|
||||||
@@ -95,28 +126,28 @@ class Comments extends Component
|
|||||||
|
|
||||||
public function addComment(): void
|
public function addComment(): void
|
||||||
{
|
{
|
||||||
$rules = ['newComment' => ['required', 'string', 'min:1']];
|
$data = $this->commentForm->getState();
|
||||||
|
|
||||||
if (Config::areAttachmentsEnabled()) {
|
if (CommentsConfig::areAttachmentsEnabled()) {
|
||||||
$maxSize = Config::getAttachmentMaxSize();
|
$maxSize = CommentsConfig::getAttachmentMaxSize();
|
||||||
$allowedTypes = implode(',', Config::getAttachmentAllowedTypes());
|
$allowedTypes = implode(',', CommentsConfig::getAttachmentAllowedTypes());
|
||||||
$rules['attachments.*'] = ['nullable', 'file', "max:{$maxSize}", "mimetypes:{$allowedTypes}"];
|
$this->validate([
|
||||||
|
'attachments.*' => ['nullable', 'file', "max:{$maxSize}", "mimetypes:{$allowedTypes}"],
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->validate($rules);
|
$this->authorize('create', CommentsConfig::getCommentModel());
|
||||||
|
|
||||||
$this->authorize('create', Config::getCommentModel());
|
$user = CommentsConfig::resolveAuthenticatedUser();
|
||||||
|
|
||||||
$user = Config::resolveAuthenticatedUser();
|
|
||||||
|
|
||||||
$comment = $this->model->comments()->create([
|
$comment = $this->model->comments()->create([
|
||||||
'body' => $this->newComment,
|
'body' => $data['body'] ?? '',
|
||||||
'user_id' => $user->getKey(),
|
'commenter_id' => $user->getKey(),
|
||||||
'user_type' => $user->getMorphClass(),
|
'commenter_type' => $user->getMorphClass(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (Config::areAttachmentsEnabled() && ! empty($this->attachments)) {
|
if (CommentsConfig::areAttachmentsEnabled() && ! empty($this->attachments)) {
|
||||||
$disk = Config::getAttachmentDisk();
|
$disk = CommentsConfig::getAttachmentDisk();
|
||||||
|
|
||||||
foreach ($this->attachments as $file) {
|
foreach ($this->attachments as $file) {
|
||||||
$path = $file->store("comments/attachments/{$comment->id}", $disk);
|
$path = $file->store("comments/attachments/{$comment->id}", $disk);
|
||||||
@@ -135,7 +166,8 @@ class Comments extends Component
|
|||||||
|
|
||||||
app(MentionParser::class)->syncMentions($comment);
|
app(MentionParser::class)->syncMentions($comment);
|
||||||
|
|
||||||
$this->reset('newComment', 'attachments');
|
$this->commentForm->fill();
|
||||||
|
$this->reset('attachments');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function removeAttachment(int $index): void
|
public function removeAttachment(int $index): void
|
||||||
@@ -163,8 +195,8 @@ class Comments extends Component
|
|||||||
'commentUpdated' => 'refreshComments',
|
'commentUpdated' => 'refreshComments',
|
||||||
];
|
];
|
||||||
|
|
||||||
if (Config::isBroadcastingEnabled()) {
|
if (CommentsConfig::isBroadcastingEnabled()) {
|
||||||
$prefix = Config::getBroadcastChannelPrefix();
|
$prefix = CommentsConfig::getBroadcastChannelPrefix();
|
||||||
$type = $this->model->getMorphClass();
|
$type = $this->model->getMorphClass();
|
||||||
$id = $this->model->getKey();
|
$id = $this->model->getKey();
|
||||||
$channel = "echo-private:{$prefix}.{$type}.{$id}";
|
$channel = "echo-private:{$prefix}.{$type}.{$id}";
|
||||||
@@ -180,26 +212,7 @@ class Comments extends Component
|
|||||||
|
|
||||||
public function refreshComments(): void
|
public function refreshComments(): void
|
||||||
{
|
{
|
||||||
unset($this->comments, $this->totalCount, $this->hasMore);
|
unset($this->comments, $this->totalCount, $this->hasMore, $this->allCommentsCount);
|
||||||
}
|
|
||||||
|
|
||||||
/** @return array<int, array{id: int, name: string, avatar_url: ?string}> */
|
|
||||||
public function searchUsers(string $query): array
|
|
||||||
{
|
|
||||||
if (mb_strlen($query) < 1) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
$resolver = app(MentionResolver::class);
|
|
||||||
|
|
||||||
return $resolver->search($query)
|
|
||||||
->map(fn ($user) => [
|
|
||||||
'id' => $user->getKey(),
|
|
||||||
'name' => $user->getCommentName(),
|
|
||||||
'avatar_url' => $user->getCommentAvatarUrl(),
|
|
||||||
])
|
|
||||||
->values()
|
|
||||||
->all();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function render(): View
|
public function render(): View
|
||||||
|
|||||||
@@ -5,9 +5,9 @@ namespace Relaticle\Comments\Livewire;
|
|||||||
use Illuminate\Contracts\View\View;
|
use Illuminate\Contracts\View\View;
|
||||||
use Livewire\Attributes\Computed;
|
use Livewire\Attributes\Computed;
|
||||||
use Livewire\Component;
|
use Livewire\Component;
|
||||||
use Relaticle\Comments\Comment;
|
use Relaticle\Comments\CommentsConfig;
|
||||||
use Relaticle\Comments\Config;
|
|
||||||
use Relaticle\Comments\Events\CommentReacted;
|
use Relaticle\Comments\Events\CommentReacted;
|
||||||
|
use Relaticle\Comments\Models\Comment;
|
||||||
|
|
||||||
class Reactions extends Component
|
class Reactions extends Component
|
||||||
{
|
{
|
||||||
@@ -22,19 +22,19 @@ class Reactions extends Component
|
|||||||
|
|
||||||
public function toggleReaction(string $reaction): void
|
public function toggleReaction(string $reaction): void
|
||||||
{
|
{
|
||||||
$user = Config::resolveAuthenticatedUser();
|
$user = CommentsConfig::resolveAuthenticatedUser();
|
||||||
|
|
||||||
if (! $user) {
|
if (! $user) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (! in_array($reaction, Config::getAllowedReactions())) {
|
if (! in_array($reaction, CommentsConfig::getAllowedReactions())) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$existing = $this->comment->reactions()
|
$existing = $this->comment->reactions()
|
||||||
->where('user_id', $user->getKey())
|
->where('commenter_id', $user->getKey())
|
||||||
->where('user_type', $user->getMorphClass())
|
->where('commenter_type', $user->getMorphClass())
|
||||||
->where('reaction', $reaction)
|
->where('reaction', $reaction)
|
||||||
->first();
|
->first();
|
||||||
|
|
||||||
@@ -44,8 +44,8 @@ class Reactions extends Component
|
|||||||
event(new CommentReacted($this->comment, $user, $reaction, 'removed'));
|
event(new CommentReacted($this->comment, $user, $reaction, 'removed'));
|
||||||
} else {
|
} else {
|
||||||
$this->comment->reactions()->create([
|
$this->comment->reactions()->create([
|
||||||
'user_id' => $user->getKey(),
|
'commenter_id' => $user->getKey(),
|
||||||
'user_type' => $user->getMorphClass(),
|
'commenter_type' => $user->getMorphClass(),
|
||||||
'reaction' => $reaction,
|
'reaction' => $reaction,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -66,13 +66,13 @@ class Reactions extends Component
|
|||||||
#[Computed]
|
#[Computed]
|
||||||
public function reactionSummary(): array
|
public function reactionSummary(): array
|
||||||
{
|
{
|
||||||
$user = Config::resolveAuthenticatedUser();
|
$user = CommentsConfig::resolveAuthenticatedUser();
|
||||||
$userId = $user?->getKey();
|
$userId = $user?->getKey();
|
||||||
$userType = $user?->getMorphClass();
|
$userType = $user?->getMorphClass();
|
||||||
|
|
||||||
$reactions = $this->comment->reactions()->with('user')->get();
|
$reactions = $this->comment->reactions()->with('commenter')->get();
|
||||||
|
|
||||||
$emojiSet = Config::getReactionEmojiSet();
|
$emojiSet = CommentsConfig::getReactionEmojiSet();
|
||||||
|
|
||||||
return $reactions
|
return $reactions
|
||||||
->groupBy('reaction')
|
->groupBy('reaction')
|
||||||
@@ -81,10 +81,10 @@ class Reactions extends Component
|
|||||||
'reaction' => $key,
|
'reaction' => $key,
|
||||||
'emoji' => $emojiSet[$key] ?? $key,
|
'emoji' => $emojiSet[$key] ?? $key,
|
||||||
'count' => $group->count(),
|
'count' => $group->count(),
|
||||||
'names' => $group->pluck('user.name')->filter()->take(3)->values()->all(),
|
'names' => $group->pluck('commenter.name')->filter()->take(3)->values()->all(),
|
||||||
'total_reactors' => $group->count(),
|
'total_reactors' => $group->count(),
|
||||||
'reacted_by_user' => $group->contains(
|
'reacted_by_user' => $group->contains(
|
||||||
fn ($r) => $r->user_id == $userId && $r->user_type === $userType
|
fn ($r) => $r->commenter_id == $userId && $r->commenter_type === $userType
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ namespace Relaticle\Comments\Mentions;
|
|||||||
|
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
use Illuminate\Support\Collection;
|
use Illuminate\Support\Collection;
|
||||||
use Relaticle\Comments\Config;
|
use Relaticle\Comments\CommentsConfig;
|
||||||
use Relaticle\Comments\Contracts\MentionResolver;
|
use Relaticle\Comments\Contracts\MentionResolver;
|
||||||
|
|
||||||
class DefaultMentionResolver implements MentionResolver
|
class DefaultMentionResolver implements MentionResolver
|
||||||
@@ -12,18 +12,18 @@ class DefaultMentionResolver implements MentionResolver
|
|||||||
/** @return Collection<int, Model> */
|
/** @return Collection<int, Model> */
|
||||||
public function search(string $query): Collection
|
public function search(string $query): Collection
|
||||||
{
|
{
|
||||||
$model = Config::getCommenterModel();
|
$model = CommentsConfig::getCommenterModel();
|
||||||
|
|
||||||
return $model::query()
|
return $model::query()
|
||||||
->where('name', 'like', "{$query}%")
|
->where('name', 'like', "{$query}%")
|
||||||
->limit(Config::getMentionMaxResults())
|
->limit(CommentsConfig::getMentionMaxResults())
|
||||||
->get();
|
->get();
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @return Collection<int, Model> */
|
/** @return Collection<int, Model> */
|
||||||
public function resolveByNames(array $names): Collection
|
public function resolveByNames(array $names): Collection
|
||||||
{
|
{
|
||||||
$model = Config::getCommenterModel();
|
$model = CommentsConfig::getCommenterModel();
|
||||||
|
|
||||||
return $model::query()
|
return $model::query()
|
||||||
->whereIn('name', $names)
|
->whereIn('name', $names)
|
||||||
|
|||||||
@@ -3,10 +3,10 @@
|
|||||||
namespace Relaticle\Comments\Mentions;
|
namespace Relaticle\Comments\Mentions;
|
||||||
|
|
||||||
use Illuminate\Support\Collection;
|
use Illuminate\Support\Collection;
|
||||||
use Relaticle\Comments\Comment;
|
use Relaticle\Comments\CommentsConfig;
|
||||||
use Relaticle\Comments\Config;
|
|
||||||
use Relaticle\Comments\Contracts\MentionResolver;
|
use Relaticle\Comments\Contracts\MentionResolver;
|
||||||
use Relaticle\Comments\Events\UserMentioned;
|
use Relaticle\Comments\Events\UserMentioned;
|
||||||
|
use Relaticle\Comments\Models\Comment;
|
||||||
|
|
||||||
class MentionParser
|
class MentionParser
|
||||||
{
|
{
|
||||||
@@ -16,6 +16,32 @@ class MentionParser
|
|||||||
|
|
||||||
/** @return Collection<int, int> */
|
/** @return Collection<int, int> */
|
||||||
public function parse(string $body): Collection
|
public function parse(string $body): Collection
|
||||||
|
{
|
||||||
|
$ids = $this->parseRichEditorMentions($body);
|
||||||
|
|
||||||
|
if ($ids->isNotEmpty()) {
|
||||||
|
return $ids;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->parsePlainTextMentions($body);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return Collection<int, int> */
|
||||||
|
protected function parseRichEditorMentions(string $body): Collection
|
||||||
|
{
|
||||||
|
preg_match_all('/data-type=["\']mention["\'][^>]*data-id=["\'](\d+)["\']/', $body, $matches);
|
||||||
|
|
||||||
|
if (empty($matches[1])) {
|
||||||
|
preg_match_all('/data-id=["\'](\d+)["\'][^>]*data-type=["\']mention["\']/', $body, $matches);
|
||||||
|
}
|
||||||
|
|
||||||
|
$ids = array_unique(array_map('intval', $matches[1] ?? []));
|
||||||
|
|
||||||
|
return collect($ids);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return Collection<int, int> */
|
||||||
|
protected function parsePlainTextMentions(string $body): Collection
|
||||||
{
|
{
|
||||||
$text = html_entity_decode(strip_tags($body), ENT_QUOTES, 'UTF-8');
|
$text = html_entity_decode(strip_tags($body), ENT_QUOTES, 'UTF-8');
|
||||||
|
|
||||||
@@ -33,13 +59,13 @@ class MentionParser
|
|||||||
public function syncMentions(Comment $comment): void
|
public function syncMentions(Comment $comment): void
|
||||||
{
|
{
|
||||||
$newMentionIds = $this->parse($comment->body);
|
$newMentionIds = $this->parse($comment->body);
|
||||||
$existingMentionIds = $comment->mentions()->pluck('comment_mentions.user_id');
|
$existingMentionIds = $comment->mentions()->pluck('comment_mentions.commenter_id');
|
||||||
|
|
||||||
$addedIds = $newMentionIds->diff($existingMentionIds);
|
$addedIds = $newMentionIds->diff($existingMentionIds);
|
||||||
|
|
||||||
$comment->mentions()->sync($newMentionIds->all());
|
$comment->mentions()->sync($newMentionIds->all());
|
||||||
|
|
||||||
$commenterModel = Config::getCommenterModel();
|
$commenterModel = CommentsConfig::getCommenterModel();
|
||||||
|
|
||||||
$addedIds->each(function ($userId) use ($comment, $commenterModel) {
|
$addedIds->each(function ($userId) use ($comment, $commenterModel) {
|
||||||
$mentionedUser = $commenterModel::find($userId);
|
$mentionedUser = $commenterModel::find($userId);
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace Relaticle\Comments;
|
namespace Relaticle\Comments\Models;
|
||||||
|
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
use Illuminate\Support\Facades\Storage;
|
use Illuminate\Support\Facades\Storage;
|
||||||
use Illuminate\Support\Number;
|
use Illuminate\Support\Number;
|
||||||
|
use Relaticle\Comments\CommentsConfig;
|
||||||
|
|
||||||
class CommentAttachment extends Model
|
class Attachment extends Model
|
||||||
{
|
{
|
||||||
protected $fillable = [
|
protected $fillable = [
|
||||||
'comment_id',
|
'comment_id',
|
||||||
@@ -20,12 +21,12 @@ class CommentAttachment extends Model
|
|||||||
|
|
||||||
public function getTable(): string
|
public function getTable(): string
|
||||||
{
|
{
|
||||||
return 'comment_attachments';
|
return CommentsConfig::getTableName('attachments');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function comment(): BelongsTo
|
public function comment(): BelongsTo
|
||||||
{
|
{
|
||||||
return $this->belongsTo(Config::getCommentModel());
|
return $this->belongsTo(CommentsConfig::getCommentModel());
|
||||||
}
|
}
|
||||||
|
|
||||||
public function isImage(): bool
|
public function isImage(): bool
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace Relaticle\Comments;
|
namespace Relaticle\Comments\Models;
|
||||||
|
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
@@ -9,7 +9,7 @@ use Illuminate\Database\Eloquent\Relations\HasMany;
|
|||||||
use Illuminate\Database\Eloquent\Relations\MorphTo;
|
use Illuminate\Database\Eloquent\Relations\MorphTo;
|
||||||
use Illuminate\Database\Eloquent\Relations\MorphToMany;
|
use Illuminate\Database\Eloquent\Relations\MorphToMany;
|
||||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||||
use Illuminate\Support\Str;
|
use Relaticle\Comments\CommentsConfig;
|
||||||
use Relaticle\Comments\Database\Factories\CommentFactory;
|
use Relaticle\Comments\Database\Factories\CommentFactory;
|
||||||
|
|
||||||
class Comment extends Model
|
class Comment extends Model
|
||||||
@@ -22,7 +22,11 @@ class Comment extends Model
|
|||||||
parent::boot();
|
parent::boot();
|
||||||
|
|
||||||
static::saving(function (self $comment): void {
|
static::saving(function (self $comment): void {
|
||||||
$comment->body = Str::sanitizeHtml($comment->body);
|
$comment->body = app('comments.html_sanitizer')->sanitize($comment->body);
|
||||||
|
});
|
||||||
|
|
||||||
|
static::deleting(function (self $comment): void {
|
||||||
|
$comment->replies()->each(fn ($reply) => $reply->delete());
|
||||||
});
|
});
|
||||||
|
|
||||||
static::forceDeleting(function (self $comment): void {
|
static::forceDeleting(function (self $comment): void {
|
||||||
@@ -35,14 +39,14 @@ class Comment extends Model
|
|||||||
protected $fillable = [
|
protected $fillable = [
|
||||||
'body',
|
'body',
|
||||||
'parent_id',
|
'parent_id',
|
||||||
'user_id',
|
'commenter_id',
|
||||||
'user_type',
|
'commenter_type',
|
||||||
'edited_at',
|
'edited_at',
|
||||||
];
|
];
|
||||||
|
|
||||||
public function getTable(): string
|
public function getTable(): string
|
||||||
{
|
{
|
||||||
return Config::getCommentTable();
|
return CommentsConfig::getCommentTable();
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @return array<string, string> */
|
/** @return array<string, string> */
|
||||||
@@ -63,39 +67,39 @@ class Comment extends Model
|
|||||||
return $this->morphTo();
|
return $this->morphTo();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function user(): MorphTo
|
public function commenter(): MorphTo
|
||||||
{
|
{
|
||||||
return $this->morphTo();
|
return $this->morphTo();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function parent(): BelongsTo
|
public function parent(): BelongsTo
|
||||||
{
|
{
|
||||||
return $this->belongsTo(Config::getCommentModel(), 'parent_id');
|
return $this->belongsTo(CommentsConfig::getCommentModel(), 'parent_id');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function replies(): HasMany
|
public function replies(): HasMany
|
||||||
{
|
{
|
||||||
return $this->hasMany(Config::getCommentModel(), 'parent_id');
|
return $this->hasMany(CommentsConfig::getCommentModel(), 'parent_id');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function reactions(): HasMany
|
public function reactions(): HasMany
|
||||||
{
|
{
|
||||||
return $this->hasMany(CommentReaction::class);
|
return $this->hasMany(Reaction::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function attachments(): HasMany
|
public function attachments(): HasMany
|
||||||
{
|
{
|
||||||
return $this->hasMany(CommentAttachment::class);
|
return $this->hasMany(Attachment::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function mentions(): MorphToMany
|
public function mentions(): MorphToMany
|
||||||
{
|
{
|
||||||
return $this->morphedByMany(
|
return $this->morphedByMany(
|
||||||
Config::getCommenterModel(),
|
CommentsConfig::getCommenterModel(),
|
||||||
'user',
|
'commenter',
|
||||||
'comment_mentions',
|
CommentsConfig::getTableName('mentions'),
|
||||||
'comment_id',
|
'comment_id',
|
||||||
'user_id',
|
'commenter_id',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -121,21 +125,18 @@ class Comment extends Model
|
|||||||
|
|
||||||
public function canReply(): bool
|
public function canReply(): bool
|
||||||
{
|
{
|
||||||
return $this->depth() < Config::getMaxDepth();
|
return $this->depth() < CommentsConfig::getMaxDepth();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function depth(): int
|
public function depth(): int
|
||||||
{
|
{
|
||||||
$depth = 0;
|
$depth = 0;
|
||||||
$comment = $this;
|
$maxDepth = CommentsConfig::getMaxDepth();
|
||||||
|
$parentId = $this->parent_id;
|
||||||
|
|
||||||
while ($comment->parent_id !== null) {
|
while ($parentId !== null && $depth < $maxDepth) {
|
||||||
$comment = $comment->parent;
|
|
||||||
$depth++;
|
$depth++;
|
||||||
|
$parentId = static::where('id', $parentId)->value('parent_id');
|
||||||
if ($depth >= Config::getMaxDepth()) {
|
|
||||||
return Config::getMaxDepth();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return $depth;
|
return $depth;
|
||||||
@@ -144,15 +145,23 @@ class Comment extends Model
|
|||||||
public function renderBodyWithMentions(): string
|
public function renderBodyWithMentions(): string
|
||||||
{
|
{
|
||||||
$body = $this->body;
|
$body = $this->body;
|
||||||
|
|
||||||
$mentionNames = $this->mentions->pluck('name')->filter()->unique();
|
$mentionNames = $this->mentions->pluck('name')->filter()->unique();
|
||||||
|
|
||||||
foreach ($mentionNames as $name) {
|
foreach ($mentionNames as $name) {
|
||||||
$escapedName = e($name);
|
$escapedName = e($name);
|
||||||
$styledSpan = '<span class="comment-mention inline rounded bg-primary-50 px-1 font-medium text-primary-700 dark:bg-primary-900/30 dark:text-primary-300">@'.$escapedName.'</span>';
|
$styledSpan = '<span class="comment-mention">@'.$escapedName.'</span>';
|
||||||
|
|
||||||
|
$pattern = '/<(?:span|a)[^>]*data-type="mention"[^>]*>@?'.preg_quote($escapedName, '/').'<\/(?:span|a)>/';
|
||||||
|
|
||||||
|
if (preg_match($pattern, $body)) {
|
||||||
|
$body = preg_replace($pattern, $styledSpan, $body);
|
||||||
|
} else {
|
||||||
|
// Fallback for plain-text mentions
|
||||||
$body = str_replace("@{$name}", $styledSpan, $body);
|
$body = str_replace("@{$name}", $styledSpan, $body);
|
||||||
$body = str_replace("@{$name}", $styledSpan, $body);
|
$body = str_replace("@{$name}", $styledSpan, $body);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return $body;
|
return $body;
|
||||||
}
|
}
|
||||||
@@ -1,31 +1,32 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace Relaticle\Comments;
|
namespace Relaticle\Comments\Models;
|
||||||
|
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
use Illuminate\Database\Eloquent\Relations\MorphTo;
|
use Illuminate\Database\Eloquent\Relations\MorphTo;
|
||||||
|
use Relaticle\Comments\CommentsConfig;
|
||||||
|
|
||||||
class CommentReaction extends Model
|
class Reaction extends Model
|
||||||
{
|
{
|
||||||
protected $fillable = [
|
protected $fillable = [
|
||||||
'comment_id',
|
'comment_id',
|
||||||
'user_id',
|
'commenter_id',
|
||||||
'user_type',
|
'commenter_type',
|
||||||
'reaction',
|
'reaction',
|
||||||
];
|
];
|
||||||
|
|
||||||
public function getTable(): string
|
public function getTable(): string
|
||||||
{
|
{
|
||||||
return 'comment_reactions';
|
return CommentsConfig::getTableName('reactions');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function comment(): BelongsTo
|
public function comment(): BelongsTo
|
||||||
{
|
{
|
||||||
return $this->belongsTo(Config::getCommentModel());
|
return $this->belongsTo(CommentsConfig::getCommentModel());
|
||||||
}
|
}
|
||||||
|
|
||||||
public function user(): MorphTo
|
public function commenter(): MorphTo
|
||||||
{
|
{
|
||||||
return $this->morphTo();
|
return $this->morphTo();
|
||||||
}
|
}
|
||||||
@@ -1,25 +1,26 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace Relaticle\Comments;
|
namespace Relaticle\Comments\Models;
|
||||||
|
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
use Illuminate\Database\Eloquent\Relations\MorphTo;
|
use Illuminate\Database\Eloquent\Relations\MorphTo;
|
||||||
use Illuminate\Support\Collection;
|
use Illuminate\Support\Collection;
|
||||||
|
use Relaticle\Comments\CommentsConfig;
|
||||||
|
|
||||||
class CommentSubscription extends Model
|
class Subscription extends Model
|
||||||
{
|
{
|
||||||
public const UPDATED_AT = null;
|
public const UPDATED_AT = null;
|
||||||
|
|
||||||
protected $fillable = [
|
protected $fillable = [
|
||||||
'commentable_type',
|
'commentable_type',
|
||||||
'commentable_id',
|
'commentable_id',
|
||||||
'user_type',
|
'commenter_type',
|
||||||
'user_id',
|
'commenter_id',
|
||||||
];
|
];
|
||||||
|
|
||||||
public function getTable(): string
|
public function getTable(): string
|
||||||
{
|
{
|
||||||
return 'comment_subscriptions';
|
return CommentsConfig::getTableName('subscriptions');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function commentable(): MorphTo
|
public function commentable(): MorphTo
|
||||||
@@ -27,7 +28,7 @@ class CommentSubscription extends Model
|
|||||||
return $this->morphTo();
|
return $this->morphTo();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function user(): MorphTo
|
public function commenter(): MorphTo
|
||||||
{
|
{
|
||||||
return $this->morphTo();
|
return $this->morphTo();
|
||||||
}
|
}
|
||||||
@@ -37,8 +38,8 @@ class CommentSubscription extends Model
|
|||||||
return static::where([
|
return static::where([
|
||||||
'commentable_type' => $commentable->getMorphClass(),
|
'commentable_type' => $commentable->getMorphClass(),
|
||||||
'commentable_id' => $commentable->getKey(),
|
'commentable_id' => $commentable->getKey(),
|
||||||
'user_type' => $user->getMorphClass(),
|
'commenter_type' => $user->getMorphClass(),
|
||||||
'user_id' => $user->getKey(),
|
'commenter_id' => $user->getKey(),
|
||||||
])->exists();
|
])->exists();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -47,8 +48,8 @@ class CommentSubscription extends Model
|
|||||||
static::firstOrCreate([
|
static::firstOrCreate([
|
||||||
'commentable_type' => $commentable->getMorphClass(),
|
'commentable_type' => $commentable->getMorphClass(),
|
||||||
'commentable_id' => $commentable->getKey(),
|
'commentable_id' => $commentable->getKey(),
|
||||||
'user_type' => $user->getMorphClass(),
|
'commenter_type' => $user->getMorphClass(),
|
||||||
'user_id' => $user->getKey(),
|
'commenter_id' => $user->getKey(),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -57,8 +58,8 @@ class CommentSubscription extends Model
|
|||||||
static::where([
|
static::where([
|
||||||
'commentable_type' => $commentable->getMorphClass(),
|
'commentable_type' => $commentable->getMorphClass(),
|
||||||
'commentable_id' => $commentable->getKey(),
|
'commentable_id' => $commentable->getKey(),
|
||||||
'user_type' => $user->getMorphClass(),
|
'commenter_type' => $user->getMorphClass(),
|
||||||
'user_id' => $user->getKey(),
|
'commenter_id' => $user->getKey(),
|
||||||
])->delete();
|
])->delete();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -68,6 +69,6 @@ class CommentSubscription extends Model
|
|||||||
return static::where([
|
return static::where([
|
||||||
'commentable_type' => $commentable->getMorphClass(),
|
'commentable_type' => $commentable->getMorphClass(),
|
||||||
'commentable_id' => $commentable->getKey(),
|
'commentable_id' => $commentable->getKey(),
|
||||||
])->with('user')->get()->pluck('user')->filter()->values();
|
])->with('commenter')->get()->pluck('commenter')->filter()->values();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -5,8 +5,8 @@ namespace Relaticle\Comments\Notifications;
|
|||||||
use Illuminate\Notifications\Messages\MailMessage;
|
use Illuminate\Notifications\Messages\MailMessage;
|
||||||
use Illuminate\Notifications\Notification;
|
use Illuminate\Notifications\Notification;
|
||||||
use Illuminate\Support\Str;
|
use Illuminate\Support\Str;
|
||||||
use Relaticle\Comments\Comment;
|
use Relaticle\Comments\CommentsConfig;
|
||||||
use Relaticle\Comments\Config;
|
use Relaticle\Comments\Models\Comment;
|
||||||
|
|
||||||
class CommentRepliedNotification extends Notification
|
class CommentRepliedNotification extends Notification
|
||||||
{
|
{
|
||||||
@@ -15,7 +15,7 @@ class CommentRepliedNotification extends Notification
|
|||||||
/** @return array<int, string> */
|
/** @return array<int, string> */
|
||||||
public function via(mixed $notifiable): array
|
public function via(mixed $notifiable): array
|
||||||
{
|
{
|
||||||
return Config::getNotificationChannels();
|
return CommentsConfig::getNotificationChannels();
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @return array<string, mixed> */
|
/** @return array<string, mixed> */
|
||||||
@@ -25,14 +25,14 @@ class CommentRepliedNotification extends Notification
|
|||||||
'comment_id' => $this->comment->id,
|
'comment_id' => $this->comment->id,
|
||||||
'commentable_type' => $this->comment->commentable_type,
|
'commentable_type' => $this->comment->commentable_type,
|
||||||
'commentable_id' => $this->comment->commentable_id,
|
'commentable_id' => $this->comment->commentable_id,
|
||||||
'commenter_name' => $this->comment->user->getCommentName(),
|
'commenter_name' => $this->comment->commenter->getCommentDisplayName(),
|
||||||
'body' => Str::limit(strip_tags($this->comment->body), 100),
|
'body' => Str::limit(strip_tags($this->comment->body), 100),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
public function toMail(mixed $notifiable): MailMessage
|
public function toMail(mixed $notifiable): MailMessage
|
||||||
{
|
{
|
||||||
$commenterName = $this->comment->user->getCommentName();
|
$commenterName = $this->comment->commenter->getCommentDisplayName();
|
||||||
|
|
||||||
return (new MailMessage)
|
return (new MailMessage)
|
||||||
->subject('New reply to your comment')
|
->subject('New reply to your comment')
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ use Illuminate\Database\Eloquent\Model;
|
|||||||
use Illuminate\Notifications\Messages\MailMessage;
|
use Illuminate\Notifications\Messages\MailMessage;
|
||||||
use Illuminate\Notifications\Notification;
|
use Illuminate\Notifications\Notification;
|
||||||
use Illuminate\Support\Str;
|
use Illuminate\Support\Str;
|
||||||
use Relaticle\Comments\Comment;
|
use Relaticle\Comments\CommentsConfig;
|
||||||
use Relaticle\Comments\Config;
|
use Relaticle\Comments\Models\Comment;
|
||||||
|
|
||||||
class UserMentionedNotification extends Notification
|
class UserMentionedNotification extends Notification
|
||||||
{
|
{
|
||||||
@@ -19,7 +19,7 @@ class UserMentionedNotification extends Notification
|
|||||||
/** @return array<int, string> */
|
/** @return array<int, string> */
|
||||||
public function via(mixed $notifiable): array
|
public function via(mixed $notifiable): array
|
||||||
{
|
{
|
||||||
return Config::getNotificationChannels();
|
return CommentsConfig::getNotificationChannels();
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @return array<string, mixed> */
|
/** @return array<string, mixed> */
|
||||||
@@ -29,14 +29,14 @@ class UserMentionedNotification extends Notification
|
|||||||
'comment_id' => $this->comment->id,
|
'comment_id' => $this->comment->id,
|
||||||
'commentable_type' => $this->comment->commentable_type,
|
'commentable_type' => $this->comment->commentable_type,
|
||||||
'commentable_id' => $this->comment->commentable_id,
|
'commentable_id' => $this->comment->commentable_id,
|
||||||
'mentioner_name' => $this->mentionedBy->getCommentName(),
|
'mentioner_name' => $this->mentionedBy->getCommentDisplayName(),
|
||||||
'body' => Str::limit(strip_tags($this->comment->body), 100),
|
'body' => Str::limit(strip_tags($this->comment->body), 100),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
public function toMail(mixed $notifiable): MailMessage
|
public function toMail(mixed $notifiable): MailMessage
|
||||||
{
|
{
|
||||||
$mentionerName = $this->mentionedBy->getCommentName();
|
$mentionerName = $this->mentionedBy->getCommentDisplayName();
|
||||||
|
|
||||||
return (new MailMessage)
|
return (new MailMessage)
|
||||||
->subject('You were mentioned in a comment')
|
->subject('You were mentioned in a comment')
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
namespace Relaticle\Comments\Policies;
|
namespace Relaticle\Comments\Policies;
|
||||||
|
|
||||||
use Illuminate\Contracts\Auth\Authenticatable;
|
use Illuminate\Contracts\Auth\Authenticatable;
|
||||||
use Relaticle\Comments\Comment;
|
use Relaticle\Comments\Models\Comment;
|
||||||
|
|
||||||
class CommentPolicy
|
class CommentPolicy
|
||||||
{
|
{
|
||||||
@@ -19,14 +19,14 @@ class CommentPolicy
|
|||||||
|
|
||||||
public function update(Authenticatable $user, Comment $comment): bool
|
public function update(Authenticatable $user, Comment $comment): bool
|
||||||
{
|
{
|
||||||
return $user->getKey() === $comment->user_id
|
return $user->getKey() === $comment->commenter_id
|
||||||
&& $user->getMorphClass() === $comment->user_type;
|
&& $user->getMorphClass() === $comment->commenter_type;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function delete(Authenticatable $user, Comment $comment): bool
|
public function delete(Authenticatable $user, Comment $comment): bool
|
||||||
{
|
{
|
||||||
return $user->getKey() === $comment->user_id
|
return $user->getKey() === $comment->commenter_id
|
||||||
&& $user->getMorphClass() === $comment->user_type;
|
&& $user->getMorphClass() === $comment->commenter_type;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function reply(Authenticatable $user, Comment $comment): bool
|
public function reply(Authenticatable $user, Comment $comment): bool
|
||||||
|
|||||||
@@ -3,11 +3,11 @@
|
|||||||
use Illuminate\Http\UploadedFile;
|
use Illuminate\Http\UploadedFile;
|
||||||
use Illuminate\Support\Facades\Storage;
|
use Illuminate\Support\Facades\Storage;
|
||||||
use Livewire\Livewire;
|
use Livewire\Livewire;
|
||||||
use Relaticle\Comments\Comment;
|
use Relaticle\Comments\CommentsConfig;
|
||||||
use Relaticle\Comments\CommentAttachment;
|
|
||||||
use Relaticle\Comments\Config;
|
|
||||||
use Relaticle\Comments\Livewire\CommentItem;
|
use Relaticle\Comments\Livewire\CommentItem;
|
||||||
use Relaticle\Comments\Livewire\Comments;
|
use Relaticle\Comments\Livewire\Comments;
|
||||||
|
use Relaticle\Comments\Models\Attachment;
|
||||||
|
use Relaticle\Comments\Models\Comment;
|
||||||
use Relaticle\Comments\Tests\Models\Post;
|
use Relaticle\Comments\Tests\Models\Post;
|
||||||
use Relaticle\Comments\Tests\Models\User;
|
use Relaticle\Comments\Tests\Models\User;
|
||||||
|
|
||||||
@@ -22,14 +22,13 @@ it('creates comment with file attachment via Livewire component', function () {
|
|||||||
$file = UploadedFile::fake()->image('photo.jpg', 100, 100);
|
$file = UploadedFile::fake()->image('photo.jpg', 100, 100);
|
||||||
|
|
||||||
Livewire::test(Comments::class, ['model' => $post])
|
Livewire::test(Comments::class, ['model' => $post])
|
||||||
->set('newComment', '<p>Comment with attachment</p>')
|
->set('commentData.body', '<p>Comment with attachment</p>')
|
||||||
->set('attachments', [$file])
|
->set('attachments', [$file])
|
||||||
->call('addComment')
|
->call('addComment')
|
||||||
->assertSet('newComment', '')
|
|
||||||
->assertSet('attachments', []);
|
->assertSet('attachments', []);
|
||||||
|
|
||||||
expect(Comment::count())->toBe(1);
|
expect(Comment::count())->toBe(1);
|
||||||
expect(CommentAttachment::count())->toBe(1);
|
expect(Attachment::count())->toBe(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('stores attachment with correct metadata', function () {
|
it('stores attachment with correct metadata', function () {
|
||||||
@@ -43,11 +42,11 @@ it('stores attachment with correct metadata', function () {
|
|||||||
$file = UploadedFile::fake()->image('vacation.jpg', 200, 200)->size(512);
|
$file = UploadedFile::fake()->image('vacation.jpg', 200, 200)->size(512);
|
||||||
|
|
||||||
Livewire::test(Comments::class, ['model' => $post])
|
Livewire::test(Comments::class, ['model' => $post])
|
||||||
->set('newComment', '<p>Vacation photos</p>')
|
->set('commentData.body', '<p>Vacation photos</p>')
|
||||||
->set('attachments', [$file])
|
->set('attachments', [$file])
|
||||||
->call('addComment');
|
->call('addComment');
|
||||||
|
|
||||||
$attachment = CommentAttachment::first();
|
$attachment = Attachment::first();
|
||||||
$comment = Comment::first();
|
$comment = Comment::first();
|
||||||
|
|
||||||
expect($attachment->original_name)->toBe('vacation.jpg')
|
expect($attachment->original_name)->toBe('vacation.jpg')
|
||||||
@@ -69,11 +68,11 @@ it('stores file on configured disk at comments/attachments/{comment_id}/ path',
|
|||||||
$file = UploadedFile::fake()->image('test.png', 50, 50);
|
$file = UploadedFile::fake()->image('test.png', 50, 50);
|
||||||
|
|
||||||
Livewire::test(Comments::class, ['model' => $post])
|
Livewire::test(Comments::class, ['model' => $post])
|
||||||
->set('newComment', '<p>File path test</p>')
|
->set('commentData.body', '<p>File path test</p>')
|
||||||
->set('attachments', [$file])
|
->set('attachments', [$file])
|
||||||
->call('addComment');
|
->call('addComment');
|
||||||
|
|
||||||
$attachment = CommentAttachment::first();
|
$attachment = Attachment::first();
|
||||||
|
|
||||||
Storage::disk('public')->assertExists($attachment->file_path);
|
Storage::disk('public')->assertExists($attachment->file_path);
|
||||||
expect($attachment->file_path)->toContain("comments/attachments/{$attachment->comment_id}/");
|
expect($attachment->file_path)->toContain("comments/attachments/{$attachment->comment_id}/");
|
||||||
@@ -88,15 +87,15 @@ it('displays image attachment thumbnail in comment item view', function () {
|
|||||||
$comment = Comment::factory()->create([
|
$comment = Comment::factory()->create([
|
||||||
'commentable_id' => $post->id,
|
'commentable_id' => $post->id,
|
||||||
'commentable_type' => $post->getMorphClass(),
|
'commentable_type' => $post->getMorphClass(),
|
||||||
'user_id' => $user->getKey(),
|
'commenter_id' => $user->getKey(),
|
||||||
'user_type' => $user->getMorphClass(),
|
'commenter_type' => $user->getMorphClass(),
|
||||||
'body' => '<p>Image comment</p>',
|
'body' => '<p>Image comment</p>',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$file = UploadedFile::fake()->image('photo.jpg', 100, 100);
|
$file = UploadedFile::fake()->image('photo.jpg', 100, 100);
|
||||||
$path = $file->store("comments/attachments/{$comment->id}", 'public');
|
$path = $file->store("comments/attachments/{$comment->id}", 'public');
|
||||||
|
|
||||||
CommentAttachment::create([
|
Attachment::create([
|
||||||
'comment_id' => $comment->id,
|
'comment_id' => $comment->id,
|
||||||
'file_path' => $path,
|
'file_path' => $path,
|
||||||
'original_name' => 'photo.jpg',
|
'original_name' => 'photo.jpg',
|
||||||
@@ -123,15 +122,15 @@ it('displays non-image attachment as download link', function () {
|
|||||||
$comment = Comment::factory()->create([
|
$comment = Comment::factory()->create([
|
||||||
'commentable_id' => $post->id,
|
'commentable_id' => $post->id,
|
||||||
'commentable_type' => $post->getMorphClass(),
|
'commentable_type' => $post->getMorphClass(),
|
||||||
'user_id' => $user->getKey(),
|
'commenter_id' => $user->getKey(),
|
||||||
'user_type' => $user->getMorphClass(),
|
'commenter_type' => $user->getMorphClass(),
|
||||||
'body' => '<p>PDF comment</p>',
|
'body' => '<p>PDF comment</p>',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$file = UploadedFile::fake()->create('document.pdf', 2048, 'application/pdf');
|
$file = UploadedFile::fake()->create('document.pdf', 2048, 'application/pdf');
|
||||||
$path = $file->store("comments/attachments/{$comment->id}", 'public');
|
$path = $file->store("comments/attachments/{$comment->id}", 'public');
|
||||||
|
|
||||||
CommentAttachment::create([
|
Attachment::create([
|
||||||
'comment_id' => $comment->id,
|
'comment_id' => $comment->id,
|
||||||
'file_path' => $path,
|
'file_path' => $path,
|
||||||
'original_name' => 'document.pdf',
|
'original_name' => 'document.pdf',
|
||||||
@@ -157,16 +156,16 @@ it('rejects file exceeding max size', function () {
|
|||||||
|
|
||||||
$this->actingAs($user);
|
$this->actingAs($user);
|
||||||
|
|
||||||
$oversizedFile = UploadedFile::fake()->create('big.pdf', Config::getAttachmentMaxSize() + 1, 'application/pdf');
|
$oversizedFile = UploadedFile::fake()->create('big.pdf', CommentsConfig::getAttachmentMaxSize() + 1, 'application/pdf');
|
||||||
|
|
||||||
Livewire::test(Comments::class, ['model' => $post])
|
Livewire::test(Comments::class, ['model' => $post])
|
||||||
->set('newComment', '<p>Oversized file</p>')
|
->set('commentData.body', '<p>Oversized file</p>')
|
||||||
->set('attachments', [$oversizedFile])
|
->set('attachments', [$oversizedFile])
|
||||||
->call('addComment')
|
->call('addComment')
|
||||||
->assertHasErrors('attachments.0');
|
->assertHasErrors('attachments.0');
|
||||||
|
|
||||||
expect(Comment::count())->toBe(0);
|
expect(Comment::count())->toBe(0);
|
||||||
expect(CommentAttachment::count())->toBe(0);
|
expect(Attachment::count())->toBe(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('rejects disallowed file type', function () {
|
it('rejects disallowed file type', function () {
|
||||||
@@ -180,13 +179,13 @@ it('rejects disallowed file type', function () {
|
|||||||
$exeFile = UploadedFile::fake()->create('script.exe', 100, 'application/x-msdownload');
|
$exeFile = UploadedFile::fake()->create('script.exe', 100, 'application/x-msdownload');
|
||||||
|
|
||||||
Livewire::test(Comments::class, ['model' => $post])
|
Livewire::test(Comments::class, ['model' => $post])
|
||||||
->set('newComment', '<p>Malicious file</p>')
|
->set('commentData.body', '<p>Malicious file</p>')
|
||||||
->set('attachments', [$exeFile])
|
->set('attachments', [$exeFile])
|
||||||
->call('addComment')
|
->call('addComment')
|
||||||
->assertHasErrors('attachments.0');
|
->assertHasErrors('attachments.0');
|
||||||
|
|
||||||
expect(Comment::count())->toBe(0);
|
expect(Comment::count())->toBe(0);
|
||||||
expect(CommentAttachment::count())->toBe(0);
|
expect(Attachment::count())->toBe(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('accepts allowed file types', function () {
|
it('accepts allowed file types', function () {
|
||||||
@@ -200,13 +199,13 @@ it('accepts allowed file types', function () {
|
|||||||
$imageFile = UploadedFile::fake()->image('photo.jpg', 100, 100);
|
$imageFile = UploadedFile::fake()->image('photo.jpg', 100, 100);
|
||||||
|
|
||||||
Livewire::test(Comments::class, ['model' => $post])
|
Livewire::test(Comments::class, ['model' => $post])
|
||||||
->set('newComment', '<p>Valid file</p>')
|
->set('commentData.body', '<p>Valid file</p>')
|
||||||
->set('attachments', [$imageFile])
|
->set('attachments', [$imageFile])
|
||||||
->call('addComment')
|
->call('addComment')
|
||||||
->assertHasNoErrors('attachments.0');
|
->assertHasNoErrors('attachments.0');
|
||||||
|
|
||||||
expect(Comment::count())->toBe(1);
|
expect(Comment::count())->toBe(1);
|
||||||
expect(CommentAttachment::count())->toBe(1);
|
expect(Attachment::count())->toBe(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('hides upload UI when attachments disabled', function () {
|
it('hides upload UI when attachments disabled', function () {
|
||||||
@@ -218,7 +217,7 @@ it('hides upload UI when attachments disabled', function () {
|
|||||||
$this->actingAs($user);
|
$this->actingAs($user);
|
||||||
|
|
||||||
Livewire::test(Comments::class, ['model' => $post])
|
Livewire::test(Comments::class, ['model' => $post])
|
||||||
->assertDontSeeHtml('Attach files');
|
->assertDontSeeHtml('wire:model="attachments"');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('shows upload UI when attachments enabled', function () {
|
it('shows upload UI when attachments enabled', function () {
|
||||||
@@ -228,7 +227,7 @@ it('shows upload UI when attachments enabled', function () {
|
|||||||
$this->actingAs($user);
|
$this->actingAs($user);
|
||||||
|
|
||||||
Livewire::test(Comments::class, ['model' => $post])
|
Livewire::test(Comments::class, ['model' => $post])
|
||||||
->assertSeeHtml('Attach files');
|
->assertSeeHtml('wire:model="attachments"');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('creates comment with multiple file attachments', function () {
|
it('creates comment with multiple file attachments', function () {
|
||||||
@@ -243,14 +242,14 @@ it('creates comment with multiple file attachments', function () {
|
|||||||
$file2 = UploadedFile::fake()->create('notes.pdf', 512, 'application/pdf');
|
$file2 = UploadedFile::fake()->create('notes.pdf', 512, 'application/pdf');
|
||||||
|
|
||||||
Livewire::test(Comments::class, ['model' => $post])
|
Livewire::test(Comments::class, ['model' => $post])
|
||||||
->set('newComment', '<p>Multiple files</p>')
|
->set('commentData.body', '<p>Multiple files</p>')
|
||||||
->set('attachments', [$file1, $file2])
|
->set('attachments', [$file1, $file2])
|
||||||
->call('addComment');
|
->call('addComment');
|
||||||
|
|
||||||
expect(Comment::count())->toBe(1);
|
expect(Comment::count())->toBe(1);
|
||||||
expect(CommentAttachment::count())->toBe(2);
|
expect(Attachment::count())->toBe(2);
|
||||||
|
|
||||||
$attachments = CommentAttachment::all();
|
$attachments = Attachment::all();
|
||||||
expect($attachments->pluck('original_name')->toArray())
|
expect($attachments->pluck('original_name')->toArray())
|
||||||
->toContain('photo1.jpg')
|
->toContain('photo1.jpg')
|
||||||
->toContain('notes.pdf');
|
->toContain('notes.pdf');
|
||||||
@@ -265,8 +264,8 @@ it('creates reply with file attachment via CommentItem component', function () {
|
|||||||
$comment = Comment::factory()->create([
|
$comment = Comment::factory()->create([
|
||||||
'commentable_id' => $post->id,
|
'commentable_id' => $post->id,
|
||||||
'commentable_type' => $post->getMorphClass(),
|
'commentable_type' => $post->getMorphClass(),
|
||||||
'user_id' => $user->getKey(),
|
'commenter_id' => $user->getKey(),
|
||||||
'user_type' => $user->getMorphClass(),
|
'commenter_type' => $user->getMorphClass(),
|
||||||
'body' => '<p>Parent comment</p>',
|
'body' => '<p>Parent comment</p>',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -276,11 +275,10 @@ it('creates reply with file attachment via CommentItem component', function () {
|
|||||||
|
|
||||||
Livewire::test(CommentItem::class, ['comment' => $comment])
|
Livewire::test(CommentItem::class, ['comment' => $comment])
|
||||||
->call('startReply')
|
->call('startReply')
|
||||||
->set('replyBody', '<p>Reply with attachment</p>')
|
->set('replyData.body', '<p>Reply with attachment</p>')
|
||||||
->set('replyAttachments', [$file])
|
->set('replyAttachments', [$file])
|
||||||
->call('addReply')
|
->call('addReply')
|
||||||
->assertSet('isReplying', false)
|
->assertSet('isReplying', false)
|
||||||
->assertSet('replyBody', '')
|
|
||||||
->assertSet('replyAttachments', []);
|
->assertSet('replyAttachments', []);
|
||||||
|
|
||||||
$reply = Comment::where('parent_id', $comment->id)->first();
|
$reply = Comment::where('parent_id', $comment->id)->first();
|
||||||
|
|||||||
@@ -2,11 +2,11 @@
|
|||||||
|
|
||||||
use Illuminate\Broadcasting\PrivateChannel;
|
use Illuminate\Broadcasting\PrivateChannel;
|
||||||
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
|
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
|
||||||
use Relaticle\Comments\Comment;
|
|
||||||
use Relaticle\Comments\Events\CommentCreated;
|
use Relaticle\Comments\Events\CommentCreated;
|
||||||
use Relaticle\Comments\Events\CommentDeleted;
|
use Relaticle\Comments\Events\CommentDeleted;
|
||||||
use Relaticle\Comments\Events\CommentReacted;
|
use Relaticle\Comments\Events\CommentReacted;
|
||||||
use Relaticle\Comments\Events\CommentUpdated;
|
use Relaticle\Comments\Events\CommentUpdated;
|
||||||
|
use Relaticle\Comments\Models\Comment;
|
||||||
use Relaticle\Comments\Tests\Models\Post;
|
use Relaticle\Comments\Tests\Models\Post;
|
||||||
use Relaticle\Comments\Tests\Models\User;
|
use Relaticle\Comments\Tests\Models\User;
|
||||||
|
|
||||||
@@ -17,8 +17,8 @@ it('CommentCreated event implements ShouldBroadcast', function () {
|
|||||||
$comment = Comment::factory()->create([
|
$comment = Comment::factory()->create([
|
||||||
'commentable_id' => $post->id,
|
'commentable_id' => $post->id,
|
||||||
'commentable_type' => $post->getMorphClass(),
|
'commentable_type' => $post->getMorphClass(),
|
||||||
'user_id' => $user->getKey(),
|
'commenter_id' => $user->getKey(),
|
||||||
'user_type' => $user->getMorphClass(),
|
'commenter_type' => $user->getMorphClass(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$event = new CommentCreated($comment);
|
$event = new CommentCreated($comment);
|
||||||
@@ -33,8 +33,8 @@ it('CommentUpdated event implements ShouldBroadcast', function () {
|
|||||||
$comment = Comment::factory()->create([
|
$comment = Comment::factory()->create([
|
||||||
'commentable_id' => $post->id,
|
'commentable_id' => $post->id,
|
||||||
'commentable_type' => $post->getMorphClass(),
|
'commentable_type' => $post->getMorphClass(),
|
||||||
'user_id' => $user->getKey(),
|
'commenter_id' => $user->getKey(),
|
||||||
'user_type' => $user->getMorphClass(),
|
'commenter_type' => $user->getMorphClass(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$event = new CommentUpdated($comment);
|
$event = new CommentUpdated($comment);
|
||||||
@@ -49,8 +49,8 @@ it('CommentDeleted event implements ShouldBroadcast', function () {
|
|||||||
$comment = Comment::factory()->create([
|
$comment = Comment::factory()->create([
|
||||||
'commentable_id' => $post->id,
|
'commentable_id' => $post->id,
|
||||||
'commentable_type' => $post->getMorphClass(),
|
'commentable_type' => $post->getMorphClass(),
|
||||||
'user_id' => $user->getKey(),
|
'commenter_id' => $user->getKey(),
|
||||||
'user_type' => $user->getMorphClass(),
|
'commenter_type' => $user->getMorphClass(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$event = new CommentDeleted($comment);
|
$event = new CommentDeleted($comment);
|
||||||
@@ -65,8 +65,8 @@ it('CommentReacted event implements ShouldBroadcast', function () {
|
|||||||
$comment = Comment::factory()->create([
|
$comment = Comment::factory()->create([
|
||||||
'commentable_id' => $post->id,
|
'commentable_id' => $post->id,
|
||||||
'commentable_type' => $post->getMorphClass(),
|
'commentable_type' => $post->getMorphClass(),
|
||||||
'user_id' => $user->getKey(),
|
'commenter_id' => $user->getKey(),
|
||||||
'user_type' => $user->getMorphClass(),
|
'commenter_type' => $user->getMorphClass(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$event = new CommentReacted($comment, $user, 'thumbs_up', 'added');
|
$event = new CommentReacted($comment, $user, 'thumbs_up', 'added');
|
||||||
@@ -81,8 +81,8 @@ it('broadcastOn returns PrivateChannel with correct channel name', function () {
|
|||||||
$comment = Comment::factory()->create([
|
$comment = Comment::factory()->create([
|
||||||
'commentable_id' => $post->id,
|
'commentable_id' => $post->id,
|
||||||
'commentable_type' => $post->getMorphClass(),
|
'commentable_type' => $post->getMorphClass(),
|
||||||
'user_id' => $user->getKey(),
|
'commenter_id' => $user->getKey(),
|
||||||
'user_type' => $user->getMorphClass(),
|
'commenter_type' => $user->getMorphClass(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$event = new CommentCreated($comment);
|
$event = new CommentCreated($comment);
|
||||||
@@ -100,8 +100,8 @@ it('broadcastWhen returns false when broadcasting is disabled', function () {
|
|||||||
$comment = Comment::factory()->create([
|
$comment = Comment::factory()->create([
|
||||||
'commentable_id' => $post->id,
|
'commentable_id' => $post->id,
|
||||||
'commentable_type' => $post->getMorphClass(),
|
'commentable_type' => $post->getMorphClass(),
|
||||||
'user_id' => $user->getKey(),
|
'commenter_id' => $user->getKey(),
|
||||||
'user_type' => $user->getMorphClass(),
|
'commenter_type' => $user->getMorphClass(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$event = new CommentCreated($comment);
|
$event = new CommentCreated($comment);
|
||||||
@@ -118,8 +118,8 @@ it('broadcastWhen returns true when broadcasting is enabled', function () {
|
|||||||
$comment = Comment::factory()->create([
|
$comment = Comment::factory()->create([
|
||||||
'commentable_id' => $post->id,
|
'commentable_id' => $post->id,
|
||||||
'commentable_type' => $post->getMorphClass(),
|
'commentable_type' => $post->getMorphClass(),
|
||||||
'user_id' => $user->getKey(),
|
'commenter_id' => $user->getKey(),
|
||||||
'user_type' => $user->getMorphClass(),
|
'commenter_type' => $user->getMorphClass(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$event = new CommentCreated($comment);
|
$event = new CommentCreated($comment);
|
||||||
@@ -134,8 +134,8 @@ it('broadcastWith returns array with comment_id for CommentCreated', function ()
|
|||||||
$comment = Comment::factory()->create([
|
$comment = Comment::factory()->create([
|
||||||
'commentable_id' => $post->id,
|
'commentable_id' => $post->id,
|
||||||
'commentable_type' => $post->getMorphClass(),
|
'commentable_type' => $post->getMorphClass(),
|
||||||
'user_id' => $user->getKey(),
|
'commenter_id' => $user->getKey(),
|
||||||
'user_type' => $user->getMorphClass(),
|
'commenter_type' => $user->getMorphClass(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$event = new CommentCreated($comment);
|
$event = new CommentCreated($comment);
|
||||||
@@ -154,8 +154,8 @@ it('broadcastWith returns array with comment_id, reaction, and action for Commen
|
|||||||
$comment = Comment::factory()->create([
|
$comment = Comment::factory()->create([
|
||||||
'commentable_id' => $post->id,
|
'commentable_id' => $post->id,
|
||||||
'commentable_type' => $post->getMorphClass(),
|
'commentable_type' => $post->getMorphClass(),
|
||||||
'user_id' => $user->getKey(),
|
'commenter_id' => $user->getKey(),
|
||||||
'user_type' => $user->getMorphClass(),
|
'commenter_type' => $user->getMorphClass(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$event = new CommentReacted($comment, $user, 'thumbs_up', 'added');
|
$event = new CommentReacted($comment, $user, 'thumbs_up', 'added');
|
||||||
@@ -176,8 +176,8 @@ it('uses custom channel prefix from config in broadcastOn', function () {
|
|||||||
$comment = Comment::factory()->create([
|
$comment = Comment::factory()->create([
|
||||||
'commentable_id' => $post->id,
|
'commentable_id' => $post->id,
|
||||||
'commentable_type' => $post->getMorphClass(),
|
'commentable_type' => $post->getMorphClass(),
|
||||||
'user_id' => $user->getKey(),
|
'commenter_id' => $user->getKey(),
|
||||||
'user_type' => $user->getMorphClass(),
|
'commenter_type' => $user->getMorphClass(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$event = new CommentCreated($comment);
|
$event = new CommentCreated($comment);
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
use Relaticle\Comments\Comment;
|
use Relaticle\Comments\CommentsConfig;
|
||||||
use Relaticle\Comments\CommentAttachment;
|
use Relaticle\Comments\Models\Attachment;
|
||||||
use Relaticle\Comments\Config;
|
use Relaticle\Comments\Models\Comment;
|
||||||
use Relaticle\Comments\Tests\Models\Post;
|
use Relaticle\Comments\Tests\Models\Post;
|
||||||
use Relaticle\Comments\Tests\Models\User;
|
use Relaticle\Comments\Tests\Models\User;
|
||||||
|
|
||||||
@@ -13,12 +13,12 @@ it('creates a comment attachment with all metadata fields', function () {
|
|||||||
$comment = Comment::factory()->create([
|
$comment = Comment::factory()->create([
|
||||||
'commentable_id' => $post->id,
|
'commentable_id' => $post->id,
|
||||||
'commentable_type' => $post->getMorphClass(),
|
'commentable_type' => $post->getMorphClass(),
|
||||||
'user_id' => $user->getKey(),
|
'commenter_id' => $user->getKey(),
|
||||||
'user_type' => $user->getMorphClass(),
|
'commenter_type' => $user->getMorphClass(),
|
||||||
'body' => '<p>Test comment</p>',
|
'body' => '<p>Test comment</p>',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$attachment = CommentAttachment::create([
|
$attachment = Attachment::create([
|
||||||
'comment_id' => $comment->id,
|
'comment_id' => $comment->id,
|
||||||
'file_path' => 'comments/attachments/1/photo.jpg',
|
'file_path' => 'comments/attachments/1/photo.jpg',
|
||||||
'original_name' => 'photo.jpg',
|
'original_name' => 'photo.jpg',
|
||||||
@@ -27,7 +27,7 @@ it('creates a comment attachment with all metadata fields', function () {
|
|||||||
'disk' => 'public',
|
'disk' => 'public',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
expect($attachment)->toBeInstanceOf(CommentAttachment::class)
|
expect($attachment)->toBeInstanceOf(Attachment::class)
|
||||||
->and($attachment->file_path)->toBe('comments/attachments/1/photo.jpg')
|
->and($attachment->file_path)->toBe('comments/attachments/1/photo.jpg')
|
||||||
->and($attachment->original_name)->toBe('photo.jpg')
|
->and($attachment->original_name)->toBe('photo.jpg')
|
||||||
->and($attachment->mime_type)->toBe('image/jpeg')
|
->and($attachment->mime_type)->toBe('image/jpeg')
|
||||||
@@ -42,12 +42,12 @@ it('belongs to a comment via comment() relationship', function () {
|
|||||||
$comment = Comment::factory()->create([
|
$comment = Comment::factory()->create([
|
||||||
'commentable_id' => $post->id,
|
'commentable_id' => $post->id,
|
||||||
'commentable_type' => $post->getMorphClass(),
|
'commentable_type' => $post->getMorphClass(),
|
||||||
'user_id' => $user->getKey(),
|
'commenter_id' => $user->getKey(),
|
||||||
'user_type' => $user->getMorphClass(),
|
'commenter_type' => $user->getMorphClass(),
|
||||||
'body' => '<p>Test</p>',
|
'body' => '<p>Test</p>',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$attachment = CommentAttachment::create([
|
$attachment = Attachment::create([
|
||||||
'comment_id' => $comment->id,
|
'comment_id' => $comment->id,
|
||||||
'file_path' => 'comments/attachments/1/test.png',
|
'file_path' => 'comments/attachments/1/test.png',
|
||||||
'original_name' => 'test.png',
|
'original_name' => 'test.png',
|
||||||
@@ -67,12 +67,12 @@ it('has attachments() hasMany relationship on Comment', function () {
|
|||||||
$comment = Comment::factory()->create([
|
$comment = Comment::factory()->create([
|
||||||
'commentable_id' => $post->id,
|
'commentable_id' => $post->id,
|
||||||
'commentable_type' => $post->getMorphClass(),
|
'commentable_type' => $post->getMorphClass(),
|
||||||
'user_id' => $user->getKey(),
|
'commenter_id' => $user->getKey(),
|
||||||
'user_type' => $user->getMorphClass(),
|
'commenter_type' => $user->getMorphClass(),
|
||||||
'body' => '<p>Test</p>',
|
'body' => '<p>Test</p>',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
CommentAttachment::create([
|
Attachment::create([
|
||||||
'comment_id' => $comment->id,
|
'comment_id' => $comment->id,
|
||||||
'file_path' => 'comments/attachments/1/file1.png',
|
'file_path' => 'comments/attachments/1/file1.png',
|
||||||
'original_name' => 'file1.png',
|
'original_name' => 'file1.png',
|
||||||
@@ -81,7 +81,7 @@ it('has attachments() hasMany relationship on Comment', function () {
|
|||||||
'disk' => 'public',
|
'disk' => 'public',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
CommentAttachment::create([
|
Attachment::create([
|
||||||
'comment_id' => $comment->id,
|
'comment_id' => $comment->id,
|
||||||
'file_path' => 'comments/attachments/1/file2.pdf',
|
'file_path' => 'comments/attachments/1/file2.pdf',
|
||||||
'original_name' => 'file2.pdf',
|
'original_name' => 'file2.pdf',
|
||||||
@@ -91,7 +91,7 @@ it('has attachments() hasMany relationship on Comment', function () {
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
expect($comment->attachments)->toHaveCount(2)
|
expect($comment->attachments)->toHaveCount(2)
|
||||||
->and($comment->attachments->first())->toBeInstanceOf(CommentAttachment::class);
|
->and($comment->attachments->first())->toBeInstanceOf(Attachment::class);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('cascade deletes attachments when comment is force deleted', function () {
|
it('cascade deletes attachments when comment is force deleted', function () {
|
||||||
@@ -101,12 +101,12 @@ it('cascade deletes attachments when comment is force deleted', function () {
|
|||||||
$comment = Comment::factory()->create([
|
$comment = Comment::factory()->create([
|
||||||
'commentable_id' => $post->id,
|
'commentable_id' => $post->id,
|
||||||
'commentable_type' => $post->getMorphClass(),
|
'commentable_type' => $post->getMorphClass(),
|
||||||
'user_id' => $user->getKey(),
|
'commenter_id' => $user->getKey(),
|
||||||
'user_type' => $user->getMorphClass(),
|
'commenter_type' => $user->getMorphClass(),
|
||||||
'body' => '<p>Test</p>',
|
'body' => '<p>Test</p>',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
CommentAttachment::create([
|
Attachment::create([
|
||||||
'comment_id' => $comment->id,
|
'comment_id' => $comment->id,
|
||||||
'file_path' => 'comments/attachments/1/photo.jpg',
|
'file_path' => 'comments/attachments/1/photo.jpg',
|
||||||
'original_name' => 'photo.jpg',
|
'original_name' => 'photo.jpg',
|
||||||
@@ -115,15 +115,15 @@ it('cascade deletes attachments when comment is force deleted', function () {
|
|||||||
'disk' => 'public',
|
'disk' => 'public',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
expect(CommentAttachment::where('comment_id', $comment->id)->count())->toBe(1);
|
expect(Attachment::where('comment_id', $comment->id)->count())->toBe(1);
|
||||||
|
|
||||||
$comment->forceDelete();
|
$comment->forceDelete();
|
||||||
|
|
||||||
expect(CommentAttachment::where('comment_id', $comment->id)->count())->toBe(0);
|
expect(Attachment::where('comment_id', $comment->id)->count())->toBe(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('correctly identifies image and non-image mime types via isImage()', function (string $mimeType, bool $expected) {
|
it('correctly identifies image and non-image mime types via isImage()', function (string $mimeType, bool $expected) {
|
||||||
$attachment = new CommentAttachment(['mime_type' => $mimeType]);
|
$attachment = new Attachment(['mime_type' => $mimeType]);
|
||||||
|
|
||||||
expect($attachment->isImage())->toBe($expected);
|
expect($attachment->isImage())->toBe($expected);
|
||||||
})->with([
|
})->with([
|
||||||
@@ -142,12 +142,12 @@ it('formats bytes into human-readable size via formattedSize()', function () {
|
|||||||
$comment = Comment::factory()->create([
|
$comment = Comment::factory()->create([
|
||||||
'commentable_id' => $post->id,
|
'commentable_id' => $post->id,
|
||||||
'commentable_type' => $post->getMorphClass(),
|
'commentable_type' => $post->getMorphClass(),
|
||||||
'user_id' => $user->getKey(),
|
'commenter_id' => $user->getKey(),
|
||||||
'user_type' => $user->getMorphClass(),
|
'commenter_type' => $user->getMorphClass(),
|
||||||
'body' => '<p>Test</p>',
|
'body' => '<p>Test</p>',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$attachment = CommentAttachment::create([
|
$attachment = Attachment::create([
|
||||||
'comment_id' => $comment->id,
|
'comment_id' => $comment->id,
|
||||||
'file_path' => 'comments/attachments/1/file.pdf',
|
'file_path' => 'comments/attachments/1/file.pdf',
|
||||||
'original_name' => 'file.pdf',
|
'original_name' => 'file.pdf',
|
||||||
@@ -160,15 +160,15 @@ it('formats bytes into human-readable size via formattedSize()', function () {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('returns default attachment disk as public', function () {
|
it('returns default attachment disk as public', function () {
|
||||||
expect(Config::getAttachmentDisk())->toBe('public');
|
expect(CommentsConfig::getAttachmentDisk())->toBe('public');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns default attachment max size as 10240', function () {
|
it('returns default attachment max size as 10240', function () {
|
||||||
expect(Config::getAttachmentMaxSize())->toBe(10240);
|
expect(CommentsConfig::getAttachmentMaxSize())->toBe(10240);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns default allowed attachment types', function () {
|
it('returns default allowed attachment types', function () {
|
||||||
$allowedTypes = Config::getAttachmentAllowedTypes();
|
$allowedTypes = CommentsConfig::getAttachmentAllowedTypes();
|
||||||
|
|
||||||
expect($allowedTypes)->toBeArray()
|
expect($allowedTypes)->toBeArray()
|
||||||
->toContain('image/jpeg')
|
->toContain('image/jpeg')
|
||||||
@@ -181,17 +181,17 @@ it('respects custom config overrides for attachment settings', function () {
|
|||||||
config(['comments.attachments.max_size' => 5120]);
|
config(['comments.attachments.max_size' => 5120]);
|
||||||
config(['comments.attachments.allowed_types' => ['image/png']]);
|
config(['comments.attachments.allowed_types' => ['image/png']]);
|
||||||
|
|
||||||
expect(Config::getAttachmentDisk())->toBe('s3')
|
expect(CommentsConfig::getAttachmentDisk())->toBe('s3')
|
||||||
->and(Config::getAttachmentMaxSize())->toBe(5120)
|
->and(CommentsConfig::getAttachmentMaxSize())->toBe(5120)
|
||||||
->and(Config::getAttachmentAllowedTypes())->toBe(['image/png']);
|
->and(CommentsConfig::getAttachmentAllowedTypes())->toBe(['image/png']);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('reports attachments as enabled by default', function () {
|
it('reports attachments as enabled by default', function () {
|
||||||
expect(Config::areAttachmentsEnabled())->toBeTrue();
|
expect(CommentsConfig::areAttachmentsEnabled())->toBeTrue();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('respects disabled attachments config', function () {
|
it('respects disabled attachments config', function () {
|
||||||
config(['comments.attachments.enabled' => false]);
|
config(['comments.attachments.enabled' => false]);
|
||||||
|
|
||||||
expect(Config::areAttachmentsEnabled())->toBeFalse();
|
expect(CommentsConfig::areAttachmentsEnabled())->toBeFalse();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,12 +2,12 @@
|
|||||||
|
|
||||||
use Illuminate\Support\Facades\Event;
|
use Illuminate\Support\Facades\Event;
|
||||||
use Livewire\Livewire;
|
use Livewire\Livewire;
|
||||||
use Relaticle\Comments\Comment;
|
|
||||||
use Relaticle\Comments\Events\CommentCreated;
|
use Relaticle\Comments\Events\CommentCreated;
|
||||||
use Relaticle\Comments\Events\CommentDeleted;
|
use Relaticle\Comments\Events\CommentDeleted;
|
||||||
use Relaticle\Comments\Events\CommentUpdated;
|
use Relaticle\Comments\Events\CommentUpdated;
|
||||||
use Relaticle\Comments\Livewire\CommentItem;
|
use Relaticle\Comments\Livewire\CommentItem;
|
||||||
use Relaticle\Comments\Livewire\Comments;
|
use Relaticle\Comments\Livewire\Comments;
|
||||||
|
use Relaticle\Comments\Models\Comment;
|
||||||
use Relaticle\Comments\Tests\Models\Post;
|
use Relaticle\Comments\Tests\Models\Post;
|
||||||
use Relaticle\Comments\Tests\Models\User;
|
use Relaticle\Comments\Tests\Models\User;
|
||||||
|
|
||||||
@@ -20,7 +20,7 @@ it('fires CommentCreated event when adding a comment', function () {
|
|||||||
$this->actingAs($user);
|
$this->actingAs($user);
|
||||||
|
|
||||||
Livewire::test(Comments::class, ['model' => $post])
|
Livewire::test(Comments::class, ['model' => $post])
|
||||||
->set('newComment', '<p>New comment</p>')
|
->set('commentData.body', '<p>New comment</p>')
|
||||||
->call('addComment');
|
->call('addComment');
|
||||||
|
|
||||||
Event::assertDispatched(CommentCreated::class, function (CommentCreated $event) use ($post) {
|
Event::assertDispatched(CommentCreated::class, function (CommentCreated $event) use ($post) {
|
||||||
@@ -38,8 +38,8 @@ it('fires CommentUpdated event when editing a comment', function () {
|
|||||||
$comment = Comment::factory()->create([
|
$comment = Comment::factory()->create([
|
||||||
'commentable_id' => $post->id,
|
'commentable_id' => $post->id,
|
||||||
'commentable_type' => $post->getMorphClass(),
|
'commentable_type' => $post->getMorphClass(),
|
||||||
'user_id' => $user->getKey(),
|
'commenter_id' => $user->getKey(),
|
||||||
'user_type' => $user->getMorphClass(),
|
'commenter_type' => $user->getMorphClass(),
|
||||||
'body' => '<p>Original</p>',
|
'body' => '<p>Original</p>',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -47,7 +47,7 @@ it('fires CommentUpdated event when editing a comment', function () {
|
|||||||
|
|
||||||
Livewire::test(CommentItem::class, ['comment' => $comment])
|
Livewire::test(CommentItem::class, ['comment' => $comment])
|
||||||
->call('startEdit')
|
->call('startEdit')
|
||||||
->set('editBody', '<p>Edited</p>')
|
->set('editData.body', '<p>Edited</p>')
|
||||||
->call('saveEdit');
|
->call('saveEdit');
|
||||||
|
|
||||||
Event::assertDispatched(CommentUpdated::class, function (CommentUpdated $event) use ($comment) {
|
Event::assertDispatched(CommentUpdated::class, function (CommentUpdated $event) use ($comment) {
|
||||||
@@ -64,8 +64,8 @@ it('fires CommentDeleted event when deleting a comment', function () {
|
|||||||
$comment = Comment::factory()->create([
|
$comment = Comment::factory()->create([
|
||||||
'commentable_id' => $post->id,
|
'commentable_id' => $post->id,
|
||||||
'commentable_type' => $post->getMorphClass(),
|
'commentable_type' => $post->getMorphClass(),
|
||||||
'user_id' => $user->getKey(),
|
'commenter_id' => $user->getKey(),
|
||||||
'user_type' => $user->getMorphClass(),
|
'commenter_type' => $user->getMorphClass(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$this->actingAs($user);
|
$this->actingAs($user);
|
||||||
@@ -87,15 +87,15 @@ it('fires CommentCreated event when adding a reply', function () {
|
|||||||
$comment = Comment::factory()->create([
|
$comment = Comment::factory()->create([
|
||||||
'commentable_id' => $post->id,
|
'commentable_id' => $post->id,
|
||||||
'commentable_type' => $post->getMorphClass(),
|
'commentable_type' => $post->getMorphClass(),
|
||||||
'user_id' => $user->getKey(),
|
'commenter_id' => $user->getKey(),
|
||||||
'user_type' => $user->getMorphClass(),
|
'commenter_type' => $user->getMorphClass(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$this->actingAs($user);
|
$this->actingAs($user);
|
||||||
|
|
||||||
Livewire::test(CommentItem::class, ['comment' => $comment])
|
Livewire::test(CommentItem::class, ['comment' => $comment])
|
||||||
->call('startReply')
|
->call('startReply')
|
||||||
->set('replyBody', '<p>Reply text</p>')
|
->set('replyData.body', '<p>Reply text</p>')
|
||||||
->call('addReply');
|
->call('addReply');
|
||||||
|
|
||||||
Event::assertDispatched(CommentCreated::class, function (CommentCreated $event) use ($comment) {
|
Event::assertDispatched(CommentCreated::class, function (CommentCreated $event) use ($comment) {
|
||||||
@@ -113,12 +113,12 @@ it('carries correct comment and commentable in event payload', function () {
|
|||||||
$this->actingAs($user);
|
$this->actingAs($user);
|
||||||
|
|
||||||
Livewire::test(Comments::class, ['model' => $post])
|
Livewire::test(Comments::class, ['model' => $post])
|
||||||
->set('newComment', '<p>Payload test</p>')
|
->set('commentData.body', '<p>Payload test</p>')
|
||||||
->call('addComment');
|
->call('addComment');
|
||||||
|
|
||||||
Event::assertDispatched(CommentCreated::class, function (CommentCreated $event) use ($post, $user) {
|
Event::assertDispatched(CommentCreated::class, function (CommentCreated $event) use ($post, $user) {
|
||||||
return $event->comment instanceof Comment
|
return $event->comment instanceof Comment
|
||||||
&& $event->commentable->id === $post->id
|
&& $event->commentable->id === $post->id
|
||||||
&& $event->comment->user_id === $user->id;
|
&& $event->comment->commenter_id === $user->id;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
use Livewire\Livewire;
|
use Livewire\Livewire;
|
||||||
use Relaticle\Comments\Comment;
|
|
||||||
use Relaticle\Comments\Livewire\CommentItem;
|
use Relaticle\Comments\Livewire\CommentItem;
|
||||||
|
use Relaticle\Comments\Models\Comment;
|
||||||
use Relaticle\Comments\Tests\Models\Post;
|
use Relaticle\Comments\Tests\Models\Post;
|
||||||
use Relaticle\Comments\Tests\Models\User;
|
use Relaticle\Comments\Tests\Models\User;
|
||||||
|
|
||||||
@@ -13,8 +13,8 @@ it('allows author to start and save edit on their comment', function () {
|
|||||||
$comment = Comment::factory()->create([
|
$comment = Comment::factory()->create([
|
||||||
'commentable_id' => $post->id,
|
'commentable_id' => $post->id,
|
||||||
'commentable_type' => $post->getMorphClass(),
|
'commentable_type' => $post->getMorphClass(),
|
||||||
'user_id' => $user->getKey(),
|
'commenter_id' => $user->getKey(),
|
||||||
'user_type' => $user->getMorphClass(),
|
'commenter_type' => $user->getMorphClass(),
|
||||||
'body' => '<p>Original body</p>',
|
'body' => '<p>Original body</p>',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -23,11 +23,9 @@ it('allows author to start and save edit on their comment', function () {
|
|||||||
Livewire::test(CommentItem::class, ['comment' => $comment])
|
Livewire::test(CommentItem::class, ['comment' => $comment])
|
||||||
->call('startEdit')
|
->call('startEdit')
|
||||||
->assertSet('isEditing', true)
|
->assertSet('isEditing', true)
|
||||||
->assertSet('editBody', '<p>Original body</p>')
|
->set('editData.body', '<p>Updated body</p>')
|
||||||
->set('editBody', '<p>Updated body</p>')
|
|
||||||
->call('saveEdit')
|
->call('saveEdit')
|
||||||
->assertSet('isEditing', false)
|
->assertSet('isEditing', false);
|
||||||
->assertSet('editBody', '');
|
|
||||||
|
|
||||||
$comment->refresh();
|
$comment->refresh();
|
||||||
|
|
||||||
@@ -42,8 +40,8 @@ it('marks edited comment with edited indicator', function () {
|
|||||||
$comment = Comment::factory()->create([
|
$comment = Comment::factory()->create([
|
||||||
'commentable_id' => $post->id,
|
'commentable_id' => $post->id,
|
||||||
'commentable_type' => $post->getMorphClass(),
|
'commentable_type' => $post->getMorphClass(),
|
||||||
'user_id' => $user->getKey(),
|
'commenter_id' => $user->getKey(),
|
||||||
'user_type' => $user->getMorphClass(),
|
'commenter_type' => $user->getMorphClass(),
|
||||||
'body' => '<p>Original</p>',
|
'body' => '<p>Original</p>',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -53,7 +51,7 @@ it('marks edited comment with edited indicator', function () {
|
|||||||
|
|
||||||
Livewire::test(CommentItem::class, ['comment' => $comment])
|
Livewire::test(CommentItem::class, ['comment' => $comment])
|
||||||
->call('startEdit')
|
->call('startEdit')
|
||||||
->set('editBody', '<p>Changed</p>')
|
->set('editData.body', '<p>Changed</p>')
|
||||||
->call('saveEdit');
|
->call('saveEdit');
|
||||||
|
|
||||||
$comment->refresh();
|
$comment->refresh();
|
||||||
@@ -70,8 +68,8 @@ it('prevents non-author from editing a comment', function () {
|
|||||||
$comment = Comment::factory()->create([
|
$comment = Comment::factory()->create([
|
||||||
'commentable_id' => $post->id,
|
'commentable_id' => $post->id,
|
||||||
'commentable_type' => $post->getMorphClass(),
|
'commentable_type' => $post->getMorphClass(),
|
||||||
'user_id' => $author->getKey(),
|
'commenter_id' => $author->getKey(),
|
||||||
'user_type' => $author->getMorphClass(),
|
'commenter_type' => $author->getMorphClass(),
|
||||||
'body' => '<p>Author comment</p>',
|
'body' => '<p>Author comment</p>',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -89,8 +87,8 @@ it('allows author to delete their own comment', function () {
|
|||||||
$comment = Comment::factory()->create([
|
$comment = Comment::factory()->create([
|
||||||
'commentable_id' => $post->id,
|
'commentable_id' => $post->id,
|
||||||
'commentable_type' => $post->getMorphClass(),
|
'commentable_type' => $post->getMorphClass(),
|
||||||
'user_id' => $user->getKey(),
|
'commenter_id' => $user->getKey(),
|
||||||
'user_type' => $user->getMorphClass(),
|
'commenter_type' => $user->getMorphClass(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$this->actingAs($user);
|
$this->actingAs($user);
|
||||||
@@ -108,8 +106,8 @@ it('preserves replies when parent comment is deleted', function () {
|
|||||||
$attrs = [
|
$attrs = [
|
||||||
'commentable_id' => $post->id,
|
'commentable_id' => $post->id,
|
||||||
'commentable_type' => $post->getMorphClass(),
|
'commentable_type' => $post->getMorphClass(),
|
||||||
'user_id' => $user->getKey(),
|
'commenter_id' => $user->getKey(),
|
||||||
'user_type' => $user->getMorphClass(),
|
'commenter_type' => $user->getMorphClass(),
|
||||||
];
|
];
|
||||||
|
|
||||||
$parent = Comment::factory()->create($attrs);
|
$parent = Comment::factory()->create($attrs);
|
||||||
@@ -133,8 +131,8 @@ it('prevents non-author from deleting a comment', function () {
|
|||||||
$comment = Comment::factory()->create([
|
$comment = Comment::factory()->create([
|
||||||
'commentable_id' => $post->id,
|
'commentable_id' => $post->id,
|
||||||
'commentable_type' => $post->getMorphClass(),
|
'commentable_type' => $post->getMorphClass(),
|
||||||
'user_id' => $author->getKey(),
|
'commenter_id' => $author->getKey(),
|
||||||
'user_type' => $author->getMorphClass(),
|
'commenter_type' => $author->getMorphClass(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$this->actingAs($otherUser);
|
$this->actingAs($otherUser);
|
||||||
@@ -151,8 +149,8 @@ it('allows user to reply to a comment', function () {
|
|||||||
$comment = Comment::factory()->create([
|
$comment = Comment::factory()->create([
|
||||||
'commentable_id' => $post->id,
|
'commentable_id' => $post->id,
|
||||||
'commentable_type' => $post->getMorphClass(),
|
'commentable_type' => $post->getMorphClass(),
|
||||||
'user_id' => $user->getKey(),
|
'commenter_id' => $user->getKey(),
|
||||||
'user_type' => $user->getMorphClass(),
|
'commenter_type' => $user->getMorphClass(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$this->actingAs($user);
|
$this->actingAs($user);
|
||||||
@@ -160,16 +158,15 @@ it('allows user to reply to a comment', function () {
|
|||||||
Livewire::test(CommentItem::class, ['comment' => $comment])
|
Livewire::test(CommentItem::class, ['comment' => $comment])
|
||||||
->call('startReply')
|
->call('startReply')
|
||||||
->assertSet('isReplying', true)
|
->assertSet('isReplying', true)
|
||||||
->set('replyBody', '<p>My reply</p>')
|
->set('replyData.body', '<p>My reply</p>')
|
||||||
->call('addReply')
|
->call('addReply')
|
||||||
->assertSet('isReplying', false)
|
->assertSet('isReplying', false);
|
||||||
->assertSet('replyBody', '');
|
|
||||||
|
|
||||||
$reply = Comment::where('parent_id', $comment->id)->first();
|
$reply = Comment::where('parent_id', $comment->id)->first();
|
||||||
|
|
||||||
expect($reply)->not->toBeNull();
|
expect($reply)->not->toBeNull();
|
||||||
expect($reply->body)->toBe('<p>My reply</p>');
|
expect($reply->body)->toBe('<p>My reply</p>');
|
||||||
expect($reply->user_id)->toBe($user->id);
|
expect($reply->commenter_id)->toBe($user->id);
|
||||||
expect($reply->commentable_id)->toBe($post->id);
|
expect($reply->commentable_id)->toBe($post->id);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -179,8 +176,8 @@ it('respects max depth for replies', function () {
|
|||||||
$attrs = [
|
$attrs = [
|
||||||
'commentable_id' => $post->id,
|
'commentable_id' => $post->id,
|
||||||
'commentable_type' => $post->getMorphClass(),
|
'commentable_type' => $post->getMorphClass(),
|
||||||
'user_id' => $user->getKey(),
|
'commenter_id' => $user->getKey(),
|
||||||
'user_type' => $user->getMorphClass(),
|
'commenter_type' => $user->getMorphClass(),
|
||||||
];
|
];
|
||||||
|
|
||||||
config(['comments.threading.max_depth' => 1]);
|
config(['comments.threading.max_depth' => 1]);
|
||||||
@@ -202,8 +199,8 @@ it('resets state when cancelling edit', function () {
|
|||||||
$comment = Comment::factory()->create([
|
$comment = Comment::factory()->create([
|
||||||
'commentable_id' => $post->id,
|
'commentable_id' => $post->id,
|
||||||
'commentable_type' => $post->getMorphClass(),
|
'commentable_type' => $post->getMorphClass(),
|
||||||
'user_id' => $user->getKey(),
|
'commenter_id' => $user->getKey(),
|
||||||
'user_type' => $user->getMorphClass(),
|
'commenter_type' => $user->getMorphClass(),
|
||||||
'body' => '<p>Some body</p>',
|
'body' => '<p>Some body</p>',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -213,8 +210,7 @@ it('resets state when cancelling edit', function () {
|
|||||||
->call('startEdit')
|
->call('startEdit')
|
||||||
->assertSet('isEditing', true)
|
->assertSet('isEditing', true)
|
||||||
->call('cancelEdit')
|
->call('cancelEdit')
|
||||||
->assertSet('isEditing', false)
|
->assertSet('isEditing', false);
|
||||||
->assertSet('editBody', '');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('resets state when cancelling reply', function () {
|
it('resets state when cancelling reply', function () {
|
||||||
@@ -224,8 +220,8 @@ it('resets state when cancelling reply', function () {
|
|||||||
$comment = Comment::factory()->create([
|
$comment = Comment::factory()->create([
|
||||||
'commentable_id' => $post->id,
|
'commentable_id' => $post->id,
|
||||||
'commentable_type' => $post->getMorphClass(),
|
'commentable_type' => $post->getMorphClass(),
|
||||||
'user_id' => $user->getKey(),
|
'commenter_id' => $user->getKey(),
|
||||||
'user_type' => $user->getMorphClass(),
|
'commenter_type' => $user->getMorphClass(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$this->actingAs($user);
|
$this->actingAs($user);
|
||||||
@@ -233,10 +229,9 @@ it('resets state when cancelling reply', function () {
|
|||||||
Livewire::test(CommentItem::class, ['comment' => $comment])
|
Livewire::test(CommentItem::class, ['comment' => $comment])
|
||||||
->call('startReply')
|
->call('startReply')
|
||||||
->assertSet('isReplying', true)
|
->assertSet('isReplying', true)
|
||||||
->set('replyBody', '<p>Draft reply</p>')
|
->set('replyData.body', '<p>Draft reply</p>')
|
||||||
->call('cancelReply')
|
->call('cancelReply')
|
||||||
->assertSet('isReplying', false)
|
->assertSet('isReplying', false);
|
||||||
->assertSet('replyBody', '');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('loads all replies within a thread eagerly', function () {
|
it('loads all replies within a thread eagerly', function () {
|
||||||
@@ -245,14 +240,14 @@ it('loads all replies within a thread eagerly', function () {
|
|||||||
$attrs = [
|
$attrs = [
|
||||||
'commentable_id' => $post->id,
|
'commentable_id' => $post->id,
|
||||||
'commentable_type' => $post->getMorphClass(),
|
'commentable_type' => $post->getMorphClass(),
|
||||||
'user_id' => $user->getKey(),
|
'commenter_id' => $user->getKey(),
|
||||||
'user_type' => $user->getMorphClass(),
|
'commenter_type' => $user->getMorphClass(),
|
||||||
];
|
];
|
||||||
|
|
||||||
$parent = Comment::factory()->create($attrs);
|
$parent = Comment::factory()->create($attrs);
|
||||||
Comment::factory()->count(3)->withParent($parent)->create($attrs);
|
Comment::factory()->count(3)->withParent($parent)->create($attrs);
|
||||||
|
|
||||||
$parentWithReplies = Comment::with('replies.user')->find($parent->id);
|
$parentWithReplies = Comment::with('replies.commenter')->find($parent->id);
|
||||||
|
|
||||||
$this->actingAs($user);
|
$this->actingAs($user);
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
use Illuminate\Database\QueryException;
|
use Illuminate\Database\QueryException;
|
||||||
use Relaticle\Comments\Comment;
|
|
||||||
use Relaticle\Comments\CommentReaction;
|
|
||||||
use Relaticle\Comments\Events\CommentReacted;
|
use Relaticle\Comments\Events\CommentReacted;
|
||||||
|
use Relaticle\Comments\Models\Comment;
|
||||||
|
use Relaticle\Comments\Models\Reaction;
|
||||||
use Relaticle\Comments\Tests\Models\Post;
|
use Relaticle\Comments\Tests\Models\Post;
|
||||||
use Relaticle\Comments\Tests\Models\User;
|
use Relaticle\Comments\Tests\Models\User;
|
||||||
|
|
||||||
@@ -14,15 +14,15 @@ it('belongs to a comment via comment() relationship', function () {
|
|||||||
$comment = Comment::factory()->create([
|
$comment = Comment::factory()->create([
|
||||||
'commentable_id' => $post->id,
|
'commentable_id' => $post->id,
|
||||||
'commentable_type' => $post->getMorphClass(),
|
'commentable_type' => $post->getMorphClass(),
|
||||||
'user_id' => $user->getKey(),
|
'commenter_id' => $user->getKey(),
|
||||||
'user_type' => $user->getMorphClass(),
|
'commenter_type' => $user->getMorphClass(),
|
||||||
'body' => '<p>Test</p>',
|
'body' => '<p>Test</p>',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$reaction = CommentReaction::create([
|
$reaction = Reaction::create([
|
||||||
'comment_id' => $comment->id,
|
'comment_id' => $comment->id,
|
||||||
'user_id' => $user->getKey(),
|
'commenter_id' => $user->getKey(),
|
||||||
'user_type' => $user->getMorphClass(),
|
'commenter_type' => $user->getMorphClass(),
|
||||||
'reaction' => 'thumbs_up',
|
'reaction' => 'thumbs_up',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -30,27 +30,27 @@ it('belongs to a comment via comment() relationship', function () {
|
|||||||
->and($reaction->comment->id)->toBe($comment->id);
|
->and($reaction->comment->id)->toBe($comment->id);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('belongs to a user via polymorphic user() relationship', function () {
|
it('belongs to a commenter via polymorphic commenter() relationship', function () {
|
||||||
$user = User::factory()->create();
|
$user = User::factory()->create();
|
||||||
$post = Post::factory()->create();
|
$post = Post::factory()->create();
|
||||||
|
|
||||||
$comment = Comment::factory()->create([
|
$comment = Comment::factory()->create([
|
||||||
'commentable_id' => $post->id,
|
'commentable_id' => $post->id,
|
||||||
'commentable_type' => $post->getMorphClass(),
|
'commentable_type' => $post->getMorphClass(),
|
||||||
'user_id' => $user->getKey(),
|
'commenter_id' => $user->getKey(),
|
||||||
'user_type' => $user->getMorphClass(),
|
'commenter_type' => $user->getMorphClass(),
|
||||||
'body' => '<p>Test</p>',
|
'body' => '<p>Test</p>',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$reaction = CommentReaction::create([
|
$reaction = Reaction::create([
|
||||||
'comment_id' => $comment->id,
|
'comment_id' => $comment->id,
|
||||||
'user_id' => $user->getKey(),
|
'commenter_id' => $user->getKey(),
|
||||||
'user_type' => $user->getMorphClass(),
|
'commenter_type' => $user->getMorphClass(),
|
||||||
'reaction' => 'heart',
|
'reaction' => 'heart',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
expect($reaction->user)->toBeInstanceOf(User::class)
|
expect($reaction->commenter)->toBeInstanceOf(User::class)
|
||||||
->and($reaction->user->id)->toBe($user->id);
|
->and($reaction->commenter->id)->toBe($user->id);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('prevents duplicate reactions with unique constraint', function () {
|
it('prevents duplicate reactions with unique constraint', function () {
|
||||||
@@ -60,22 +60,22 @@ it('prevents duplicate reactions with unique constraint', function () {
|
|||||||
$comment = Comment::factory()->create([
|
$comment = Comment::factory()->create([
|
||||||
'commentable_id' => $post->id,
|
'commentable_id' => $post->id,
|
||||||
'commentable_type' => $post->getMorphClass(),
|
'commentable_type' => $post->getMorphClass(),
|
||||||
'user_id' => $user->getKey(),
|
'commenter_id' => $user->getKey(),
|
||||||
'user_type' => $user->getMorphClass(),
|
'commenter_type' => $user->getMorphClass(),
|
||||||
'body' => '<p>Test</p>',
|
'body' => '<p>Test</p>',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
CommentReaction::create([
|
Reaction::create([
|
||||||
'comment_id' => $comment->id,
|
'comment_id' => $comment->id,
|
||||||
'user_id' => $user->getKey(),
|
'commenter_id' => $user->getKey(),
|
||||||
'user_type' => $user->getMorphClass(),
|
'commenter_type' => $user->getMorphClass(),
|
||||||
'reaction' => 'thumbs_up',
|
'reaction' => 'thumbs_up',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
expect(fn () => CommentReaction::create([
|
expect(fn () => Reaction::create([
|
||||||
'comment_id' => $comment->id,
|
'comment_id' => $comment->id,
|
||||||
'user_id' => $user->getKey(),
|
'commenter_id' => $user->getKey(),
|
||||||
'user_type' => $user->getMorphClass(),
|
'commenter_type' => $user->getMorphClass(),
|
||||||
'reaction' => 'thumbs_up',
|
'reaction' => 'thumbs_up',
|
||||||
]))->toThrow(QueryException::class);
|
]))->toThrow(QueryException::class);
|
||||||
});
|
});
|
||||||
@@ -87,8 +87,8 @@ it('carries comment, user, reaction key, and action in CommentReacted event', fu
|
|||||||
$comment = Comment::factory()->create([
|
$comment = Comment::factory()->create([
|
||||||
'commentable_id' => $post->id,
|
'commentable_id' => $post->id,
|
||||||
'commentable_type' => $post->getMorphClass(),
|
'commentable_type' => $post->getMorphClass(),
|
||||||
'user_id' => $user->getKey(),
|
'commenter_id' => $user->getKey(),
|
||||||
'user_type' => $user->getMorphClass(),
|
'commenter_type' => $user->getMorphClass(),
|
||||||
'body' => '<p>Test</p>',
|
'body' => '<p>Test</p>',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
use Relaticle\Comments\Comment;
|
use Relaticle\Comments\CommentsConfig;
|
||||||
use Relaticle\Comments\CommentSubscription;
|
use Relaticle\Comments\Models\Subscription;
|
||||||
use Relaticle\Comments\Config;
|
|
||||||
use Relaticle\Comments\Tests\Models\Post;
|
use Relaticle\Comments\Tests\Models\Post;
|
||||||
use Relaticle\Comments\Tests\Models\User;
|
use Relaticle\Comments\Tests\Models\User;
|
||||||
|
|
||||||
@@ -10,98 +9,98 @@ it('has commentable morphTo relationship', function () {
|
|||||||
$user = User::factory()->create();
|
$user = User::factory()->create();
|
||||||
$post = Post::factory()->create();
|
$post = Post::factory()->create();
|
||||||
|
|
||||||
$subscription = CommentSubscription::create([
|
$subscription = Subscription::create([
|
||||||
'commentable_id' => $post->id,
|
'commentable_id' => $post->id,
|
||||||
'commentable_type' => $post->getMorphClass(),
|
'commentable_type' => $post->getMorphClass(),
|
||||||
'user_id' => $user->getKey(),
|
'commenter_id' => $user->getKey(),
|
||||||
'user_type' => $user->getMorphClass(),
|
'commenter_type' => $user->getMorphClass(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
expect($subscription->commentable)->toBeInstanceOf(Post::class)
|
expect($subscription->commentable)->toBeInstanceOf(Post::class)
|
||||||
->and($subscription->commentable->id)->toBe($post->id);
|
->and($subscription->commentable->id)->toBe($post->id);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('has user morphTo relationship', function () {
|
it('has commenter morphTo relationship', function () {
|
||||||
$user = User::factory()->create();
|
$user = User::factory()->create();
|
||||||
$post = Post::factory()->create();
|
$post = Post::factory()->create();
|
||||||
|
|
||||||
$subscription = CommentSubscription::create([
|
$subscription = Subscription::create([
|
||||||
'commentable_id' => $post->id,
|
'commentable_id' => $post->id,
|
||||||
'commentable_type' => $post->getMorphClass(),
|
'commentable_type' => $post->getMorphClass(),
|
||||||
'user_id' => $user->getKey(),
|
'commenter_id' => $user->getKey(),
|
||||||
'user_type' => $user->getMorphClass(),
|
'commenter_type' => $user->getMorphClass(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
expect($subscription->user)->toBeInstanceOf(User::class)
|
expect($subscription->commenter)->toBeInstanceOf(User::class)
|
||||||
->and($subscription->user->id)->toBe($user->id);
|
->and($subscription->commenter->id)->toBe($user->id);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns database as default notification channel', function () {
|
it('returns database as default notification channel', function () {
|
||||||
expect(Config::getNotificationChannels())->toBe(['database']);
|
expect(CommentsConfig::getNotificationChannels())->toBe(['database']);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns custom channels when configured', function () {
|
it('returns custom channels when configured', function () {
|
||||||
config()->set('comments.notifications.channels', ['database', 'mail']);
|
config()->set('comments.notifications.channels', ['database', 'mail']);
|
||||||
|
|
||||||
expect(Config::getNotificationChannels())->toBe(['database', 'mail']);
|
expect(CommentsConfig::getNotificationChannels())->toBe(['database', 'mail']);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns true for shouldAutoSubscribe by default', function () {
|
it('returns true for shouldAutoSubscribe by default', function () {
|
||||||
expect(Config::shouldAutoSubscribe())->toBeTrue();
|
expect(CommentsConfig::shouldAutoSubscribe())->toBeTrue();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns false for shouldAutoSubscribe when configured', function () {
|
it('returns false for shouldAutoSubscribe when configured', function () {
|
||||||
config()->set('comments.subscriptions.auto_subscribe', false);
|
config()->set('comments.subscriptions.auto_subscribe', false);
|
||||||
|
|
||||||
expect(Config::shouldAutoSubscribe())->toBeFalse();
|
expect(CommentsConfig::shouldAutoSubscribe())->toBeFalse();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('checks if user is subscribed to a commentable via isSubscribed()', function () {
|
it('checks if user is subscribed to a commentable via isSubscribed()', function () {
|
||||||
$user = User::factory()->create();
|
$user = User::factory()->create();
|
||||||
$post = Post::factory()->create();
|
$post = Post::factory()->create();
|
||||||
|
|
||||||
expect(CommentSubscription::isSubscribed($post, $user))->toBeFalse();
|
expect(Subscription::isSubscribed($post, $user))->toBeFalse();
|
||||||
|
|
||||||
CommentSubscription::create([
|
Subscription::create([
|
||||||
'commentable_id' => $post->id,
|
'commentable_id' => $post->id,
|
||||||
'commentable_type' => $post->getMorphClass(),
|
'commentable_type' => $post->getMorphClass(),
|
||||||
'user_id' => $user->getKey(),
|
'commenter_id' => $user->getKey(),
|
||||||
'user_type' => $user->getMorphClass(),
|
'commenter_type' => $user->getMorphClass(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
expect(CommentSubscription::isSubscribed($post, $user))->toBeTrue();
|
expect(Subscription::isSubscribed($post, $user))->toBeTrue();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('creates subscription via subscribe() static method', function () {
|
it('creates subscription via subscribe() static method', function () {
|
||||||
$user = User::factory()->create();
|
$user = User::factory()->create();
|
||||||
$post = Post::factory()->create();
|
$post = Post::factory()->create();
|
||||||
|
|
||||||
CommentSubscription::subscribe($post, $user);
|
Subscription::subscribe($post, $user);
|
||||||
|
|
||||||
expect(CommentSubscription::isSubscribed($post, $user))->toBeTrue();
|
expect(Subscription::isSubscribed($post, $user))->toBeTrue();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('removes subscription via unsubscribe() static method', function () {
|
it('removes subscription via unsubscribe() static method', function () {
|
||||||
$user = User::factory()->create();
|
$user = User::factory()->create();
|
||||||
$post = Post::factory()->create();
|
$post = Post::factory()->create();
|
||||||
|
|
||||||
CommentSubscription::subscribe($post, $user);
|
Subscription::subscribe($post, $user);
|
||||||
CommentSubscription::unsubscribe($post, $user);
|
Subscription::unsubscribe($post, $user);
|
||||||
|
|
||||||
expect(CommentSubscription::isSubscribed($post, $user))->toBeFalse();
|
expect(Subscription::isSubscribed($post, $user))->toBeFalse();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('is idempotent when subscribing twice', function () {
|
it('is idempotent when subscribing twice', function () {
|
||||||
$user = User::factory()->create();
|
$user = User::factory()->create();
|
||||||
$post = Post::factory()->create();
|
$post = Post::factory()->create();
|
||||||
|
|
||||||
CommentSubscription::subscribe($post, $user);
|
Subscription::subscribe($post, $user);
|
||||||
CommentSubscription::subscribe($post, $user);
|
Subscription::subscribe($post, $user);
|
||||||
|
|
||||||
expect(CommentSubscription::where([
|
expect(Subscription::where([
|
||||||
'commentable_id' => $post->id,
|
'commentable_id' => $post->id,
|
||||||
'commentable_type' => $post->getMorphClass(),
|
'commentable_type' => $post->getMorphClass(),
|
||||||
'user_id' => $user->getKey(),
|
'commenter_id' => $user->getKey(),
|
||||||
'user_type' => $user->getMorphClass(),
|
'commenter_type' => $user->getMorphClass(),
|
||||||
])->count())->toBe(1);
|
])->count())->toBe(1);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
use Carbon\Carbon;
|
use Carbon\Carbon;
|
||||||
use Relaticle\Comments\Comment;
|
use Relaticle\Comments\Models\Comment;
|
||||||
use Relaticle\Comments\Tests\Models\Post;
|
use Relaticle\Comments\Tests\Models\Post;
|
||||||
use Relaticle\Comments\Tests\Models\User;
|
use Relaticle\Comments\Tests\Models\User;
|
||||||
|
|
||||||
@@ -12,14 +12,14 @@ it('can be created with factory', function () {
|
|||||||
$comment = Comment::factory()->create([
|
$comment = Comment::factory()->create([
|
||||||
'commentable_type' => $post->getMorphClass(),
|
'commentable_type' => $post->getMorphClass(),
|
||||||
'commentable_id' => $post->id,
|
'commentable_id' => $post->id,
|
||||||
'user_type' => $user->getMorphClass(),
|
'commenter_type' => $user->getMorphClass(),
|
||||||
'user_id' => $user->id,
|
'commenter_id' => $user->id,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
expect($comment)->toBeInstanceOf(Comment::class);
|
expect($comment)->toBeInstanceOf(Comment::class);
|
||||||
expect($comment->body)->toBeString();
|
expect($comment->body)->toBeString();
|
||||||
expect($comment->commentable_id)->toBe($post->id);
|
expect($comment->commentable_id)->toBe($post->id);
|
||||||
expect($comment->user_id)->toBe($user->id);
|
expect($comment->commenter_id)->toBe($user->id);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('belongs to a commentable model via morphTo', function () {
|
it('belongs to a commentable model via morphTo', function () {
|
||||||
@@ -29,27 +29,27 @@ it('belongs to a commentable model via morphTo', function () {
|
|||||||
$comment = Comment::factory()->create([
|
$comment = Comment::factory()->create([
|
||||||
'commentable_type' => $post->getMorphClass(),
|
'commentable_type' => $post->getMorphClass(),
|
||||||
'commentable_id' => $post->id,
|
'commentable_id' => $post->id,
|
||||||
'user_type' => $user->getMorphClass(),
|
'commenter_type' => $user->getMorphClass(),
|
||||||
'user_id' => $user->id,
|
'commenter_id' => $user->id,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
expect($comment->commentable)->toBeInstanceOf(Post::class);
|
expect($comment->commentable)->toBeInstanceOf(Post::class);
|
||||||
expect($comment->commentable->id)->toBe($post->id);
|
expect($comment->commentable->id)->toBe($post->id);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('belongs to a user via morphTo', function () {
|
it('belongs to a commenter via morphTo', function () {
|
||||||
$user = User::factory()->create();
|
$user = User::factory()->create();
|
||||||
$post = Post::factory()->create();
|
$post = Post::factory()->create();
|
||||||
|
|
||||||
$comment = Comment::factory()->create([
|
$comment = Comment::factory()->create([
|
||||||
'commentable_type' => $post->getMorphClass(),
|
'commentable_type' => $post->getMorphClass(),
|
||||||
'commentable_id' => $post->id,
|
'commentable_id' => $post->id,
|
||||||
'user_type' => $user->getMorphClass(),
|
'commenter_type' => $user->getMorphClass(),
|
||||||
'user_id' => $user->id,
|
'commenter_id' => $user->id,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
expect($comment->user)->toBeInstanceOf(User::class);
|
expect($comment->commenter)->toBeInstanceOf(User::class);
|
||||||
expect($comment->user->id)->toBe($user->id);
|
expect($comment->commenter->id)->toBe($user->id);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('supports threading with parent and replies', function () {
|
it('supports threading with parent and replies', function () {
|
||||||
@@ -59,15 +59,15 @@ it('supports threading with parent and replies', function () {
|
|||||||
$parent = Comment::factory()->create([
|
$parent = Comment::factory()->create([
|
||||||
'commentable_type' => $post->getMorphClass(),
|
'commentable_type' => $post->getMorphClass(),
|
||||||
'commentable_id' => $post->id,
|
'commentable_id' => $post->id,
|
||||||
'user_type' => $user->getMorphClass(),
|
'commenter_type' => $user->getMorphClass(),
|
||||||
'user_id' => $user->id,
|
'commenter_id' => $user->id,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$reply = Comment::factory()->withParent($parent)->create([
|
$reply = Comment::factory()->withParent($parent)->create([
|
||||||
'commentable_type' => $post->getMorphClass(),
|
'commentable_type' => $post->getMorphClass(),
|
||||||
'commentable_id' => $post->id,
|
'commentable_id' => $post->id,
|
||||||
'user_type' => $user->getMorphClass(),
|
'commenter_type' => $user->getMorphClass(),
|
||||||
'user_id' => $user->id,
|
'commenter_id' => $user->id,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
expect($reply->parent->id)->toBe($parent->id);
|
expect($reply->parent->id)->toBe($parent->id);
|
||||||
@@ -82,15 +82,15 @@ it('identifies top-level vs reply comments', function () {
|
|||||||
$topLevel = Comment::factory()->create([
|
$topLevel = Comment::factory()->create([
|
||||||
'commentable_type' => $post->getMorphClass(),
|
'commentable_type' => $post->getMorphClass(),
|
||||||
'commentable_id' => $post->id,
|
'commentable_id' => $post->id,
|
||||||
'user_type' => $user->getMorphClass(),
|
'commenter_type' => $user->getMorphClass(),
|
||||||
'user_id' => $user->id,
|
'commenter_id' => $user->id,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$reply = Comment::factory()->withParent($topLevel)->create([
|
$reply = Comment::factory()->withParent($topLevel)->create([
|
||||||
'commentable_type' => $post->getMorphClass(),
|
'commentable_type' => $post->getMorphClass(),
|
||||||
'commentable_id' => $post->id,
|
'commentable_id' => $post->id,
|
||||||
'user_type' => $user->getMorphClass(),
|
'commenter_type' => $user->getMorphClass(),
|
||||||
'user_id' => $user->id,
|
'commenter_id' => $user->id,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
expect($topLevel->isTopLevel())->toBeTrue();
|
expect($topLevel->isTopLevel())->toBeTrue();
|
||||||
@@ -105,8 +105,8 @@ it('calculates depth correctly', function () {
|
|||||||
$attrs = [
|
$attrs = [
|
||||||
'commentable_type' => $post->getMorphClass(),
|
'commentable_type' => $post->getMorphClass(),
|
||||||
'commentable_id' => $post->id,
|
'commentable_id' => $post->id,
|
||||||
'user_type' => $user->getMorphClass(),
|
'commenter_type' => $user->getMorphClass(),
|
||||||
'user_id' => $user->id,
|
'commenter_id' => $user->id,
|
||||||
];
|
];
|
||||||
|
|
||||||
$level0 = Comment::factory()->create($attrs);
|
$level0 = Comment::factory()->create($attrs);
|
||||||
@@ -124,8 +124,8 @@ it('checks canReply based on max depth', function () {
|
|||||||
$attrs = [
|
$attrs = [
|
||||||
'commentable_type' => $post->getMorphClass(),
|
'commentable_type' => $post->getMorphClass(),
|
||||||
'commentable_id' => $post->id,
|
'commentable_id' => $post->id,
|
||||||
'user_type' => $user->getMorphClass(),
|
'commenter_type' => $user->getMorphClass(),
|
||||||
'user_id' => $user->id,
|
'commenter_id' => $user->id,
|
||||||
];
|
];
|
||||||
|
|
||||||
$level0 = Comment::factory()->create($attrs);
|
$level0 = Comment::factory()->create($attrs);
|
||||||
@@ -144,8 +144,8 @@ it('supports soft deletes', function () {
|
|||||||
$comment = Comment::factory()->create([
|
$comment = Comment::factory()->create([
|
||||||
'commentable_type' => $post->getMorphClass(),
|
'commentable_type' => $post->getMorphClass(),
|
||||||
'commentable_id' => $post->id,
|
'commentable_id' => $post->id,
|
||||||
'user_type' => $user->getMorphClass(),
|
'commenter_type' => $user->getMorphClass(),
|
||||||
'user_id' => $user->id,
|
'commenter_id' => $user->id,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$comment->delete();
|
$comment->delete();
|
||||||
@@ -162,8 +162,8 @@ it('tracks edited state', function () {
|
|||||||
$comment = Comment::factory()->create([
|
$comment = Comment::factory()->create([
|
||||||
'commentable_type' => $post->getMorphClass(),
|
'commentable_type' => $post->getMorphClass(),
|
||||||
'commentable_id' => $post->id,
|
'commentable_id' => $post->id,
|
||||||
'user_type' => $user->getMorphClass(),
|
'commenter_type' => $user->getMorphClass(),
|
||||||
'user_id' => $user->id,
|
'commenter_id' => $user->id,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
expect($comment->isEdited())->toBeFalse();
|
expect($comment->isEdited())->toBeFalse();
|
||||||
@@ -171,8 +171,8 @@ it('tracks edited state', function () {
|
|||||||
$edited = Comment::factory()->edited()->create([
|
$edited = Comment::factory()->edited()->create([
|
||||||
'commentable_type' => $post->getMorphClass(),
|
'commentable_type' => $post->getMorphClass(),
|
||||||
'commentable_id' => $post->id,
|
'commentable_id' => $post->id,
|
||||||
'user_type' => $user->getMorphClass(),
|
'commenter_type' => $user->getMorphClass(),
|
||||||
'user_id' => $user->id,
|
'commenter_id' => $user->id,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
expect($edited->isEdited())->toBeTrue();
|
expect($edited->isEdited())->toBeTrue();
|
||||||
@@ -185,8 +185,8 @@ it('detects when it has replies', function () {
|
|||||||
$attrs = [
|
$attrs = [
|
||||||
'commentable_type' => $post->getMorphClass(),
|
'commentable_type' => $post->getMorphClass(),
|
||||||
'commentable_id' => $post->id,
|
'commentable_id' => $post->id,
|
||||||
'user_type' => $user->getMorphClass(),
|
'commenter_type' => $user->getMorphClass(),
|
||||||
'user_id' => $user->id,
|
'commenter_id' => $user->id,
|
||||||
];
|
];
|
||||||
|
|
||||||
$parent = Comment::factory()->create($attrs);
|
$parent = Comment::factory()->create($attrs);
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
use Relaticle\Comments\Comment;
|
|
||||||
use Relaticle\Comments\Filament\Actions\CommentsAction;
|
use Relaticle\Comments\Filament\Actions\CommentsAction;
|
||||||
|
use Relaticle\Comments\Models\Comment;
|
||||||
use Relaticle\Comments\Tests\Models\Post;
|
use Relaticle\Comments\Tests\Models\Post;
|
||||||
use Relaticle\Comments\Tests\Models\User;
|
use Relaticle\Comments\Tests\Models\User;
|
||||||
|
|
||||||
@@ -29,10 +29,11 @@ it('has a chat bubble icon', function () {
|
|||||||
expect($action->getIcon())->toBe('heroicon-o-chat-bubble-left-right');
|
expect($action->getIcon())->toBe('heroicon-o-chat-bubble-left-right');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('has modal content configured', function () {
|
it('disables modal submit and cancel actions', function () {
|
||||||
$action = CommentsAction::make('comments');
|
$action = CommentsAction::make('comments');
|
||||||
|
|
||||||
expect($action->hasModalContent())->toBeTrue();
|
expect($action->getModalSubmitAction())->toBeFalsy()
|
||||||
|
->and($action->getModalCancelAction())->toBeFalsy();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('shows badge with comment count when comments exist', function () {
|
it('shows badge with comment count when comments exist', function () {
|
||||||
@@ -42,8 +43,8 @@ it('shows badge with comment count when comments exist', function () {
|
|||||||
Comment::factory()->count(3)->create([
|
Comment::factory()->count(3)->create([
|
||||||
'commentable_id' => $post->id,
|
'commentable_id' => $post->id,
|
||||||
'commentable_type' => $post->getMorphClass(),
|
'commentable_type' => $post->getMorphClass(),
|
||||||
'user_id' => $user->getKey(),
|
'commenter_id' => $user->getKey(),
|
||||||
'user_type' => $user->getMorphClass(),
|
'commenter_type' => $user->getMorphClass(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$action = CommentsAction::make('comments');
|
$action = CommentsAction::make('comments');
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
use Livewire\Livewire;
|
use Livewire\Livewire;
|
||||||
use Relaticle\Comments\Comment;
|
|
||||||
use Relaticle\Comments\Livewire\Comments;
|
use Relaticle\Comments\Livewire\Comments;
|
||||||
|
use Relaticle\Comments\Models\Comment;
|
||||||
use Relaticle\Comments\Tests\Models\Post;
|
use Relaticle\Comments\Tests\Models\Post;
|
||||||
use Relaticle\Comments\Tests\Models\User;
|
use Relaticle\Comments\Tests\Models\User;
|
||||||
|
|
||||||
@@ -13,9 +13,8 @@ it('allows authenticated user to create a comment on a post', function () {
|
|||||||
$this->actingAs($user);
|
$this->actingAs($user);
|
||||||
|
|
||||||
Livewire::test(Comments::class, ['model' => $post])
|
Livewire::test(Comments::class, ['model' => $post])
|
||||||
->set('newComment', '<p>Hello World</p>')
|
->set('commentData.body', '<p>Hello World</p>')
|
||||||
->call('addComment')
|
->call('addComment');
|
||||||
->assertSet('newComment', '');
|
|
||||||
|
|
||||||
expect(Comment::count())->toBe(1);
|
expect(Comment::count())->toBe(1);
|
||||||
expect(Comment::first()->body)->toBe('<p>Hello World</p>');
|
expect(Comment::first()->body)->toBe('<p>Hello World</p>');
|
||||||
@@ -28,13 +27,13 @@ it('associates new comment with the authenticated user', function () {
|
|||||||
$this->actingAs($user);
|
$this->actingAs($user);
|
||||||
|
|
||||||
Livewire::test(Comments::class, ['model' => $post])
|
Livewire::test(Comments::class, ['model' => $post])
|
||||||
->set('newComment', '<p>Test</p>')
|
->set('commentData.body', '<p>Test</p>')
|
||||||
->call('addComment');
|
->call('addComment');
|
||||||
|
|
||||||
$comment = Comment::first();
|
$comment = Comment::first();
|
||||||
|
|
||||||
expect($comment->user_id)->toBe($user->id);
|
expect($comment->commenter_id)->toBe($user->id);
|
||||||
expect($comment->user_type)->toBe($user->getMorphClass());
|
expect($comment->commenter_type)->toBe($user->getMorphClass());
|
||||||
expect($comment->commentable_id)->toBe($post->id);
|
expect($comment->commentable_id)->toBe($post->id);
|
||||||
expect($comment->commentable_type)->toBe($post->getMorphClass());
|
expect($comment->commentable_type)->toBe($post->getMorphClass());
|
||||||
});
|
});
|
||||||
@@ -43,7 +42,7 @@ it('requires authentication to create a comment', function () {
|
|||||||
$post = Post::factory()->create();
|
$post = Post::factory()->create();
|
||||||
|
|
||||||
Livewire::test(Comments::class, ['model' => $post])
|
Livewire::test(Comments::class, ['model' => $post])
|
||||||
->set('newComment', '<p>Hello</p>')
|
->set('commentData.body', '<p>Hello</p>')
|
||||||
->call('addComment')
|
->call('addComment')
|
||||||
->assertForbidden();
|
->assertForbidden();
|
||||||
});
|
});
|
||||||
@@ -55,9 +54,9 @@ it('validates that comment body is not empty', function () {
|
|||||||
$this->actingAs($user);
|
$this->actingAs($user);
|
||||||
|
|
||||||
Livewire::test(Comments::class, ['model' => $post])
|
Livewire::test(Comments::class, ['model' => $post])
|
||||||
->set('newComment', '')
|
->set('commentData.body', '')
|
||||||
->call('addComment')
|
->call('addComment')
|
||||||
->assertHasErrors('newComment');
|
->assertHasErrors('commentData.body');
|
||||||
|
|
||||||
expect(Comment::count())->toBe(0);
|
expect(Comment::count())->toBe(0);
|
||||||
});
|
});
|
||||||
@@ -71,8 +70,8 @@ it('paginates top-level comments with load more', function () {
|
|||||||
Comment::factory()->count(12)->create([
|
Comment::factory()->count(12)->create([
|
||||||
'commentable_id' => $post->id,
|
'commentable_id' => $post->id,
|
||||||
'commentable_type' => $post->getMorphClass(),
|
'commentable_type' => $post->getMorphClass(),
|
||||||
'user_id' => $user->getKey(),
|
'commenter_id' => $user->getKey(),
|
||||||
'user_type' => $user->getMorphClass(),
|
'commenter_type' => $user->getMorphClass(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$this->actingAs($user);
|
$this->actingAs($user);
|
||||||
@@ -99,8 +98,8 @@ it('hides load more button when all comments are loaded', function () {
|
|||||||
Comment::factory()->count(5)->create([
|
Comment::factory()->count(5)->create([
|
||||||
'commentable_id' => $post->id,
|
'commentable_id' => $post->id,
|
||||||
'commentable_type' => $post->getMorphClass(),
|
'commentable_type' => $post->getMorphClass(),
|
||||||
'user_id' => $user->getKey(),
|
'commenter_id' => $user->getKey(),
|
||||||
'user_type' => $user->getMorphClass(),
|
'commenter_type' => $user->getMorphClass(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$this->actingAs($user);
|
$this->actingAs($user);
|
||||||
@@ -130,8 +129,8 @@ it('returns comments in correct sort order via computed property', function () {
|
|||||||
$older = Comment::factory()->create([
|
$older = Comment::factory()->create([
|
||||||
'commentable_id' => $post->id,
|
'commentable_id' => $post->id,
|
||||||
'commentable_type' => $post->getMorphClass(),
|
'commentable_type' => $post->getMorphClass(),
|
||||||
'user_id' => $user->getKey(),
|
'commenter_id' => $user->getKey(),
|
||||||
'user_type' => $user->getMorphClass(),
|
'commenter_type' => $user->getMorphClass(),
|
||||||
'body' => '<p>Older comment</p>',
|
'body' => '<p>Older comment</p>',
|
||||||
'created_at' => now()->subHour(),
|
'created_at' => now()->subHour(),
|
||||||
]);
|
]);
|
||||||
@@ -139,8 +138,8 @@ it('returns comments in correct sort order via computed property', function () {
|
|||||||
$newer = Comment::factory()->create([
|
$newer = Comment::factory()->create([
|
||||||
'commentable_id' => $post->id,
|
'commentable_id' => $post->id,
|
||||||
'commentable_type' => $post->getMorphClass(),
|
'commentable_type' => $post->getMorphClass(),
|
||||||
'user_id' => $user->getKey(),
|
'commenter_id' => $user->getKey(),
|
||||||
'user_type' => $user->getMorphClass(),
|
'commenter_type' => $user->getMorphClass(),
|
||||||
'body' => '<p>Newer comment</p>',
|
'body' => '<p>Newer comment</p>',
|
||||||
'created_at' => now(),
|
'created_at' => now(),
|
||||||
]);
|
]);
|
||||||
@@ -167,8 +166,8 @@ it('displays total comment count', function () {
|
|||||||
Comment::factory()->count(3)->create([
|
Comment::factory()->count(3)->create([
|
||||||
'commentable_id' => $post->id,
|
'commentable_id' => $post->id,
|
||||||
'commentable_type' => $post->getMorphClass(),
|
'commentable_type' => $post->getMorphClass(),
|
||||||
'user_id' => $user->getKey(),
|
'commenter_id' => $user->getKey(),
|
||||||
'user_type' => $user->getMorphClass(),
|
'commenter_type' => $user->getMorphClass(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$this->actingAs($user);
|
$this->actingAs($user);
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
use Relaticle\Comments\Comment;
|
|
||||||
use Relaticle\Comments\Filament\Actions\CommentsTableAction;
|
use Relaticle\Comments\Filament\Actions\CommentsTableAction;
|
||||||
|
use Relaticle\Comments\Models\Comment;
|
||||||
use Relaticle\Comments\Tests\Models\Post;
|
use Relaticle\Comments\Tests\Models\Post;
|
||||||
use Relaticle\Comments\Tests\Models\User;
|
use Relaticle\Comments\Tests\Models\User;
|
||||||
|
|
||||||
@@ -17,10 +17,11 @@ it('configures as a slide-over', function () {
|
|||||||
expect($action->isModalSlideOver())->toBeTrue();
|
expect($action->isModalSlideOver())->toBeTrue();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('has modal content configured', function () {
|
it('disables modal submit and cancel actions', function () {
|
||||||
$action = CommentsTableAction::make('comments');
|
$action = CommentsTableAction::make('comments');
|
||||||
|
|
||||||
expect($action->hasModalContent())->toBeTrue();
|
expect($action->getModalSubmitAction())->toBeFalsy()
|
||||||
|
->and($action->getModalCancelAction())->toBeFalsy();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('shows badge with comment count for the record', function () {
|
it('shows badge with comment count for the record', function () {
|
||||||
@@ -30,8 +31,8 @@ it('shows badge with comment count for the record', function () {
|
|||||||
Comment::factory()->count(5)->create([
|
Comment::factory()->count(5)->create([
|
||||||
'commentable_id' => $post->id,
|
'commentable_id' => $post->id,
|
||||||
'commentable_type' => $post->getMorphClass(),
|
'commentable_type' => $post->getMorphClass(),
|
||||||
'user_id' => $user->getKey(),
|
'commenter_id' => $user->getKey(),
|
||||||
'user_type' => $user->getMorphClass(),
|
'commenter_type' => $user->getMorphClass(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$action = CommentsTableAction::make('comments');
|
$action = CommentsTableAction::make('comments');
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
use Livewire\Livewire;
|
use Livewire\Livewire;
|
||||||
use Relaticle\Comments\Comment;
|
|
||||||
use Relaticle\Comments\Livewire\Comments;
|
use Relaticle\Comments\Livewire\Comments;
|
||||||
|
use Relaticle\Comments\Models\Comment;
|
||||||
use Relaticle\Comments\Tests\Models\Post;
|
use Relaticle\Comments\Tests\Models\Post;
|
||||||
use Relaticle\Comments\Tests\Models\User;
|
use Relaticle\Comments\Tests\Models\User;
|
||||||
|
|
||||||
@@ -13,8 +13,8 @@ it('strips script tags from comment body on create', function () {
|
|||||||
$comment = Comment::factory()->create([
|
$comment = Comment::factory()->create([
|
||||||
'commentable_id' => $post->id,
|
'commentable_id' => $post->id,
|
||||||
'commentable_type' => $post->getMorphClass(),
|
'commentable_type' => $post->getMorphClass(),
|
||||||
'user_id' => $user->getKey(),
|
'commenter_id' => $user->getKey(),
|
||||||
'user_type' => $user->getMorphClass(),
|
'commenter_type' => $user->getMorphClass(),
|
||||||
'body' => '<p>Hello</p><script>alert(1)</script>',
|
'body' => '<p>Hello</p><script>alert(1)</script>',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -29,8 +29,8 @@ it('strips event handler attributes from comment body', function () {
|
|||||||
$comment = Comment::factory()->create([
|
$comment = Comment::factory()->create([
|
||||||
'commentable_id' => $post->id,
|
'commentable_id' => $post->id,
|
||||||
'commentable_type' => $post->getMorphClass(),
|
'commentable_type' => $post->getMorphClass(),
|
||||||
'user_id' => $user->getKey(),
|
'commenter_id' => $user->getKey(),
|
||||||
'user_type' => $user->getMorphClass(),
|
'commenter_type' => $user->getMorphClass(),
|
||||||
'body' => '<img onerror="alert(1)" src="x">',
|
'body' => '<img onerror="alert(1)" src="x">',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -45,8 +45,8 @@ it('strips style tags from comment body', function () {
|
|||||||
$comment = Comment::factory()->create([
|
$comment = Comment::factory()->create([
|
||||||
'commentable_id' => $post->id,
|
'commentable_id' => $post->id,
|
||||||
'commentable_type' => $post->getMorphClass(),
|
'commentable_type' => $post->getMorphClass(),
|
||||||
'user_id' => $user->getKey(),
|
'commenter_id' => $user->getKey(),
|
||||||
'user_type' => $user->getMorphClass(),
|
'commenter_type' => $user->getMorphClass(),
|
||||||
'body' => '<p>Hi</p><style>body{display:none}</style>',
|
'body' => '<p>Hi</p><style>body{display:none}</style>',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -61,8 +61,8 @@ it('strips iframe tags from comment body', function () {
|
|||||||
$comment = Comment::factory()->create([
|
$comment = Comment::factory()->create([
|
||||||
'commentable_id' => $post->id,
|
'commentable_id' => $post->id,
|
||||||
'commentable_type' => $post->getMorphClass(),
|
'commentable_type' => $post->getMorphClass(),
|
||||||
'user_id' => $user->getKey(),
|
'commenter_id' => $user->getKey(),
|
||||||
'user_type' => $user->getMorphClass(),
|
'commenter_type' => $user->getMorphClass(),
|
||||||
'body' => '<p>Hi</p><iframe src="evil.com"></iframe>',
|
'body' => '<p>Hi</p><iframe src="evil.com"></iframe>',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -85,8 +85,8 @@ it('preserves safe HTML formatting through sanitization', function () {
|
|||||||
$comment = Comment::factory()->create([
|
$comment = Comment::factory()->create([
|
||||||
'commentable_id' => $post->id,
|
'commentable_id' => $post->id,
|
||||||
'commentable_type' => $post->getMorphClass(),
|
'commentable_type' => $post->getMorphClass(),
|
||||||
'user_id' => $user->getKey(),
|
'commenter_id' => $user->getKey(),
|
||||||
'user_type' => $user->getMorphClass(),
|
'commenter_type' => $user->getMorphClass(),
|
||||||
'body' => $safeHtml,
|
'body' => $safeHtml,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -108,8 +108,8 @@ it('sanitizes comment body on update', function () {
|
|||||||
$comment = Comment::factory()->create([
|
$comment = Comment::factory()->create([
|
||||||
'commentable_id' => $post->id,
|
'commentable_id' => $post->id,
|
||||||
'commentable_type' => $post->getMorphClass(),
|
'commentable_type' => $post->getMorphClass(),
|
||||||
'user_id' => $user->getKey(),
|
'commenter_id' => $user->getKey(),
|
||||||
'user_type' => $user->getMorphClass(),
|
'commenter_type' => $user->getMorphClass(),
|
||||||
'body' => '<p>Clean content</p>',
|
'body' => '<p>Clean content</p>',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -130,8 +130,8 @@ it('strips javascript protocol from link href', function () {
|
|||||||
$comment = Comment::factory()->create([
|
$comment = Comment::factory()->create([
|
||||||
'commentable_id' => $post->id,
|
'commentable_id' => $post->id,
|
||||||
'commentable_type' => $post->getMorphClass(),
|
'commentable_type' => $post->getMorphClass(),
|
||||||
'user_id' => $user->getKey(),
|
'commenter_id' => $user->getKey(),
|
||||||
'user_type' => $user->getMorphClass(),
|
'commenter_type' => $user->getMorphClass(),
|
||||||
'body' => '<a href="javascript:alert(1)">click me</a>',
|
'body' => '<a href="javascript:alert(1)">click me</a>',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -146,8 +146,8 @@ it('strips onclick handler from elements', function () {
|
|||||||
$comment = Comment::factory()->create([
|
$comment = Comment::factory()->create([
|
||||||
'commentable_id' => $post->id,
|
'commentable_id' => $post->id,
|
||||||
'commentable_type' => $post->getMorphClass(),
|
'commentable_type' => $post->getMorphClass(),
|
||||||
'user_id' => $user->getKey(),
|
'commenter_id' => $user->getKey(),
|
||||||
'user_type' => $user->getMorphClass(),
|
'commenter_type' => $user->getMorphClass(),
|
||||||
'body' => '<div onclick="alert(1)">click me</div>',
|
'body' => '<div onclick="alert(1)">click me</div>',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -155,6 +155,26 @@ it('strips onclick handler from elements', function () {
|
|||||||
expect($comment->body)->toContain('click me');
|
expect($comment->body)->toContain('click me');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('preserves mention data attributes in comment body', function () {
|
||||||
|
$user = User::factory()->create();
|
||||||
|
$post = Post::factory()->create();
|
||||||
|
|
||||||
|
$body = '<span data-type="mention" data-id="1" data-label="max" data-char="@">@max</span>';
|
||||||
|
|
||||||
|
$comment = Comment::factory()->create([
|
||||||
|
'commentable_id' => $post->id,
|
||||||
|
'commentable_type' => $post->getMorphClass(),
|
||||||
|
'commenter_id' => $user->getKey(),
|
||||||
|
'commenter_type' => $user->getMorphClass(),
|
||||||
|
'body' => $body,
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect($comment->body)->toContain('data-type="mention"');
|
||||||
|
expect($comment->body)->toContain('data-id="1"');
|
||||||
|
expect($comment->body)->toContain('data-label="max"');
|
||||||
|
expect($comment->body)->toContain('data-char="@"');
|
||||||
|
});
|
||||||
|
|
||||||
it('sanitizes content submitted through livewire component', function () {
|
it('sanitizes content submitted through livewire component', function () {
|
||||||
$user = User::factory()->create();
|
$user = User::factory()->create();
|
||||||
$post = Post::factory()->create();
|
$post = Post::factory()->create();
|
||||||
@@ -162,7 +182,7 @@ it('sanitizes content submitted through livewire component', function () {
|
|||||||
$this->actingAs($user);
|
$this->actingAs($user);
|
||||||
|
|
||||||
Livewire::test(Comments::class, ['model' => $post])
|
Livewire::test(Comments::class, ['model' => $post])
|
||||||
->set('newComment', '<p>Hello</p><script>alert("xss")</script>')
|
->set('commentData.body', '<p>Hello</p><script>alert("xss")</script>')
|
||||||
->call('addComment');
|
->call('addComment');
|
||||||
|
|
||||||
$comment = Comment::first();
|
$comment = Comment::first();
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
use Relaticle\Comments\Comment;
|
use Relaticle\Comments\Models\Comment;
|
||||||
use Relaticle\Comments\Tests\Models\Post;
|
use Relaticle\Comments\Tests\Models\Post;
|
||||||
use Relaticle\Comments\Tests\Models\User;
|
use Relaticle\Comments\Tests\Models\User;
|
||||||
|
|
||||||
@@ -11,8 +11,8 @@ it('provides comments relationship on commentable model', function () {
|
|||||||
Comment::factory()->count(3)->create([
|
Comment::factory()->count(3)->create([
|
||||||
'commentable_type' => $post->getMorphClass(),
|
'commentable_type' => $post->getMorphClass(),
|
||||||
'commentable_id' => $post->id,
|
'commentable_id' => $post->id,
|
||||||
'user_type' => $user->getMorphClass(),
|
'commenter_type' => $user->getMorphClass(),
|
||||||
'user_id' => $user->id,
|
'commenter_id' => $user->id,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
expect($post->comments)->toHaveCount(3);
|
expect($post->comments)->toHaveCount(3);
|
||||||
@@ -25,8 +25,8 @@ it('provides topLevelComments excluding replies', function () {
|
|||||||
$attrs = [
|
$attrs = [
|
||||||
'commentable_type' => $post->getMorphClass(),
|
'commentable_type' => $post->getMorphClass(),
|
||||||
'commentable_id' => $post->id,
|
'commentable_id' => $post->id,
|
||||||
'user_type' => $user->getMorphClass(),
|
'commenter_type' => $user->getMorphClass(),
|
||||||
'user_id' => $user->id,
|
'commenter_id' => $user->id,
|
||||||
];
|
];
|
||||||
|
|
||||||
$topLevel = Comment::factory()->create($attrs);
|
$topLevel = Comment::factory()->create($attrs);
|
||||||
@@ -46,8 +46,8 @@ it('provides comment count', function () {
|
|||||||
Comment::factory()->count(5)->create([
|
Comment::factory()->count(5)->create([
|
||||||
'commentable_type' => $post->getMorphClass(),
|
'commentable_type' => $post->getMorphClass(),
|
||||||
'commentable_id' => $post->id,
|
'commentable_id' => $post->id,
|
||||||
'user_type' => $user->getMorphClass(),
|
'commenter_type' => $user->getMorphClass(),
|
||||||
'user_id' => $user->id,
|
'commenter_id' => $user->id,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
expect($post->commentCount())->toBe(5);
|
expect($post->commentCount())->toBe(5);
|
||||||
@@ -61,15 +61,15 @@ it('scopes comments to the specific commentable', function () {
|
|||||||
Comment::factory()->count(3)->create([
|
Comment::factory()->count(3)->create([
|
||||||
'commentable_type' => $post1->getMorphClass(),
|
'commentable_type' => $post1->getMorphClass(),
|
||||||
'commentable_id' => $post1->id,
|
'commentable_id' => $post1->id,
|
||||||
'user_type' => $user->getMorphClass(),
|
'commenter_type' => $user->getMorphClass(),
|
||||||
'user_id' => $user->id,
|
'commenter_id' => $user->id,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
Comment::factory()->count(2)->create([
|
Comment::factory()->count(2)->create([
|
||||||
'commentable_type' => $post2->getMorphClass(),
|
'commentable_type' => $post2->getMorphClass(),
|
||||||
'commentable_id' => $post2->id,
|
'commentable_id' => $post2->id,
|
||||||
'user_type' => $user->getMorphClass(),
|
'commenter_type' => $user->getMorphClass(),
|
||||||
'user_id' => $user->id,
|
'commenter_id' => $user->id,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
expect($post1->commentCount())->toBe(3);
|
expect($post1->commentCount())->toBe(3);
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
use Livewire\Livewire;
|
use Livewire\Livewire;
|
||||||
use Relaticle\Comments\Comment;
|
|
||||||
use Relaticle\Comments\Livewire\CommentItem;
|
use Relaticle\Comments\Livewire\CommentItem;
|
||||||
|
use Relaticle\Comments\Models\Comment;
|
||||||
use Relaticle\Comments\Tests\Models\Post;
|
use Relaticle\Comments\Tests\Models\Post;
|
||||||
use Relaticle\Comments\Tests\Models\User;
|
use Relaticle\Comments\Tests\Models\User;
|
||||||
|
|
||||||
@@ -14,12 +14,12 @@ it('renders mention with styled span', function () {
|
|||||||
$comment = Comment::factory()->create([
|
$comment = Comment::factory()->create([
|
||||||
'commentable_id' => $post->id,
|
'commentable_id' => $post->id,
|
||||||
'commentable_type' => $post->getMorphClass(),
|
'commentable_type' => $post->getMorphClass(),
|
||||||
'user_id' => $user->getKey(),
|
'commenter_id' => $user->getKey(),
|
||||||
'user_type' => $user->getMorphClass(),
|
'commenter_type' => $user->getMorphClass(),
|
||||||
'body' => '<p>@Alice said hi</p>',
|
'body' => '@Alice said hi',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$comment->mentions()->attach($alice->id, ['user_type' => $alice->getMorphClass()]);
|
$comment->mentions()->attach($alice->id, ['commenter_type' => $alice->getMorphClass()]);
|
||||||
|
|
||||||
$rendered = $comment->renderBodyWithMentions();
|
$rendered = $comment->renderBodyWithMentions();
|
||||||
|
|
||||||
@@ -36,13 +36,13 @@ it('renders multiple mentions with styled spans', function () {
|
|||||||
$comment = Comment::factory()->create([
|
$comment = Comment::factory()->create([
|
||||||
'commentable_id' => $post->id,
|
'commentable_id' => $post->id,
|
||||||
'commentable_type' => $post->getMorphClass(),
|
'commentable_type' => $post->getMorphClass(),
|
||||||
'user_id' => $user->getKey(),
|
'commenter_id' => $user->getKey(),
|
||||||
'user_type' => $user->getMorphClass(),
|
'commenter_type' => $user->getMorphClass(),
|
||||||
'body' => '<p>@Alice and @Bob</p>',
|
'body' => '@Alice and @Bob',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$comment->mentions()->attach($alice->id, ['user_type' => $alice->getMorphClass()]);
|
$comment->mentions()->attach($alice->id, ['commenter_type' => $alice->getMorphClass()]);
|
||||||
$comment->mentions()->attach($bob->id, ['user_type' => $bob->getMorphClass()]);
|
$comment->mentions()->attach($bob->id, ['commenter_type' => $bob->getMorphClass()]);
|
||||||
|
|
||||||
$rendered = $comment->renderBodyWithMentions();
|
$rendered = $comment->renderBodyWithMentions();
|
||||||
|
|
||||||
@@ -51,6 +51,28 @@ it('renders multiple mentions with styled spans', function () {
|
|||||||
expect($rendered)->toContain('comment-mention');
|
expect($rendered)->toContain('comment-mention');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('renders rich-editor mention span as styled mention', function () {
|
||||||
|
$user = User::factory()->create();
|
||||||
|
$alice = User::factory()->create(['name' => 'Alice']);
|
||||||
|
$post = Post::factory()->create();
|
||||||
|
|
||||||
|
$comment = Comment::factory()->create([
|
||||||
|
'commentable_id' => $post->id,
|
||||||
|
'commentable_type' => $post->getMorphClass(),
|
||||||
|
'commenter_id' => $user->getKey(),
|
||||||
|
'commenter_type' => $user->getMorphClass(),
|
||||||
|
'body' => '<p><span data-type="mention" data-id="'.$alice->id.'" data-label="Alice" data-char="@">@Alice</span> said hi</p>',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$comment->mentions()->attach($alice->id, ['commenter_type' => $alice->getMorphClass()]);
|
||||||
|
|
||||||
|
$rendered = $comment->renderBodyWithMentions();
|
||||||
|
|
||||||
|
expect($rendered)->toContain('comment-mention');
|
||||||
|
expect($rendered)->toContain('@Alice</span>');
|
||||||
|
expect($rendered)->not->toContain('data-type="mention"');
|
||||||
|
});
|
||||||
|
|
||||||
it('does not style non-mentioned @text', function () {
|
it('does not style non-mentioned @text', function () {
|
||||||
$user = User::factory()->create();
|
$user = User::factory()->create();
|
||||||
$post = Post::factory()->create();
|
$post = Post::factory()->create();
|
||||||
@@ -58,9 +80,9 @@ it('does not style non-mentioned @text', function () {
|
|||||||
$comment = Comment::factory()->create([
|
$comment = Comment::factory()->create([
|
||||||
'commentable_id' => $post->id,
|
'commentable_id' => $post->id,
|
||||||
'commentable_type' => $post->getMorphClass(),
|
'commentable_type' => $post->getMorphClass(),
|
||||||
'user_id' => $user->getKey(),
|
'commenter_id' => $user->getKey(),
|
||||||
'user_type' => $user->getMorphClass(),
|
'commenter_type' => $user->getMorphClass(),
|
||||||
'body' => '<p>@ghost is not here</p>',
|
'body' => '@ghost is not here',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$rendered = $comment->renderBodyWithMentions();
|
$rendered = $comment->renderBodyWithMentions();
|
||||||
@@ -76,12 +98,12 @@ it('renders comment-mention class in Livewire component', function () {
|
|||||||
$comment = Comment::factory()->create([
|
$comment = Comment::factory()->create([
|
||||||
'commentable_id' => $post->id,
|
'commentable_id' => $post->id,
|
||||||
'commentable_type' => $post->getMorphClass(),
|
'commentable_type' => $post->getMorphClass(),
|
||||||
'user_id' => $user->getKey(),
|
'commenter_id' => $user->getKey(),
|
||||||
'user_type' => $user->getMorphClass(),
|
'commenter_type' => $user->getMorphClass(),
|
||||||
'body' => '<p>Hello @Alice</p>',
|
'body' => 'Hello @Alice',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$comment->mentions()->attach($alice->id, ['user_type' => $alice->getMorphClass()]);
|
$comment->mentions()->attach($alice->id, ['commenter_type' => $alice->getMorphClass()]);
|
||||||
|
|
||||||
$this->actingAs($user);
|
$this->actingAs($user);
|
||||||
|
|
||||||
|
|||||||
@@ -2,14 +2,25 @@
|
|||||||
|
|
||||||
use Illuminate\Support\Collection;
|
use Illuminate\Support\Collection;
|
||||||
use Illuminate\Support\Facades\Event;
|
use Illuminate\Support\Facades\Event;
|
||||||
use Relaticle\Comments\Comment;
|
|
||||||
use Relaticle\Comments\Contracts\MentionResolver;
|
use Relaticle\Comments\Contracts\MentionResolver;
|
||||||
use Relaticle\Comments\Events\UserMentioned;
|
use Relaticle\Comments\Events\UserMentioned;
|
||||||
use Relaticle\Comments\Mentions\DefaultMentionResolver;
|
use Relaticle\Comments\Mentions\DefaultMentionResolver;
|
||||||
use Relaticle\Comments\Mentions\MentionParser;
|
use Relaticle\Comments\Mentions\MentionParser;
|
||||||
|
use Relaticle\Comments\Models\Comment;
|
||||||
use Relaticle\Comments\Tests\Models\Post;
|
use Relaticle\Comments\Tests\Models\Post;
|
||||||
use Relaticle\Comments\Tests\Models\User;
|
use Relaticle\Comments\Tests\Models\User;
|
||||||
|
|
||||||
|
it('parses rich-editor mention spans using data-id', function () {
|
||||||
|
$john = User::factory()->create(['name' => 'john']);
|
||||||
|
|
||||||
|
$parser = app(MentionParser::class);
|
||||||
|
$body = '<p>Hello <span data-type="mention" data-id="'.$john->id.'" data-label="john" data-char="@">@john</span></p>';
|
||||||
|
$result = $parser->parse($body);
|
||||||
|
|
||||||
|
expect($result)->toHaveCount(1);
|
||||||
|
expect($result->first())->toBe($john->id);
|
||||||
|
});
|
||||||
|
|
||||||
it('parses @username from plain text body', function () {
|
it('parses @username from plain text body', function () {
|
||||||
User::factory()->create(['name' => 'john']);
|
User::factory()->create(['name' => 'john']);
|
||||||
User::factory()->create(['name' => 'jane']);
|
User::factory()->create(['name' => 'jane']);
|
||||||
@@ -62,8 +73,8 @@ it('stores mentions in comment_mentions table on create', function () {
|
|||||||
$comment = Comment::factory()->create([
|
$comment = Comment::factory()->create([
|
||||||
'commentable_id' => $post->id,
|
'commentable_id' => $post->id,
|
||||||
'commentable_type' => $post->getMorphClass(),
|
'commentable_type' => $post->getMorphClass(),
|
||||||
'user_id' => $user->getKey(),
|
'commenter_id' => $user->getKey(),
|
||||||
'user_type' => $user->getMorphClass(),
|
'commenter_type' => $user->getMorphClass(),
|
||||||
'body' => '<p>Hello @john and @jane</p>',
|
'body' => '<p>Hello @john and @jane</p>',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -85,8 +96,8 @@ it('dispatches UserMentioned event for each mentioned user', function () {
|
|||||||
$comment = Comment::factory()->create([
|
$comment = Comment::factory()->create([
|
||||||
'commentable_id' => $post->id,
|
'commentable_id' => $post->id,
|
||||||
'commentable_type' => $post->getMorphClass(),
|
'commentable_type' => $post->getMorphClass(),
|
||||||
'user_id' => $user->getKey(),
|
'commenter_id' => $user->getKey(),
|
||||||
'user_type' => $user->getMorphClass(),
|
'commenter_type' => $user->getMorphClass(),
|
||||||
'body' => '<p>Hello @john and @jane</p>',
|
'body' => '<p>Hello @john and @jane</p>',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -116,8 +127,8 @@ it('only dispatches UserMentioned for newly added mentions on update', function
|
|||||||
$comment = Comment::factory()->create([
|
$comment = Comment::factory()->create([
|
||||||
'commentable_id' => $post->id,
|
'commentable_id' => $post->id,
|
||||||
'commentable_type' => $post->getMorphClass(),
|
'commentable_type' => $post->getMorphClass(),
|
||||||
'user_id' => $user->getKey(),
|
'commenter_id' => $user->getKey(),
|
||||||
'user_type' => $user->getMorphClass(),
|
'commenter_type' => $user->getMorphClass(),
|
||||||
'body' => '<p>Hello @john</p>',
|
'body' => '<p>Hello @john</p>',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -146,8 +157,8 @@ it('removes mentions from pivot when user removed from body', function () {
|
|||||||
$comment = Comment::factory()->create([
|
$comment = Comment::factory()->create([
|
||||||
'commentable_id' => $post->id,
|
'commentable_id' => $post->id,
|
||||||
'commentable_type' => $post->getMorphClass(),
|
'commentable_type' => $post->getMorphClass(),
|
||||||
'user_id' => $user->getKey(),
|
'commenter_id' => $user->getKey(),
|
||||||
'user_type' => $user->getMorphClass(),
|
'commenter_type' => $user->getMorphClass(),
|
||||||
'body' => '<p>Hello @john and @jane</p>',
|
'body' => '<p>Hello @john and @jane</p>',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|||||||
@@ -1,72 +1,12 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
use Livewire\Livewire;
|
use Livewire\Livewire;
|
||||||
use Relaticle\Comments\Comment;
|
|
||||||
use Relaticle\Comments\Livewire\CommentItem;
|
use Relaticle\Comments\Livewire\CommentItem;
|
||||||
use Relaticle\Comments\Livewire\Comments;
|
use Relaticle\Comments\Livewire\Comments;
|
||||||
|
use Relaticle\Comments\Models\Comment;
|
||||||
use Relaticle\Comments\Tests\Models\Post;
|
use Relaticle\Comments\Tests\Models\Post;
|
||||||
use Relaticle\Comments\Tests\Models\User;
|
use Relaticle\Comments\Tests\Models\User;
|
||||||
|
|
||||||
it('returns matching users for search query', function () {
|
|
||||||
$alice = User::factory()->create(['name' => 'Alice']);
|
|
||||||
User::factory()->create(['name' => 'Bob']);
|
|
||||||
$post = Post::factory()->create();
|
|
||||||
|
|
||||||
$this->actingAs($alice);
|
|
||||||
|
|
||||||
$component = Livewire::test(Comments::class, ['model' => $post]);
|
|
||||||
$results = $component->instance()->searchUsers('Ali');
|
|
||||||
|
|
||||||
expect($results)->toHaveCount(1);
|
|
||||||
expect($results[0])->toMatchArray([
|
|
||||||
'id' => $alice->id,
|
|
||||||
'name' => 'Alice',
|
|
||||||
]);
|
|
||||||
expect($results[0])->toHaveKey('avatar_url');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('returns empty array for empty query', function () {
|
|
||||||
$user = User::factory()->create();
|
|
||||||
$post = Post::factory()->create();
|
|
||||||
|
|
||||||
$this->actingAs($user);
|
|
||||||
|
|
||||||
$component = Livewire::test(Comments::class, ['model' => $post]);
|
|
||||||
$results = $component->instance()->searchUsers('');
|
|
||||||
|
|
||||||
expect($results)->toBeEmpty();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('returns empty array for no matches', function () {
|
|
||||||
$user = User::factory()->create(['name' => 'Alice']);
|
|
||||||
$post = Post::factory()->create();
|
|
||||||
|
|
||||||
$this->actingAs($user);
|
|
||||||
|
|
||||||
$component = Livewire::test(Comments::class, ['model' => $post]);
|
|
||||||
$results = $component->instance()->searchUsers('zzz');
|
|
||||||
|
|
||||||
expect($results)->toBeEmpty();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('limits search results to configured max', function () {
|
|
||||||
$user = User::factory()->create(['name' => 'Admin']);
|
|
||||||
$post = Post::factory()->create();
|
|
||||||
|
|
||||||
for ($i = 1; $i <= 10; $i++) {
|
|
||||||
User::factory()->create(['name' => "Test User {$i}"]);
|
|
||||||
}
|
|
||||||
|
|
||||||
config(['comments.mentions.max_results' => 3]);
|
|
||||||
|
|
||||||
$this->actingAs($user);
|
|
||||||
|
|
||||||
$component = Livewire::test(Comments::class, ['model' => $post]);
|
|
||||||
$results = $component->instance()->searchUsers('Test');
|
|
||||||
|
|
||||||
expect($results)->toHaveCount(3);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('stores mentions when creating comment with @mention', function () {
|
it('stores mentions when creating comment with @mention', function () {
|
||||||
$user = User::factory()->create();
|
$user = User::factory()->create();
|
||||||
$alice = User::factory()->create(['name' => 'Alice']);
|
$alice = User::factory()->create(['name' => 'Alice']);
|
||||||
@@ -75,7 +15,7 @@ it('stores mentions when creating comment with @mention', function () {
|
|||||||
$this->actingAs($user);
|
$this->actingAs($user);
|
||||||
|
|
||||||
Livewire::test(Comments::class, ['model' => $post])
|
Livewire::test(Comments::class, ['model' => $post])
|
||||||
->set('newComment', '<p>Hey @Alice check this</p>')
|
->set('commentData.body', '<p>Hey @Alice check this</p>')
|
||||||
->call('addComment');
|
->call('addComment');
|
||||||
|
|
||||||
$comment = Comment::first();
|
$comment = Comment::first();
|
||||||
@@ -92,8 +32,8 @@ it('stores mentions when editing comment with @mention', function () {
|
|||||||
$comment = Comment::factory()->create([
|
$comment = Comment::factory()->create([
|
||||||
'commentable_id' => $post->id,
|
'commentable_id' => $post->id,
|
||||||
'commentable_type' => $post->getMorphClass(),
|
'commentable_type' => $post->getMorphClass(),
|
||||||
'user_id' => $user->getKey(),
|
'commenter_id' => $user->getKey(),
|
||||||
'user_type' => $user->getMorphClass(),
|
'commenter_type' => $user->getMorphClass(),
|
||||||
'body' => '<p>Original comment</p>',
|
'body' => '<p>Original comment</p>',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -101,7 +41,7 @@ it('stores mentions when editing comment with @mention', function () {
|
|||||||
|
|
||||||
Livewire::test(CommentItem::class, ['comment' => $comment])
|
Livewire::test(CommentItem::class, ['comment' => $comment])
|
||||||
->call('startEdit')
|
->call('startEdit')
|
||||||
->set('editBody', '<p>Updated @Bob</p>')
|
->set('editData.body', '<p>Updated @Bob</p>')
|
||||||
->call('saveEdit');
|
->call('saveEdit');
|
||||||
|
|
||||||
$comment->refresh();
|
$comment->refresh();
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
use Illuminate\Support\Facades\Notification;
|
use Illuminate\Support\Facades\Notification;
|
||||||
use Relaticle\Comments\Comment;
|
|
||||||
use Relaticle\Comments\CommentSubscription;
|
|
||||||
use Relaticle\Comments\Events\CommentCreated;
|
use Relaticle\Comments\Events\CommentCreated;
|
||||||
use Relaticle\Comments\Events\UserMentioned;
|
use Relaticle\Comments\Events\UserMentioned;
|
||||||
use Relaticle\Comments\Listeners\SendCommentRepliedNotification;
|
use Relaticle\Comments\Listeners\SendCommentRepliedNotification;
|
||||||
use Relaticle\Comments\Listeners\SendUserMentionedNotification;
|
use Relaticle\Comments\Listeners\SendUserMentionedNotification;
|
||||||
|
use Relaticle\Comments\Models\Comment;
|
||||||
|
use Relaticle\Comments\Models\Subscription;
|
||||||
use Relaticle\Comments\Notifications\CommentRepliedNotification;
|
use Relaticle\Comments\Notifications\CommentRepliedNotification;
|
||||||
use Relaticle\Comments\Notifications\UserMentionedNotification;
|
use Relaticle\Comments\Notifications\UserMentionedNotification;
|
||||||
use Relaticle\Comments\Tests\Models\Post;
|
use Relaticle\Comments\Tests\Models\Post;
|
||||||
@@ -19,21 +19,21 @@ it('sends CommentRepliedNotification to parent comment author when reply is crea
|
|||||||
$replyAuthor = User::factory()->create();
|
$replyAuthor = User::factory()->create();
|
||||||
$post = Post::factory()->create();
|
$post = Post::factory()->create();
|
||||||
|
|
||||||
CommentSubscription::subscribe($post, $parentAuthor);
|
Subscription::subscribe($post, $parentAuthor);
|
||||||
|
|
||||||
$parentComment = Comment::factory()->create([
|
$parentComment = Comment::factory()->create([
|
||||||
'commentable_id' => $post->id,
|
'commentable_id' => $post->id,
|
||||||
'commentable_type' => $post->getMorphClass(),
|
'commentable_type' => $post->getMorphClass(),
|
||||||
'user_id' => $parentAuthor->getKey(),
|
'commenter_id' => $parentAuthor->getKey(),
|
||||||
'user_type' => $parentAuthor->getMorphClass(),
|
'commenter_type' => $parentAuthor->getMorphClass(),
|
||||||
'body' => '<p>Parent comment</p>',
|
'body' => '<p>Parent comment</p>',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$reply = Comment::factory()->create([
|
$reply = Comment::factory()->create([
|
||||||
'commentable_id' => $post->id,
|
'commentable_id' => $post->id,
|
||||||
'commentable_type' => $post->getMorphClass(),
|
'commentable_type' => $post->getMorphClass(),
|
||||||
'user_id' => $replyAuthor->getKey(),
|
'commenter_id' => $replyAuthor->getKey(),
|
||||||
'user_type' => $replyAuthor->getMorphClass(),
|
'commenter_type' => $replyAuthor->getMorphClass(),
|
||||||
'parent_id' => $parentComment->id,
|
'parent_id' => $parentComment->id,
|
||||||
'body' => '<p>A reply</p>',
|
'body' => '<p>A reply</p>',
|
||||||
]);
|
]);
|
||||||
@@ -51,13 +51,13 @@ it('does NOT send reply notification for top-level comments', function () {
|
|||||||
$subscriber = User::factory()->create();
|
$subscriber = User::factory()->create();
|
||||||
$post = Post::factory()->create();
|
$post = Post::factory()->create();
|
||||||
|
|
||||||
CommentSubscription::subscribe($post, $subscriber);
|
Subscription::subscribe($post, $subscriber);
|
||||||
|
|
||||||
$comment = Comment::factory()->create([
|
$comment = Comment::factory()->create([
|
||||||
'commentable_id' => $post->id,
|
'commentable_id' => $post->id,
|
||||||
'commentable_type' => $post->getMorphClass(),
|
'commentable_type' => $post->getMorphClass(),
|
||||||
'user_id' => $author->getKey(),
|
'commenter_id' => $author->getKey(),
|
||||||
'user_type' => $author->getMorphClass(),
|
'commenter_type' => $author->getMorphClass(),
|
||||||
'body' => '<p>Top-level comment</p>',
|
'body' => '<p>Top-level comment</p>',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -73,21 +73,21 @@ it('does NOT send reply notification to the reply author', function () {
|
|||||||
$user = User::factory()->create();
|
$user = User::factory()->create();
|
||||||
$post = Post::factory()->create();
|
$post = Post::factory()->create();
|
||||||
|
|
||||||
CommentSubscription::subscribe($post, $user);
|
Subscription::subscribe($post, $user);
|
||||||
|
|
||||||
$parentComment = Comment::factory()->create([
|
$parentComment = Comment::factory()->create([
|
||||||
'commentable_id' => $post->id,
|
'commentable_id' => $post->id,
|
||||||
'commentable_type' => $post->getMorphClass(),
|
'commentable_type' => $post->getMorphClass(),
|
||||||
'user_id' => $user->getKey(),
|
'commenter_id' => $user->getKey(),
|
||||||
'user_type' => $user->getMorphClass(),
|
'commenter_type' => $user->getMorphClass(),
|
||||||
'body' => '<p>My comment</p>',
|
'body' => '<p>My comment</p>',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$reply = Comment::factory()->create([
|
$reply = Comment::factory()->create([
|
||||||
'commentable_id' => $post->id,
|
'commentable_id' => $post->id,
|
||||||
'commentable_type' => $post->getMorphClass(),
|
'commentable_type' => $post->getMorphClass(),
|
||||||
'user_id' => $user->getKey(),
|
'commenter_id' => $user->getKey(),
|
||||||
'user_type' => $user->getMorphClass(),
|
'commenter_type' => $user->getMorphClass(),
|
||||||
'parent_id' => $parentComment->id,
|
'parent_id' => $parentComment->id,
|
||||||
'body' => '<p>My own reply</p>',
|
'body' => '<p>My own reply</p>',
|
||||||
]);
|
]);
|
||||||
@@ -108,8 +108,8 @@ it('sends UserMentionedNotification when a user is mentioned', function () {
|
|||||||
$comment = Comment::factory()->create([
|
$comment = Comment::factory()->create([
|
||||||
'commentable_id' => $post->id,
|
'commentable_id' => $post->id,
|
||||||
'commentable_type' => $post->getMorphClass(),
|
'commentable_type' => $post->getMorphClass(),
|
||||||
'user_id' => $author->getKey(),
|
'commenter_id' => $author->getKey(),
|
||||||
'user_type' => $author->getMorphClass(),
|
'commenter_type' => $author->getMorphClass(),
|
||||||
'body' => '<p>Hey @someone</p>',
|
'body' => '<p>Hey @someone</p>',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -128,8 +128,8 @@ it('does NOT send mention notification to the comment author', function () {
|
|||||||
$comment = Comment::factory()->create([
|
$comment = Comment::factory()->create([
|
||||||
'commentable_id' => $post->id,
|
'commentable_id' => $post->id,
|
||||||
'commentable_type' => $post->getMorphClass(),
|
'commentable_type' => $post->getMorphClass(),
|
||||||
'user_id' => $author->getKey(),
|
'commenter_id' => $author->getKey(),
|
||||||
'user_type' => $author->getMorphClass(),
|
'commenter_type' => $author->getMorphClass(),
|
||||||
'body' => '<p>Hey @myself</p>',
|
'body' => '<p>Hey @myself</p>',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -146,22 +146,22 @@ it('does NOT send reply notification to unsubscribed user', function () {
|
|||||||
$unsubscribedUser = User::factory()->create();
|
$unsubscribedUser = User::factory()->create();
|
||||||
$post = Post::factory()->create();
|
$post = Post::factory()->create();
|
||||||
|
|
||||||
CommentSubscription::subscribe($post, $unsubscribedUser);
|
Subscription::subscribe($post, $unsubscribedUser);
|
||||||
CommentSubscription::unsubscribe($post, $unsubscribedUser);
|
Subscription::unsubscribe($post, $unsubscribedUser);
|
||||||
|
|
||||||
$parentComment = Comment::factory()->create([
|
$parentComment = Comment::factory()->create([
|
||||||
'commentable_id' => $post->id,
|
'commentable_id' => $post->id,
|
||||||
'commentable_type' => $post->getMorphClass(),
|
'commentable_type' => $post->getMorphClass(),
|
||||||
'user_id' => $unsubscribedUser->getKey(),
|
'commenter_id' => $unsubscribedUser->getKey(),
|
||||||
'user_type' => $unsubscribedUser->getMorphClass(),
|
'commenter_type' => $unsubscribedUser->getMorphClass(),
|
||||||
'body' => '<p>Original</p>',
|
'body' => '<p>Original</p>',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$reply = Comment::factory()->create([
|
$reply = Comment::factory()->create([
|
||||||
'commentable_id' => $post->id,
|
'commentable_id' => $post->id,
|
||||||
'commentable_type' => $post->getMorphClass(),
|
'commentable_type' => $post->getMorphClass(),
|
||||||
'user_id' => $author->getKey(),
|
'commenter_id' => $author->getKey(),
|
||||||
'user_type' => $author->getMorphClass(),
|
'commenter_type' => $author->getMorphClass(),
|
||||||
'parent_id' => $parentComment->id,
|
'parent_id' => $parentComment->id,
|
||||||
'body' => '<p>Reply</p>',
|
'body' => '<p>Reply</p>',
|
||||||
]);
|
]);
|
||||||
@@ -178,20 +178,20 @@ it('auto-subscribes the comment author when creating a comment', function () {
|
|||||||
$author = User::factory()->create();
|
$author = User::factory()->create();
|
||||||
$post = Post::factory()->create();
|
$post = Post::factory()->create();
|
||||||
|
|
||||||
expect(CommentSubscription::isSubscribed($post, $author))->toBeFalse();
|
expect(Subscription::isSubscribed($post, $author))->toBeFalse();
|
||||||
|
|
||||||
$comment = Comment::factory()->create([
|
$comment = Comment::factory()->create([
|
||||||
'commentable_id' => $post->id,
|
'commentable_id' => $post->id,
|
||||||
'commentable_type' => $post->getMorphClass(),
|
'commentable_type' => $post->getMorphClass(),
|
||||||
'user_id' => $author->getKey(),
|
'commenter_id' => $author->getKey(),
|
||||||
'user_type' => $author->getMorphClass(),
|
'commenter_type' => $author->getMorphClass(),
|
||||||
'body' => '<p>My comment</p>',
|
'body' => '<p>My comment</p>',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$listener = new SendCommentRepliedNotification;
|
$listener = new SendCommentRepliedNotification;
|
||||||
$listener->handle(new CommentCreated($comment));
|
$listener->handle(new CommentCreated($comment));
|
||||||
|
|
||||||
expect(CommentSubscription::isSubscribed($post, $author))->toBeTrue();
|
expect(Subscription::isSubscribed($post, $author))->toBeTrue();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('suppresses all notifications when notifications are disabled via config', function () {
|
it('suppresses all notifications when notifications are disabled via config', function () {
|
||||||
@@ -203,21 +203,21 @@ it('suppresses all notifications when notifications are disabled via config', fu
|
|||||||
$mentioned = User::factory()->create();
|
$mentioned = User::factory()->create();
|
||||||
$post = Post::factory()->create();
|
$post = Post::factory()->create();
|
||||||
|
|
||||||
CommentSubscription::subscribe($post, $subscriber);
|
Subscription::subscribe($post, $subscriber);
|
||||||
|
|
||||||
$parentComment = Comment::factory()->create([
|
$parentComment = Comment::factory()->create([
|
||||||
'commentable_id' => $post->id,
|
'commentable_id' => $post->id,
|
||||||
'commentable_type' => $post->getMorphClass(),
|
'commentable_type' => $post->getMorphClass(),
|
||||||
'user_id' => $subscriber->getKey(),
|
'commenter_id' => $subscriber->getKey(),
|
||||||
'user_type' => $subscriber->getMorphClass(),
|
'commenter_type' => $subscriber->getMorphClass(),
|
||||||
'body' => '<p>Original</p>',
|
'body' => '<p>Original</p>',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$reply = Comment::factory()->create([
|
$reply = Comment::factory()->create([
|
||||||
'commentable_id' => $post->id,
|
'commentable_id' => $post->id,
|
||||||
'commentable_type' => $post->getMorphClass(),
|
'commentable_type' => $post->getMorphClass(),
|
||||||
'user_id' => $author->getKey(),
|
'commenter_id' => $author->getKey(),
|
||||||
'user_type' => $author->getMorphClass(),
|
'commenter_type' => $author->getMorphClass(),
|
||||||
'parent_id' => $parentComment->id,
|
'parent_id' => $parentComment->id,
|
||||||
'body' => '<p>Reply</p>',
|
'body' => '<p>Reply</p>',
|
||||||
]);
|
]);
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
use Illuminate\Support\Facades\Notification;
|
use Illuminate\Support\Facades\Notification;
|
||||||
use Relaticle\Comments\Comment;
|
|
||||||
use Relaticle\Comments\CommentSubscription;
|
|
||||||
use Relaticle\Comments\Config;
|
|
||||||
use Relaticle\Comments\Events\CommentCreated;
|
use Relaticle\Comments\Events\CommentCreated;
|
||||||
use Relaticle\Comments\Events\UserMentioned;
|
use Relaticle\Comments\Events\UserMentioned;
|
||||||
use Relaticle\Comments\Listeners\SendCommentRepliedNotification;
|
use Relaticle\Comments\Listeners\SendCommentRepliedNotification;
|
||||||
use Relaticle\Comments\Listeners\SendUserMentionedNotification;
|
use Relaticle\Comments\Listeners\SendUserMentionedNotification;
|
||||||
|
use Relaticle\Comments\Models\Comment;
|
||||||
|
use Relaticle\Comments\Models\Subscription;
|
||||||
use Relaticle\Comments\Notifications\CommentRepliedNotification;
|
use Relaticle\Comments\Notifications\CommentRepliedNotification;
|
||||||
use Relaticle\Comments\Notifications\UserMentionedNotification;
|
use Relaticle\Comments\Notifications\UserMentionedNotification;
|
||||||
use Relaticle\Comments\Tests\Models\Post;
|
use Relaticle\Comments\Tests\Models\Post;
|
||||||
@@ -22,8 +21,8 @@ it('returns correct via channels from config for CommentRepliedNotification', fu
|
|||||||
$comment = Comment::factory()->create([
|
$comment = Comment::factory()->create([
|
||||||
'commentable_id' => $post->id,
|
'commentable_id' => $post->id,
|
||||||
'commentable_type' => $post->getMorphClass(),
|
'commentable_type' => $post->getMorphClass(),
|
||||||
'user_id' => $user->getKey(),
|
'commenter_id' => $user->getKey(),
|
||||||
'user_type' => $user->getMorphClass(),
|
'commenter_type' => $user->getMorphClass(),
|
||||||
'body' => '<p>Hello</p>',
|
'body' => '<p>Hello</p>',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -39,8 +38,8 @@ it('returns toDatabase array with comment data for CommentRepliedNotification',
|
|||||||
$comment = Comment::factory()->create([
|
$comment = Comment::factory()->create([
|
||||||
'commentable_id' => $post->id,
|
'commentable_id' => $post->id,
|
||||||
'commentable_type' => $post->getMorphClass(),
|
'commentable_type' => $post->getMorphClass(),
|
||||||
'user_id' => $user->getKey(),
|
'commenter_id' => $user->getKey(),
|
||||||
'user_type' => $user->getMorphClass(),
|
'commenter_type' => $user->getMorphClass(),
|
||||||
'body' => '<p>This is a reply body</p>',
|
'body' => '<p>This is a reply body</p>',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -51,7 +50,7 @@ it('returns toDatabase array with comment data for CommentRepliedNotification',
|
|||||||
->and($data['comment_id'])->toBe($comment->id)
|
->and($data['comment_id'])->toBe($comment->id)
|
||||||
->and($data['commentable_type'])->toBe($post->getMorphClass())
|
->and($data['commentable_type'])->toBe($post->getMorphClass())
|
||||||
->and($data['commentable_id'])->toBe($post->id)
|
->and($data['commentable_id'])->toBe($post->id)
|
||||||
->and($data['commenter_name'])->toBe($user->getCommentName());
|
->and($data['commenter_name'])->toBe($user->getCommentDisplayName());
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns correct via channels from config for UserMentionedNotification', function () {
|
it('returns correct via channels from config for UserMentionedNotification', function () {
|
||||||
@@ -64,8 +63,8 @@ it('returns correct via channels from config for UserMentionedNotification', fun
|
|||||||
$comment = Comment::factory()->create([
|
$comment = Comment::factory()->create([
|
||||||
'commentable_id' => $post->id,
|
'commentable_id' => $post->id,
|
||||||
'commentable_type' => $post->getMorphClass(),
|
'commentable_type' => $post->getMorphClass(),
|
||||||
'user_id' => $mentionedBy->getKey(),
|
'commenter_id' => $mentionedBy->getKey(),
|
||||||
'user_type' => $mentionedBy->getMorphClass(),
|
'commenter_type' => $mentionedBy->getMorphClass(),
|
||||||
'body' => '<p>Hey @someone</p>',
|
'body' => '<p>Hey @someone</p>',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -82,8 +81,8 @@ it('returns toDatabase array with mention data for UserMentionedNotification', f
|
|||||||
$comment = Comment::factory()->create([
|
$comment = Comment::factory()->create([
|
||||||
'commentable_id' => $post->id,
|
'commentable_id' => $post->id,
|
||||||
'commentable_type' => $post->getMorphClass(),
|
'commentable_type' => $post->getMorphClass(),
|
||||||
'user_id' => $mentioner->getKey(),
|
'commenter_id' => $mentioner->getKey(),
|
||||||
'user_type' => $mentioner->getMorphClass(),
|
'commenter_type' => $mentioner->getMorphClass(),
|
||||||
'body' => '<p>Hey @mentioned</p>',
|
'body' => '<p>Hey @mentioned</p>',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -92,7 +91,7 @@ it('returns toDatabase array with mention data for UserMentionedNotification', f
|
|||||||
|
|
||||||
expect($data)->toHaveKeys(['comment_id', 'commentable_type', 'commentable_id', 'mentioner_name', 'body'])
|
expect($data)->toHaveKeys(['comment_id', 'commentable_type', 'commentable_id', 'mentioner_name', 'body'])
|
||||||
->and($data['comment_id'])->toBe($comment->id)
|
->and($data['comment_id'])->toBe($comment->id)
|
||||||
->and($data['mentioner_name'])->toBe($mentioner->getCommentName());
|
->and($data['mentioner_name'])->toBe($mentioner->getCommentDisplayName());
|
||||||
});
|
});
|
||||||
|
|
||||||
it('sends notification to subscribers when reply comment is created', function () {
|
it('sends notification to subscribers when reply comment is created', function () {
|
||||||
@@ -102,21 +101,21 @@ it('sends notification to subscribers when reply comment is created', function (
|
|||||||
$subscriber = User::factory()->create();
|
$subscriber = User::factory()->create();
|
||||||
$post = Post::factory()->create();
|
$post = Post::factory()->create();
|
||||||
|
|
||||||
CommentSubscription::subscribe($post, $subscriber);
|
Subscription::subscribe($post, $subscriber);
|
||||||
|
|
||||||
$parentComment = Comment::factory()->create([
|
$parentComment = Comment::factory()->create([
|
||||||
'commentable_id' => $post->id,
|
'commentable_id' => $post->id,
|
||||||
'commentable_type' => $post->getMorphClass(),
|
'commentable_type' => $post->getMorphClass(),
|
||||||
'user_id' => $subscriber->getKey(),
|
'commenter_id' => $subscriber->getKey(),
|
||||||
'user_type' => $subscriber->getMorphClass(),
|
'commenter_type' => $subscriber->getMorphClass(),
|
||||||
'body' => '<p>Original comment</p>',
|
'body' => '<p>Original comment</p>',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$reply = Comment::factory()->create([
|
$reply = Comment::factory()->create([
|
||||||
'commentable_id' => $post->id,
|
'commentable_id' => $post->id,
|
||||||
'commentable_type' => $post->getMorphClass(),
|
'commentable_type' => $post->getMorphClass(),
|
||||||
'user_id' => $author->getKey(),
|
'commenter_id' => $author->getKey(),
|
||||||
'user_type' => $author->getMorphClass(),
|
'commenter_type' => $author->getMorphClass(),
|
||||||
'parent_id' => $parentComment->id,
|
'parent_id' => $parentComment->id,
|
||||||
'body' => '<p>Reply to original</p>',
|
'body' => '<p>Reply to original</p>',
|
||||||
]);
|
]);
|
||||||
@@ -134,13 +133,13 @@ it('does NOT send notification for top-level comments', function () {
|
|||||||
$subscriber = User::factory()->create();
|
$subscriber = User::factory()->create();
|
||||||
$post = Post::factory()->create();
|
$post = Post::factory()->create();
|
||||||
|
|
||||||
CommentSubscription::subscribe($post, $subscriber);
|
Subscription::subscribe($post, $subscriber);
|
||||||
|
|
||||||
$comment = Comment::factory()->create([
|
$comment = Comment::factory()->create([
|
||||||
'commentable_id' => $post->id,
|
'commentable_id' => $post->id,
|
||||||
'commentable_type' => $post->getMorphClass(),
|
'commentable_type' => $post->getMorphClass(),
|
||||||
'user_id' => $author->getKey(),
|
'commenter_id' => $author->getKey(),
|
||||||
'user_type' => $author->getMorphClass(),
|
'commenter_type' => $author->getMorphClass(),
|
||||||
'body' => '<p>Top-level comment</p>',
|
'body' => '<p>Top-level comment</p>',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -156,21 +155,21 @@ it('does NOT notify the reply author themselves', function () {
|
|||||||
$user = User::factory()->create();
|
$user = User::factory()->create();
|
||||||
$post = Post::factory()->create();
|
$post = Post::factory()->create();
|
||||||
|
|
||||||
CommentSubscription::subscribe($post, $user);
|
Subscription::subscribe($post, $user);
|
||||||
|
|
||||||
$parentComment = Comment::factory()->create([
|
$parentComment = Comment::factory()->create([
|
||||||
'commentable_id' => $post->id,
|
'commentable_id' => $post->id,
|
||||||
'commentable_type' => $post->getMorphClass(),
|
'commentable_type' => $post->getMorphClass(),
|
||||||
'user_id' => $user->getKey(),
|
'commenter_id' => $user->getKey(),
|
||||||
'user_type' => $user->getMorphClass(),
|
'commenter_type' => $user->getMorphClass(),
|
||||||
'body' => '<p>My comment</p>',
|
'body' => '<p>My comment</p>',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$reply = Comment::factory()->create([
|
$reply = Comment::factory()->create([
|
||||||
'commentable_id' => $post->id,
|
'commentable_id' => $post->id,
|
||||||
'commentable_type' => $post->getMorphClass(),
|
'commentable_type' => $post->getMorphClass(),
|
||||||
'user_id' => $user->getKey(),
|
'commenter_id' => $user->getKey(),
|
||||||
'user_type' => $user->getMorphClass(),
|
'commenter_type' => $user->getMorphClass(),
|
||||||
'parent_id' => $parentComment->id,
|
'parent_id' => $parentComment->id,
|
||||||
'body' => '<p>My own reply</p>',
|
'body' => '<p>My own reply</p>',
|
||||||
]);
|
]);
|
||||||
@@ -187,20 +186,20 @@ it('auto-subscribes comment author to the thread', function () {
|
|||||||
$author = User::factory()->create();
|
$author = User::factory()->create();
|
||||||
$post = Post::factory()->create();
|
$post = Post::factory()->create();
|
||||||
|
|
||||||
expect(CommentSubscription::isSubscribed($post, $author))->toBeFalse();
|
expect(Subscription::isSubscribed($post, $author))->toBeFalse();
|
||||||
|
|
||||||
$comment = Comment::factory()->create([
|
$comment = Comment::factory()->create([
|
||||||
'commentable_id' => $post->id,
|
'commentable_id' => $post->id,
|
||||||
'commentable_type' => $post->getMorphClass(),
|
'commentable_type' => $post->getMorphClass(),
|
||||||
'user_id' => $author->getKey(),
|
'commenter_id' => $author->getKey(),
|
||||||
'user_type' => $author->getMorphClass(),
|
'commenter_type' => $author->getMorphClass(),
|
||||||
'body' => '<p>Comment</p>',
|
'body' => '<p>Comment</p>',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$listener = new SendCommentRepliedNotification;
|
$listener = new SendCommentRepliedNotification;
|
||||||
$listener->handle(new CommentCreated($comment));
|
$listener->handle(new CommentCreated($comment));
|
||||||
|
|
||||||
expect(CommentSubscription::isSubscribed($post, $author))->toBeTrue();
|
expect(Subscription::isSubscribed($post, $author))->toBeTrue();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('only notifies subscribed users for reply notifications', function () {
|
it('only notifies subscribed users for reply notifications', function () {
|
||||||
@@ -211,21 +210,21 @@ it('only notifies subscribed users for reply notifications', function () {
|
|||||||
$nonSubscriber = User::factory()->create();
|
$nonSubscriber = User::factory()->create();
|
||||||
$post = Post::factory()->create();
|
$post = Post::factory()->create();
|
||||||
|
|
||||||
CommentSubscription::subscribe($post, $subscriber);
|
Subscription::subscribe($post, $subscriber);
|
||||||
|
|
||||||
$parentComment = Comment::factory()->create([
|
$parentComment = Comment::factory()->create([
|
||||||
'commentable_id' => $post->id,
|
'commentable_id' => $post->id,
|
||||||
'commentable_type' => $post->getMorphClass(),
|
'commentable_type' => $post->getMorphClass(),
|
||||||
'user_id' => $subscriber->getKey(),
|
'commenter_id' => $subscriber->getKey(),
|
||||||
'user_type' => $subscriber->getMorphClass(),
|
'commenter_type' => $subscriber->getMorphClass(),
|
||||||
'body' => '<p>Original</p>',
|
'body' => '<p>Original</p>',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$reply = Comment::factory()->create([
|
$reply = Comment::factory()->create([
|
||||||
'commentable_id' => $post->id,
|
'commentable_id' => $post->id,
|
||||||
'commentable_type' => $post->getMorphClass(),
|
'commentable_type' => $post->getMorphClass(),
|
||||||
'user_id' => $author->getKey(),
|
'commenter_id' => $author->getKey(),
|
||||||
'user_type' => $author->getMorphClass(),
|
'commenter_type' => $author->getMorphClass(),
|
||||||
'parent_id' => $parentComment->id,
|
'parent_id' => $parentComment->id,
|
||||||
'body' => '<p>Reply</p>',
|
'body' => '<p>Reply</p>',
|
||||||
]);
|
]);
|
||||||
@@ -247,8 +246,8 @@ it('sends mention notification to mentioned user', function () {
|
|||||||
$comment = Comment::factory()->create([
|
$comment = Comment::factory()->create([
|
||||||
'commentable_id' => $post->id,
|
'commentable_id' => $post->id,
|
||||||
'commentable_type' => $post->getMorphClass(),
|
'commentable_type' => $post->getMorphClass(),
|
||||||
'user_id' => $author->getKey(),
|
'commenter_id' => $author->getKey(),
|
||||||
'user_type' => $author->getMorphClass(),
|
'commenter_type' => $author->getMorphClass(),
|
||||||
'body' => '<p>Hey @mentioned</p>',
|
'body' => '<p>Hey @mentioned</p>',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -268,8 +267,8 @@ it('does NOT send mention notification to the comment author', function () {
|
|||||||
$comment = Comment::factory()->create([
|
$comment = Comment::factory()->create([
|
||||||
'commentable_id' => $post->id,
|
'commentable_id' => $post->id,
|
||||||
'commentable_type' => $post->getMorphClass(),
|
'commentable_type' => $post->getMorphClass(),
|
||||||
'user_id' => $author->getKey(),
|
'commenter_id' => $author->getKey(),
|
||||||
'user_type' => $author->getMorphClass(),
|
'commenter_type' => $author->getMorphClass(),
|
||||||
'body' => '<p>Hey @myself</p>',
|
'body' => '<p>Hey @myself</p>',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -287,13 +286,13 @@ it('auto-subscribes mentioned user to the thread', function () {
|
|||||||
$mentioned = User::factory()->create();
|
$mentioned = User::factory()->create();
|
||||||
$post = Post::factory()->create();
|
$post = Post::factory()->create();
|
||||||
|
|
||||||
expect(CommentSubscription::isSubscribed($post, $mentioned))->toBeFalse();
|
expect(Subscription::isSubscribed($post, $mentioned))->toBeFalse();
|
||||||
|
|
||||||
$comment = Comment::factory()->create([
|
$comment = Comment::factory()->create([
|
||||||
'commentable_id' => $post->id,
|
'commentable_id' => $post->id,
|
||||||
'commentable_type' => $post->getMorphClass(),
|
'commentable_type' => $post->getMorphClass(),
|
||||||
'user_id' => $author->getKey(),
|
'commenter_id' => $author->getKey(),
|
||||||
'user_type' => $author->getMorphClass(),
|
'commenter_type' => $author->getMorphClass(),
|
||||||
'body' => '<p>Hey @mentioned</p>',
|
'body' => '<p>Hey @mentioned</p>',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -301,7 +300,7 @@ it('auto-subscribes mentioned user to the thread', function () {
|
|||||||
$listener = new SendUserMentionedNotification;
|
$listener = new SendUserMentionedNotification;
|
||||||
$listener->handle($event);
|
$listener->handle($event);
|
||||||
|
|
||||||
expect(CommentSubscription::isSubscribed($post, $mentioned))->toBeTrue();
|
expect(Subscription::isSubscribed($post, $mentioned))->toBeTrue();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('does not send notifications when notifications are disabled', function () {
|
it('does not send notifications when notifications are disabled', function () {
|
||||||
@@ -313,21 +312,21 @@ it('does not send notifications when notifications are disabled', function () {
|
|||||||
$mentioned = User::factory()->create();
|
$mentioned = User::factory()->create();
|
||||||
$post = Post::factory()->create();
|
$post = Post::factory()->create();
|
||||||
|
|
||||||
CommentSubscription::subscribe($post, $subscriber);
|
Subscription::subscribe($post, $subscriber);
|
||||||
|
|
||||||
$parentComment = Comment::factory()->create([
|
$parentComment = Comment::factory()->create([
|
||||||
'commentable_id' => $post->id,
|
'commentable_id' => $post->id,
|
||||||
'commentable_type' => $post->getMorphClass(),
|
'commentable_type' => $post->getMorphClass(),
|
||||||
'user_id' => $subscriber->getKey(),
|
'commenter_id' => $subscriber->getKey(),
|
||||||
'user_type' => $subscriber->getMorphClass(),
|
'commenter_type' => $subscriber->getMorphClass(),
|
||||||
'body' => '<p>Original</p>',
|
'body' => '<p>Original</p>',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$reply = Comment::factory()->create([
|
$reply = Comment::factory()->create([
|
||||||
'commentable_id' => $post->id,
|
'commentable_id' => $post->id,
|
||||||
'commentable_type' => $post->getMorphClass(),
|
'commentable_type' => $post->getMorphClass(),
|
||||||
'user_id' => $author->getKey(),
|
'commenter_id' => $author->getKey(),
|
||||||
'user_type' => $author->getMorphClass(),
|
'commenter_type' => $author->getMorphClass(),
|
||||||
'parent_id' => $parentComment->id,
|
'parent_id' => $parentComment->id,
|
||||||
'body' => '<p>Reply</p>',
|
'body' => '<p>Reply</p>',
|
||||||
]);
|
]);
|
||||||
|
|||||||
@@ -1,33 +1,33 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
use Relaticle\Comments\Config;
|
use Relaticle\Comments\CommentsConfig;
|
||||||
|
|
||||||
it('returns false for isBroadcastingEnabled by default', function () {
|
it('returns false for isBroadcastingEnabled by default', function () {
|
||||||
expect(Config::isBroadcastingEnabled())->toBeFalse();
|
expect(CommentsConfig::isBroadcastingEnabled())->toBeFalse();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns true for isBroadcastingEnabled when config overridden', function () {
|
it('returns true for isBroadcastingEnabled when config overridden', function () {
|
||||||
config()->set('comments.broadcasting.enabled', true);
|
config()->set('comments.broadcasting.enabled', true);
|
||||||
|
|
||||||
expect(Config::isBroadcastingEnabled())->toBeTrue();
|
expect(CommentsConfig::isBroadcastingEnabled())->toBeTrue();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns comments as default broadcast channel prefix', function () {
|
it('returns comments as default broadcast channel prefix', function () {
|
||||||
expect(Config::getBroadcastChannelPrefix())->toBe('comments');
|
expect(CommentsConfig::getBroadcastChannelPrefix())->toBe('comments');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns custom broadcast channel prefix when overridden', function () {
|
it('returns custom broadcast channel prefix when overridden', function () {
|
||||||
config()->set('comments.broadcasting.channel_prefix', 'my-app-comments');
|
config()->set('comments.broadcasting.channel_prefix', 'my-app-comments');
|
||||||
|
|
||||||
expect(Config::getBroadcastChannelPrefix())->toBe('my-app-comments');
|
expect(CommentsConfig::getBroadcastChannelPrefix())->toBe('my-app-comments');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns 10s as default polling interval', function () {
|
it('returns 10s as default polling interval', function () {
|
||||||
expect(Config::getPollingInterval())->toBe('10s');
|
expect(CommentsConfig::getPollingInterval())->toBe('10s');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns custom polling interval when overridden', function () {
|
it('returns custom polling interval when overridden', function () {
|
||||||
config()->set('comments.polling.interval', '30s');
|
config()->set('comments.polling.interval', '30s');
|
||||||
|
|
||||||
expect(Config::getPollingInterval())->toBe('30s');
|
expect(CommentsConfig::getPollingInterval())->toBe('30s');
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,11 +2,11 @@
|
|||||||
|
|
||||||
use Illuminate\Support\Facades\Event;
|
use Illuminate\Support\Facades\Event;
|
||||||
use Livewire\Livewire;
|
use Livewire\Livewire;
|
||||||
use Relaticle\Comments\Comment;
|
use Relaticle\Comments\CommentsConfig;
|
||||||
use Relaticle\Comments\CommentReaction;
|
|
||||||
use Relaticle\Comments\Config;
|
|
||||||
use Relaticle\Comments\Events\CommentReacted;
|
use Relaticle\Comments\Events\CommentReacted;
|
||||||
use Relaticle\Comments\Livewire\Reactions;
|
use Relaticle\Comments\Livewire\Reactions;
|
||||||
|
use Relaticle\Comments\Models\Comment;
|
||||||
|
use Relaticle\Comments\Models\Reaction;
|
||||||
use Relaticle\Comments\Tests\Models\Post;
|
use Relaticle\Comments\Tests\Models\Post;
|
||||||
use Relaticle\Comments\Tests\Models\User;
|
use Relaticle\Comments\Tests\Models\User;
|
||||||
|
|
||||||
@@ -17,8 +17,8 @@ it('adds a reaction when user clicks an emoji', function () {
|
|||||||
$comment = Comment::factory()->create([
|
$comment = Comment::factory()->create([
|
||||||
'commentable_id' => $post->id,
|
'commentable_id' => $post->id,
|
||||||
'commentable_type' => $post->getMorphClass(),
|
'commentable_type' => $post->getMorphClass(),
|
||||||
'user_id' => $user->getKey(),
|
'commenter_id' => $user->getKey(),
|
||||||
'user_type' => $user->getMorphClass(),
|
'commenter_type' => $user->getMorphClass(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$this->actingAs($user);
|
$this->actingAs($user);
|
||||||
@@ -26,10 +26,10 @@ it('adds a reaction when user clicks an emoji', function () {
|
|||||||
Livewire::test(Reactions::class, ['comment' => $comment])
|
Livewire::test(Reactions::class, ['comment' => $comment])
|
||||||
->call('toggleReaction', 'thumbs_up');
|
->call('toggleReaction', 'thumbs_up');
|
||||||
|
|
||||||
expect(CommentReaction::where([
|
expect(Reaction::where([
|
||||||
'comment_id' => $comment->id,
|
'comment_id' => $comment->id,
|
||||||
'user_id' => $user->getKey(),
|
'commenter_id' => $user->getKey(),
|
||||||
'user_type' => $user->getMorphClass(),
|
'commenter_type' => $user->getMorphClass(),
|
||||||
'reaction' => 'thumbs_up',
|
'reaction' => 'thumbs_up',
|
||||||
])->exists())->toBeTrue();
|
])->exists())->toBeTrue();
|
||||||
});
|
});
|
||||||
@@ -41,14 +41,14 @@ it('removes a reaction when toggling same emoji', function () {
|
|||||||
$comment = Comment::factory()->create([
|
$comment = Comment::factory()->create([
|
||||||
'commentable_id' => $post->id,
|
'commentable_id' => $post->id,
|
||||||
'commentable_type' => $post->getMorphClass(),
|
'commentable_type' => $post->getMorphClass(),
|
||||||
'user_id' => $user->getKey(),
|
'commenter_id' => $user->getKey(),
|
||||||
'user_type' => $user->getMorphClass(),
|
'commenter_type' => $user->getMorphClass(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
CommentReaction::create([
|
Reaction::create([
|
||||||
'comment_id' => $comment->id,
|
'comment_id' => $comment->id,
|
||||||
'user_id' => $user->getKey(),
|
'commenter_id' => $user->getKey(),
|
||||||
'user_type' => $user->getMorphClass(),
|
'commenter_type' => $user->getMorphClass(),
|
||||||
'reaction' => 'thumbs_up',
|
'reaction' => 'thumbs_up',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -57,10 +57,10 @@ it('removes a reaction when toggling same emoji', function () {
|
|||||||
Livewire::test(Reactions::class, ['comment' => $comment])
|
Livewire::test(Reactions::class, ['comment' => $comment])
|
||||||
->call('toggleReaction', 'thumbs_up');
|
->call('toggleReaction', 'thumbs_up');
|
||||||
|
|
||||||
expect(CommentReaction::where([
|
expect(Reaction::where([
|
||||||
'comment_id' => $comment->id,
|
'comment_id' => $comment->id,
|
||||||
'user_id' => $user->getKey(),
|
'commenter_id' => $user->getKey(),
|
||||||
'user_type' => $user->getMorphClass(),
|
'commenter_type' => $user->getMorphClass(),
|
||||||
'reaction' => 'thumbs_up',
|
'reaction' => 'thumbs_up',
|
||||||
])->exists())->toBeFalse();
|
])->exists())->toBeFalse();
|
||||||
});
|
});
|
||||||
@@ -74,8 +74,8 @@ it('fires CommentReacted event with added action', function () {
|
|||||||
$comment = Comment::factory()->create([
|
$comment = Comment::factory()->create([
|
||||||
'commentable_id' => $post->id,
|
'commentable_id' => $post->id,
|
||||||
'commentable_type' => $post->getMorphClass(),
|
'commentable_type' => $post->getMorphClass(),
|
||||||
'user_id' => $user->getKey(),
|
'commenter_id' => $user->getKey(),
|
||||||
'user_type' => $user->getMorphClass(),
|
'commenter_type' => $user->getMorphClass(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$this->actingAs($user);
|
$this->actingAs($user);
|
||||||
@@ -98,14 +98,14 @@ it('fires CommentReacted event with removed action', function () {
|
|||||||
$comment = Comment::factory()->create([
|
$comment = Comment::factory()->create([
|
||||||
'commentable_id' => $post->id,
|
'commentable_id' => $post->id,
|
||||||
'commentable_type' => $post->getMorphClass(),
|
'commentable_type' => $post->getMorphClass(),
|
||||||
'user_id' => $user->getKey(),
|
'commenter_id' => $user->getKey(),
|
||||||
'user_type' => $user->getMorphClass(),
|
'commenter_type' => $user->getMorphClass(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
CommentReaction::create([
|
Reaction::create([
|
||||||
'comment_id' => $comment->id,
|
'comment_id' => $comment->id,
|
||||||
'user_id' => $user->getKey(),
|
'commenter_id' => $user->getKey(),
|
||||||
'user_type' => $user->getMorphClass(),
|
'commenter_type' => $user->getMorphClass(),
|
||||||
'reaction' => 'heart',
|
'reaction' => 'heart',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -133,35 +133,35 @@ it('returns correct reaction summary with counts', function () {
|
|||||||
$comment = Comment::factory()->create([
|
$comment = Comment::factory()->create([
|
||||||
'commentable_id' => $post->id,
|
'commentable_id' => $post->id,
|
||||||
'commentable_type' => $post->getMorphClass(),
|
'commentable_type' => $post->getMorphClass(),
|
||||||
'user_id' => $user1->getKey(),
|
'commenter_id' => $user1->getKey(),
|
||||||
'user_type' => $user1->getMorphClass(),
|
'commenter_type' => $user1->getMorphClass(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
CommentReaction::create([
|
Reaction::create([
|
||||||
'comment_id' => $comment->id,
|
'comment_id' => $comment->id,
|
||||||
'user_id' => $user1->getKey(),
|
'commenter_id' => $user1->getKey(),
|
||||||
'user_type' => $user1->getMorphClass(),
|
'commenter_type' => $user1->getMorphClass(),
|
||||||
'reaction' => 'thumbs_up',
|
'reaction' => 'thumbs_up',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
CommentReaction::create([
|
Reaction::create([
|
||||||
'comment_id' => $comment->id,
|
'comment_id' => $comment->id,
|
||||||
'user_id' => $user2->getKey(),
|
'commenter_id' => $user2->getKey(),
|
||||||
'user_type' => $user2->getMorphClass(),
|
'commenter_type' => $user2->getMorphClass(),
|
||||||
'reaction' => 'thumbs_up',
|
'reaction' => 'thumbs_up',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
CommentReaction::create([
|
Reaction::create([
|
||||||
'comment_id' => $comment->id,
|
'comment_id' => $comment->id,
|
||||||
'user_id' => $user3->getKey(),
|
'commenter_id' => $user3->getKey(),
|
||||||
'user_type' => $user3->getMorphClass(),
|
'commenter_type' => $user3->getMorphClass(),
|
||||||
'reaction' => 'thumbs_up',
|
'reaction' => 'thumbs_up',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
CommentReaction::create([
|
Reaction::create([
|
||||||
'comment_id' => $comment->id,
|
'comment_id' => $comment->id,
|
||||||
'user_id' => $user1->getKey(),
|
'commenter_id' => $user1->getKey(),
|
||||||
'user_type' => $user1->getMorphClass(),
|
'commenter_type' => $user1->getMorphClass(),
|
||||||
'reaction' => 'heart',
|
'reaction' => 'heart',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -187,14 +187,14 @@ it('requires authentication to react', function () {
|
|||||||
$comment = Comment::factory()->create([
|
$comment = Comment::factory()->create([
|
||||||
'commentable_id' => $post->id,
|
'commentable_id' => $post->id,
|
||||||
'commentable_type' => $post->getMorphClass(),
|
'commentable_type' => $post->getMorphClass(),
|
||||||
'user_id' => $user->getKey(),
|
'commenter_id' => $user->getKey(),
|
||||||
'user_type' => $user->getMorphClass(),
|
'commenter_type' => $user->getMorphClass(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
Livewire::test(Reactions::class, ['comment' => $comment])
|
Livewire::test(Reactions::class, ['comment' => $comment])
|
||||||
->call('toggleReaction', 'thumbs_up');
|
->call('toggleReaction', 'thumbs_up');
|
||||||
|
|
||||||
expect(CommentReaction::count())->toBe(0);
|
expect(Reaction::count())->toBe(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('allows multiple reaction types from same user', function () {
|
it('allows multiple reaction types from same user', function () {
|
||||||
@@ -204,8 +204,8 @@ it('allows multiple reaction types from same user', function () {
|
|||||||
$comment = Comment::factory()->create([
|
$comment = Comment::factory()->create([
|
||||||
'commentable_id' => $post->id,
|
'commentable_id' => $post->id,
|
||||||
'commentable_type' => $post->getMorphClass(),
|
'commentable_type' => $post->getMorphClass(),
|
||||||
'user_id' => $user->getKey(),
|
'commenter_id' => $user->getKey(),
|
||||||
'user_type' => $user->getMorphClass(),
|
'commenter_type' => $user->getMorphClass(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$this->actingAs($user);
|
$this->actingAs($user);
|
||||||
@@ -215,17 +215,17 @@ it('allows multiple reaction types from same user', function () {
|
|||||||
$component->call('toggleReaction', 'thumbs_up');
|
$component->call('toggleReaction', 'thumbs_up');
|
||||||
$component->call('toggleReaction', 'heart');
|
$component->call('toggleReaction', 'heart');
|
||||||
|
|
||||||
expect(CommentReaction::where([
|
expect(Reaction::where([
|
||||||
'comment_id' => $comment->id,
|
'comment_id' => $comment->id,
|
||||||
'user_id' => $user->getKey(),
|
'commenter_id' => $user->getKey(),
|
||||||
'user_type' => $user->getMorphClass(),
|
'commenter_type' => $user->getMorphClass(),
|
||||||
'reaction' => 'thumbs_up',
|
'reaction' => 'thumbs_up',
|
||||||
])->exists())->toBeTrue();
|
])->exists())->toBeTrue();
|
||||||
|
|
||||||
expect(CommentReaction::where([
|
expect(Reaction::where([
|
||||||
'comment_id' => $comment->id,
|
'comment_id' => $comment->id,
|
||||||
'user_id' => $user->getKey(),
|
'commenter_id' => $user->getKey(),
|
||||||
'user_type' => $user->getMorphClass(),
|
'commenter_type' => $user->getMorphClass(),
|
||||||
'reaction' => 'heart',
|
'reaction' => 'heart',
|
||||||
])->exists())->toBeTrue();
|
])->exists())->toBeTrue();
|
||||||
});
|
});
|
||||||
@@ -238,8 +238,8 @@ it('allows same reaction from multiple users', function () {
|
|||||||
$comment = Comment::factory()->create([
|
$comment = Comment::factory()->create([
|
||||||
'commentable_id' => $post->id,
|
'commentable_id' => $post->id,
|
||||||
'commentable_type' => $post->getMorphClass(),
|
'commentable_type' => $post->getMorphClass(),
|
||||||
'user_id' => $user1->getKey(),
|
'commenter_id' => $user1->getKey(),
|
||||||
'user_type' => $user1->getMorphClass(),
|
'commenter_type' => $user1->getMorphClass(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$this->actingAs($user1);
|
$this->actingAs($user1);
|
||||||
@@ -250,7 +250,7 @@ it('allows same reaction from multiple users', function () {
|
|||||||
Livewire::test(Reactions::class, ['comment' => $comment])
|
Livewire::test(Reactions::class, ['comment' => $comment])
|
||||||
->call('toggleReaction', 'thumbs_up');
|
->call('toggleReaction', 'thumbs_up');
|
||||||
|
|
||||||
expect(CommentReaction::where([
|
expect(Reaction::where([
|
||||||
'comment_id' => $comment->id,
|
'comment_id' => $comment->id,
|
||||||
'reaction' => 'thumbs_up',
|
'reaction' => 'thumbs_up',
|
||||||
])->count())->toBe(2);
|
])->count())->toBe(2);
|
||||||
@@ -263,8 +263,8 @@ it('rejects invalid reaction keys', function () {
|
|||||||
$comment = Comment::factory()->create([
|
$comment = Comment::factory()->create([
|
||||||
'commentable_id' => $post->id,
|
'commentable_id' => $post->id,
|
||||||
'commentable_type' => $post->getMorphClass(),
|
'commentable_type' => $post->getMorphClass(),
|
||||||
'user_id' => $user->getKey(),
|
'commenter_id' => $user->getKey(),
|
||||||
'user_type' => $user->getMorphClass(),
|
'commenter_type' => $user->getMorphClass(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$this->actingAs($user);
|
$this->actingAs($user);
|
||||||
@@ -272,7 +272,7 @@ it('rejects invalid reaction keys', function () {
|
|||||||
Livewire::test(Reactions::class, ['comment' => $comment])
|
Livewire::test(Reactions::class, ['comment' => $comment])
|
||||||
->call('toggleReaction', 'invalid_emoji');
|
->call('toggleReaction', 'invalid_emoji');
|
||||||
|
|
||||||
expect(CommentReaction::count())->toBe(0);
|
expect(Reaction::count())->toBe(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('marks reacted_by_user correctly in summary', function () {
|
it('marks reacted_by_user correctly in summary', function () {
|
||||||
@@ -283,14 +283,14 @@ it('marks reacted_by_user correctly in summary', function () {
|
|||||||
$comment = Comment::factory()->create([
|
$comment = Comment::factory()->create([
|
||||||
'commentable_id' => $post->id,
|
'commentable_id' => $post->id,
|
||||||
'commentable_type' => $post->getMorphClass(),
|
'commentable_type' => $post->getMorphClass(),
|
||||||
'user_id' => $userA->getKey(),
|
'commenter_id' => $userA->getKey(),
|
||||||
'user_type' => $userA->getMorphClass(),
|
'commenter_type' => $userA->getMorphClass(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
CommentReaction::create([
|
Reaction::create([
|
||||||
'comment_id' => $comment->id,
|
'comment_id' => $comment->id,
|
||||||
'user_id' => $userA->getKey(),
|
'commenter_id' => $userA->getKey(),
|
||||||
'user_type' => $userA->getMorphClass(),
|
'commenter_type' => $userA->getMorphClass(),
|
||||||
'reaction' => 'thumbs_up',
|
'reaction' => 'thumbs_up',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -310,7 +310,7 @@ it('marks reacted_by_user correctly in summary', function () {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('returns configured emoji set from config', function () {
|
it('returns configured emoji set from config', function () {
|
||||||
$emojiSet = Config::getReactionEmojiSet();
|
$emojiSet = CommentsConfig::getReactionEmojiSet();
|
||||||
|
|
||||||
expect($emojiSet)->toBeArray();
|
expect($emojiSet)->toBeArray();
|
||||||
expect($emojiSet)->toHaveKey('thumbs_up');
|
expect($emojiSet)->toHaveKey('thumbs_up');
|
||||||
@@ -322,7 +322,7 @@ it('returns configured emoji set from config', function () {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('returns allowed reaction keys from config', function () {
|
it('returns allowed reaction keys from config', function () {
|
||||||
$allowed = Config::getAllowedReactions();
|
$allowed = CommentsConfig::getAllowedReactions();
|
||||||
|
|
||||||
expect($allowed)->toBeArray();
|
expect($allowed)->toBeArray();
|
||||||
expect($allowed)->toContain('thumbs_up');
|
expect($allowed)->toContain('thumbs_up');
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
use Livewire\Livewire;
|
use Livewire\Livewire;
|
||||||
use Relaticle\Comments\Comment;
|
use Relaticle\Comments\CommentsConfig;
|
||||||
use Relaticle\Comments\Config;
|
|
||||||
use Relaticle\Comments\Livewire\CommentItem;
|
use Relaticle\Comments\Livewire\CommentItem;
|
||||||
use Relaticle\Comments\Livewire\Comments;
|
use Relaticle\Comments\Livewire\Comments;
|
||||||
|
use Relaticle\Comments\Models\Comment;
|
||||||
use Relaticle\Comments\Tests\Models\Post;
|
use Relaticle\Comments\Tests\Models\Post;
|
||||||
use Relaticle\Comments\Tests\Models\User;
|
use Relaticle\Comments\Tests\Models\User;
|
||||||
|
|
||||||
@@ -17,7 +17,7 @@ it('creates a comment with rich HTML content preserved', function () {
|
|||||||
$html = '<p>Hello <strong>bold</strong> and <em>italic</em> world</p>';
|
$html = '<p>Hello <strong>bold</strong> and <em>italic</em> world</p>';
|
||||||
|
|
||||||
Livewire::test(Comments::class, ['model' => $post])
|
Livewire::test(Comments::class, ['model' => $post])
|
||||||
->set('newComment', $html)
|
->set('commentData.body', $html)
|
||||||
->call('addComment');
|
->call('addComment');
|
||||||
|
|
||||||
$comment = Comment::first();
|
$comment = Comment::first();
|
||||||
@@ -35,16 +35,22 @@ it('pre-fills editBody with existing comment HTML when starting edit', function
|
|||||||
$comment = Comment::factory()->create([
|
$comment = Comment::factory()->create([
|
||||||
'commentable_id' => $post->id,
|
'commentable_id' => $post->id,
|
||||||
'commentable_type' => $post->getMorphClass(),
|
'commentable_type' => $post->getMorphClass(),
|
||||||
'user_id' => $user->getKey(),
|
'commenter_id' => $user->getKey(),
|
||||||
'user_type' => $user->getMorphClass(),
|
'commenter_type' => $user->getMorphClass(),
|
||||||
'body' => $originalHtml,
|
'body' => $originalHtml,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$this->actingAs($user);
|
$this->actingAs($user);
|
||||||
|
|
||||||
Livewire::test(CommentItem::class, ['comment' => $comment])
|
$component = Livewire::test(CommentItem::class, ['comment' => $comment])
|
||||||
->call('startEdit')
|
->call('startEdit');
|
||||||
->assertSet('editBody', $originalHtml);
|
|
||||||
|
$editBody = $component->get('editData')['body'];
|
||||||
|
$bodyJson = json_encode($editBody);
|
||||||
|
|
||||||
|
expect($bodyJson)->toContain('Hello ');
|
||||||
|
expect($bodyJson)->toContain('world');
|
||||||
|
expect($bodyJson)->toContain('bold');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('saves edited HTML content through edit form', function () {
|
it('saves edited HTML content through edit form', function () {
|
||||||
@@ -54,8 +60,8 @@ it('saves edited HTML content through edit form', function () {
|
|||||||
$comment = Comment::factory()->create([
|
$comment = Comment::factory()->create([
|
||||||
'commentable_id' => $post->id,
|
'commentable_id' => $post->id,
|
||||||
'commentable_type' => $post->getMorphClass(),
|
'commentable_type' => $post->getMorphClass(),
|
||||||
'user_id' => $user->getKey(),
|
'commenter_id' => $user->getKey(),
|
||||||
'user_type' => $user->getMorphClass(),
|
'commenter_type' => $user->getMorphClass(),
|
||||||
'body' => '<p>Original</p>',
|
'body' => '<p>Original</p>',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -65,13 +71,14 @@ it('saves edited HTML content through edit form', function () {
|
|||||||
|
|
||||||
Livewire::test(CommentItem::class, ['comment' => $comment])
|
Livewire::test(CommentItem::class, ['comment' => $comment])
|
||||||
->call('startEdit')
|
->call('startEdit')
|
||||||
->set('editBody', $updatedHtml)
|
->set('editData.body', $updatedHtml)
|
||||||
->call('saveEdit');
|
->call('saveEdit');
|
||||||
|
|
||||||
$comment->refresh();
|
$comment->refresh();
|
||||||
|
|
||||||
expect($comment->body)->toContain('<strong>bold</strong>');
|
expect($comment->body)->toContain('<strong>bold</strong>');
|
||||||
expect($comment->body)->toContain('<a href="https://example.com">a link</a>');
|
expect($comment->body)->toContain('href="https://example.com"');
|
||||||
|
expect($comment->body)->toContain('>a link</a>');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('creates reply with rich HTML content', function () {
|
it('creates reply with rich HTML content', function () {
|
||||||
@@ -81,8 +88,8 @@ it('creates reply with rich HTML content', function () {
|
|||||||
$comment = Comment::factory()->create([
|
$comment = Comment::factory()->create([
|
||||||
'commentable_id' => $post->id,
|
'commentable_id' => $post->id,
|
||||||
'commentable_type' => $post->getMorphClass(),
|
'commentable_type' => $post->getMorphClass(),
|
||||||
'user_id' => $user->getKey(),
|
'commenter_id' => $user->getKey(),
|
||||||
'user_type' => $user->getMorphClass(),
|
'commenter_type' => $user->getMorphClass(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$this->actingAs($user);
|
$this->actingAs($user);
|
||||||
@@ -91,7 +98,7 @@ it('creates reply with rich HTML content', function () {
|
|||||||
|
|
||||||
Livewire::test(CommentItem::class, ['comment' => $comment])
|
Livewire::test(CommentItem::class, ['comment' => $comment])
|
||||||
->call('startReply')
|
->call('startReply')
|
||||||
->set('replyBody', $replyHtml)
|
->set('replyData.body', $replyHtml)
|
||||||
->call('addReply');
|
->call('addReply');
|
||||||
|
|
||||||
$reply = Comment::where('parent_id', $comment->id)->first();
|
$reply = Comment::where('parent_id', $comment->id)->first();
|
||||||
@@ -108,8 +115,8 @@ it('renders comment body with fi-prose class', function () {
|
|||||||
$comment = Comment::factory()->create([
|
$comment = Comment::factory()->create([
|
||||||
'commentable_id' => $post->id,
|
'commentable_id' => $post->id,
|
||||||
'commentable_type' => $post->getMorphClass(),
|
'commentable_type' => $post->getMorphClass(),
|
||||||
'user_id' => $user->getKey(),
|
'commenter_id' => $user->getKey(),
|
||||||
'user_type' => $user->getMorphClass(),
|
'commenter_type' => $user->getMorphClass(),
|
||||||
'body' => '<p>Styled comment</p>',
|
'body' => '<p>Styled comment</p>',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -120,7 +127,7 @@ it('renders comment body with fi-prose class', function () {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('returns editor toolbar configuration as nested array', function () {
|
it('returns editor toolbar configuration as nested array', function () {
|
||||||
$toolbar = Config::getEditorToolbar();
|
$toolbar = CommentsConfig::getEditorToolbar();
|
||||||
|
|
||||||
expect($toolbar)->toBeArray();
|
expect($toolbar)->toBeArray();
|
||||||
expect($toolbar)->not->toBeEmpty();
|
expect($toolbar)->not->toBeEmpty();
|
||||||
@@ -134,7 +141,7 @@ it('uses custom toolbar config when overridden', function () {
|
|||||||
['bold', 'italic'],
|
['bold', 'italic'],
|
||||||
]]);
|
]]);
|
||||||
|
|
||||||
$toolbar = Config::getEditorToolbar();
|
$toolbar = CommentsConfig::getEditorToolbar();
|
||||||
|
|
||||||
expect($toolbar)->toHaveCount(1);
|
expect($toolbar)->toHaveCount(1);
|
||||||
expect($toolbar[0])->toBe(['bold', 'italic']);
|
expect($toolbar[0])->toBe(['bold', 'italic']);
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
use Illuminate\Database\Eloquent\Relations\Relation;
|
use Illuminate\Database\Eloquent\Relations\Relation;
|
||||||
use Relaticle\Comments\Comment;
|
use Relaticle\Comments\CommentsConfig;
|
||||||
use Relaticle\Comments\Config;
|
use Relaticle\Comments\Models\Comment;
|
||||||
|
|
||||||
it('registers the config file', function () {
|
it('registers the config file', function () {
|
||||||
expect(config('comments'))->toBeArray();
|
expect(config('comments'))->toBeArray();
|
||||||
@@ -11,15 +11,15 @@ it('registers the config file', function () {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('resolves the comment model from config', function () {
|
it('resolves the comment model from config', function () {
|
||||||
expect(Config::getCommentModel())->toBe(Comment::class);
|
expect(CommentsConfig::getCommentModel())->toBe(Comment::class);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('resolves the comment table from config', function () {
|
it('resolves the comment table from config', function () {
|
||||||
expect(Config::getCommentTable())->toBe('comments');
|
expect(CommentsConfig::getCommentTable())->toBe('comments');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('resolves max depth from config', function () {
|
it('resolves max depth from config', function () {
|
||||||
expect(Config::getMaxDepth())->toBe(2);
|
expect(CommentsConfig::getMaxDepth())->toBe(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('registers the morph map for comment', function () {
|
it('registers the morph map for comment', function () {
|
||||||
@@ -32,7 +32,7 @@ it('creates the comments table via migration', function () {
|
|||||||
expect(Schema::hasTable('comments'))->toBeTrue();
|
expect(Schema::hasTable('comments'))->toBeTrue();
|
||||||
expect(Schema::hasColumns('comments', [
|
expect(Schema::hasColumns('comments', [
|
||||||
'id', 'commentable_type', 'commentable_id',
|
'id', 'commentable_type', 'commentable_id',
|
||||||
'user_type', 'user_id', 'parent_id', 'body',
|
'commenter_type', 'commenter_id', 'parent_id', 'body',
|
||||||
'edited_at', 'deleted_at', 'created_at', 'updated_at',
|
'edited_at', 'deleted_at', 'created_at', 'updated_at',
|
||||||
]))->toBeTrue();
|
]))->toBeTrue();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
use Livewire\Livewire;
|
use Livewire\Livewire;
|
||||||
use Relaticle\Comments\CommentSubscription;
|
|
||||||
use Relaticle\Comments\Livewire\Comments;
|
use Relaticle\Comments\Livewire\Comments;
|
||||||
|
use Relaticle\Comments\Models\Subscription;
|
||||||
use Relaticle\Comments\Tests\Models\Post;
|
use Relaticle\Comments\Tests\Models\Post;
|
||||||
use Relaticle\Comments\Tests\Models\User;
|
use Relaticle\Comments\Tests\Models\User;
|
||||||
|
|
||||||
@@ -12,35 +12,35 @@ it('subscribes user when toggling from unsubscribed state', function () {
|
|||||||
|
|
||||||
$this->actingAs($user);
|
$this->actingAs($user);
|
||||||
|
|
||||||
expect(CommentSubscription::isSubscribed($post, $user))->toBeFalse();
|
expect(Subscription::isSubscribed($post, $user))->toBeFalse();
|
||||||
|
|
||||||
Livewire::test(Comments::class, ['model' => $post])
|
Livewire::test(Comments::class, ['model' => $post])
|
||||||
->call('toggleSubscription');
|
->call('toggleSubscription');
|
||||||
|
|
||||||
expect(CommentSubscription::isSubscribed($post, $user))->toBeTrue();
|
expect(Subscription::isSubscribed($post, $user))->toBeTrue();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('unsubscribes user when toggling from subscribed state', function () {
|
it('unsubscribes user when toggling from subscribed state', function () {
|
||||||
$user = User::factory()->create();
|
$user = User::factory()->create();
|
||||||
$post = Post::factory()->create();
|
$post = Post::factory()->create();
|
||||||
|
|
||||||
CommentSubscription::subscribe($post, $user);
|
Subscription::subscribe($post, $user);
|
||||||
|
|
||||||
$this->actingAs($user);
|
$this->actingAs($user);
|
||||||
|
|
||||||
expect(CommentSubscription::isSubscribed($post, $user))->toBeTrue();
|
expect(Subscription::isSubscribed($post, $user))->toBeTrue();
|
||||||
|
|
||||||
Livewire::test(Comments::class, ['model' => $post])
|
Livewire::test(Comments::class, ['model' => $post])
|
||||||
->call('toggleSubscription');
|
->call('toggleSubscription');
|
||||||
|
|
||||||
expect(CommentSubscription::isSubscribed($post, $user))->toBeFalse();
|
expect(Subscription::isSubscribed($post, $user))->toBeFalse();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns true for isSubscribed computed when user is subscribed', function () {
|
it('returns true for isSubscribed computed when user is subscribed', function () {
|
||||||
$user = User::factory()->create();
|
$user = User::factory()->create();
|
||||||
$post = Post::factory()->create();
|
$post = Post::factory()->create();
|
||||||
|
|
||||||
CommentSubscription::subscribe($post, $user);
|
Subscription::subscribe($post, $user);
|
||||||
|
|
||||||
$this->actingAs($user);
|
$this->actingAs($user);
|
||||||
|
|
||||||
@@ -64,7 +64,7 @@ it('renders Subscribed text for subscribed user', function () {
|
|||||||
$user = User::factory()->create();
|
$user = User::factory()->create();
|
||||||
$post = Post::factory()->create();
|
$post = Post::factory()->create();
|
||||||
|
|
||||||
CommentSubscription::subscribe($post, $user);
|
Subscription::subscribe($post, $user);
|
||||||
|
|
||||||
$this->actingAs($user);
|
$this->actingAs($user);
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
use Relaticle\Comments\Comment;
|
|
||||||
use Relaticle\Comments\Events\UserMentioned;
|
use Relaticle\Comments\Events\UserMentioned;
|
||||||
|
use Relaticle\Comments\Models\Comment;
|
||||||
use Relaticle\Comments\Tests\Models\Post;
|
use Relaticle\Comments\Tests\Models\Post;
|
||||||
use Relaticle\Comments\Tests\Models\User;
|
use Relaticle\Comments\Tests\Models\User;
|
||||||
|
|
||||||
@@ -13,8 +13,8 @@ it('carries correct comment and mentioned user in payload', function () {
|
|||||||
$comment = Comment::factory()->create([
|
$comment = Comment::factory()->create([
|
||||||
'commentable_id' => $post->id,
|
'commentable_id' => $post->id,
|
||||||
'commentable_type' => $post->getMorphClass(),
|
'commentable_type' => $post->getMorphClass(),
|
||||||
'user_id' => $user->getKey(),
|
'commenter_id' => $user->getKey(),
|
||||||
'user_type' => $user->getMorphClass(),
|
'commenter_type' => $user->getMorphClass(),
|
||||||
'body' => '<p>@john</p>',
|
'body' => '<p>@john</p>',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|||||||
@@ -5,14 +5,14 @@ namespace Relaticle\Comments\Tests\Models;
|
|||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
use Illuminate\Foundation\Auth\User as Authenticatable;
|
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||||
use Illuminate\Notifications\Notifiable;
|
use Illuminate\Notifications\Notifiable;
|
||||||
use Relaticle\Comments\Concerns\IsCommenter;
|
use Relaticle\Comments\Concerns\CanComment;
|
||||||
use Relaticle\Comments\Contracts\Commenter;
|
use Relaticle\Comments\Contracts\Commentator;
|
||||||
use Relaticle\Comments\Tests\Database\Factories\UserFactory;
|
use Relaticle\Comments\Tests\Database\Factories\UserFactory;
|
||||||
|
|
||||||
class User extends Authenticatable implements Commenter
|
class User extends Authenticatable implements Commentator
|
||||||
{
|
{
|
||||||
|
use CanComment;
|
||||||
use HasFactory;
|
use HasFactory;
|
||||||
use IsCommenter;
|
|
||||||
use Notifiable;
|
use Notifiable;
|
||||||
|
|
||||||
protected $table = 'users';
|
protected $table = 'users';
|
||||||
|
|||||||
@@ -2,7 +2,12 @@
|
|||||||
|
|
||||||
namespace Relaticle\Comments\Tests;
|
namespace Relaticle\Comments\Tests;
|
||||||
|
|
||||||
|
use BladeUI\Heroicons\BladeHeroiconsServiceProvider;
|
||||||
|
use BladeUI\Icons\BladeIconsServiceProvider;
|
||||||
|
use Filament\Actions\ActionsServiceProvider;
|
||||||
use Filament\FilamentServiceProvider;
|
use Filament\FilamentServiceProvider;
|
||||||
|
use Filament\Forms\FormsServiceProvider;
|
||||||
|
use Filament\Schemas\SchemasServiceProvider;
|
||||||
use Filament\Support\SupportServiceProvider;
|
use Filament\Support\SupportServiceProvider;
|
||||||
use Illuminate\Database\Schema\Blueprint;
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
use Illuminate\Support\Facades\Schema;
|
use Illuminate\Support\Facades\Schema;
|
||||||
@@ -26,7 +31,12 @@ abstract class TestCase extends Orchestra
|
|||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
LivewireServiceProvider::class,
|
LivewireServiceProvider::class,
|
||||||
|
BladeIconsServiceProvider::class,
|
||||||
|
BladeHeroiconsServiceProvider::class,
|
||||||
SupportServiceProvider::class,
|
SupportServiceProvider::class,
|
||||||
|
SchemasServiceProvider::class,
|
||||||
|
FormsServiceProvider::class,
|
||||||
|
ActionsServiceProvider::class,
|
||||||
FilamentServiceProvider::class,
|
FilamentServiceProvider::class,
|
||||||
CommentsServiceProvider::class,
|
CommentsServiceProvider::class,
|
||||||
];
|
];
|
||||||
@@ -48,13 +58,13 @@ abstract class TestCase extends Orchestra
|
|||||||
$table->timestamps();
|
$table->timestamps();
|
||||||
});
|
});
|
||||||
|
|
||||||
Schema::create(config('comments.tables.comments', 'comments'), function (Blueprint $table) {
|
Schema::create(config('comments.table_names.comments', 'comments'), function (Blueprint $table) {
|
||||||
$table->id();
|
$table->id();
|
||||||
$table->morphs('commentable');
|
$table->morphs('commentable');
|
||||||
$table->morphs('user');
|
$table->morphs('commenter');
|
||||||
$table->foreignId('parent_id')
|
$table->foreignId('parent_id')
|
||||||
->nullable()
|
->nullable()
|
||||||
->constrained(config('comments.tables.comments', 'comments'))
|
->constrained(config('comments.table_names.comments', 'comments'))
|
||||||
->cascadeOnDelete();
|
->cascadeOnDelete();
|
||||||
$table->text('body');
|
$table->text('body');
|
||||||
$table->timestamp('edited_at')->nullable();
|
$table->timestamp('edited_at')->nullable();
|
||||||
@@ -67,30 +77,30 @@ abstract class TestCase extends Orchestra
|
|||||||
Schema::create('comment_mentions', function (Blueprint $table) {
|
Schema::create('comment_mentions', function (Blueprint $table) {
|
||||||
$table->id();
|
$table->id();
|
||||||
$table->foreignId('comment_id')
|
$table->foreignId('comment_id')
|
||||||
->constrained(config('comments.tables.comments', 'comments'))
|
->constrained(config('comments.table_names.comments', 'comments'))
|
||||||
->cascadeOnDelete();
|
->cascadeOnDelete();
|
||||||
$table->morphs('user');
|
$table->morphs('commenter');
|
||||||
$table->timestamps();
|
$table->timestamps();
|
||||||
|
|
||||||
$table->unique(['comment_id', 'user_id', 'user_type']);
|
$table->unique(['comment_id', 'commenter_id', 'commenter_type']);
|
||||||
});
|
});
|
||||||
|
|
||||||
Schema::create('comment_reactions', function (Blueprint $table) {
|
Schema::create('comment_reactions', function (Blueprint $table) {
|
||||||
$table->id();
|
$table->id();
|
||||||
$table->foreignId('comment_id')
|
$table->foreignId('comment_id')
|
||||||
->constrained(config('comments.tables.comments', 'comments'))
|
->constrained(config('comments.table_names.comments', 'comments'))
|
||||||
->cascadeOnDelete();
|
->cascadeOnDelete();
|
||||||
$table->morphs('user');
|
$table->morphs('commenter');
|
||||||
$table->string('reaction');
|
$table->string('reaction');
|
||||||
$table->timestamps();
|
$table->timestamps();
|
||||||
|
|
||||||
$table->unique(['comment_id', 'user_id', 'user_type', 'reaction']);
|
$table->unique(['comment_id', 'commenter_id', 'commenter_type', 'reaction']);
|
||||||
});
|
});
|
||||||
|
|
||||||
Schema::create('comment_attachments', function (Blueprint $table) {
|
Schema::create('comment_attachments', function (Blueprint $table) {
|
||||||
$table->id();
|
$table->id();
|
||||||
$table->foreignId('comment_id')
|
$table->foreignId('comment_id')
|
||||||
->constrained(config('comments.tables.comments', 'comments'))
|
->constrained(config('comments.table_names.comments', 'comments'))
|
||||||
->cascadeOnDelete();
|
->cascadeOnDelete();
|
||||||
$table->string('file_path');
|
$table->string('file_path');
|
||||||
$table->string('original_name');
|
$table->string('original_name');
|
||||||
@@ -112,10 +122,10 @@ abstract class TestCase extends Orchestra
|
|||||||
Schema::create('comment_subscriptions', function (Blueprint $table) {
|
Schema::create('comment_subscriptions', function (Blueprint $table) {
|
||||||
$table->id();
|
$table->id();
|
||||||
$table->morphs('commentable');
|
$table->morphs('commentable');
|
||||||
$table->morphs('user');
|
$table->morphs('commenter');
|
||||||
$table->timestamp('created_at')->nullable();
|
$table->timestamp('created_at')->nullable();
|
||||||
|
|
||||||
$table->unique(['commentable_type', 'commentable_id', 'user_type', 'user_id'], 'comment_subscriptions_unique');
|
$table->unique(['commentable_type', 'commentable_id', 'commenter_type', 'commenter_id'], 'comment_subscriptions_unique');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user