Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Communication: Add emoji support to messages #9595

Merged
merged 10 commits into from
Oct 27, 2024
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();
}
krusche marked this conversation as resolved.
Show resolved Hide resolved

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
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);
krusche marked this conversation as resolved.
Show resolved Hide resolved
asliayk marked this conversation as resolved.
Show resolved Hide resolved
}

/**
* 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 };
}
krusche marked this conversation as resolved.
Show resolved Hide resolved

/**
* 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);
}
}
krusche marked this conversation as resolved.
Show resolved Hide resolved

/**
* 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();
});
krusche marked this conversation as resolved.
Show resolved Hide resolved

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);
krusche marked this conversation as resolved.
Show resolved Hide resolved
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);
krusche marked this conversation as resolved.
Show resolved Hide resolved
}
}

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"
krusche marked this conversation as resolved.
Show resolved Hide resolved
},
"visualEditor": {
"hintTooltip": "Add a hint here (visible during the quiz via ?-Button)",
Expand Down
Loading
Loading