Skip to content

Commit

Permalink
Communication: Add emoji support to messages (#9595)
Browse files Browse the repository at this point in the history
  • Loading branch information
asliayk authored Oct 27, 2024
1 parent 981fe87 commit 795ca8e
Show file tree
Hide file tree
Showing 11 changed files with 398 additions and 41 deletions.
20 changes: 20 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
"@siemens/ngx-datatable": "22.4.1",
"@swimlane/ngx-charts": "20.5.0",
"@swimlane/ngx-graph": "8.4.0",
"@types/emoji-js": "^3.5.2",
"@vscode/codicons": "0.0.36",
"@vscode/markdown-it-katex": "1.1.0",
"bootstrap": "5.3.3",
Expand All @@ -49,6 +50,7 @@
"dayjs": "1.11.13",
"diff-match-patch-typescript": "1.1.0",
"dompurify": "3.1.7",
"emoji-js": "^3.8.0",
"export-to-csv": "1.4.0",
"fast-json-patch": "3.1.1",
"franc-min": "6.2.0",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,7 @@
</div>

<ng-template #simpleActionButton let-action="action">
<button type="button" class="btn btn-sm py-0" [ngbTooltip]="action.icon ? (action.translationKey | artemisTranslate) : undefined" (click)="action.executeInCurrentEditor()">
<button type="button" class="btn btn-sm py-0" [ngbTooltip]="action.icon ? (action.translationKey | artemisTranslate) : undefined" (click)="handleActionClick($event, action)">
@if (action.icon) {
<fa-icon [icon]="action.icon" />
} @else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ import { parseMarkdownForDomainActions } from 'app/shared/markdown-editor/monaco
import { COMMUNICATION_MARKDOWN_EDITOR_OPTIONS, DEFAULT_MARKDOWN_EDITOR_OPTIONS } from 'app/shared/monaco-editor/monaco-editor-option.helper';
import { MetisService } from 'app/shared/metis/metis.service';
import { UPLOAD_MARKDOWN_FILE_EXTENSIONS } from 'app/shared/constants/file-extensions.constants';
import { EmojiAction } from 'app/shared/monaco-editor/model/actions/emoji.action';

export enum MarkdownEditorHeight {
INLINE = 125,
Expand Down Expand Up @@ -290,6 +291,16 @@ export class MarkdownEditorMonacoComponent implements AfterContentInit, AfterVie
return action?.hideInEditor ? undefined : action;
}

handleActionClick(event: MouseEvent, action: TextEditorAction): void {
const x = event.clientX;
const y = event.clientY;
if (action instanceof EmojiAction) {
action.setPoint({ x, y });
}

action.executeInCurrentEditor();
}

ngAfterViewInit(): void {
this.adjustEditorDimensions();
this.monacoEditor.setWordWrap(true);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,10 @@ import {
ViewEncapsulation,
computed,
forwardRef,
inject,
input,
} from '@angular/core';
import { ViewContainerRef } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
import { MetisService } from 'app/shared/metis/metis.service';
import { LectureService } from 'app/lecture/lecture.service';
Expand All @@ -35,6 +37,8 @@ import { LectureAttachmentReferenceAction } from 'app/shared/monaco-editor/model
import { UrlAction } from 'app/shared/monaco-editor/model/actions/url.action';
import { AttachmentAction } from 'app/shared/monaco-editor/model/actions/attachment.action';
import { ConversationDTO } from 'app/entities/metis/conversation/conversation.model';
import { EmojiAction } from 'app/shared/monaco-editor/model/actions/emoji.action';
import { Overlay, OverlayPositionBuilder } from '@angular/cdk/overlay';

@Component({
selector: 'jhi-posting-markdown-editor',
Expand Down Expand Up @@ -69,13 +73,16 @@ export class PostingMarkdownEditorComponent implements OnInit, ControlValueAcces
fallbackConversationId = computed<number | undefined>(() => this.activeConversation()?.id);

protected readonly MarkdownEditorHeight = MarkdownEditorHeight;
private overlay = inject(Overlay);

constructor(
private cdref: ChangeDetectorRef,
private metisService: MetisService,
private courseManagementService: CourseManagementService,
private lectureService: LectureService,
private channelService: ChannelService,
public viewContainerRef: ViewContainerRef,
private positionBuilder: OverlayPositionBuilder,
) {}

/**
Expand All @@ -90,6 +97,7 @@ export class PostingMarkdownEditorComponent implements OnInit, ControlValueAcces
new BoldAction(),
new ItalicAction(),
new UnderlineAction(),
new EmojiAction(this.viewContainerRef, this.overlay, this.positionBuilder),
new QuoteAction(),
new CodeAction(),
new CodeBlockAction(),
Expand Down
121 changes: 121 additions & 0 deletions src/main/webapp/app/shared/monaco-editor/model/actions/emoji.action.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import { TextEditorAction } from 'app/shared/monaco-editor/model/actions/text-editor-action.model';
import { faSmile } from '@fortawesome/free-solid-svg-icons';
import { TextEditor } from 'app/shared/monaco-editor/model/actions/adapter/text-editor.interface';
import { ViewContainerRef } from '@angular/core';
import { EmojiPickerComponent } from 'app/shared/metis/emoji/emoji-picker.component';
import { Overlay, OverlayPositionBuilder, OverlayRef } from '@angular/cdk/overlay';
import { ComponentPortal } from '@angular/cdk/portal';
import { TextEditorPosition } from 'app/shared/monaco-editor/model/actions/adapter/text-editor-position.model';
import { TextEditorRange } from 'app/shared/monaco-editor/model/actions/adapter/text-editor-range.model';

/**
* Action to open the emoji picker and insert the selected emoji into the editor.
*/
export class EmojiAction extends TextEditorAction {
static readonly ID = 'emoji.action';
private overlayRef: OverlayRef | null = null;
private position?: { x: number; y: number };

constructor(
private viewContainerRef: ViewContainerRef,
private overlay: Overlay,
private positionBuilder: OverlayPositionBuilder,
) {
super(EmojiAction.ID, 'artemisApp.multipleChoiceQuestion.editor.emoji', faSmile, undefined);
}

/**
* Sets the position where the emoji picker should appear.
* @param param The {x, y} coordinates.
*/
setPoint(param: { x: number; y: number }): void {
this.position = { x: param.x, y: param.y };
}

/**
* Triggers the opening of the emoji picker and attaches it to the view container.
* @param editor The editor in which to insert the emoji.
*/
run(editor: TextEditor): void {
if (this.overlayRef) {
this.destroyEmojiPicker();
return;
}

if (this.position) {
this.createEmojiPicker(editor, this.position);
}
}

/**
* Creates and attaches the emoji picker component dynamically and handles emoji selection.
* @param editor The editor instance where the emoji will be inserted.
* @param position The {x, y} coordinates where the picker should appear.
*/
private createEmojiPicker(editor: TextEditor, position: { x: number; y: number }): void {
const positionStrategy = this.positionBuilder
.global()
.left(`${position.x - 15}px`)
.top(`${position.y - 15}px`);

this.overlayRef = this.overlay.create({
positionStrategy,
hasBackdrop: true,
backdropClass: 'cdk-overlay-transparent-backdrop',
scrollStrategy: this.overlay.scrollStrategies.reposition(),
width: '0',
});

const emojiPickerPortal = new ComponentPortal(EmojiPickerComponent, this.viewContainerRef);
const componentRef = this.overlayRef.attach(emojiPickerPortal);
const pickerElement = componentRef.location.nativeElement;
pickerElement.style.transform = 'translate(-100%, -100%)';

componentRef.instance.emojiSelect.subscribe((selection: { emoji: any; event: PointerEvent }) => {
this.insertEmojiAtCursor(editor, selection.emoji.native);
this.destroyEmojiPicker();
});

this.overlayRef.backdropClick().subscribe(() => {
this.destroyEmojiPicker();
});
}

/**
* Inserts the selected emoji into the editor at the current cursor position.
* @param editor The editor instance.
* @param emoji The emoji to insert.
*/
insertEmojiAtCursor(editor: TextEditor, emoji: string): void {
const position = editor.getPosition();
if (!position) {
return;
}

this.insertTextAtPosition(editor, position, emoji);

const newPosition = new TextEditorPosition(position.getLineNumber(), position.getColumn() + 2);
editor.setPosition(newPosition);
editor.focus();
}

/**
* Inserts the given emoji text at the current cursor position.
* @param editor The editor instance.
* @param position The current cursor position.
* @param emoji The emoji text to insert.
*/
insertTextAtPosition(editor: TextEditor, position: TextEditorPosition, emoji: string): void {
this.replaceTextAtRange(editor, new TextEditorRange(position, position), emoji);
}

/**
* Destroys the emoji picker component after an emoji is selected or toggled.
*/
private destroyEmojiPicker(): void {
if (this.overlayRef) {
this.overlayRef.dispose();
this.overlayRef = null;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import { MonacoTextEditorAdapter } from 'app/shared/monaco-editor/model/actions/
import { MonacoEditorService } from 'app/shared/monaco-editor/monaco-editor.service';
import { getOS } from 'app/shared/util/os-detector.util';

import EmojiConvertor from 'emoji-js';

export const MAX_TAB_SIZE = 8;

@Component({
Expand All @@ -34,6 +36,7 @@ export class MonacoEditorComponent implements OnInit, OnDestroy {
private readonly _editor: monaco.editor.IStandaloneCodeEditor;
private readonly textEditorAdapter: MonacoTextEditorAdapter;
private readonly monacoEditorContainerElement: HTMLElement;
private emojiConvertor: EmojiConvertor;

/*
* Elements, models, and actions of the editor.
Expand Down Expand Up @@ -86,6 +89,10 @@ export class MonacoEditorComponent implements OnInit, OnDestroy {
this.textEditorAdapter = new MonacoTextEditorAdapter(this._editor);
this.renderer.appendChild(this.elementRef.nativeElement, this.monacoEditorContainerElement);

this.emojiConvertor = new EmojiConvertor();
this.emojiConvertor.replace_mode = 'unified';
this.emojiConvertor.allow_native = true;

effect(() => {
// TODO: The CSS class below allows the editor to shrink in the CodeEditorContainerComponent. We should eventually remove this class and handle the editor size differently in the code editor grid.
if (this.shrinkToFit()) {
Expand All @@ -103,6 +110,10 @@ export class MonacoEditorComponent implements OnInit, OnDestroy {
});
}

convertTextToEmoji(text: string): string {
return this.emojiConvertor.replace_emoticons(text);
}

ngOnInit(): void {
const resizeObserver = new ResizeObserver(() => {
this._editor.layout();
Expand Down Expand Up @@ -173,9 +184,18 @@ export class MonacoEditorComponent implements OnInit, OnDestroy {
return this._editor.getContentHeight() + this._editor.getOption(monaco.editor.EditorOption.lineHeight);
}

isConvertedToEmoji(originalText: string, convertedText: string): boolean {
return originalText !== convertedText;
}

setText(text: string): void {
if (this.getText() !== text) {
this._editor.setValue(text);
const convertedText = this.convertTextToEmoji(text);
if (this.isConvertedToEmoji(text, convertedText)) {
this._editor.setValue(convertedText);
this.setPosition({ column: this.getPosition().column + 2 + text.length, lineNumber: this.getPosition().lineNumber });
}
if (this.getText() !== convertedText) {
this._editor.setValue(convertedText);
}
}

Expand Down
3 changes: 2 additions & 1 deletion src/main/webapp/i18n/de/multipleChoiceQuestion.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,8 @@
"style": "Überschriften",
"codeBlock": "Code Block",
"code": "Code",
"color": "Farbe"
"color": "Farbe",
"emoji": "Emoji"
},
"visualEditor": {
"hintTooltip": "Füge hier einen Hinweis hinzu (sichtbar während des Quiz über den ?-Button)",
Expand Down
3 changes: 2 additions & 1 deletion src/main/webapp/i18n/en/multipleChoiceQuestion.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,8 @@
"codeBlock": "Code Block",
"code": "Code",
"color": "Color",
"style": "Style"
"style": "Style",
"emoji": "Emoji"
},
"visualEditor": {
"hintTooltip": "Add a hint here (visible during the quiz via ?-Button)",
Expand Down
Loading

0 comments on commit 795ca8e

Please sign in to comment.