diff --git a/app-modules/in-app-communication/resources/js/TipTap/Extentions/Mention.js b/app-modules/in-app-communication/resources/js/TipTap/Extentions/Mention.js
new file mode 100644
index 0000000000..a796c79e27
--- /dev/null
+++ b/app-modules/in-app-communication/resources/js/TipTap/Extentions/Mention.js
@@ -0,0 +1,305 @@
+/*
+
+
+ Copyright © 2022-2023, Canyon GBS LLC. All rights reserved.
+
+ Advising App™ is licensed under the Elastic License 2.0. For more details,
+ see https://github.com/canyongbs/advisingapp/blob/main/LICENSE.
+
+ Notice:
+
+ - You may not provide the software to third parties as a hosted or managed
+ service, where the service provides users with access to any substantial set of
+ the features or functionality of the software.
+ - You may not move, change, disable, or circumvent the license key functionality
+ in the software, and you may not remove or obscure any functionality in the
+ software that is protected by the license key.
+ - You may not alter, remove, or obscure any licensing, copyright, or other notices
+ of the licensor in the software. Any use of the licensor’s trademarks is subject
+ to applicable law.
+ - Canyon GBS LLC respects the intellectual property rights of others and expects the
+ same in return. Canyon GBS™ and Advising App™ are registered trademarks of
+ Canyon GBS LLC, and we are committed to enforcing and protecting our trademarks
+ vigorously.
+ - The software solution, including services, infrastructure, and code, is offered as a
+ Software as a Service (SaaS) by Canyon GBS LLC.
+ - Use of this software implies agreement to the license terms and conditions as stated
+ in the Elastic License 2.0.
+
+ For more information or inquiries please visit our website at
+ https://www.canyongbs.com or contact us via email at legal@canyongbs.com.
+
+
+*/
+import { mergeAttributes, Node } from '@tiptap/core';
+import { PluginKey } from '@tiptap/pm/state';
+import Suggestion from '@tiptap/suggestion';
+import tippy from 'tippy.js';
+
+export const MentionPluginKey = new PluginKey('mention');
+
+export const Mention = Node.create({
+ name: 'mention',
+
+ group: 'inline',
+
+ inline: true,
+
+ selectable: false,
+
+ atom: true,
+
+ addAttributes() {
+ return {
+ id: {
+ default: null,
+ parseHTML: (element) => element.getAttribute('data-id'),
+ renderHTML: (attributes) => {
+ if (!attributes.id) {
+ return {};
+ }
+
+ return {
+ 'data-id': attributes.id,
+ };
+ },
+ },
+ };
+ },
+
+ parseHTML() {
+ return [
+ {
+ user: `span[data-type='${this.name}']`,
+ },
+ ];
+ },
+
+ renderHTML({ node, HTMLAttributes }) {
+ return [
+ 'span',
+ mergeAttributes({ 'data-type': this.name }, HTMLAttributes),
+ `@${this.options.users[node.attrs.id] ?? node.attrs.id}`,
+ ];
+ },
+
+ renderText({ node }) {
+ return `@${this.options.users[node.attrs.id] ?? node.attrs.id}`;
+ },
+
+ addKeyboardShortcuts() {
+ return {
+ Backspace: () =>
+ this.editor.commands.command(({ tr, state }) => {
+ let isMention = false;
+ const { selection } = state;
+ const { empty, anchor } = selection;
+
+ if (!empty) {
+ return false;
+ }
+
+ state.doc.nodesBetween(anchor - 1, anchor, (node, pos) => {
+ if (node.type.name === this.name) {
+ isMention = true;
+ tr.insertText('@', pos, pos + node.nodeSize);
+
+ return false;
+ }
+ });
+
+ return isMention;
+ }),
+ };
+ },
+
+ addCommands() {
+ return {
+ insertMention:
+ (attributes) =>
+ ({ chain }) => {
+ const currentChain = chain();
+
+ if (![null, undefined].includes(attributes.coordinates?.pos)) {
+ currentChain.insertContentAt(
+ { from: attributes.coordinates.pos, to: attributes.coordinates.pos },
+ [
+ { type: this.name, attrs: { id: attributes.user } },
+ { type: 'text', text: ' ' },
+ ],
+ );
+
+ return currentChain;
+ }
+ },
+ };
+ },
+
+ addProseMirrorPlugins() {
+ return [
+ Suggestion({
+ editor: this.editor,
+ char: '@',
+ items: ({ query }) =>
+ Object.fromEntries(
+ Object.entries(this.options.users)
+ .filter(([id, name]) => name.toLowerCase().startsWith(query.toLowerCase()))
+ .slice(0, 5),
+ ),
+ pluginKey: MentionPluginKey,
+ command: ({ editor, range, props }) => {
+ const nodeAfter = editor.view.state.selection.$to.nodeAfter;
+ const overrideSpace = nodeAfter?.text?.startsWith(' ');
+
+ if (overrideSpace) {
+ range.to += 1;
+ }
+
+ editor
+ .chain()
+ .focus()
+ .insertContentAt(range, [
+ {
+ type: this.name,
+ attrs: props,
+ },
+ {
+ type: 'text',
+ text: ' ',
+ },
+ ])
+ .run();
+
+ window.getSelection()?.collapseToEnd();
+ },
+ allow: ({ state, range }) => {
+ const $from = state.doc.resolve(range.from);
+ const type = state.schema.nodes[this.name];
+ const allow = !!$from.parent.type.contentMatch.matchType(type);
+
+ return allow;
+ },
+ render: () => {
+ let component;
+ let popup;
+
+ return {
+ onStart: (props) => {
+ if (!props.clientRect) {
+ return;
+ }
+
+ const html = `
+
+
+
+
+
+ `;
+
+ component = document.createElement('div');
+ component.innerHTML = html;
+ component.addEventListener('mentions-select', (event) => {
+ props.command({ id: event.detail.item });
+ });
+
+ popup = tippy('body', {
+ getReferenceClientRect: props.clientRect,
+ appendTo: () => document.body,
+ content: component,
+ allowHTML: true,
+ showOnCreate: true,
+ interactive: true,
+ trigger: 'manual',
+ placement: 'bottom-start',
+ });
+ },
+
+ onUpdate(props) {
+ if (!props.items.length) {
+ popup[0].hide();
+
+ return;
+ }
+
+ popup[0].show();
+
+ component.dispatchEvent(new CustomEvent('mentions-update-items', { detail: props.items }));
+ },
+
+ onKeyDown(props) {
+ component.dispatchEvent(new CustomEvent('mentions-key-down', { detail: props.event }));
+ },
+
+ onExit() {
+ popup[0].destroy();
+ },
+ };
+ },
+ }),
+ ];
+ },
+});
diff --git a/app-modules/in-app-communication/resources/js/userToUserChat.js b/app-modules/in-app-communication/resources/js/userToUserChat.js
index c6eae94bbe..d42be2b458 100644
--- a/app-modules/in-app-communication/resources/js/userToUserChat.js
+++ b/app-modules/in-app-communication/resources/js/userToUserChat.js
@@ -35,6 +35,7 @@ document.addEventListener('alpine:init', () => {
global = globalThis;
const { generateHTML } = require('@tiptap/html');
const { Editor } = require('@tiptap/core');
+ const { Mention } = require('./TipTap/Extentions/Mention');
const { Placeholder } = require('@tiptap/extension-placeholder');
const { StarterKit } = require('@tiptap/starter-kit');
const { Underline } = require('@tiptap/extension-underline');
@@ -46,7 +47,7 @@ document.addEventListener('alpine:init', () => {
let conversationsClient = null;
- Alpine.data('userToUserChat', (selectedConversation) => ({
+ Alpine.data('userToUserChat', ({ selectedConversation, users }) => ({
loading: true,
loadingMessage: 'Loading chat…',
error: false,
@@ -285,11 +286,17 @@ document.addEventListener('alpine:init', () => {
.catch((error) => console.error('Error handler failed to handle error: ', error));
},
generateHTML: (content) => {
- return generateHTML(content, [StarterKit, Underline]);
+ return generateHTML(content, [
+ Mention.configure({
+ users,
+ }),
+ StarterKit,
+ Underline,
+ ]);
},
}));
- Alpine.data('chatEditor', () => {
+ Alpine.data('chatEditor', ({ currentUser, users }) => {
let editor;
return {
@@ -299,9 +306,14 @@ document.addEventListener('alpine:init', () => {
init() {
const _this = this;
+ delete users[currentUser];
+
editor = new Editor({
element: this.$refs.element,
extensions: [
+ Mention.configure({
+ users,
+ }),
StarterKit,
Underline,
Placeholder.configure({
diff --git a/app-modules/in-app-communication/resources/views/filament/pages/user-chat.blade.php b/app-modules/in-app-communication/resources/views/filament/pages/user-chat.blade.php
index a5cd2897b4..56562b0238 100644
--- a/app-modules/in-app-communication/resources/views/filament/pages/user-chat.blade.php
+++ b/app-modules/in-app-communication/resources/views/filament/pages/user-chat.blade.php
@@ -152,7 +152,7 @@ class="flex flex-col gap-y-1 rounded-xl border border-gray-950/5 bg-white p-2 sh
@if ($conversation)
@@ -327,8 +328,9 @@ class="h-5 w-5"
- Post
+ Send
+
{{ $this->addUserToChannelAction }}
{{ $this->leaveChannelAction }}
-
@endif
diff --git a/app-modules/in-app-communication/src/Filament/Pages/UserChat.php b/app-modules/in-app-communication/src/Filament/Pages/UserChat.php
index 378faf20c6..75a13e3fa4 100644
--- a/app-modules/in-app-communication/src/Filament/Pages/UserChat.php
+++ b/app-modules/in-app-communication/src/Filament/Pages/UserChat.php
@@ -479,7 +479,9 @@ public function leaveChannelAction(): Action
if ($this->conversation->managers()->find($user)) {
if ($this->conversation->managers()->whereKeyNot($user->getKey())->exists()) {
$action->modalDescription(
- new HtmlString("You will be removed as a channel manager.
{$action->getModalDescription()}")
+ new HtmlString(
+ "You will be removed as a channel manager.
{$action->getModalDescription()}"
+ )
);
} else {
$action->modalHeading('Unable to leave channel.')
@@ -622,4 +624,11 @@ public function handleError(mixed $error): void
->danger()
->send();
}
+
+ protected function getViewData(): array
+ {
+ return [
+ 'users' => $this->conversation?->participants()->pluck('name', 'id')->all() ?? [],
+ ];
+ }
}
diff --git a/package-lock.json b/package-lock.json
index 291ae50523..42b5f0700c 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -33,6 +33,7 @@
"@tiptap/html": "^2.2.1",
"@tiptap/pm": "^2.2.1",
"@tiptap/starter-kit": "^2.2.1",
+ "@tiptap/suggestion": "^2.2.1",
"@vitejs/plugin-vue": "^4.4.0",
"autoprefixer": "^10.4.14",
"cross-env": "^7.0.3",
@@ -1338,6 +1339,20 @@
"url": "https://github.com/sponsors/ueberdosis"
}
},
+ "node_modules/@tiptap/suggestion": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/@tiptap/suggestion/-/suggestion-2.2.1.tgz",
+ "integrity": "sha512-DIT4zR5CoP0Q5n9BJj68EqmbFGzdyDgDon8yfX1NkynW8PtDrcfGhe5/31tcH6YuM6ZijqNV3fdCt+LPrVr/Nw==",
+ "dev": true,
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ },
+ "peerDependencies": {
+ "@tiptap/core": "^2.0.0",
+ "@tiptap/pm": "^2.0.0"
+ }
+ },
"node_modules/@twilio/conversations": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/@twilio/conversations/-/conversations-2.5.0.tgz",
diff --git a/package.json b/package.json
index 7dc0d8f273..85ec570fc1 100644
--- a/package.json
+++ b/package.json
@@ -28,6 +28,7 @@
"@tiptap/html": "^2.2.1",
"@tiptap/pm": "^2.2.1",
"@tiptap/starter-kit": "^2.2.1",
+ "@tiptap/suggestion": "^2.2.1",
"@vitejs/plugin-vue": "^4.4.0",
"autoprefixer": "^10.4.14",
"cross-env": "^7.0.3",