From e87555f9ea6227658483550e58ff1c1bedbdd4cf Mon Sep 17 00:00:00 2001 From: Joe Dixon Date: Mon, 15 Nov 2021 09:01:48 +0000 Subject: [PATCH 01/24] wip --- app/Http/Livewire/Editor.php | 20 ++++++++++ package-lock.json | 45 +++++++++++++---------- package.json | 3 +- resources/js/app.js | 2 +- resources/js/editor.js | 22 +++++++++++ resources/views/livewire/editor.blade.php | 18 ++++++++- 6 files changed, 88 insertions(+), 22 deletions(-) diff --git a/app/Http/Livewire/Editor.php b/app/Http/Livewire/Editor.php index 69d22ed22..5b5c2b38f 100644 --- a/app/Http/Livewire/Editor.php +++ b/app/Http/Livewire/Editor.php @@ -2,7 +2,9 @@ namespace App\Http\Livewire; +use App\Models\User; use Livewire\Component; +use Illuminate\Support\Str; class Editor extends Component { @@ -20,6 +22,13 @@ class Editor extends Component public $buttonIcon; + public $users; + + public function mount() + { + $this->users = collect(); + } + public function render() { $this->body = old('body', $this->body); @@ -27,6 +36,17 @@ public function render() return view('livewire.editor'); } + public function getUsers($search) + { + if (! $search) { + return; + } + + $search = Str::after($search, '@'); + + return $this->users = User::where('username', 'like', "{$search}%")->take(5)->get(); + } + public function getPreviewProperty() { return replace_links(md_to_html($this->body ?: '')); diff --git a/package-lock.json b/package-lock.json index af5cf0820..4b025c358 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11212,8 +11212,7 @@ "ajv-keywords": { "version": "3.5.2", "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", - "dev": true, - "requires": {} + "dev": true }, "algoliasearch": { "version": "4.10.2", @@ -12275,8 +12274,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/cssnano-utils/-/cssnano-utils-2.0.1.tgz", "integrity": "sha512-i8vLRZTnEH9ubIyfdZCAdIdgnHAUeQeByEeQ2I7oTilvP9oHO6RScpeq3GsFUVqeB8uZgOQ9pw8utofNn32hhQ==", - "dev": true, - "requires": {} + "dev": true }, "csso": { "version": "4.2.0", @@ -13378,8 +13376,7 @@ "icss-utils": { "version": "5.1.0", "integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==", - "dev": true, - "requires": {} + "dev": true }, "ieee754": { "version": "1.2.1", @@ -14666,29 +14663,25 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/postcss-discard-comments/-/postcss-discard-comments-5.0.1.tgz", "integrity": "sha512-lgZBPTDvWrbAYY1v5GYEv8fEO/WhKOu/hmZqmCYfrpD6eyDWWzAOsl2rF29lpvziKO02Gc5GJQtlpkTmakwOWg==", - "dev": true, - "requires": {} + "dev": true }, "postcss-discard-duplicates": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/postcss-discard-duplicates/-/postcss-discard-duplicates-5.0.1.tgz", "integrity": "sha512-svx747PWHKOGpAXXQkCc4k/DsWo+6bc5LsVrAsw+OU+Ibi7klFZCyX54gjYzX4TH+f2uzXjRviLARxkMurA2bA==", - "dev": true, - "requires": {} + "dev": true }, "postcss-discard-empty": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/postcss-discard-empty/-/postcss-discard-empty-5.0.1.tgz", "integrity": "sha512-vfU8CxAQ6YpMxV2SvMcMIyF2LX1ZzWpy0lqHDsOdaKKLQVQGVP1pzhrI9JlsO65s66uQTfkQBKBD/A5gp9STFw==", - "dev": true, - "requires": {} + "dev": true }, "postcss-discard-overridden": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/postcss-discard-overridden/-/postcss-discard-overridden-5.0.1.tgz", "integrity": "sha512-Y28H7y93L2BpJhrdUR2SR2fnSsT+3TVx1NmVQLbcnZWwIUpJ7mfcTC6Za9M2PG6w8j7UQRfzxqn8jU2VwFxo3Q==", - "dev": true, - "requires": {} + "dev": true }, "postcss-js": { "version": "3.0.3", @@ -14787,8 +14780,7 @@ "postcss-modules-extract-imports": { "version": "3.0.0", "integrity": "sha512-bdHleFnP3kZ4NYDhuGlVK+CMrQ/pqUm8bx/oGL93K6gVwiclvX5x0n76fYMKuIGKzlABOy13zsvqjb0f92TEXw==", - "dev": true, - "requires": {} + "dev": true }, "postcss-modules-local-by-default": { "version": "4.0.0", @@ -14829,8 +14821,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/postcss-normalize-charset/-/postcss-normalize-charset-5.0.1.tgz", "integrity": "sha512-6J40l6LNYnBdPSk+BHZ8SF+HAkS4q2twe5jnocgd+xWpz/mx/5Sa32m3W1AA8uE8XaXN+eg8trIlfu8V9x61eg==", - "dev": true, - "requires": {} + "dev": true }, "postcss-normalize-display-values": { "version": "5.0.1", @@ -15669,6 +15660,17 @@ "xtend": "^4.0.0" } }, + "string-width": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.2.tgz", + "integrity": "sha512-XBJbT3N4JhVumXE0eoLU9DCjcaF92KLNqTmFCnG1pf8duUxFGwtP6AD6nkjw9a3IdiRtL3E2w3JDiE/xi3vOeA==", + "dev": true, + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.0" + } + }, "string_decoder": { "version": "1.3.0", "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", @@ -15887,6 +15889,11 @@ "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=", "dev": true }, + "textarea-caret": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/textarea-caret/-/textarea-caret-3.1.0.tgz", + "integrity": "sha512-cXAvzO9pP5CGa6NKx0WYHl+8CHKZs8byMkt3PCJBCmq2a34YA9pO1NrQET5pzeqnBjBdToF5No4rrmkDUgQC2Q==" + }, "thunky": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz", @@ -16455,4 +16462,4 @@ "dev": true } } -} +} \ No newline at end of file diff --git a/package.json b/package.json index bf5216ca8..3cbbb8dcb 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "tailwindcss": "^3.0.2", "algoliasearch": "^4.8.4", "alpinejs": "^3.0", - "choices.js": "^9.0.1" + "choices.js": "^9.0.1", + "textarea-caret": "^3.1.0" } } \ No newline at end of file diff --git a/resources/js/app.js b/resources/js/app.js index 0b7949816..4a94bb1fe 100644 --- a/resources/js/app.js +++ b/resources/js/app.js @@ -22,4 +22,4 @@ window.highlightCode = (element) => { element.querySelectorAll('pre code').forEach((block) => { hljs.highlightBlock(block); }); -}; +}; \ No newline at end of file diff --git a/resources/js/editor.js b/resources/js/editor.js index 6bb3f3c05..c59f50236 100644 --- a/resources/js/editor.js +++ b/resources/js/editor.js @@ -1,3 +1,5 @@ +import getCaretCoordinates from 'textarea-caret'; + // Handle the click event of the style buttons inside the editor. window.handleClick = (style, element) => { const { styles } = editorConfig(); @@ -61,11 +63,31 @@ window.editorConfig = (body) => { after: ')', }, }, + cursorTop: 0, + cursorLeft: 0, + cursorPosition: 0, body: body, mode: 'write', + showMentions: false, + search: '', submit: function (event) { event.target.closest('form').submit(); }, + updateCursorPosition: function (event) { + const coordinates = getCaretCoordinates(event.target); + this.cursorTop = coordinates.top+20+'px'; + this.cursorLeft = coordinates.left+20+'px'; + }, + updateSearch: function (event) { + if(this.showMentions) { + this.search += event.key + this.$wire.getUsers(this.search) + } else { + this.search = ''; + } + + console.log(this.search) + } }; }; diff --git a/resources/views/livewire/editor.blade.php b/resources/views/livewire/editor.blade.php index 98a798fcd..80c146e20 100644 --- a/resources/views/livewire/editor.blade.php +++ b/resources/views/livewire/editor.blade.php @@ -42,8 +42,24 @@ class="w-full h-full absolute left-0 top-0 right-0 bottom-0 overflow-y-hidden re required @keydown.cmd.enter="submit($event)" @keydown.ctrl.enter="submit($event)" + @keydown.@="updateCursorPosition($event); showMentions = true;" + @keydown.space="showMentions = false" + @click.away="showMentions = false" + @keydown.debounce="updateSearch($event)" > - + + +
From 1534a0bd4a0c3fefe3fae3822b4c13f9fc30d661 Mon Sep 17 00:00:00 2001 From: Joe Dixon Date: Sun, 9 Jan 2022 21:12:33 +0000 Subject: [PATCH 02/24] Capture input --- app/Http/Livewire/Editor.php | 2 +- package-lock.json | 51 +++++++++-------- resources/js/editor.js | 69 +++++++++++++++++++---- resources/views/livewire/editor.blade.php | 19 +++++-- 4 files changed, 103 insertions(+), 38 deletions(-) diff --git a/app/Http/Livewire/Editor.php b/app/Http/Livewire/Editor.php index 5b5c2b38f..c3ffb3cba 100644 --- a/app/Http/Livewire/Editor.php +++ b/app/Http/Livewire/Editor.php @@ -39,7 +39,7 @@ public function render() public function getUsers($search) { if (! $search) { - return; + return $this->users = collect(); } $search = Str::after($search, '@'); diff --git a/package-lock.json b/package-lock.json index 4b025c358..515875ee4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4,7 +4,6 @@ "requires": true, "packages": { "": { - "name": "laravelio", "devDependencies": { "@tailwindcss/aspect-ratio": "^0.2.0", "@tailwindcss/forms": "^0.4.0", @@ -17,7 +16,8 @@ "highlight.js": "^10.5.0", "laravel-mix": "^6.0.23", "postcss": "^8.4.5", - "tailwindcss": "^3.0.2" + "tailwindcss": "^3.0.2", + "textarea-caret": "^3.1.0" } }, "node_modules/@algolia/cache-browser-local-storage": { @@ -8613,6 +8613,12 @@ "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=", "dev": true }, + "node_modules/textarea-caret": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/textarea-caret/-/textarea-caret-3.1.0.tgz", + "integrity": "sha512-cXAvzO9pP5CGa6NKx0WYHl+8CHKZs8byMkt3PCJBCmq2a34YA9pO1NrQET5pzeqnBjBdToF5No4rrmkDUgQC2Q==", + "dev": true + }, "node_modules/thunky": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz", @@ -11212,7 +11218,8 @@ "ajv-keywords": { "version": "3.5.2", "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", - "dev": true + "dev": true, + "requires": {} }, "algoliasearch": { "version": "4.10.2", @@ -12274,7 +12281,8 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/cssnano-utils/-/cssnano-utils-2.0.1.tgz", "integrity": "sha512-i8vLRZTnEH9ubIyfdZCAdIdgnHAUeQeByEeQ2I7oTilvP9oHO6RScpeq3GsFUVqeB8uZgOQ9pw8utofNn32hhQ==", - "dev": true + "dev": true, + "requires": {} }, "csso": { "version": "4.2.0", @@ -13376,7 +13384,8 @@ "icss-utils": { "version": "5.1.0", "integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==", - "dev": true + "dev": true, + "requires": {} }, "ieee754": { "version": "1.2.1", @@ -14663,25 +14672,29 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/postcss-discard-comments/-/postcss-discard-comments-5.0.1.tgz", "integrity": "sha512-lgZBPTDvWrbAYY1v5GYEv8fEO/WhKOu/hmZqmCYfrpD6eyDWWzAOsl2rF29lpvziKO02Gc5GJQtlpkTmakwOWg==", - "dev": true + "dev": true, + "requires": {} }, "postcss-discard-duplicates": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/postcss-discard-duplicates/-/postcss-discard-duplicates-5.0.1.tgz", "integrity": "sha512-svx747PWHKOGpAXXQkCc4k/DsWo+6bc5LsVrAsw+OU+Ibi7klFZCyX54gjYzX4TH+f2uzXjRviLARxkMurA2bA==", - "dev": true + "dev": true, + "requires": {} }, "postcss-discard-empty": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/postcss-discard-empty/-/postcss-discard-empty-5.0.1.tgz", "integrity": "sha512-vfU8CxAQ6YpMxV2SvMcMIyF2LX1ZzWpy0lqHDsOdaKKLQVQGVP1pzhrI9JlsO65s66uQTfkQBKBD/A5gp9STFw==", - "dev": true + "dev": true, + "requires": {} }, "postcss-discard-overridden": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/postcss-discard-overridden/-/postcss-discard-overridden-5.0.1.tgz", "integrity": "sha512-Y28H7y93L2BpJhrdUR2SR2fnSsT+3TVx1NmVQLbcnZWwIUpJ7mfcTC6Za9M2PG6w8j7UQRfzxqn8jU2VwFxo3Q==", - "dev": true + "dev": true, + "requires": {} }, "postcss-js": { "version": "3.0.3", @@ -14780,7 +14793,8 @@ "postcss-modules-extract-imports": { "version": "3.0.0", "integrity": "sha512-bdHleFnP3kZ4NYDhuGlVK+CMrQ/pqUm8bx/oGL93K6gVwiclvX5x0n76fYMKuIGKzlABOy13zsvqjb0f92TEXw==", - "dev": true + "dev": true, + "requires": {} }, "postcss-modules-local-by-default": { "version": "4.0.0", @@ -14821,7 +14835,8 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/postcss-normalize-charset/-/postcss-normalize-charset-5.0.1.tgz", "integrity": "sha512-6J40l6LNYnBdPSk+BHZ8SF+HAkS4q2twe5jnocgd+xWpz/mx/5Sa32m3W1AA8uE8XaXN+eg8trIlfu8V9x61eg==", - "dev": true + "dev": true, + "requires": {} }, "postcss-normalize-display-values": { "version": "5.0.1", @@ -15660,17 +15675,6 @@ "xtend": "^4.0.0" } }, - "string-width": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.2.tgz", - "integrity": "sha512-XBJbT3N4JhVumXE0eoLU9DCjcaF92KLNqTmFCnG1pf8duUxFGwtP6AD6nkjw9a3IdiRtL3E2w3JDiE/xi3vOeA==", - "dev": true, - "requires": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.0" - } - }, "string_decoder": { "version": "1.3.0", "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", @@ -15892,7 +15896,8 @@ "textarea-caret": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/textarea-caret/-/textarea-caret-3.1.0.tgz", - "integrity": "sha512-cXAvzO9pP5CGa6NKx0WYHl+8CHKZs8byMkt3PCJBCmq2a34YA9pO1NrQET5pzeqnBjBdToF5No4rrmkDUgQC2Q==" + "integrity": "sha512-cXAvzO9pP5CGa6NKx0WYHl+8CHKZs8byMkt3PCJBCmq2a34YA9pO1NrQET5pzeqnBjBdToF5No4rrmkDUgQC2Q==", + "dev": true }, "thunky": { "version": "1.1.0", diff --git a/resources/js/editor.js b/resources/js/editor.js index c59f50236..b7c50474c 100644 --- a/resources/js/editor.js +++ b/resources/js/editor.js @@ -73,20 +73,69 @@ window.editorConfig = (body) => { submit: function (event) { event.target.closest('form').submit(); }, - updateCursorPosition: function (event) { - const coordinates = getCaretCoordinates(event.target); - this.cursorTop = coordinates.top+20+'px'; - this.cursorLeft = coordinates.left+20+'px'; + updateCursorPosition: function (element, position) { + const coordinates = getCaretCoordinates(element, position); + this.cursorTop = coordinates.top + 25 + 'px'; + this.cursorLeft = coordinates.left + 'px'; }, updateSearch: function (event) { - if(this.showMentions) { - this.search += event.key - this.$wire.getUsers(this.search) - } else { - this.search = ''; + const element = event.target; + const content = element.value; + const cursorPosition = element.selectionEnd; + const matches = event.target.value.match(/@[\w\d]+/g) + + if (!matches) { + return this.resetSearch(); + } + + const shouldSearch = matches.some(match => { + const startPosition = content.search(match) + const endPosition = startPosition + match.length + + if (cursorPosition >= startPosition && cursorPosition <= endPosition) { + console.log(this.$wire.users.length); + this.updateCursorPosition(event.target, startPosition) + this.showMentions = true; + this.search = match.slice(1) + this.$wire.getUsers(this.search) + return true; + } + + return false; + }) + + if (!shouldSearch) { + this.resetSearch() } + }, + resetSearch: function () { + console.log('Resetting'); + this.showMentions = false; + this.search = ''; + }, + showUserSelect: function () { + console.log({ show: this.showMentions, total: this.$wire.users.length }) + return this.showMentions && this.$wire.users.length > 0 + }, + selectUser: function (username) { + let content = this.$refs.editor.value + const cursorPosition = this.$refs.editor.selectionEnd + const matches = content.match(/@[\w\d]+/g) + + if (!matches) { + return; + } + + matches.forEach(match => { + const startPosition = content.search(match) + const endPosition = startPosition + match.length - console.log(this.search) + if (cursorPosition >= startPosition && cursorPosition <= endPosition) { + this.body = content.substring(0, startPosition) + '@' + username + content.substring(endPosition) + ' '; + this.$refs.editor.focus() + this.resetSearch() + } + }) } }; }; diff --git a/resources/views/livewire/editor.blade.php b/resources/views/livewire/editor.blade.php index 80c146e20..45099383a 100644 --- a/resources/views/livewire/editor.blade.php +++ b/resources/views/livewire/editor.blade.php @@ -40,17 +40,27 @@ class="w-full h-full absolute left-0 top-0 right-0 bottom-0 overflow-y-hidden re placeholder="{{ $placeholder }}" x-model=body required + x-ref="editor" @keydown.cmd.enter="submit($event)" @keydown.ctrl.enter="submit($event)" - @keydown.@="updateCursorPosition($event); showMentions = true;" @keydown.space="showMentions = false" @click.away="showMentions = false" - @keydown.debounce="updateSearch($event)" + @keydown.debounce.500ms="updateSearch($event)" > -
    + @if ($users->count()) +
      @foreach ($users as $user) -
    • +
    • @@ -59,6 +69,7 @@ class="w-full h-full absolute left-0 top-0 right-0 bottom-0 overflow-y-hidden re
    • @endforeach
    + @endif
From deac41908aa3afc7c7ba397e3f986a1d355a9d18 Mon Sep 17 00:00:00 2001 From: Joe Dixon Date: Tue, 18 Jan 2022 21:35:08 +0000 Subject: [PATCH 03/24] Add support for participants --- app/Concerns/ReceivesReplies.php | 18 ++++++++++++++++++ app/Http/Livewire/Editor.php | 14 +++++++++++--- app/Models/Thread.php | 8 ++++++++ resources/js/editor.js | 4 ++-- resources/views/forum/threads/show.blade.php | 1 + 5 files changed, 40 insertions(+), 5 deletions(-) diff --git a/app/Concerns/ReceivesReplies.php b/app/Concerns/ReceivesReplies.php index 96e39f4d4..667693b5a 100644 --- a/app/Concerns/ReceivesReplies.php +++ b/app/Concerns/ReceivesReplies.php @@ -3,7 +3,10 @@ namespace App\Concerns; use App\Models\Reply; +use App\Models\User; +use Illuminate\Database\Eloquent\Relations\HasManyThrough; use Illuminate\Database\Eloquent\Relations\MorphMany; +use Illuminate\Database\Eloquent\Relations\Relation; trait ReceivesReplies { @@ -15,6 +18,21 @@ public function replies() return $this->repliesRelation; } + public function replyAuthors(): HasManyThrough + { + return $this->hasManyThrough( + User::class, + Reply::class, + 'replyable_id', + 'id', + 'id', + 'author_id' + )->where( + 'replyable_type', + array_search(static::class, Relation::morphMap()) + ); + } + /** * @return \Illuminate\Database\Eloquent\Collection */ diff --git a/app/Http/Livewire/Editor.php b/app/Http/Livewire/Editor.php index c3ffb3cba..ea90bd7a1 100644 --- a/app/Http/Livewire/Editor.php +++ b/app/Http/Livewire/Editor.php @@ -24,6 +24,8 @@ class Editor extends Component public $users; + public $participants; + public function mount() { $this->users = collect(); @@ -39,12 +41,18 @@ public function render() public function getUsers($search) { if (! $search) { - return $this->users = collect(); + return $this->users = $this->participants; } $search = Str::after($search, '@'); - - return $this->users = User::where('username', 'like', "{$search}%")->take(5)->get(); + $users = User::where('username', 'like', "{$search}%")->take(5)->get(); + $users = $this->participants->filter(function ($participant) use ($search) { + return Str::startsWith($participant->username, $search); + }) + ->merge($users) + ->unique('id'); + + return $this->users = $users; } public function getPreviewProperty() diff --git a/app/Models/Thread.php b/app/Models/Thread.php index 32da97723..e4092a031 100644 --- a/app/Models/Thread.php +++ b/app/Models/Thread.php @@ -340,4 +340,12 @@ public function scopeUnlocked(Builder $query): Builder { return $query->whereNull('locked_at'); } + + public function participants(): SupportCollection + { + return $this->replyAuthors() + ->get() + ->prepend($this->author()) + ->unique(); + } } diff --git a/resources/js/editor.js b/resources/js/editor.js index b7c50474c..7c1d18e09 100644 --- a/resources/js/editor.js +++ b/resources/js/editor.js @@ -82,7 +82,7 @@ window.editorConfig = (body) => { const element = event.target; const content = element.value; const cursorPosition = element.selectionEnd; - const matches = event.target.value.match(/@[\w\d]+/g) + const matches = event.target.value.match(/@[\w\d]*/g) if (!matches) { return this.resetSearch(); @@ -120,7 +120,7 @@ window.editorConfig = (body) => { selectUser: function (username) { let content = this.$refs.editor.value const cursorPosition = this.$refs.editor.selectionEnd - const matches = content.match(/@[\w\d]+/g) + const matches = content.match(/@[\w\d]*/g) if (!matches) { return; diff --git a/resources/views/forum/threads/show.blade.php b/resources/views/forum/threads/show.blade.php index 426e13c4d..a1da29afd 100644 --- a/resources/views/forum/threads/show.blade.php +++ b/resources/views/forum/threads/show.blade.php @@ -75,6 +75,7 @@ class="text-lio-500 hover:text-lio-600" buttonLabel="Reply" buttonIcon="send" label="Write a reply" + :participants="$thread->participants()" /> @error('body') From 0df98b15c0615776e0baa68aa6ae49459e38eff0 Mon Sep 17 00:00:00 2001 From: StyleCI Bot Date: Tue, 18 Jan 2022 21:35:27 +0000 Subject: [PATCH 04/24] Apply fixes from StyleCI --- app/Concerns/ReceivesReplies.php | 6 ++--- app/Http/Livewire/Editor.php | 2 +- resources/js/app.js | 2 +- resources/js/editor.js | 45 ++++++++++++++++---------------- 4 files changed, 28 insertions(+), 27 deletions(-) diff --git a/app/Concerns/ReceivesReplies.php b/app/Concerns/ReceivesReplies.php index 667693b5a..104e11aac 100644 --- a/app/Concerns/ReceivesReplies.php +++ b/app/Concerns/ReceivesReplies.php @@ -26,10 +26,10 @@ public function replyAuthors(): HasManyThrough 'replyable_id', 'id', 'id', - 'author_id' + 'author_id', )->where( - 'replyable_type', - array_search(static::class, Relation::morphMap()) + 'replyable_type', + array_search(static::class, Relation::morphMap()), ); } diff --git a/app/Http/Livewire/Editor.php b/app/Http/Livewire/Editor.php index ea90bd7a1..a7cccf696 100644 --- a/app/Http/Livewire/Editor.php +++ b/app/Http/Livewire/Editor.php @@ -3,8 +3,8 @@ namespace App\Http\Livewire; use App\Models\User; -use Livewire\Component; use Illuminate\Support\Str; +use Livewire\Component; class Editor extends Component { diff --git a/resources/js/app.js b/resources/js/app.js index 4a94bb1fe..0b7949816 100644 --- a/resources/js/app.js +++ b/resources/js/app.js @@ -22,4 +22,4 @@ window.highlightCode = (element) => { element.querySelectorAll('pre code').forEach((block) => { hljs.highlightBlock(block); }); -}; \ No newline at end of file +}; diff --git a/resources/js/editor.js b/resources/js/editor.js index 7c1d18e09..28b54d643 100644 --- a/resources/js/editor.js +++ b/resources/js/editor.js @@ -82,30 +82,30 @@ window.editorConfig = (body) => { const element = event.target; const content = element.value; const cursorPosition = element.selectionEnd; - const matches = event.target.value.match(/@[\w\d]*/g) + const matches = event.target.value.match(/@[\w\d]*/g); if (!matches) { return this.resetSearch(); } - const shouldSearch = matches.some(match => { - const startPosition = content.search(match) - const endPosition = startPosition + match.length + const shouldSearch = matches.some((match) => { + const startPosition = content.search(match); + const endPosition = startPosition + match.length; if (cursorPosition >= startPosition && cursorPosition <= endPosition) { console.log(this.$wire.users.length); - this.updateCursorPosition(event.target, startPosition) + this.updateCursorPosition(event.target, startPosition); this.showMentions = true; - this.search = match.slice(1) - this.$wire.getUsers(this.search) + this.search = match.slice(1); + this.$wire.getUsers(this.search); return true; } return false; - }) + }); if (!shouldSearch) { - this.resetSearch() + this.resetSearch(); } }, resetSearch: function () { @@ -114,29 +114,30 @@ window.editorConfig = (body) => { this.search = ''; }, showUserSelect: function () { - console.log({ show: this.showMentions, total: this.$wire.users.length }) - return this.showMentions && this.$wire.users.length > 0 + console.log({ show: this.showMentions, total: this.$wire.users.length }); + return this.showMentions && this.$wire.users.length > 0; }, selectUser: function (username) { - let content = this.$refs.editor.value - const cursorPosition = this.$refs.editor.selectionEnd - const matches = content.match(/@[\w\d]*/g) + let content = this.$refs.editor.value; + const cursorPosition = this.$refs.editor.selectionEnd; + const matches = content.match(/@[\w\d]*/g); if (!matches) { return; } - matches.forEach(match => { - const startPosition = content.search(match) - const endPosition = startPosition + match.length + matches.forEach((match) => { + const startPosition = content.search(match); + const endPosition = startPosition + match.length; if (cursorPosition >= startPosition && cursorPosition <= endPosition) { - this.body = content.substring(0, startPosition) + '@' + username + content.substring(endPosition) + ' '; - this.$refs.editor.focus() - this.resetSearch() + this.body = + content.substring(0, startPosition) + '@' + username + content.substring(endPosition) + ' '; + this.$refs.editor.focus(); + this.resetSearch(); } - }) - } + }); + }, }; }; From 8c089ea563d43dfbb09405e8437a35ac2ad8b5c8 Mon Sep 17 00:00:00 2001 From: Joe Dixon Date: Wed, 19 Jan 2022 21:43:13 +0000 Subject: [PATCH 05/24] Refactor user select --- resources/js/editor.js | 97 ++++++++++++++--------- resources/views/livewire/editor.blade.php | 39 +++++---- 2 files changed, 79 insertions(+), 57 deletions(-) diff --git a/resources/js/editor.js b/resources/js/editor.js index 28b54d643..bdb1ce3b1 100644 --- a/resources/js/editor.js +++ b/resources/js/editor.js @@ -65,38 +65,41 @@ window.editorConfig = (body) => { }, cursorTop: 0, cursorLeft: 0, - cursorPosition: 0, body: body, mode: 'write', showMentions: false, search: '', - submit: function (event) { - event.target.closest('form').submit(); + + // Gets the current cursor position. + cursorPosition: function () { + return this.$refs.editor.selectionEnd; + }, + + // Submits the form enclosing the editor. + submit: function () { + this.$refs.editor.closest('form').submit(); }, - updateCursorPosition: function (element, position) { + + // Updates the position of the listbox by calculating the caret position and applying an offset. + updateListboxPosition: function (element, position) { const coordinates = getCaretCoordinates(element, position); this.cursorTop = coordinates.top + 25 + 'px'; this.cursorLeft = coordinates.left + 'px'; }, - updateSearch: function (event) { - const element = event.target; - const content = element.value; - const cursorPosition = element.selectionEnd; - const matches = event.target.value.match(/@[\w\d]*/g); - - if (!matches) { - return this.resetSearch(); - } - const shouldSearch = matches.some((match) => { - const startPosition = content.search(match); - const endPosition = startPosition + match.length; + // Takes the user input, determines if a mention is active and initiates the search. + updateUserSearch: function () { + const mentions = this.extractMentions(); + + if (!mentions) { + return this.resetUserSearch(); + } - if (cursorPosition >= startPosition && cursorPosition <= endPosition) { - console.log(this.$wire.users.length); - this.updateCursorPosition(event.target, startPosition); + const shouldSearch = mentions.some(({ mention, start, end }) => { + if (this.isAtCursor(start, end)) { + this.updateListboxPosition(this.$refs.editor, start); this.showMentions = true; - this.search = match.slice(1); + this.search = mention.slice(1); this.$wire.getUsers(this.search); return true; } @@ -105,40 +108,60 @@ window.editorConfig = (body) => { }); if (!shouldSearch) { - this.resetSearch(); + this.resetUserSearch(); } }, - resetSearch: function () { - console.log('Resetting'); + + // Resets the user search parameters. + resetUserSearch: function () { this.showMentions = false; this.search = ''; }, - showUserSelect: function () { - console.log({ show: this.showMentions, total: this.$wire.users.length }); + + // Determines whether or not the user listbox should be rendered. + showUserListbox: function () { return this.showMentions && this.$wire.users.length > 0; }, + + // Takes the selected user from the listbox and populates the value in the correct place in the editor. selectUser: function (username) { - let content = this.$refs.editor.value; - const cursorPosition = this.$refs.editor.selectionEnd; - const matches = content.match(/@[\w\d]*/g); + const mentions = this.extractMentions(); - if (!matches) { + if (!mentions) { return; } - matches.forEach((match) => { - const startPosition = content.search(match); - const endPosition = startPosition + match.length; - - if (cursorPosition >= startPosition && cursorPosition <= endPosition) { + mentions.forEach(({ start, end }) => { + if (this.isAtCursor(start, end)) { this.body = - content.substring(0, startPosition) + '@' + username + content.substring(endPosition) + ' '; + this.body.substring(0, start) + '@' + username + this.body.substring(end) + ' '; this.$refs.editor.focus(); - this.resetSearch(); + this.resetUserSearch(); } }); }, - }; + + // Extracts all the mentions from the input along with their start and end position in the string. + extractMentions: function () { + const mentionRegex = /@[\w\d]*/g + let mention; + let mentions = []; + while ((mention = mentionRegex.exec(this.body)) !== null) { + mentions.push({ + mention: mention[0], + start: mention.index, + end: mention.index + mention[0].length + }) + } + + return mentions; + }, + + // Detects whether or not the provided start and end position overlap the current cursor position. + isAtCursor: function (start, end) { + return this.cursorPosition() >= start && this.cursorPosition() <= end + } + } }; Livewire.on('previewRequested', () => { diff --git a/resources/views/livewire/editor.blade.php b/resources/views/livewire/editor.blade.php index 45099383a..2460cd59d 100644 --- a/resources/views/livewire/editor.blade.php +++ b/resources/views/livewire/editor.blade.php @@ -45,30 +45,29 @@ class="w-full h-full absolute left-0 top-0 right-0 bottom-0 overflow-y-hidden re @keydown.ctrl.enter="submit($event)" @keydown.space="showMentions = false" @click.away="showMentions = false" - @keydown.debounce.500ms="updateSearch($event)" + @keydown.debounce.500ms="updateUserSearch($event)" > @if ($users->count()) -
    - @foreach ($users as $user) -
  • - +
      + @foreach ($users as $user) +
    • + - - {{ $user->username() }} - -
    • - @endforeach -
    + + {{ $user->username() }} + +
  • + @endforeach +
@endif
From f86d46bbb228b198e6efffa2791d3a9aa40f94b9 Mon Sep 17 00:00:00 2001 From: StyleCI Bot Date: Wed, 19 Jan 2022 21:43:43 +0000 Subject: [PATCH 06/24] Apply fixes from StyleCI --- resources/js/editor.js | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/resources/js/editor.js b/resources/js/editor.js index bdb1ce3b1..a586aff53 100644 --- a/resources/js/editor.js +++ b/resources/js/editor.js @@ -133,8 +133,7 @@ window.editorConfig = (body) => { mentions.forEach(({ start, end }) => { if (this.isAtCursor(start, end)) { - this.body = - this.body.substring(0, start) + '@' + username + this.body.substring(end) + ' '; + this.body = this.body.substring(0, start) + '@' + username + this.body.substring(end) + ' '; this.$refs.editor.focus(); this.resetUserSearch(); } @@ -143,15 +142,15 @@ window.editorConfig = (body) => { // Extracts all the mentions from the input along with their start and end position in the string. extractMentions: function () { - const mentionRegex = /@[\w\d]*/g + const mentionRegex = /@[\w\d]*/g; let mention; let mentions = []; while ((mention = mentionRegex.exec(this.body)) !== null) { mentions.push({ mention: mention[0], start: mention.index, - end: mention.index + mention[0].length - }) + end: mention.index + mention[0].length, + }); } return mentions; @@ -159,9 +158,9 @@ window.editorConfig = (body) => { // Detects whether or not the provided start and end position overlap the current cursor position. isAtCursor: function (start, end) { - return this.cursorPosition() >= start && this.cursorPosition() <= end - } - } + return this.cursorPosition() >= start && this.cursorPosition() <= end; + }, + }; }; Livewire.on('previewRequested', () => { From 1b794b9a89088c63cdb82f1654df4de9cf095d8f Mon Sep 17 00:00:00 2001 From: Joe Dixon Date: Thu, 20 Jan 2022 21:53:55 +0000 Subject: [PATCH 07/24] Add keyboard navigation --- app/Http/Livewire/Editor.php | 5 +- resources/css/app.css | 4 ++ resources/js/editor.js | 80 ++++++++++++++++++++++- resources/views/livewire/editor.blade.php | 29 +++++--- 4 files changed, 104 insertions(+), 14 deletions(-) diff --git a/app/Http/Livewire/Editor.php b/app/Http/Livewire/Editor.php index a7cccf696..3cd1562f1 100644 --- a/app/Http/Livewire/Editor.php +++ b/app/Http/Livewire/Editor.php @@ -26,9 +26,10 @@ class Editor extends Component public $participants; - public function mount() + public function mount($participants = null) { $this->users = collect(); + $this->participants = $participants ?: collect(); } public function render() @@ -40,7 +41,7 @@ public function render() public function getUsers($search) { - if (! $search) { + if (! $search && $this->participants->isNotEmpty()) { return $this->users = $this->participants; } diff --git a/resources/css/app.css b/resources/css/app.css index 626e29130..869382f7d 100644 --- a/resources/css/app.css +++ b/resources/css/app.css @@ -144,3 +144,7 @@ svg .secondary { .choices[data-type*='select-one'] .choices__input { @apply rounded-none border-b-2 border-gray-300 !important; } + +.editor li[aria-selected="true"] { + @apply bg-lio-100; +} diff --git a/resources/js/editor.js b/resources/js/editor.js index a586aff53..0382ea231 100644 --- a/resources/js/editor.js +++ b/resources/js/editor.js @@ -88,7 +88,11 @@ window.editorConfig = (body) => { }, // Takes the user input, determines if a mention is active and initiates the search. - updateUserSearch: function () { + updateUserSearch: function (event) { + if (this.isArrowKey(event.keyCode)) { + return; + } + const mentions = this.extractMentions(); if (!mentions) { @@ -123,6 +127,70 @@ window.editorConfig = (body) => { return this.showMentions && this.$wire.users.length > 0; }, + // Get the currently highlighted user in the listbox. + getHighlightedUser: function () { + if (!this.showUserListbox()) { + return false; + } + + const highlighted = this.$refs.users.querySelectorAll('li[aria-selected=true]'); + + return highlighted[0] ? highlighted[0] : false; + }, + + // Highlight the next user in the listbox. + highlightNextUser: function (event) { + const highlighted = this.getHighlightedUser(); + + if (!highlighted) { + return; + } + + const next = highlighted.nextElementSibling; + + if (!next) { + return; + } + + event.preventDefault(); + + highlighted.setAttribute('aria-selected', false) + next.setAttribute('aria-selected', true) + }, + + // Highlight the previous user in the listbox. + highlightPreviousUser: function (event) { + const highlighted = this.getHighlightedUser(); + + if (!highlighted) { + return; + } + + const previous = highlighted.previousElementSibling; + + if (!previous) { + return; + } + + event.preventDefault(); + + highlighted.setAttribute('aria-selected', false) + previous.setAttribute('aria-selected', true) + }, + + // Take the selected user and put the name into the editor. + selectHighlightedUser: function (event) { + const highlighted = this.getHighlightedUser(); + + if (!highlighted) { + return; + } + + event.preventDefault(); + + this.selectUser(highlighted.dataset.username); + }, + // Takes the selected user from the listbox and populates the value in the correct place in the editor. selectUser: function (username) { const mentions = this.extractMentions(); @@ -130,12 +198,16 @@ window.editorConfig = (body) => { if (!mentions) { return; } + const editor = this.$refs.editor; mentions.forEach(({ start, end }) => { if (this.isAtCursor(start, end)) { this.body = this.body.substring(0, start) + '@' + username + this.body.substring(end) + ' '; - this.$refs.editor.focus(); this.resetUserSearch(); + this.$nextTick(() => { + editor.focus(); + editor.setSelectionRange(start + username.length + 2, start + username.length + 2); + }) } }); }, @@ -160,6 +232,10 @@ window.editorConfig = (body) => { isAtCursor: function (start, end) { return this.cursorPosition() >= start && this.cursorPosition() <= end; }, + + isArrowKey: function (code) { + return code == 38 || code == 40; + } }; }; diff --git a/resources/views/livewire/editor.blade.php b/resources/views/livewire/editor.blade.php index 2460cd59d..f60c0cb4e 100644 --- a/resources/views/livewire/editor.blade.php +++ b/resources/views/livewire/editor.blade.php @@ -1,4 +1,4 @@ -
+
@if ($label) {{ $label }} @@ -44,6 +44,9 @@ class="w-full h-full absolute left-0 top-0 right-0 bottom-0 overflow-y-hidden re @keydown.cmd.enter="submit($event)" @keydown.ctrl.enter="submit($event)" @keydown.space="showMentions = false" + @keydown.down="highlightNextUser(event)" + @keydown.up="highlightPreviousUser(event)" + @keydown.enter="selectHighlightedUser(event)" @click.away="showMentions = false" @keydown.debounce.500ms="updateUserSearch($event)" > @@ -51,20 +54,26 @@ class="w-full h-full absolute left-0 top-0 right-0 bottom-0 overflow-y-hidden re @if ($users->count())
    @foreach ($users as $user) -
  • - + - - {{ $user->username() }} - + + {{ $user->username() }} +
  • @endforeach
From f36de718b3b9733ca5868edc3a257f37e0af9532 Mon Sep 17 00:00:00 2001 From: StyleCI Bot Date: Thu, 20 Jan 2022 21:54:19 +0000 Subject: [PATCH 08/24] Apply fixes from StyleCI --- resources/css/app.css | 2 +- resources/js/editor.js | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/resources/css/app.css b/resources/css/app.css index 869382f7d..87d09c26f 100644 --- a/resources/css/app.css +++ b/resources/css/app.css @@ -145,6 +145,6 @@ svg .secondary { @apply rounded-none border-b-2 border-gray-300 !important; } -.editor li[aria-selected="true"] { +.editor li[aria-selected='true'] { @apply bg-lio-100; } diff --git a/resources/js/editor.js b/resources/js/editor.js index 0382ea231..c3df36698 100644 --- a/resources/js/editor.js +++ b/resources/js/editor.js @@ -154,8 +154,8 @@ window.editorConfig = (body) => { event.preventDefault(); - highlighted.setAttribute('aria-selected', false) - next.setAttribute('aria-selected', true) + highlighted.setAttribute('aria-selected', false); + next.setAttribute('aria-selected', true); }, // Highlight the previous user in the listbox. @@ -174,8 +174,8 @@ window.editorConfig = (body) => { event.preventDefault(); - highlighted.setAttribute('aria-selected', false) - previous.setAttribute('aria-selected', true) + highlighted.setAttribute('aria-selected', false); + previous.setAttribute('aria-selected', true); }, // Take the selected user and put the name into the editor. @@ -207,7 +207,7 @@ window.editorConfig = (body) => { this.$nextTick(() => { editor.focus(); editor.setSelectionRange(start + username.length + 2, start + username.length + 2); - }) + }); } }); }, @@ -235,7 +235,7 @@ window.editorConfig = (body) => { isArrowKey: function (code) { return code == 38 || code == 40; - } + }, }; }; From b1320a4ff630f5ffec2982081a6164e7f360a537 Mon Sep 17 00:00:00 2001 From: Joe Dixon Date: Fri, 21 Jan 2022 09:23:55 +0000 Subject: [PATCH 09/24] Fix user loading --- app/Http/Livewire/Editor.php | 15 +++++++++------ resources/js/editor.js | 10 ++++++++++ resources/views/livewire/editor.blade.php | 1 + 3 files changed, 20 insertions(+), 6 deletions(-) diff --git a/app/Http/Livewire/Editor.php b/app/Http/Livewire/Editor.php index 3cd1562f1..b81ee8245 100644 --- a/app/Http/Livewire/Editor.php +++ b/app/Http/Livewire/Editor.php @@ -41,17 +41,20 @@ public function render() public function getUsers($search) { - if (! $search && $this->participants->isNotEmpty()) { + if (! $search) { return $this->users = $this->participants; } $search = Str::after($search, '@'); $users = User::where('username', 'like', "{$search}%")->take(5)->get(); - $users = $this->participants->filter(function ($participant) use ($search) { - return Str::startsWith($participant->username, $search); - }) - ->merge($users) - ->unique('id'); + + if($this->participants->isNotEmpty()) { + $users = $this->participants->filter(function ($participant) use ($search) { + return Str::startsWith($participant->username(), $search); + }) + ->merge($users) + ->unique('id'); + } return $this->users = $users; } diff --git a/resources/js/editor.js b/resources/js/editor.js index c3df36698..f7c8e93e8 100644 --- a/resources/js/editor.js +++ b/resources/js/editor.js @@ -89,6 +89,10 @@ window.editorConfig = (body) => { // Takes the user input, determines if a mention is active and initiates the search. updateUserSearch: function (event) { + if (this.isEscapeKey(event.keyCode)) { + return; + } + if (this.isArrowKey(event.keyCode)) { return; } @@ -233,9 +237,15 @@ window.editorConfig = (body) => { return this.cursorPosition() >= start && this.cursorPosition() <= end; }, + // Detects whether or not the given key code is an up or down arrow. isArrowKey: function (code) { return code == 38 || code == 40; }, + + // Detects whether or not the given key code is they escape key. + isEscapeKey: function (code) { + return code == 27; + } }; }; diff --git a/resources/views/livewire/editor.blade.php b/resources/views/livewire/editor.blade.php index f60c0cb4e..fb74d4669 100644 --- a/resources/views/livewire/editor.blade.php +++ b/resources/views/livewire/editor.blade.php @@ -47,6 +47,7 @@ class="w-full h-full absolute left-0 top-0 right-0 bottom-0 overflow-y-hidden re @keydown.down="highlightNextUser(event)" @keydown.up="highlightPreviousUser(event)" @keydown.enter="selectHighlightedUser(event)" + @keydown.escape="showMentions = false" @click.away="showMentions = false" @keydown.debounce.500ms="updateUserSearch($event)" > From ae2bc1ae2e0580260fff96d07f2826f6864938cd Mon Sep 17 00:00:00 2001 From: StyleCI Bot Date: Fri, 21 Jan 2022 09:24:13 +0000 Subject: [PATCH 10/24] Apply fixes from StyleCI --- app/Http/Livewire/Editor.php | 4 ++-- resources/js/editor.js | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/Http/Livewire/Editor.php b/app/Http/Livewire/Editor.php index b81ee8245..c272b19e7 100644 --- a/app/Http/Livewire/Editor.php +++ b/app/Http/Livewire/Editor.php @@ -47,8 +47,8 @@ public function getUsers($search) $search = Str::after($search, '@'); $users = User::where('username', 'like', "{$search}%")->take(5)->get(); - - if($this->participants->isNotEmpty()) { + + if ($this->participants->isNotEmpty()) { $users = $this->participants->filter(function ($participant) use ($search) { return Str::startsWith($participant->username(), $search); }) diff --git a/resources/js/editor.js b/resources/js/editor.js index f7c8e93e8..dea807142 100644 --- a/resources/js/editor.js +++ b/resources/js/editor.js @@ -245,7 +245,7 @@ window.editorConfig = (body) => { // Detects whether or not the given key code is they escape key. isEscapeKey: function (code) { return code == 27; - } + }, }; }; From 8dda827a9629b9abea00c0379602a695eef5885b Mon Sep 17 00:00:00 2001 From: Joe Dixon Date: Sun, 23 Jan 2022 11:54:15 +0000 Subject: [PATCH 11/24] Make mentions optional --- app/Http/Livewire/Editor.php | 6 ++++++ resources/js/editor.js | 7 ++++++- resources/views/components/threads/form.blade.php | 1 + resources/views/forum/threads/show.blade.php | 1 + resources/views/livewire/editor.blade.php | 2 +- 5 files changed, 15 insertions(+), 2 deletions(-) diff --git a/app/Http/Livewire/Editor.php b/app/Http/Livewire/Editor.php index c272b19e7..7048c4cbd 100644 --- a/app/Http/Livewire/Editor.php +++ b/app/Http/Livewire/Editor.php @@ -22,6 +22,8 @@ class Editor extends Component public $buttonIcon; + public $hasMentions = false; + public $users; public $participants; @@ -41,6 +43,10 @@ public function render() public function getUsers($search) { + if (! $this->hasMentions) { + return $this->users; + } + if (! $search) { return $this->users = $this->participants; } diff --git a/resources/js/editor.js b/resources/js/editor.js index dea807142..be4b9722d 100644 --- a/resources/js/editor.js +++ b/resources/js/editor.js @@ -33,7 +33,7 @@ const insertCharactersAtPosition = (string, character, position) => { }; // Configuration object for the text editor. -window.editorConfig = (body) => { +window.editorConfig = (body, hasMentions) => { return { styles: { header: { @@ -66,6 +66,7 @@ window.editorConfig = (body) => { cursorTop: 0, cursorLeft: 0, body: body, + hasMentions: hasMentions, mode: 'write', showMentions: false, search: '', @@ -89,6 +90,10 @@ window.editorConfig = (body) => { // Takes the user input, determines if a mention is active and initiates the search. updateUserSearch: function (event) { + if (!this.hasMentions) { + return; + } + if (this.isEscapeKey(event.keyCode)) { return; } diff --git a/resources/views/components/threads/form.blade.php b/resources/views/components/threads/form.blade.php index fca4fe9d7..1d2f54a2a 100644 --- a/resources/views/components/threads/form.blade.php +++ b/resources/views/components/threads/form.blade.php @@ -53,6 +53,7 @@ @endif -
+
  • From 0a067e3db1648fa0e08230c4dffbee23e8d653ac Mon Sep 17 00:00:00 2001 From: Joe Dixon Date: Sun, 23 Jan 2022 11:57:50 +0000 Subject: [PATCH 12/24] Tidy component --- app/Http/Livewire/Editor.php | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/app/Http/Livewire/Editor.php b/app/Http/Livewire/Editor.php index 7048c4cbd..4ffec5b18 100644 --- a/app/Http/Livewire/Editor.php +++ b/app/Http/Livewire/Editor.php @@ -3,6 +3,7 @@ namespace App\Http\Livewire; use App\Models\User; +use Illuminate\Support\Collection; use Illuminate\Support\Str; use Livewire\Component; @@ -41,22 +42,22 @@ public function render() return view('livewire.editor'); } - public function getUsers($search) + public function getUsers($query): Collection { if (! $this->hasMentions) { return $this->users; } - - if (! $search) { + + if (! $query) { return $this->users = $this->participants; } - $search = Str::after($search, '@'); - $users = User::where('username', 'like', "{$search}%")->take(5)->get(); + $query = Str::after($query, '@'); + $users = User::where('username', 'like', "{$query}%")->take(5)->get(); if ($this->participants->isNotEmpty()) { - $users = $this->participants->filter(function ($participant) use ($search) { - return Str::startsWith($participant->username(), $search); + $users = $this->participants->filter(function ($participant) use ($query) { + return Str::startsWith($participant->username(), $query); }) ->merge($users) ->unique('id'); @@ -65,12 +66,12 @@ public function getUsers($search) return $this->users = $users; } - public function getPreviewProperty() + public function getPreviewProperty(): string { return replace_links(md_to_html($this->body ?: '')); } - public function preview() + public function preview(): void { $this->emit('previewRequested'); } From 94baeadc0c79df56d156cd8e3221d58858e550ee Mon Sep 17 00:00:00 2001 From: Joe Dixon Date: Sun, 23 Jan 2022 12:07:09 +0000 Subject: [PATCH 13/24] Match backend regex --- resources/js/editor.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/resources/js/editor.js b/resources/js/editor.js index be4b9722d..617353188 100644 --- a/resources/js/editor.js +++ b/resources/js/editor.js @@ -67,8 +67,9 @@ window.editorConfig = (body, hasMentions) => { cursorLeft: 0, body: body, hasMentions: hasMentions, - mode: 'write', showMentions: false, + mentionRegex: /@[a-z\d]*(?:[a-z\d]|-(?=[a-z\d])){0,38}(?!\w)/g, + mode: 'write', search: '', // Gets the current cursor position. @@ -223,10 +224,9 @@ window.editorConfig = (body, hasMentions) => { // Extracts all the mentions from the input along with their start and end position in the string. extractMentions: function () { - const mentionRegex = /@[\w\d]*/g; let mention; let mentions = []; - while ((mention = mentionRegex.exec(this.body)) !== null) { + while ((mention = this.mentionRegex.exec(this.body)) !== null) { mentions.push({ mention: mention[0], start: mention.index, From 9a3416bf587854937d7c4430897cb68948100d6b Mon Sep 17 00:00:00 2001 From: Joe Dixon Date: Sun, 23 Jan 2022 13:24:08 +0000 Subject: [PATCH 14/24] wip --- app/Concerns/HasMentions.php | 17 ++++++++ app/Events/ThreadWasCreated.php | 16 +++++++ app/Jobs/CreateThread.php | 7 ++- app/Listeners/NotifyMentionedUsers.php | 18 ++++++++ .../SubscribeMentionedUsersToThread.php | 28 ++++++++++++ app/Mail/MentionEmail.php | 23 ++++++++++ app/Models/MentionAble.php | 14 ++++++ app/Models/Reply.php | 4 +- app/Models/Thread.php | 11 ++++- app/Notifications/MentionNotification.php | 43 +++++++++++++++++++ app/Providers/EventServiceProvider.php | 7 +++ resources/views/emails/mention.blade.php | 13 ++++++ 12 files changed, 197 insertions(+), 4 deletions(-) create mode 100644 app/Concerns/HasMentions.php create mode 100644 app/Events/ThreadWasCreated.php create mode 100644 app/Listeners/NotifyMentionedUsers.php create mode 100644 app/Listeners/SubscribeMentionedUsersToThread.php create mode 100644 app/Mail/MentionEmail.php create mode 100644 app/Models/MentionAble.php create mode 100644 app/Notifications/MentionNotification.php create mode 100644 resources/views/emails/mention.blade.php diff --git a/app/Concerns/HasMentions.php b/app/Concerns/HasMentions.php new file mode 100644 index 000000000..a0be70808 --- /dev/null +++ b/app/Concerns/HasMentions.php @@ -0,0 +1,17 @@ +replyAble(); + } +} diff --git a/app/Events/ThreadWasCreated.php b/app/Events/ThreadWasCreated.php new file mode 100644 index 000000000..f976c650e --- /dev/null +++ b/app/Events/ThreadWasCreated.php @@ -0,0 +1,16 @@ +uuid = Uuid::uuid4()->toString(); $subscription->userRelation()->associate($this->author); $subscription->subscriptionAbleRelation()->associate($thread); - + $thread->subscriptionsRelation()->save($subscription); - + + event(new ThreadWasCreated($thread)); + return $thread; } } diff --git a/app/Listeners/NotifyMentionedUsers.php b/app/Listeners/NotifyMentionedUsers.php new file mode 100644 index 000000000..2d49bbf45 --- /dev/null +++ b/app/Listeners/NotifyMentionedUsers.php @@ -0,0 +1,18 @@ +thread->getMentionedUsers()->each(function ($user) use ($event) { + $user->notify(new MentionNotification($event->thread)); + }); + } +} diff --git a/app/Listeners/SubscribeMentionedUsersToThread.php b/app/Listeners/SubscribeMentionedUsersToThread.php new file mode 100644 index 000000000..9f40cfa82 --- /dev/null +++ b/app/Listeners/SubscribeMentionedUsersToThread.php @@ -0,0 +1,28 @@ +thread->getMentionedUsers()->each(function ($user) use ($event) { + if($event->thread->hasSubscriber($user)) { + return; + } + + $subscription = new Subscription(); + $subscription->uuid = Uuid::uuid4()->toString(); + $subscription->userRelation()->associate($user); + $subscription->subscriptionAbleRelation()->associate($event->thread); + + $event->thread->subscriptionsRelation()->save($subscription); + }); + } +} diff --git a/app/Mail/MentionEmail.php b/app/Mail/MentionEmail.php new file mode 100644 index 000000000..a6a670c5a --- /dev/null +++ b/app/Mail/MentionEmail.php @@ -0,0 +1,23 @@ +subject("Mentioned: {$this->mentionAble->mentionedOn()->subject()}") + ->markdown('emails.mention'); + } +} diff --git a/app/Models/MentionAble.php b/app/Models/MentionAble.php new file mode 100644 index 000000000..edd60a09a --- /dev/null +++ b/app/Models/MentionAble.php @@ -0,0 +1,14 @@ +prepend($this->author()) ->unique(); } + + public function getMentionedUsers(): SupportCollection + { + preg_match_all('/@([a-z\d](?:[a-z\d]|-(?=[a-z\d])){0,38}(?!\w))/', $this->body, $matches); + + return User::whereIn('username', $matches[1])->get(); + } } diff --git a/app/Notifications/MentionNotification.php b/app/Notifications/MentionNotification.php new file mode 100644 index 000000000..222c605b7 --- /dev/null +++ b/app/Notifications/MentionNotification.php @@ -0,0 +1,43 @@ +mentionAble, $user)) + ->to($user->emailAddress(), $user->name()); + } + + public function toDatabase(User $user) + { + return [ + 'type' => 'mention', + // 'reply' => $this->reply->id(), + // 'replyable_id' => $this->reply->replyable_id, + // 'replyable_type' => $this->reply->replyable_type, + // 'replyable_subject' => $this->reply->replyAble()->replyAbleSubject(), + ]; + } +} diff --git a/app/Providers/EventServiceProvider.php b/app/Providers/EventServiceProvider.php index 28d79a29c..c7dea410a 100644 --- a/app/Providers/EventServiceProvider.php +++ b/app/Providers/EventServiceProvider.php @@ -5,11 +5,14 @@ use App\Events\ArticleWasApproved; use App\Events\ArticleWasSubmittedForApproval; use App\Events\ReplyWasCreated; +use App\Events\ThreadWasCreated; use App\Listeners\MarkLastActivity; +use App\Listeners\NotifyMentionedUsers; use App\Listeners\SendArticleApprovedNotification; use App\Listeners\SendNewArticleNotification; use App\Listeners\SendNewReplyNotification; use App\Listeners\StoreTweetIdentifier; +use App\Listeners\SubscribeMentionedUsersToThread; use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider; use Illuminate\Notifications\Events\NotificationSent; @@ -21,6 +24,10 @@ class EventServiceProvider extends ServiceProvider * @var array */ protected $listen = [ + ThreadWasCreated::class => [ + SubscribeMentionedUsersToThread::class, + NotifyMentionedUsers::class, + ], ReplyWasCreated::class => [ MarkLastActivity::class, SendNewReplyNotification::class, diff --git a/resources/views/emails/mention.blade.php b/resources/views/emails/mention.blade.php new file mode 100644 index 000000000..f8e0cb0e7 --- /dev/null +++ b/resources/views/emails/mention.blade.php @@ -0,0 +1,13 @@ +@component('mail::message') + +**{{ $mentionAble->author()->username() }}** has mentioned you on this thread. + +@component('mail::panel') +{{ $mentionAble->excerpt(200) }} +@endcomponent + +@component('mail::button', ['url' => route('thread', $mentionAble->mentionedOn()->slug())]) +View Thread +@endcomponent + +@endcomponent From c6469921b7be43ece43d03d856a6b62656c42110 Mon Sep 17 00:00:00 2001 From: StyleCI Bot Date: Sun, 23 Jan 2022 13:24:28 +0000 Subject: [PATCH 15/24] Apply fixes from StyleCI --- app/Jobs/CreateThread.php | 6 +++--- app/Listeners/SubscribeMentionedUsersToThread.php | 2 +- app/Mail/MentionEmail.php | 1 - app/Notifications/MentionNotification.php | 1 - 4 files changed, 4 insertions(+), 6 deletions(-) diff --git a/app/Jobs/CreateThread.php b/app/Jobs/CreateThread.php index b742d68bc..9710e6970 100644 --- a/app/Jobs/CreateThread.php +++ b/app/Jobs/CreateThread.php @@ -46,11 +46,11 @@ public function handle(): Thread $subscription->uuid = Uuid::uuid4()->toString(); $subscription->userRelation()->associate($this->author); $subscription->subscriptionAbleRelation()->associate($thread); - + $thread->subscriptionsRelation()->save($subscription); - + event(new ThreadWasCreated($thread)); - + return $thread; } } diff --git a/app/Listeners/SubscribeMentionedUsersToThread.php b/app/Listeners/SubscribeMentionedUsersToThread.php index 9f40cfa82..92b6eaa35 100644 --- a/app/Listeners/SubscribeMentionedUsersToThread.php +++ b/app/Listeners/SubscribeMentionedUsersToThread.php @@ -13,7 +13,7 @@ final class SubscribeMentionedUsersToThread public function handle(ThreadWasCreated $event): void { $event->thread->getMentionedUsers()->each(function ($user) use ($event) { - if($event->thread->hasSubscriber($user)) { + if ($event->thread->hasSubscriber($user)) { return; } diff --git a/app/Mail/MentionEmail.php b/app/Mail/MentionEmail.php index a6a670c5a..e333f969b 100644 --- a/app/Mail/MentionEmail.php +++ b/app/Mail/MentionEmail.php @@ -12,7 +12,6 @@ public function __construct( public MentionAble $mentionAble, public User $receiver ) { - } public function build() diff --git a/app/Notifications/MentionNotification.php b/app/Notifications/MentionNotification.php index 222c605b7..64e13e6e7 100644 --- a/app/Notifications/MentionNotification.php +++ b/app/Notifications/MentionNotification.php @@ -3,7 +3,6 @@ namespace App\Notifications; use App\Mail\MentionEmail; -use App\Mail\NewReplyEmail; use App\Models\MentionAble; use App\Models\User; use Illuminate\Bus\Queueable; From 056afe3c94f103b2d57a6e7f02ced59f077ea28d Mon Sep 17 00:00:00 2001 From: Joe Dixon Date: Sun, 23 Jan 2022 20:03:24 +0000 Subject: [PATCH 16/24] Handle mentions --- app/Concerns/HasMentions.php | 11 +++++++- app/Concerns/ProvidesSubscriptions.php | 11 ++++++++ app/Listeners/NotifyUsersMentionedInReply.php | 18 ++++++++++++ ...s.php => NotifyUsersMentionedInThread.php} | 2 +- .../SubscribeMentionedUsersToThread.php | 28 ------------------- .../SubscribeUsersMentionedInReply.php | 28 +++++++++++++++++++ .../SubscribeUsersMentionedInThread.php | 21 ++++++++++++++ app/Mail/MentionEmail.php | 2 +- app/Models/MentionAble.php | 6 +++- app/Models/Thread.php | 7 ----- app/Notifications/MentionNotification.php | 10 ++++--- app/Providers/EventServiceProvider.php | 12 +++++--- resources/views/emails/mention.blade.php | 2 +- .../views/notifications/mention.blade.php | 25 +++++++++++++++++ 14 files changed, 135 insertions(+), 48 deletions(-) create mode 100644 app/Listeners/NotifyUsersMentionedInReply.php rename app/Listeners/{NotifyMentionedUsers.php => NotifyUsersMentionedInThread.php} (90%) delete mode 100644 app/Listeners/SubscribeMentionedUsersToThread.php create mode 100644 app/Listeners/SubscribeUsersMentionedInReply.php create mode 100644 app/Listeners/SubscribeUsersMentionedInThread.php create mode 100644 resources/views/notifications/mention.blade.php diff --git a/app/Concerns/HasMentions.php b/app/Concerns/HasMentions.php index a0be70808..9cbb5aec9 100644 --- a/app/Concerns/HasMentions.php +++ b/app/Concerns/HasMentions.php @@ -3,10 +3,12 @@ namespace App\Concerns; use App\Models\ReplyAble; +use App\Models\User; +use Illuminate\Support\Collection; trait HasMentions { - public function mentionedOn(): ReplyAble + public function mentionedIn(): ReplyAble { if ($this instanceof ReplyAble) { return $this; @@ -14,4 +16,11 @@ public function mentionedOn(): ReplyAble return $this->replyAble(); } + + public function getMentionedUsers(): Collection + { + preg_match_all('/@([a-z\d](?:[a-z\d]|-(?=[a-z\d])){0,38}(?!\w))/', $this->body(), $matches); + + return User::whereIn('username', $matches[1])->get(); + } } diff --git a/app/Concerns/ProvidesSubscriptions.php b/app/Concerns/ProvidesSubscriptions.php index ad51a77b7..cb6c8b09a 100644 --- a/app/Concerns/ProvidesSubscriptions.php +++ b/app/Concerns/ProvidesSubscriptions.php @@ -5,6 +5,7 @@ use App\Models\Subscription; use App\Models\User; use Illuminate\Database\Eloquent\Relations\MorphMany; +use Ramsey\Uuid\Uuid; trait ProvidesSubscriptions { @@ -38,4 +39,14 @@ public function hasSubscriber(User $user): bool ->where('user_id', $user->id()) ->exists(); } + + public function subscribe(User $user): Subscription + { + $subscription = new Subscription(); + $subscription->uuid = Uuid::uuid4()->toString(); + $subscription->userRelation()->associate($user); + $subscription->subscriptionAbleRelation()->associate($this); + + return $this->subscriptionsRelation()->save($subscription); + } } diff --git a/app/Listeners/NotifyUsersMentionedInReply.php b/app/Listeners/NotifyUsersMentionedInReply.php new file mode 100644 index 000000000..c1bdb1eb9 --- /dev/null +++ b/app/Listeners/NotifyUsersMentionedInReply.php @@ -0,0 +1,18 @@ +reply->getMentionedUsers()->each(function ($user) use ($event) { + $user->notify(new MentionNotification($event->reply)); + }); + } +} diff --git a/app/Listeners/NotifyMentionedUsers.php b/app/Listeners/NotifyUsersMentionedInThread.php similarity index 90% rename from app/Listeners/NotifyMentionedUsers.php rename to app/Listeners/NotifyUsersMentionedInThread.php index 2d49bbf45..e04ab45c4 100644 --- a/app/Listeners/NotifyMentionedUsers.php +++ b/app/Listeners/NotifyUsersMentionedInThread.php @@ -7,7 +7,7 @@ use App\Events\ThreadWasCreated; use App\Notifications\MentionNotification; -final class NotifyMentionedUsers +final class NotifyUsersMentionedInThread { public function handle(ThreadWasCreated $event): void { diff --git a/app/Listeners/SubscribeMentionedUsersToThread.php b/app/Listeners/SubscribeMentionedUsersToThread.php deleted file mode 100644 index 92b6eaa35..000000000 --- a/app/Listeners/SubscribeMentionedUsersToThread.php +++ /dev/null @@ -1,28 +0,0 @@ -thread->getMentionedUsers()->each(function ($user) use ($event) { - if ($event->thread->hasSubscriber($user)) { - return; - } - - $subscription = new Subscription(); - $subscription->uuid = Uuid::uuid4()->toString(); - $subscription->userRelation()->associate($user); - $subscription->subscriptionAbleRelation()->associate($event->thread); - - $event->thread->subscriptionsRelation()->save($subscription); - }); - } -} diff --git a/app/Listeners/SubscribeUsersMentionedInReply.php b/app/Listeners/SubscribeUsersMentionedInReply.php new file mode 100644 index 000000000..3018b49b3 --- /dev/null +++ b/app/Listeners/SubscribeUsersMentionedInReply.php @@ -0,0 +1,28 @@ +reply->getMentionedUsers()->each(function ($user) use ($event) { + $replyAble = $event->reply->mentionedIn(); + + if (! $replyAble instanceof Thread) { + return; + } + + if($replyAble->hasSubscriber($user)) { + return; + } + + $replyAble->subscribe($user); + }); + } +} diff --git a/app/Listeners/SubscribeUsersMentionedInThread.php b/app/Listeners/SubscribeUsersMentionedInThread.php new file mode 100644 index 000000000..7b60aee27 --- /dev/null +++ b/app/Listeners/SubscribeUsersMentionedInThread.php @@ -0,0 +1,21 @@ +thread->getMentionedUsers()->each(function ($user) use ($event) { + if($event->thread->hasSubscriber($user)) { + return; + } + + $event->thread->subscribe($user); + }); + } +} diff --git a/app/Mail/MentionEmail.php b/app/Mail/MentionEmail.php index e333f969b..ee816f6a0 100644 --- a/app/Mail/MentionEmail.php +++ b/app/Mail/MentionEmail.php @@ -16,7 +16,7 @@ public function __construct( public function build() { - return $this->subject("Mentioned: {$this->mentionAble->mentionedOn()->subject()}") + return $this->subject("Mentioned: {$this->mentionAble->mentionedIn()->subject()}") ->markdown('emails.mention'); } } diff --git a/app/Models/MentionAble.php b/app/Models/MentionAble.php index edd60a09a..e7b7ae1c8 100644 --- a/app/Models/MentionAble.php +++ b/app/Models/MentionAble.php @@ -2,6 +2,8 @@ namespace App\Models; +use Illuminate\Support\Collection; + interface MentionAble { public function body(): string; @@ -10,5 +12,7 @@ public function excerpt(): string; public function author(): User; - public function mentionedOn(): ReplyAble; + public function mentionedIn(): ReplyAble; + + public function getMentionedUsers(): Collection; } diff --git a/app/Models/Thread.php b/app/Models/Thread.php index 8033a90db..532f51fdf 100644 --- a/app/Models/Thread.php +++ b/app/Models/Thread.php @@ -350,11 +350,4 @@ public function participants(): SupportCollection ->prepend($this->author()) ->unique(); } - - public function getMentionedUsers(): SupportCollection - { - preg_match_all('/@([a-z\d](?:[a-z\d]|-(?=[a-z\d])){0,38}(?!\w))/', $this->body, $matches); - - return User::whereIn('username', $matches[1])->get(); - } } diff --git a/app/Notifications/MentionNotification.php b/app/Notifications/MentionNotification.php index 64e13e6e7..3302de621 100644 --- a/app/Notifications/MentionNotification.php +++ b/app/Notifications/MentionNotification.php @@ -7,6 +7,7 @@ use App\Models\User; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; +use Illuminate\Database\Eloquent\Relations\Relation; use Illuminate\Notifications\Notification; final class MentionNotification extends Notification implements ShouldQueue @@ -31,12 +32,13 @@ public function toMail(User $user) public function toDatabase(User $user) { + $replyAble = $this->mentionAble->mentionedIn(); + return [ 'type' => 'mention', - // 'reply' => $this->reply->id(), - // 'replyable_id' => $this->reply->replyable_id, - // 'replyable_type' => $this->reply->replyable_type, - // 'replyable_subject' => $this->reply->replyAble()->replyAbleSubject(), + 'replyable_id' => $replyAble->id, + 'replyable_type' => array_search(get_class($replyAble), Relation::morphMap()), + 'replyable_subject' => $this->mentionAble->mentionedIn()->subject(), ]; } } diff --git a/app/Providers/EventServiceProvider.php b/app/Providers/EventServiceProvider.php index c7dea410a..817895a74 100644 --- a/app/Providers/EventServiceProvider.php +++ b/app/Providers/EventServiceProvider.php @@ -7,12 +7,14 @@ use App\Events\ReplyWasCreated; use App\Events\ThreadWasCreated; use App\Listeners\MarkLastActivity; -use App\Listeners\NotifyMentionedUsers; +use App\Listeners\NotifyUsersMentionedInReply; +use App\Listeners\NotifyUsersMentionedInThread; use App\Listeners\SendArticleApprovedNotification; use App\Listeners\SendNewArticleNotification; use App\Listeners\SendNewReplyNotification; use App\Listeners\StoreTweetIdentifier; -use App\Listeners\SubscribeMentionedUsersToThread; +use App\Listeners\SubscribeUsersMentionedInReply; +use App\Listeners\SubscribeUsersMentionedInThread; use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider; use Illuminate\Notifications\Events\NotificationSent; @@ -25,12 +27,14 @@ class EventServiceProvider extends ServiceProvider */ protected $listen = [ ThreadWasCreated::class => [ - SubscribeMentionedUsersToThread::class, - NotifyMentionedUsers::class, + SubscribeUsersMentionedInThread::class, + NotifyUsersMentionedInThread::class, ], ReplyWasCreated::class => [ MarkLastActivity::class, SendNewReplyNotification::class, + SubscribeUsersMentionedInReply::class, + NotifyUsersMentionedInReply::class, ], ArticleWasSubmittedForApproval::class => [ SendNewArticleNotification::class, diff --git a/resources/views/emails/mention.blade.php b/resources/views/emails/mention.blade.php index f8e0cb0e7..d7ed5fb5e 100644 --- a/resources/views/emails/mention.blade.php +++ b/resources/views/emails/mention.blade.php @@ -6,7 +6,7 @@ {{ $mentionAble->excerpt(200) }} @endcomponent -@component('mail::button', ['url' => route('thread', $mentionAble->mentionedOn()->slug())]) +@component('mail::button', ['url' => route('thread', $mentionAble->mentionedIn()->slug())]) View Thread @endcomponent diff --git a/resources/views/notifications/mention.blade.php b/resources/views/notifications/mention.blade.php new file mode 100644 index 000000000..138ea5c81 --- /dev/null +++ b/resources/views/notifications/mention.blade.php @@ -0,0 +1,25 @@ +@php($data = $notification->data) + + + +
    + + +
    + You were mentioned in "{{ $data['replyable_subject'] }}". +
    +
    + + + + {{ $notification->created_at->diffForHumans() }} + + + +
    + +
    + + From b0601ddf7d73495b05e5ec3e8f8b09f7e7603de2 Mon Sep 17 00:00:00 2001 From: Joe Dixon Date: Sun, 23 Jan 2022 20:54:06 +0000 Subject: [PATCH 17/24] Stub tests --- tests/Feature/EditorTest.php | 31 +++++++++++++++++++ tests/Feature/ForumTest.php | 47 +++++++++++++++++++++++++++++ tests/Feature/ReplyTest.php | 18 +++++++++++ tests/Feature/SubscriptionsTest.php | 12 ++++++++ 4 files changed, 108 insertions(+) create mode 100644 tests/Feature/EditorTest.php diff --git a/tests/Feature/EditorTest.php b/tests/Feature/EditorTest.php new file mode 100644 index 000000000..205fb4359 --- /dev/null +++ b/tests/Feature/EditorTest.php @@ -0,0 +1,31 @@ +markTestIncomplete( + 'This test has not been implemented yet.' + ); +}); + +test('users are returned when a query is made for mentions', function () { + $this->markTestIncomplete( + 'This test has not been implemented yet.' + ); +}); + +test('participants are prioritised over users', function () { + $this->markTestIncomplete( + 'This test has not been implemented yet.' + ); +}); + +test('users are not queried when hasMentions is turned off', function () { + $this->markTestIncomplete( + 'This test has not been implemented yet.' + ); +}); \ No newline at end of file diff --git a/tests/Feature/ForumTest.php b/tests/Feature/ForumTest.php index 8559e68aa..3020c96d0 100644 --- a/tests/Feature/ForumTest.php +++ b/tests/Feature/ForumTest.php @@ -2,10 +2,16 @@ use App\Http\Livewire\LikeReply; use App\Http\Livewire\LikeThread; +use App\Mail\MentionEmail; use App\Models\Reply; use App\Models\Tag; use App\Models\Thread; +use App\Models\User; +use App\Notifications\MentionNotification; use Illuminate\Foundation\Testing\DatabaseMigrations; +use Illuminate\Notifications\DatabaseNotification; +use Illuminate\Support\Facades\Mail; +use Illuminate\Support\Facades\Notification; use Livewire\Livewire; use Tests\Feature\BrowserKitTestCase; @@ -240,3 +246,44 @@ $this->assertNotNull(Thread::latest('id')->first()->last_activity_at); }); + +test('users are notified by email when mentioned in a thread body', function () { + Notification::fake(); + $user = User::factory()->create(['username' => 'janedoe', 'email' => 'janedoe@example.com']); + + $this->login(); + + $this->post('/forum/create-thread', [ + 'subject' => 'How to work with Eloquent?', + 'body' => 'Hey @janedoe', + 'tags' => [], + ]); + + Notification::assertSentTo($user, MentionNotification::class, function ($notification) use ($user) { + return $notification->toMail($user) instanceof MentionEmail; + }); +}); + +test('users provided with a UI notification when mentioned in a thread body', function () { + $user = User::factory()->create(['username' => 'janedoe']); + + $this->login(); + + $this->post('/forum/create-thread', [ + 'subject' => 'How to work with Eloquent?', + 'body' => 'Hey @janedoe', + 'tags' => [], + ]); + + $notification = DatabaseNotification::first(); + $this->assertSame($user->id, (int) $notification->notifiable_id); + $this->assertSame('users', $notification->notifiable_type); + $this->assertSame('mention', $notification->data['type']); + $this->assertSame('How to work with Eloquent?', $notification->data['replyable_subject']); +}); + +test('users are not notified when mentioned in and edited thread', function () { + $this->markTestIncomplete( + 'This test has not been implemented yet.' + ); +}); diff --git a/tests/Feature/ReplyTest.php b/tests/Feature/ReplyTest.php index f8e57a38b..ab7822382 100644 --- a/tests/Feature/ReplyTest.php +++ b/tests/Feature/ReplyTest.php @@ -137,3 +137,21 @@ $this->assertSame('1970-01-01', $thread->fresh()->updated_at->format('Y-m-d')); }); + +test('users are notified by email when mentioned in a reply body', function () { + $this->markTestIncomplete( + 'This test has not been implemented yet.' + ); +}); + +test('users provided with a UI notification when mentioned in a reply body', function () { + $this->markTestIncomplete( + 'This test has not been implemented yet.' + ); +}); + +test('users are not notified when mentioned in an edited reply', function () { + $this->markTestIncomplete( + 'This test has not been implemented yet.' + ); +}); diff --git a/tests/Feature/SubscriptionsTest.php b/tests/Feature/SubscriptionsTest.php index 70ad27bd5..ed2f855ff 100644 --- a/tests/Feature/SubscriptionsTest.php +++ b/tests/Feature/SubscriptionsTest.php @@ -102,3 +102,15 @@ $this->notSeeInDatabase('subscriptions', ['uuid' => $subscription->uuid()]); }); + +test('users are subscribed to a thread when mentioned', function () { + $this->markTestIncomplete( + 'This test has not been implemented yet.' + ); +}); + +test('users are subscribed to a thread when mentioned in a reply', function () { + $this->markTestIncomplete( + 'This test has not been implemented yet.' + ); +}); From 373d9f8e8cf10b6d2022ce7d275e40f31a03917e Mon Sep 17 00:00:00 2001 From: StyleCI Bot Date: Sun, 23 Jan 2022 20:54:27 +0000 Subject: [PATCH 18/24] Apply fixes from StyleCI --- app/Listeners/SubscribeUsersMentionedInReply.php | 2 +- app/Listeners/SubscribeUsersMentionedInThread.php | 2 +- tests/Feature/EditorTest.php | 10 +++++----- tests/Feature/ForumTest.php | 3 +-- tests/Feature/ReplyTest.php | 6 +++--- tests/Feature/SubscriptionsTest.php | 4 ++-- 6 files changed, 13 insertions(+), 14 deletions(-) diff --git a/app/Listeners/SubscribeUsersMentionedInReply.php b/app/Listeners/SubscribeUsersMentionedInReply.php index 3018b49b3..9c6578060 100644 --- a/app/Listeners/SubscribeUsersMentionedInReply.php +++ b/app/Listeners/SubscribeUsersMentionedInReply.php @@ -18,7 +18,7 @@ public function handle(ReplyWasCreated $event): void return; } - if($replyAble->hasSubscriber($user)) { + if ($replyAble->hasSubscriber($user)) { return; } diff --git a/app/Listeners/SubscribeUsersMentionedInThread.php b/app/Listeners/SubscribeUsersMentionedInThread.php index 7b60aee27..a2fa6c62c 100644 --- a/app/Listeners/SubscribeUsersMentionedInThread.php +++ b/app/Listeners/SubscribeUsersMentionedInThread.php @@ -11,7 +11,7 @@ final class SubscribeUsersMentionedInThread public function handle(ThreadWasCreated $event): void { $event->thread->getMentionedUsers()->each(function ($user) use ($event) { - if($event->thread->hasSubscriber($user)) { + if ($event->thread->hasSubscriber($user)) { return; } diff --git a/tests/Feature/EditorTest.php b/tests/Feature/EditorTest.php index 205fb4359..7afbc219e 100644 --- a/tests/Feature/EditorTest.php +++ b/tests/Feature/EditorTest.php @@ -8,24 +8,24 @@ test('participants are rendered when mentions are invoked', function () { $this->markTestIncomplete( - 'This test has not been implemented yet.' + 'This test has not been implemented yet.', ); }); test('users are returned when a query is made for mentions', function () { $this->markTestIncomplete( - 'This test has not been implemented yet.' + 'This test has not been implemented yet.', ); }); test('participants are prioritised over users', function () { $this->markTestIncomplete( - 'This test has not been implemented yet.' + 'This test has not been implemented yet.', ); }); test('users are not queried when hasMentions is turned off', function () { $this->markTestIncomplete( - 'This test has not been implemented yet.' + 'This test has not been implemented yet.', ); -}); \ No newline at end of file +}); diff --git a/tests/Feature/ForumTest.php b/tests/Feature/ForumTest.php index 3020c96d0..14bf07330 100644 --- a/tests/Feature/ForumTest.php +++ b/tests/Feature/ForumTest.php @@ -10,7 +10,6 @@ use App\Notifications\MentionNotification; use Illuminate\Foundation\Testing\DatabaseMigrations; use Illuminate\Notifications\DatabaseNotification; -use Illuminate\Support\Facades\Mail; use Illuminate\Support\Facades\Notification; use Livewire\Livewire; use Tests\Feature\BrowserKitTestCase; @@ -284,6 +283,6 @@ test('users are not notified when mentioned in and edited thread', function () { $this->markTestIncomplete( - 'This test has not been implemented yet.' + 'This test has not been implemented yet.', ); }); diff --git a/tests/Feature/ReplyTest.php b/tests/Feature/ReplyTest.php index ab7822382..0462ff8f8 100644 --- a/tests/Feature/ReplyTest.php +++ b/tests/Feature/ReplyTest.php @@ -140,18 +140,18 @@ test('users are notified by email when mentioned in a reply body', function () { $this->markTestIncomplete( - 'This test has not been implemented yet.' + 'This test has not been implemented yet.', ); }); test('users provided with a UI notification when mentioned in a reply body', function () { $this->markTestIncomplete( - 'This test has not been implemented yet.' + 'This test has not been implemented yet.', ); }); test('users are not notified when mentioned in an edited reply', function () { $this->markTestIncomplete( - 'This test has not been implemented yet.' + 'This test has not been implemented yet.', ); }); diff --git a/tests/Feature/SubscriptionsTest.php b/tests/Feature/SubscriptionsTest.php index ed2f855ff..f1bc582a0 100644 --- a/tests/Feature/SubscriptionsTest.php +++ b/tests/Feature/SubscriptionsTest.php @@ -105,12 +105,12 @@ test('users are subscribed to a thread when mentioned', function () { $this->markTestIncomplete( - 'This test has not been implemented yet.' + 'This test has not been implemented yet.', ); }); test('users are subscribed to a thread when mentioned in a reply', function () { $this->markTestIncomplete( - 'This test has not been implemented yet.' + 'This test has not been implemented yet.', ); }); From e620e3fb10565f094fb628c04aff3c5899ddfd74 Mon Sep 17 00:00:00 2001 From: Joe Dixon Date: Wed, 26 Jan 2022 21:25:58 +0000 Subject: [PATCH 19/24] Add tests --- app/Http/Livewire/Editor.php | 2 +- tests/Feature/EditorTest.php | 72 +++++++++++++++++++++++------ tests/Feature/ForumTest.php | 20 ++++++-- tests/Feature/ReplyTest.php | 57 +++++++++++++++++++---- tests/Feature/SubscriptionsTest.php | 29 +++++++++--- 5 files changed, 147 insertions(+), 33 deletions(-) diff --git a/app/Http/Livewire/Editor.php b/app/Http/Livewire/Editor.php index 4ffec5b18..de14aa31c 100644 --- a/app/Http/Livewire/Editor.php +++ b/app/Http/Livewire/Editor.php @@ -57,7 +57,7 @@ public function getUsers($query): Collection if ($this->participants->isNotEmpty()) { $users = $this->participants->filter(function ($participant) use ($query) { - return Str::startsWith($participant->username(), $query); + return Str::startsWith($participant['username'], $query); }) ->merge($users) ->unique('id'); diff --git a/tests/Feature/EditorTest.php b/tests/Feature/EditorTest.php index 7afbc219e..608fec175 100644 --- a/tests/Feature/EditorTest.php +++ b/tests/Feature/EditorTest.php @@ -1,31 +1,75 @@ markTestIncomplete( - 'This test has not been implemented yet.', - ); + $participants = User::factory()->count(3)->create(); + + Livewire::test(Editor::class, ['participants' => $participants, 'hasMentions' => true]) + ->call('getUsers', '') + ->assertSee($participants->first()->username()) + ->assertSee($participants->get(1)->username()) + ->assertSee($participants->get(2)->username()); }); test('users are returned when a query is made for mentions', function () { - $this->markTestIncomplete( - 'This test has not been implemented yet.', - ); + $userOne = User::factory()->create(['username' => 'joedixon']); + $userTwo = User::factory()->create(['username' => 'driesvints']); + + Livewire::test(Editor::class, ['hasMentions' => true]) + ->call('getUsers', 'jo') + ->assertSee($userOne->username()) + ->assertDontSee($userTwo->username()); }); test('participants are prioritised over users', function () { - $this->markTestIncomplete( - 'This test has not been implemented yet.', - ); + $participants = User::factory() + ->count(2) + ->state(new Sequence( + ['username' => 'joedixon'], + ['username' => 'driesvints'], + )) + ->create(); + User::factory()->create(['username' => 'janedoe']); + + Livewire::test(Editor::class, ['participants' => $participants, 'hasMentions' => true]) + ->call('getUsers', 'j') + ->assertSeeInOrder(['joedixon', 'janedoe']); }); test('users are not queried when hasMentions is turned off', function () { - $this->markTestIncomplete( - 'This test has not been implemented yet.', - ); + $users = User::factory() + ->count(2) + ->state(new Sequence( + ['username' => 'joedixon'], + ['username' => 'driesvints'], + )) + ->create(); + + Livewire::test(Editor::class) + ->call('getUsers', 'j') + ->assertDontSee($users->first()->username()); +}); + +test('no users are returned when query returns no results', function () { + $users = User::factory() + ->count(2) + ->state(new Sequence( + ['username' => 'joedixon'], + ['username' => 'driesvints'], + )) + ->create(); + + Livewire::test(Editor::class, ['hasMentions' => true]) + ->call('getUsers', 'b') + ->assertDontSee($users->first()->username()) + ->assertDontSee($users->get(1)->username()); }); diff --git a/tests/Feature/ForumTest.php b/tests/Feature/ForumTest.php index 14bf07330..bd8b03ccc 100644 --- a/tests/Feature/ForumTest.php +++ b/tests/Feature/ForumTest.php @@ -282,7 +282,21 @@ }); test('users are not notified when mentioned in and edited thread', function () { - $this->markTestIncomplete( - 'This test has not been implemented yet.', - ); + Notification::fake(); + $user = $this->createUser(); + $tag = Tag::factory()->create(['name' => 'Test Tag']); + Thread::factory()->create([ + 'author_id' => $user->id(), + 'slug' => 'my-first-thread', + ]); + + $this->loginAs($user); + + $this->put('/forum/my-first-thread', [ + 'subject' => 'How to work with Eloquent?', + 'body' => 'This text explains how to work with Eloquent.', + 'tags' => [$tag->id()], + ]); + + Notification::assertNothingSent(); }); diff --git a/tests/Feature/ReplyTest.php b/tests/Feature/ReplyTest.php index 0462ff8f8..d6fb294af 100644 --- a/tests/Feature/ReplyTest.php +++ b/tests/Feature/ReplyTest.php @@ -1,9 +1,13 @@ markTestIncomplete( - 'This test has not been implemented yet.', - ); + Notification::fake(); + $user = User::factory()->create(['username' => 'janedoe']); + $thread = Thread::factory()->create(['subject' => 'The first thread', 'slug' => 'the-first-thread']); + + $this->login(); + + $this->post('/replies', [ + 'body' => 'Hey @janedoe', + 'replyable_id' => $thread->id, + 'replyable_type' => Thread::TABLE, + ]); + + Notification::assertSentTo($user, MentionNotification::class, function ($notification) use ($user) { + return $notification->toMail($user) instanceof MentionEmail; + }); }); test('users provided with a UI notification when mentioned in a reply body', function () { - $this->markTestIncomplete( - 'This test has not been implemented yet.', - ); + $user = User::factory()->create(['username' => 'janedoe']); + $thread = Thread::factory()->create(['subject' => 'The first thread', 'slug' => 'the-first-thread']); + + $this->login(); + + $this->post('/replies', [ + 'body' => 'Hey @janedoe', + 'replyable_id' => $thread->id, + 'replyable_type' => Thread::TABLE, + ]); + + $notification = DatabaseNotification::first(); + $this->assertSame($user->id, (int) $notification->notifiable_id); + $this->assertSame('users', $notification->notifiable_type); + $this->assertSame('mention', $notification->data['type']); + $this->assertSame('The first thread', $notification->data['replyable_subject']); }); test('users are not notified when mentioned in an edited reply', function () { - $this->markTestIncomplete( - 'This test has not been implemented yet.', - ); + Notification::fake(); + + $user = $this->createUser(); + $thread = Thread::factory()->create(['slug' => 'the-first-thread']); + Reply::factory()->create(['author_id' => $user->id(), 'replyable_id' => $thread->id()]); + + $this->loginAs($user); + + $this->put('/replies/1', [ + 'body' => 'The updated reply', + ]); + + Notification::assertNothingSent(); }); diff --git a/tests/Feature/SubscriptionsTest.php b/tests/Feature/SubscriptionsTest.php index f1bc582a0..5f08111fa 100644 --- a/tests/Feature/SubscriptionsTest.php +++ b/tests/Feature/SubscriptionsTest.php @@ -104,13 +104,30 @@ }); test('users are subscribed to a thread when mentioned', function () { - $this->markTestIncomplete( - 'This test has not been implemented yet.', - ); + $user = User::factory()->create(['username' => 'janedoe', 'email' => 'janedoe@example.com']); + + $this->login(); + + $this->post('/forum/create-thread', [ + 'subject' => 'How to work with Eloquent?', + 'body' => 'Hey @janedoe', + 'tags' => [], + ]); + + $this->seeInDatabase('subscriptions', ['user_id' => $user->id()]); }); test('users are subscribed to a thread when mentioned in a reply', function () { - $this->markTestIncomplete( - 'This test has not been implemented yet.', - ); + $user = User::factory()->create(['username' => 'janedoe', 'email' => 'janedoe@example.com']); + $thread = Thread::factory()->create(['subject' => 'The first thread', 'slug' => 'the-first-thread']); + + $this->login(); + + $this->post('/replies', [ + 'body' => 'Hey @janedoe', + 'replyable_id' => $thread->id, + 'replyable_type' => Thread::TABLE, + ]); + + $this->seeInDatabase('subscriptions', ['user_id' => $user->id()]); }); From ecbb4508edc2855ffae387e79dca01523d1c43fb Mon Sep 17 00:00:00 2001 From: StyleCI Bot Date: Wed, 26 Jan 2022 21:26:14 +0000 Subject: [PATCH 20/24] Apply fixes from StyleCI --- tests/Feature/EditorTest.php | 2 +- tests/Feature/ForumTest.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/Feature/EditorTest.php b/tests/Feature/EditorTest.php index 608fec175..ded60b685 100644 --- a/tests/Feature/EditorTest.php +++ b/tests/Feature/EditorTest.php @@ -23,7 +23,7 @@ test('users are returned when a query is made for mentions', function () { $userOne = User::factory()->create(['username' => 'joedixon']); $userTwo = User::factory()->create(['username' => 'driesvints']); - + Livewire::test(Editor::class, ['hasMentions' => true]) ->call('getUsers', 'jo') ->assertSee($userOne->username()) diff --git a/tests/Feature/ForumTest.php b/tests/Feature/ForumTest.php index bd8b03ccc..1a43ebed2 100644 --- a/tests/Feature/ForumTest.php +++ b/tests/Feature/ForumTest.php @@ -297,6 +297,6 @@ 'body' => 'This text explains how to work with Eloquent.', 'tags' => [$tag->id()], ]); - + Notification::assertNothingSent(); }); From 90a59ecb9c10a2fa7a9debd14abca3ad0d5a4eb1 Mon Sep 17 00:00:00 2001 From: Joe Dixon Date: Thu, 10 Feb 2022 19:27:58 +0000 Subject: [PATCH 21/24] Update formatting --- app/Listeners/SubscribeUsersMentionedInReply.php | 10 ++-------- app/Listeners/SubscribeUsersMentionedInThread.php | 6 ++---- app/Models/Reply.php | 4 ++-- app/Models/Thread.php | 4 ++-- 4 files changed, 8 insertions(+), 16 deletions(-) diff --git a/app/Listeners/SubscribeUsersMentionedInReply.php b/app/Listeners/SubscribeUsersMentionedInReply.php index 9c6578060..7366f42dc 100644 --- a/app/Listeners/SubscribeUsersMentionedInReply.php +++ b/app/Listeners/SubscribeUsersMentionedInReply.php @@ -14,15 +14,9 @@ public function handle(ReplyWasCreated $event): void $event->reply->getMentionedUsers()->each(function ($user) use ($event) { $replyAble = $event->reply->mentionedIn(); - if (! $replyAble instanceof Thread) { - return; + if ($replyAble instanceof Thread && ! $replyAble->hasSubscriber($user)) { + $replyAble->subscribe($user); } - - if ($replyAble->hasSubscriber($user)) { - return; - } - - $replyAble->subscribe($user); }); } } diff --git a/app/Listeners/SubscribeUsersMentionedInThread.php b/app/Listeners/SubscribeUsersMentionedInThread.php index a2fa6c62c..8fe0e49a9 100644 --- a/app/Listeners/SubscribeUsersMentionedInThread.php +++ b/app/Listeners/SubscribeUsersMentionedInThread.php @@ -11,11 +11,9 @@ final class SubscribeUsersMentionedInThread public function handle(ThreadWasCreated $event): void { $event->thread->getMentionedUsers()->each(function ($user) use ($event) { - if ($event->thread->hasSubscriber($user)) { - return; + if (!$event->thread->hasSubscriber($user)) { + $event->thread->subscribe($user); } - - $event->thread->subscribe($user); }); } } diff --git a/app/Models/Reply.php b/app/Models/Reply.php index 5488f5164..ebf7c85b5 100644 --- a/app/Models/Reply.php +++ b/app/Models/Reply.php @@ -16,11 +16,11 @@ final class Reply extends Model implements MentionAble { - use HasFactory; use HasAuthor; + use HasFactory; use HasLikes; - use HasTimestamps; use HasMentions; + use HasTimestamps; const TABLE = 'replies'; diff --git a/app/Models/Thread.php b/app/Models/Thread.php index 532f51fdf..a11ef2aed 100644 --- a/app/Models/Thread.php +++ b/app/Models/Thread.php @@ -29,13 +29,13 @@ final class Thread extends Model implements Feedable, ReplyAble, SubscriptionAble, MentionAble { - use HasFactory; use HasAuthor; + use HasFactory; use HasLikes; + use HasMentions; use HasSlug; use HasTags; use HasTimestamps; - use HasMentions; use PreparesSearch; use ProvidesSubscriptions; use ReceivesReplies; From d8b1c0d3734f3956b1ec66f80596f90bab610532 Mon Sep 17 00:00:00 2001 From: Joe Dixon Date: Thu, 10 Feb 2022 20:27:08 +0000 Subject: [PATCH 22/24] Update code selection --- resources/js/editor.js | 133 ++++++++++-------- .../forms/editor/control-button.blade.php | 3 +- resources/views/livewire/editor.blade.php | 2 +- 3 files changed, 78 insertions(+), 60 deletions(-) diff --git a/resources/js/editor.js b/resources/js/editor.js index 617353188..9b3aaadea 100644 --- a/resources/js/editor.js +++ b/resources/js/editor.js @@ -1,67 +1,43 @@ import getCaretCoordinates from 'textarea-caret'; -// Handle the click event of the style buttons inside the editor. -window.handleClick = (style, element) => { - const { styles } = editorConfig(); - const input = element.querySelectorAll('textarea')[0]; - - // Get the start and end positions of the current selection. - const selectionStart = input.selectionStart; - const selectionEnd = input.selectionEnd; - - // Find the style in the configuration. - const styleFormat = styles[style]; - - // Get any prefix and/or suffix characters from the selected style. - const prefix = styleFormat.before ? styleFormat.before : ''; - const suffix = styleFormat.after ? styleFormat.after : ''; - - // Insert the prefix at the relevant position. - input.value = insertCharactersAtPosition(input.value, prefix, selectionStart); - - // Insert the suffix at the relevant position. - input.value = insertCharactersAtPosition(input.value, suffix, selectionEnd + prefix.length); - - // Reselect the selection and focus the input. - input.setSelectionRange(selectionStart + prefix.length, selectionEnd + prefix.length); - input.focus(); -}; - -// Insert provided characters at the desired place in a string. -const insertCharactersAtPosition = (string, character, position) => { - return [string.slice(0, position), character, string.slice(position)].join(''); -}; - // Configuration object for the text editor. window.editorConfig = (body, hasMentions) => { return { styles: { - header: { - before: '### ', - }, - bold: { - before: '**', - after: '**', - }, - italic: { - before: '_', - after: '_', - }, - quote: { - before: '> ', - }, - code: { - before: '`', - after: '`', - }, - link: { - before: '[](', - after: ')', - }, - image: { - before: '![](', - after: ')', + singleLine: { + header: { + before: '### ', + }, + bold: { + before: '**', + after: '**', + }, + italic: { + before: '_', + after: '_', + }, + quote: { + before: '> ', + }, + code: { + before: '`', + after: '`', + }, + link: { + before: '[](', + after: ')', + }, + image: { + before: '![](', + after: ')', + }, }, + multipleLines: { + code: { + before: '```\n', + after: '\n```', + }, + } }, cursorTop: 0, cursorLeft: 0, @@ -251,6 +227,49 @@ window.editorConfig = (body, hasMentions) => { isEscapeKey: function (code) { return code == 27; }, + + // Handle the click event of the style buttons inside the editor. + handleClick: function (style) { + const editor = this.$refs.editor; + let value = editor.value; + let styleFormat; + + // Get the start and end positions of the current selection. + const selectionStart = editor.selectionStart; + const selectionEnd = editor.selectionEnd; + const selectedText = value.slice(selectionStart, selectionEnd); + const hasMultipleLines = new RegExp(/\n/g).test(selectedText); + + // Find the style in the configuration. + if (hasMultipleLines && this.styles['multipleLines'][style]) { + styleFormat = this.styles['multipleLines'][style] + } else { + styleFormat = this.styles['singleLine'][style] + } + + // Get any prefix and/or suffix characters from the selected style. + const prefix = styleFormat.before ? styleFormat.before : ''; + const suffix = styleFormat.after ? styleFormat.after : ''; + + // Insert the prefix at the relevant position. + value = this.insertCharactersAtPosition(value, prefix, selectionStart); + + // Insert the suffix at the relevant position. + value = this.insertCharactersAtPosition(value, suffix, selectionEnd + prefix.length); + + this.body = value; + + // Reselect the selection and focus the input. + this.$nextTick(() => { + this.$refs.editor.focus(); + this.$refs.editor.setSelectionRange(selectionStart + prefix.length, selectionEnd + prefix.length); + }); + }, + + // Insert provided characters at the desired place in a string. + insertCharactersAtPosition: (string, character, position) => { + return [string.slice(0, position), character, string.slice(position)].join(''); + } }; }; diff --git a/resources/views/components/forms/editor/control-button.blade.php b/resources/views/components/forms/editor/control-button.blade.php index 45902e59b..1d4cc3497 100644 --- a/resources/views/components/forms/editor/control-button.blade.php +++ b/resources/views/components/forms/editor/control-button.blade.php @@ -1,9 +1,8 @@ \ No newline at end of file diff --git a/resources/views/livewire/editor.blade.php b/resources/views/livewire/editor.blade.php index 36335a483..3203e71b7 100644 --- a/resources/views/livewire/editor.blade.php +++ b/resources/views/livewire/editor.blade.php @@ -5,7 +5,7 @@ @endif -
    +
    • From 6c9114f573886485727c95870f7f0199ff41e345 Mon Sep 17 00:00:00 2001 From: StyleCI Bot Date: Thu, 10 Feb 2022 20:27:27 +0000 Subject: [PATCH 23/24] Apply fixes from StyleCI --- app/Listeners/SubscribeUsersMentionedInThread.php | 2 +- resources/js/editor.js | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/Listeners/SubscribeUsersMentionedInThread.php b/app/Listeners/SubscribeUsersMentionedInThread.php index 8fe0e49a9..eac6deb52 100644 --- a/app/Listeners/SubscribeUsersMentionedInThread.php +++ b/app/Listeners/SubscribeUsersMentionedInThread.php @@ -11,7 +11,7 @@ final class SubscribeUsersMentionedInThread public function handle(ThreadWasCreated $event): void { $event->thread->getMentionedUsers()->each(function ($user) use ($event) { - if (!$event->thread->hasSubscriber($user)) { + if (! $event->thread->hasSubscriber($user)) { $event->thread->subscribe($user); } }); diff --git a/resources/js/editor.js b/resources/js/editor.js index 9b3aaadea..290e4e451 100644 --- a/resources/js/editor.js +++ b/resources/js/editor.js @@ -37,7 +37,7 @@ window.editorConfig = (body, hasMentions) => { before: '```\n', after: '\n```', }, - } + }, }, cursorTop: 0, cursorLeft: 0, @@ -242,9 +242,9 @@ window.editorConfig = (body, hasMentions) => { // Find the style in the configuration. if (hasMultipleLines && this.styles['multipleLines'][style]) { - styleFormat = this.styles['multipleLines'][style] + styleFormat = this.styles['multipleLines'][style]; } else { - styleFormat = this.styles['singleLine'][style] + styleFormat = this.styles['singleLine'][style]; } // Get any prefix and/or suffix characters from the selected style. @@ -269,7 +269,7 @@ window.editorConfig = (body, hasMentions) => { // Insert provided characters at the desired place in a string. insertCharactersAtPosition: (string, character, position) => { return [string.slice(0, position), character, string.slice(position)].join(''); - } + }, }; }; From 724bd4af81a34757148615ff37154146872e7341 Mon Sep 17 00:00:00 2001 From: Joe Dixon Date: Thu, 10 Feb 2022 20:30:04 +0000 Subject: [PATCH 24/24] Fix article title format --- resources/views/articles/show.blade.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/views/articles/show.blade.php b/resources/views/articles/show.blade.php index cbf71244c..60cd431f4 100644 --- a/resources/views/articles/show.blade.php +++ b/resources/views/articles/show.blade.php @@ -46,7 +46,7 @@
    @endif -

    +

    {{ $article->title() }}