From b6e280233132a90da1f25f0d91ea09393ab339da Mon Sep 17 00:00:00 2001 From: Raymond Luong Date: Wed, 9 Oct 2024 16:12:16 -0600 Subject: [PATCH 01/10] SF-3007 Add audio comments --- .../scriptureforge/models/answer.ts | 2 - .../scriptureforge/models/comment.ts | 1 + .../services/question-service.ts | 7 +- .../checking-answers.component.html | 1 + .../checking-answers.component.ts | 3 +- .../checking-comment-form.component.html | 27 ++++--- .../checking-comment-form.component.scss | 11 ++- .../checking-comment-form.component.ts | 52 ++++++------ .../checking-comments.component.html | 19 ++++- .../checking-comments.component.scss | 6 +- .../checking-comments.component.ts | 9 ++- .../checking/checking.component.spec.ts | 80 ++++++++++++++++++- .../checking/checking/checking.component.ts | 70 +++++++++------- src/SIL.XForge.Scripture/Models/Answer.cs | 1 - src/SIL.XForge.Scripture/Models/Comment.cs | 1 + .../Services/ParatextNotesMapperTests.cs | 8 +- 16 files changed, 204 insertions(+), 94 deletions(-) diff --git a/src/RealtimeServer/scriptureforge/models/answer.ts b/src/RealtimeServer/scriptureforge/models/answer.ts index 70130c9220..5fe67b2b51 100644 --- a/src/RealtimeServer/scriptureforge/models/answer.ts +++ b/src/RealtimeServer/scriptureforge/models/answer.ts @@ -13,8 +13,6 @@ export interface Answer extends Comment { scriptureText?: string; selectionStartClipped?: boolean; selectionEndClipped?: boolean; - audioUrl?: string; - text?: string; likes: Like[]; comments: Comment[]; status?: AnswerStatus; diff --git a/src/RealtimeServer/scriptureforge/models/comment.ts b/src/RealtimeServer/scriptureforge/models/comment.ts index 4f9d168051..7d19b3c046 100644 --- a/src/RealtimeServer/scriptureforge/models/comment.ts +++ b/src/RealtimeServer/scriptureforge/models/comment.ts @@ -5,6 +5,7 @@ export interface Comment extends OwnedData { deleted: boolean; syncUserRef?: string; text?: string; + audioUrl?: string; dateModified: string; dateCreated: string; } diff --git a/src/RealtimeServer/scriptureforge/services/question-service.ts b/src/RealtimeServer/scriptureforge/services/question-service.ts index 75b0e1e922..be11f00849 100644 --- a/src/RealtimeServer/scriptureforge/services/question-service.ts +++ b/src/RealtimeServer/scriptureforge/services/question-service.ts @@ -6,7 +6,7 @@ import { ANY_INDEX } from '../../common/utils/obj-path'; import { createFetchQuery, docSubmitJson0Op } from '../../common/utils/sharedb-utils'; import { Answer } from '../models/answer'; import { Comment } from '../models/comment'; -import { Question, QUESTIONS_COLLECTION, QUESTION_INDEX_PATHS } from '../models/question'; +import { QUESTIONS_COLLECTION, QUESTION_INDEX_PATHS, Question } from '../models/question'; import { SFProjectDomain } from '../models/sf-project-rights'; import { SFProjectUserConfig, SF_PROJECT_USER_CONFIGS_COLLECTION } from '../models/sf-project-user-config'; import { QUESTION_MIGRATIONS } from './question-migrations'; @@ -100,7 +100,10 @@ export class QuestionService extends SFProjectDataService { bsonType: 'string' }, text: { - bsonType: ['null', 'string'] + bsonType: 'string' + }, + audioUrl: { + bsonType: 'string' }, dateModified: { bsonType: 'string' diff --git a/src/SIL.XForge.Scripture/ClientApp/src/app/checking/checking/checking-answers/checking-answers.component.html b/src/SIL.XForge.Scripture/ClientApp/src/app/checking/checking/checking-answers/checking-answers.component.html index 5704d6c2e1..433e71314e 100644 --- a/src/SIL.XForge.Scripture/ClientApp/src/app/checking/checking/checking-answers/checking-answers.component.html +++ b/src/SIL.XForge.Scripture/ClientApp/src/app/checking/checking/checking-answers/checking-answers.component.html @@ -209,6 +209,7 @@

-
- - {{ t("your_comment") }} - - @if (showValidationError) { - {{ t("comment_cannot_be_blank") }} - } - + +
- - + +
+ + +
diff --git a/src/SIL.XForge.Scripture/ClientApp/src/app/checking/checking/checking-answers/checking-comments/checking-comment-form/checking-comment-form.component.scss b/src/SIL.XForge.Scripture/ClientApp/src/app/checking/checking/checking-answers/checking-comments/checking-comment-form/checking-comment-form.component.scss index 50aa12bfe9..25cddc38ce 100644 --- a/src/SIL.XForge.Scripture/ClientApp/src/app/checking/checking/checking-answers/checking-comments/checking-comment-form/checking-comment-form.component.scss +++ b/src/SIL.XForge.Scripture/ClientApp/src/app/checking/checking/checking-answers/checking-comments/checking-comment-form/checking-comment-form.component.scss @@ -1,9 +1,12 @@ .form-actions { + padding-top: 8px; display: flex; - justify-content: flex-end; - gap: 10px; + justify-content: space-between; + .button-actions { + gap: 10px; + } } -.mat-mdc-form-field { - width: 100%; +.comment-input ::ng-deep textarea { + height: 24px; } diff --git a/src/SIL.XForge.Scripture/ClientApp/src/app/checking/checking/checking-answers/checking-comments/checking-comment-form/checking-comment-form.component.ts b/src/SIL.XForge.Scripture/ClientApp/src/app/checking/checking/checking-answers/checking-comments/checking-comment-form/checking-comment-form.component.ts index 487ab2bdf1..6ccaed42c1 100644 --- a/src/SIL.XForge.Scripture/ClientApp/src/app/checking/checking/checking-answers/checking-comments/checking-comment-form/checking-comment-form.component.ts +++ b/src/SIL.XForge.Scripture/ClientApp/src/app/checking/checking/checking-answers/checking-comments/checking-comment-form/checking-comment-form.component.ts @@ -1,6 +1,9 @@ -import { Component, EventEmitter, Input, Output } from '@angular/core'; -import { FormControl, FormGroup, Validators } from '@angular/forms'; -import { XFValidators } from 'xforge-common/xfvalidators'; +import { Component, EventEmitter, Input, Output, ViewChild } from '@angular/core'; +import { translate } from '@ngneat/transloco'; +import { Comment } from 'realtime-server/lib/esm/scriptureforge/models/comment'; +import { NoticeService } from 'xforge-common/notice.service'; +import { TextAndAudioComponent } from '../../../../text-and-audio/text-and-audio.component'; +import { AudioAttachment } from '../../../checking-audio-recorder/checking-audio-recorder.component'; @Component({ selector: 'app-checking-comment-form', @@ -8,36 +11,33 @@ import { XFValidators } from 'xforge-common/xfvalidators'; styleUrls: ['./checking-comment-form.component.scss'] }) export class CheckingCommentFormComponent { - @Input() set text(value: string | undefined) { - if (value != null) { - this.commentForm.controls.commentText.setValue(value); - } - } - - @Output() save: EventEmitter = new EventEmitter(); + @Input() comment?: Comment; + @Output() save: EventEmitter<{ text?: string; audio?: AudioAttachment }> = new EventEmitter<{ + text?: string; + audio?: AudioAttachment; + }>(); @Output() cancel: EventEmitter = new EventEmitter(); + @ViewChild(TextAndAudioComponent) textAndAudio?: TextAndAudioComponent; - commentForm = new FormGroup({ - commentText: new FormControl('', [Validators.required, XFValidators.someNonWhitespace]) - }); - - constructor() {} + constructor(private readonly noticeService: NoticeService) {} - submit(): void { - const commentText = this.commentForm.controls.commentText.value; - if (this.commentForm.valid && typeof commentText === 'string') { - this.save.emit(commentText); - this.commentForm.reset(); + async submit(): Promise { + if (this.textAndAudio != null) { + this.textAndAudio.suppressErrors = false; + if (this.textAndAudio.audioComponent?.isRecording) { + await this.textAndAudio.audioComponent.stopRecording(); + this.noticeService.show(translate('checking_answers.recording_automatically_stopped')); + } } + if (!this.textAndAudio?.hasTextOrAudio()) { + this.textAndAudio?.text.setErrors({ invalid: true }); + return; + } + const comment = { text: this.textAndAudio?.text.value, audio: this.textAndAudio?.audioAttachment }; + this.save.emit(comment); } submitCancel(): void { - this.commentForm.reset(); this.cancel.emit(); } - - get showValidationError(): boolean { - const control = this.commentForm.controls.commentText; - return control.invalid && control.touched; - } } diff --git a/src/SIL.XForge.Scripture/ClientApp/src/app/checking/checking/checking-answers/checking-comments/checking-comments.component.html b/src/SIL.XForge.Scripture/ClientApp/src/app/checking/checking/checking-answers/checking-comments/checking-comments.component.html index a3fda1efbd..ca0474a5ef 100644 --- a/src/SIL.XForge.Scripture/ClientApp/src/app/checking/checking/checking-answers/checking-comments/checking-comments.component.html +++ b/src/SIL.XForge.Scripture/ClientApp/src/app/checking/checking/checking-answers/checking-comments/checking-comments.component.html @@ -6,8 +6,21 @@ (i + 1 < maxCommentsToShow || commentCount === maxCommentsToShow || showAllComments) ) {
- {{ comment.text }} - - + @if (comment.audioUrl != null) { + + {{ audio.playing ? "stop" : "play_arrow" }} + + } + {{ comment.text }} +
+ - + +
@if (canEditComment(comment)) {
diff --git a/src/SIL.XForge.Scripture/ClientApp/src/app/checking/checking/checking-answers/checking-comments/checking-comments.component.scss b/src/SIL.XForge.Scripture/ClientApp/src/app/checking/checking/checking-answers/checking-comments/checking-comments.component.scss index d53c16d746..c9d9f9fb9a 100644 --- a/src/SIL.XForge.Scripture/ClientApp/src/app/checking/checking/checking-answers/checking-comments/checking-comments.component.scss +++ b/src/SIL.XForge.Scripture/ClientApp/src/app/checking/checking/checking-answers/checking-comments/checking-comments.component.scss @@ -3,6 +3,8 @@ .comment { display: flex; flex-wrap: wrap; + align-items: center; + column-gap: 8px; border-bottom: 1px solid $borderColor; padding: 10px 0; @@ -21,8 +23,8 @@ } } - .divider { - padding: 0 10px; + .comment-text { + flex: 1 0 50%; } .actions { diff --git a/src/SIL.XForge.Scripture/ClientApp/src/app/checking/checking/checking-answers/checking-comments/checking-comments.component.ts b/src/SIL.XForge.Scripture/ClientApp/src/app/checking/checking/checking-answers/checking-comments/checking-comments.component.ts index 6595fdd97a..a97f5dbaf8 100644 --- a/src/SIL.XForge.Scripture/ClientApp/src/app/checking/checking/checking-answers/checking-comments/checking-comments.component.ts +++ b/src/SIL.XForge.Scripture/ClientApp/src/app/checking/checking/checking-answers/checking-comments/checking-comments.component.ts @@ -13,12 +13,14 @@ import { SubscriptionDisposable } from 'xforge-common/subscription-disposable'; import { UserService } from 'xforge-common/user.service'; import { QuestionDoc } from '../../../../core/models/question-doc'; import { SFProjectUserConfigDoc } from '../../../../core/models/sf-project-user-config-doc'; +import { AudioAttachment } from '../../checking-audio-recorder/checking-audio-recorder.component'; export interface CommentAction { action: 'delete' | 'save' | 'show-form' | 'hide-form' | 'show-comments'; comment?: Comment; answer?: Answer; text?: string; + audio?: AudioAttachment; } @Component({ @@ -189,12 +191,13 @@ export class CheckingCommentsComponent extends SubscriptionDisposable implements }); } - submit(text: string): void { + submit(comment: { text?: string; audio?: AudioAttachment }): void { this.action.emit({ action: 'save', answer: this.answer, - text, - comment: this.activeComment + text: comment.text, + comment: this.activeComment, + audio: comment?.audio }); this.hideCommentForm(); this.showAllComments = true; diff --git a/src/SIL.XForge.Scripture/ClientApp/src/app/checking/checking/checking.component.spec.ts b/src/SIL.XForge.Scripture/ClientApp/src/app/checking/checking/checking.component.spec.ts index e1088d7f66..db10e0fcd0 100644 --- a/src/SIL.XForge.Scripture/ClientApp/src/app/checking/checking/checking.component.spec.ts +++ b/src/SIL.XForge.Scripture/ClientApp/src/app/checking/checking/checking.component.spec.ts @@ -1725,6 +1725,7 @@ describe('CheckingComponent', () => { env.selectQuestion(1); env.answerQuestion('Answer question to be commented on'); env.commentOnAnswer(0, 'Response to answer'); + env.waitForSliderUpdate(); expect(env.getAnswerComments(0).length).toBe(1); flush(); })); @@ -1735,9 +1736,11 @@ describe('CheckingComponent', () => { env.selectQuestion(14); env.answerQuestion('Answer question to be commented on'); env.commentOnAnswer(0, 'Response to answer'); + env.waitForSliderUpdate(); env.commentOnAnswer(0, 'Second comment to answer'); + env.waitForSliderUpdate(); env.clickButton(env.getEditCommentButton(0, 0)); - expect(env.commentFormTextFields.length).toEqual(1); + expect(env.getYourCommentField(0)).not.toBeNull(); env.setTextFieldValue(env.getYourCommentField(0), 'Edited comment'); env.clickButton(env.getSaveCommentButton(0)); env.waitForSliderUpdate(); @@ -1752,6 +1755,7 @@ describe('CheckingComponent', () => { env.selectQuestion(1); env.answerQuestion('Answer question to be commented on'); env.commentOnAnswer(0, 'Response to answer'); + env.waitForSliderUpdate(); expect(env.getAnswerComments(0).length).toBe(1); env.clickButton(env.getDeleteCommentButton(0, 0)); env.waitForSliderUpdate(); @@ -1759,16 +1763,72 @@ describe('CheckingComponent', () => { flush(); })); + it('can record audio for a comment', fakeAsync(() => { + const env = new TestEnvironment({ user: CHECKER_USER }); + env.selectQuestion(1); + env.answerQuestion('Answer question to be commented on'); + const resolveUpload$: Subject = env.resolveFileUploadSubject('blob://audio'); + env.commentOnAnswer(0, 'comment with audio', 'audioFile.mp3'); + resolveUpload$.next(); + env.waitForSliderUpdate(); + expect(env.component.answersPanel!.answers[0].comments[0].audioUrl).toEqual('blob://audio'); + env.waitForSliderUpdate(); + expect(env.getAnswerCommentAudio(0, 0)).not.toBeNull(); + })); + + it('can remove audio from a comment', fakeAsync(() => { + const env = new TestEnvironment({ user: CHECKER_USER }); + env.selectQuestion(1); + env.answerQuestion('Answer question to be commented on'); + const resolveUpload$: Subject = env.resolveFileUploadSubject('blob://audio'); + env.commentOnAnswer(0, 'comment with audio', 'audioFile.mp3'); + resolveUpload$.next(); + env.waitForSliderUpdate(); + expect(env.component.answersPanel!.answers[0].comments[0].audioUrl).toEqual('blob://audio'); + env.waitForSliderUpdate(); + expect(env.getAnswerCommentAudio(0, 0)).not.toBeNull(); + env.clickButton(env.getEditCommentButton(0, 0)); + env.clickButton(env.removeAudioButton); + env.clickButton(env.getSaveCommentButton(0)); + env.waitForSliderUpdate(); + verify( + mockedFileService.deleteFile(FileType.Audio, 'project01', QuestionDoc.COLLECTION, anything(), anything()) + ).once(); + })); + + it('will delete comment audio when comment is deleted', fakeAsync(() => { + const env = new TestEnvironment({ user: CHECKER_USER }); + env.selectQuestion(1); + const resolveUpload$: Subject = env.resolveFileUploadSubject('blob://audio'); + env.answerQuestion('Answer question to be commented on'); + env.commentOnAnswer(0, 'comment with audio', 'audioFile.mp3'); + resolveUpload$.next(); + env.waitForSliderUpdate(); + expect(env.component.answersPanel!.answers[0].comments[0].audioUrl).toEqual('blob://audio'); + env.waitForSliderUpdate(); + expect(env.getAnswerComments(0).length).toBe(1); + expect(env.getAnswerCommentAudio(0, 0)).not.toBeNull(); + env.clickButton(env.getDeleteCommentButton(0, 0)); + env.waitForSliderUpdate(); + expect(env.getAnswerComments(0).length).toBe(0); + verify( + mockedFileService.deleteFile(FileType.Audio, 'project01', QuestionDoc.COLLECTION, anything(), anything()) + ).once(); + flush(); + })); + it('comments only appear on the relevant answer', fakeAsync(() => { const env = new TestEnvironment({ user: CHECKER_USER }); env.selectQuestion(1); env.answerQuestion('Answer question to be commented on'); env.commentOnAnswer(0, 'First comment'); env.commentOnAnswer(0, 'Second comment'); + env.waitForSliderUpdate(); expect(env.getAnswerComments(0).length).toBe(2); env.selectQuestion(2); env.answerQuestion('Second answer question to be commented on'); env.commentOnAnswer(0, 'Third comment'); + env.waitForSliderUpdate(); expect(env.getAnswerComments(0).length).toBe(1); expect(env.getAnswerCommentText(0, 0)).toBe('Third comment'); env.selectQuestion(1); @@ -2845,10 +2905,17 @@ class TestEnvironment { tick(); } - commentOnAnswer(answerIndex: number, comment: string): void { + commentOnAnswer(answerIndex: number, comment: string, audioFilename?: string): void { this.clickButton(this.getAddCommentButton(answerIndex)); + if (this.getYourCommentField(answerIndex) == null) return; this.setTextFieldValue(this.getYourCommentField(answerIndex), comment); - this.clickButton(this.getSaveCommentButton(answerIndex)); + let commentAudio: AudioAttachment | undefined; + if (audioFilename != null) { + commentAudio = { status: 'processed', blob: getAudioBlob(), fileName: audioFilename }; + } + const commentsComponent = this.fixture.debugElement.query(By.css('#answer-comments'))! + .componentInstance as CheckingCommentsComponent; + commentsComponent.submit({ text: comment, audio: commentAudio }); this.waitForSliderUpdate(); } @@ -2909,6 +2976,11 @@ class TestEnvironment { return commentText.query(By.css('.comment-text')).nativeElement.textContent; } + getAnswerCommentAudio(answerIndex: number, commentIndex: number): DebugElement { + const comment = this.getAnswerComment(answerIndex, commentIndex); + return comment.query(By.css('.comment-audio')).nativeElement; + } + getDeleteCommentButton(answerIndex: number, commentIndex: number): DebugElement { return this.getAnswerComments(answerIndex)[commentIndex].query(By.css('.comment-delete')); } @@ -2935,7 +3007,7 @@ class TestEnvironment { } getYourCommentField(answerIndex: number): DebugElement { - return this.getAnswer(answerIndex).query(By.css('[formControlName="commentText"]')); + return this.getAnswer(answerIndex).query(By.css('textarea[formControlName="text"]')); } selectQuestion(/** indexed starting at 1 */ questionNumber: number, includeReadTimer: boolean = true): DebugElement { diff --git a/src/SIL.XForge.Scripture/ClientApp/src/app/checking/checking/checking.component.ts b/src/SIL.XForge.Scripture/ClientApp/src/app/checking/checking/checking.component.ts index d2c0734b72..de6a76dd05 100644 --- a/src/SIL.XForge.Scripture/ClientApp/src/app/checking/checking/checking.component.ts +++ b/src/SIL.XForge.Scripture/ClientApp/src/app/checking/checking/checking.component.ts @@ -11,12 +11,12 @@ import { AudioTiming } from 'realtime-server/lib/esm/scriptureforge/models/audio import { Comment } from 'realtime-server/lib/esm/scriptureforge/models/comment'; import { Question } from 'realtime-server/lib/esm/scriptureforge/models/question'; import { SFProjectProfile } from 'realtime-server/lib/esm/scriptureforge/models/sf-project'; -import { SF_PROJECT_RIGHTS, SFProjectDomain } from 'realtime-server/lib/esm/scriptureforge/models/sf-project-rights'; +import { SFProjectDomain, SF_PROJECT_RIGHTS } from 'realtime-server/lib/esm/scriptureforge/models/sf-project-rights'; import { SFProjectRole } from 'realtime-server/lib/esm/scriptureforge/models/sf-project-role'; import { getTextAudioId } from 'realtime-server/lib/esm/scriptureforge/models/text-audio'; import { TextInfo } from 'realtime-server/lib/esm/scriptureforge/models/text-info'; -import { toVerseRef, VerseRefData } from 'realtime-server/lib/esm/scriptureforge/models/verse-ref-data'; -import { asyncScheduler, combineLatest, merge, Subscription } from 'rxjs'; +import { VerseRefData, toVerseRef } from 'realtime-server/lib/esm/scriptureforge/models/verse-ref-data'; +import { Subscription, asyncScheduler, combineLatest, merge } from 'rxjs'; import { distinctUntilChanged, filter, map, startWith, throttleTime } from 'rxjs/operators'; import { DataLoadingComponent } from 'xforge-common/data-loading-component'; import { I18nService } from 'xforge-common/i18n.service'; @@ -39,11 +39,12 @@ import { SFProjectService } from '../../core/sf-project.service'; import { getVerseStrFromSegmentRef } from '../../shared/utils'; import { ChapterAudioDialogData } from '../chapter-audio-dialog/chapter-audio-dialog.component'; import { ChapterAudioDialogService } from '../chapter-audio-dialog/chapter-audio-dialog.service'; -import { BookChapter, CheckingUtils, isQuestionScope, QuestionScope } from '../checking.utils'; +import { BookChapter, CheckingUtils, QuestionScope, isQuestionScope } from '../checking.utils'; import { QuestionDialogData } from '../question-dialog/question-dialog.component'; import { QuestionDialogService } from '../question-dialog/question-dialog.service'; import { AnswerAction, CheckingAnswersComponent } from './checking-answers/checking-answers.component'; import { CommentAction } from './checking-answers/checking-comments/checking-comments.component'; +import { AudioAttachment } from './checking-audio-recorder/checking-audio-recorder.component'; import { CheckingQuestionsService, PreCreationQuestionData, QuestionFilter } from './checking-questions.service'; import { CheckingQuestionsComponent, QuestionChangedEvent } from './checking-questions/checking-questions.component'; import { CheckingScriptureAudioPlayerComponent } from './checking-scripture-audio-player/checking-scripture-audio-player.component'; @@ -753,29 +754,13 @@ export class CheckingComponent extends DataLoadingComponent implements OnInit, A answer.selectionStartClipped = answerAction.selectionStartClipped; answer.selectionEndClipped = answerAction.selectionEndClipped; answer.dateModified = dateNow; - if (answerAction.audio != null) { - if (answerAction.audio.fileName != null && answerAction.audio.blob != null) { - if (answerAction.questionDoc != null) { - // Get the amended filename and save it against the answer - const urlResult = await answerAction.questionDoc.uploadFile( - FileType.Audio, - answer.dataId, - answerAction.audio.blob, - answerAction.audio.fileName - ); - if (urlResult == null) { - break; - } - answer.audioUrl = urlResult; - } - } else if (answerAction.audio.status === 'reset') { - answer.audioUrl = undefined; + // add the audio url + if (await this.addAudioUrl(answer, answerAction.questionDoc, answerAction.audio)) { + this.saveAnswer(answer, answerAction.questionDoc); + if (answerAction.savedCallback != null) { + answerAction.savedCallback(); } } - this.saveAnswer(answer, answerAction.questionDoc); - if (answerAction.savedCallback != null) { - answerAction.savedCallback(); - } break; case 'delete': if (answerAction.answer != null) { @@ -818,11 +803,10 @@ export class CheckingComponent extends DataLoadingComponent implements OnInit, A this.isQuestionsOverlayVisible = visible; } - commentAction(commentAction: CommentAction): void { + async commentAction(commentAction: CommentAction): Promise { if (this.questionsList == null) { return; } - switch (commentAction.action) { case 'save': if (commentAction.answer != null) { @@ -840,7 +824,9 @@ export class CheckingComponent extends DataLoadingComponent implements OnInit, A } comment.text = commentAction.text; comment.dateModified = dateNow; - this.saveComment(commentAction.answer, comment); + if (await this.addAudioUrl(comment, this.questionsList.activeQuestionDoc!, commentAction.audio)) { + this.saveComment(commentAction.answer, comment); + } } break; case 'show-comments': @@ -1241,6 +1227,23 @@ export class CheckingComponent extends DataLoadingComponent implements OnInit, A this.setQuestionFilter(QuestionFilter.None); } + /** Upload the attached audio and updates the url on the comment if one exists. */ + private async addAudioUrl(comment: Comment, questionDoc?: QuestionDoc, audio?: AudioAttachment): Promise { + if (audio != null) { + if (audio.fileName != null && audio.blob != null) { + if (questionDoc != null) { + // Get the amended filename and save it against the answer + const urlResult = await questionDoc.uploadFile(FileType.Audio, comment.dataId, audio.blob, audio.fileName); + if (urlResult == null) return false; + comment.audioUrl = urlResult; + } + } else if (audio.status === 'reset') { + comment.audioUrl = undefined; + } + } + return true; + } + private saveAnswer(answer: Answer, questionDoc: QuestionDoc | undefined): void { if (this.questionsList == null || questionDoc?.data == null) { return; @@ -1294,11 +1297,16 @@ export class CheckingComponent extends DataLoadingComponent implements OnInit, A const answerIndex = this.getAnswerIndex(answer); const commentIndex = answer.comments.findIndex(c => c.dataId === comment.dataId); if (commentIndex >= 0) { - activeQuestionDoc.submitJson0Op(op => + const deleteAudio: boolean = answer.comments[commentIndex].audioUrl != null && answer.audioUrl == null; + const submitPromise: Promise = activeQuestionDoc.submitJson0Op(op => op .set(q => q.answers[answerIndex].comments[commentIndex].text, comment.text) + .set(q => q.answers[answerIndex].comments[commentIndex].audioUrl, comment.audioUrl) .set(q => q.answers[answerIndex].comments[commentIndex].dateModified, comment.dateModified) ); + if (deleteAudio) { + submitPromise.then(() => activeQuestionDoc.deleteFile(FileType.Audio, comment.dataId, comment.ownerRef)); + } } else { activeQuestionDoc.submitJson0Op(op => op.insert(q => q.answers[answerIndex].comments, 0, comment)); } @@ -1316,7 +1324,9 @@ export class CheckingComponent extends DataLoadingComponent implements OnInit, A const answerIndex = this.getAnswerIndex(answer); const commentIndex = answer.comments.findIndex(c => c.dataId === comment.dataId); if (commentIndex >= 0) { - activeQuestionDoc.submitJson0Op(op => op.set(q => q.answers[answerIndex].comments[commentIndex].deleted, true)); + activeQuestionDoc + .submitJson0Op(op => op.set(q => q.answers[answerIndex].comments[commentIndex].deleted, true)) + .then(() => activeQuestionDoc.deleteFile(FileType.Audio, comment.dataId, comment.ownerRef)); } } diff --git a/src/SIL.XForge.Scripture/Models/Answer.cs b/src/SIL.XForge.Scripture/Models/Answer.cs index 131efced06..b67d2b4276 100644 --- a/src/SIL.XForge.Scripture/Models/Answer.cs +++ b/src/SIL.XForge.Scripture/Models/Answer.cs @@ -6,7 +6,6 @@ public class Answer : Comment { public VerseRefData? VerseRef { get; set; } public string? ScriptureText { get; set; } - public string? AudioUrl { get; set; } public List Likes { get; set; } = new List(); public List Comments { get; set; } = new List(); public string Status { get; set; } = AnswerStatus.None; diff --git a/src/SIL.XForge.Scripture/Models/Comment.cs b/src/SIL.XForge.Scripture/Models/Comment.cs index 02f4bb769e..a80957d1d3 100644 --- a/src/SIL.XForge.Scripture/Models/Comment.cs +++ b/src/SIL.XForge.Scripture/Models/Comment.cs @@ -14,6 +14,7 @@ public class Comment : IOwnedData /// public string? SyncUserRef { get; set; } public string? Text { get; set; } + public string? AudioUrl { get; set; } public DateTime DateModified { get; set; } public DateTime DateCreated { get; set; } diff --git a/test/SIL.XForge.Scripture.Tests/Services/ParatextNotesMapperTests.cs b/test/SIL.XForge.Scripture.Tests/Services/ParatextNotesMapperTests.cs index 0deb8ea3cb..6a098a09d7 100644 --- a/test/SIL.XForge.Scripture.Tests/Services/ParatextNotesMapperTests.cs +++ b/test/SIL.XForge.Scripture.Tests/Services/ParatextNotesMapperTests.cs @@ -269,7 +269,7 @@ await TestEnvironment.GetQuestionDocsAsync(conn), 3 - Test comment 1. + - xForge audio-only response - @@ -1017,7 +1017,7 @@ public void AddData( Id = "project01:question01", DataId = "question01", VerseRef = new VerseRefData(40, 1, 1), - Text = useAudioResponses ? "" : "Test question?", + Text = useAudioResponses ? null : "Test question?", Answers = { new Answer @@ -1026,7 +1026,7 @@ public void AddData( OwnerRef = "user02", SyncUserRef = answerSyncUserId1, DateCreated = new DateTime(2019, 1, 1, 8, 0, 0, DateTimeKind.Utc), - Text = useAudioResponses ? "" : "Test answer 1.", + Text = useAudioResponses ? null : "Test answer 1.", Comments = { new Comment @@ -1035,7 +1035,7 @@ public void AddData( OwnerRef = "user03", SyncUserRef = commentSyncUserId1, DateCreated = new DateTime(2019, 1, 1, 9, 0, 0, DateTimeKind.Utc), - Text = "Test comment 1.", + Text = useAudioResponses ? null : "Test comment 1.", }, }, }, From 5967ba774097c348ffe786ab5703dcf8105e11ab Mon Sep 17 00:00:00 2001 From: Raymond Luong Date: Thu, 17 Oct 2024 16:40:12 -0600 Subject: [PATCH 02/10] Fix stories --- .../checking-comment-form.stories.ts | 24 +++++++++++++++---- .../src/assets/i18n/checking_en.json | 1 - 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/src/SIL.XForge.Scripture/ClientApp/src/app/checking/checking/checking-answers/checking-comments/checking-comment-form/checking-comment-form.stories.ts b/src/SIL.XForge.Scripture/ClientApp/src/app/checking/checking/checking-answers/checking-comments/checking-comment-form/checking-comment-form.stories.ts index 734eaccc73..514201bc3f 100644 --- a/src/SIL.XForge.Scripture/ClientApp/src/app/checking/checking/checking-answers/checking-comments/checking-comment-form/checking-comment-form.stories.ts +++ b/src/SIL.XForge.Scripture/ClientApp/src/app/checking/checking/checking-answers/checking-comments/checking-comment-form/checking-comment-form.stories.ts @@ -4,12 +4,20 @@ import { expect } from '@storybook/jest'; import { userEvent, within } from '@storybook/testing-library'; import { I18nStoryModule } from 'xforge-common/i18n-story.module'; import { UICommonModule } from 'xforge-common/ui-common.module'; +import { AttachAudioComponent } from '../../../../attach-audio/attach-audio.component'; +import { TextAndAudioComponent } from '../../../../text-and-audio/text-and-audio.component'; +import { CheckingAudioRecorderComponent } from '../../../checking-audio-recorder/checking-audio-recorder.component'; import { CheckingCommentFormComponent } from './checking-comment-form.component'; const meta: Meta = { title: 'Checking/Comments/Comment Form', component: CheckingCommentFormComponent, - decorators: [moduleMetadata({ imports: [CommonModule, UICommonModule, I18nStoryModule] })] + decorators: [ + moduleMetadata({ + imports: [CommonModule, UICommonModule, I18nStoryModule], + declarations: [TextAndAudioComponent, AttachAudioComponent, CheckingAudioRecorderComponent] + }) + ] }; export default meta; @@ -24,7 +32,16 @@ export const NewForm: Story = { }; export const EditForm: Story = { - args: { text: 'This is a comment' }, + args: { + comment: { + dataId: 'c01', + ownerRef: 'user01', + text: 'This is a comment', + deleted: false, + dateCreated: '', + dateModified: '' + } + }, parameters: { // Disabled for the same reason the story above chromatic: { disableSnapshot: true } @@ -32,14 +49,13 @@ export const EditForm: Story = { }; export const InvalidForm: Story = { - args: { text: '' }, play: async ({ canvasElement }) => { const canvas = within(canvasElement); // Only necessary because the autofocus directive has to use setTimeout await new Promise(resolve => setTimeout(resolve, 0)); const saveButton: HTMLElement = canvas.getByRole('button', { name: /Save/i }); await userEvent.click(saveButton); - const error: HTMLElement = canvas.getByText(/You need to enter your comment before saving/i); + const error: HTMLElement = canvas.getByText(/Provide text or audio before saving/i); expect(error).toBeInTheDocument(); } }; diff --git a/src/SIL.XForge.Scripture/ClientApp/src/assets/i18n/checking_en.json b/src/SIL.XForge.Scripture/ClientApp/src/assets/i18n/checking_en.json index cd551a65cc..05e25a6fe7 100644 --- a/src/SIL.XForge.Scripture/ClientApp/src/assets/i18n/checking_en.json +++ b/src/SIL.XForge.Scripture/ClientApp/src/assets/i18n/checking_en.json @@ -266,7 +266,6 @@ }, "checking_comment_form": { "cancel": "Cancel", - "comment_cannot_be_blank": "You need to enter your comment before saving", "save_comment": "Save comment", "your_comment": "Your comment" }, From 22a8d208b8aeb48b15e046584a54244dc9229e0c Mon Sep 17 00:00:00 2001 From: Raymond Luong Date: Fri, 18 Oct 2024 12:19:22 -0600 Subject: [PATCH 03/10] Add text placeholder if comment text missing --- .../checking-comments/checking-comments.component.html | 2 +- .../src/app/checking/checking/checking.component.spec.ts | 4 +++- .../ClientApp/src/assets/i18n/checking_en.json | 1 + 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/SIL.XForge.Scripture/ClientApp/src/app/checking/checking/checking-answers/checking-comments/checking-comments.component.html b/src/SIL.XForge.Scripture/ClientApp/src/app/checking/checking/checking-answers/checking-comments/checking-comments.component.html index ca0474a5ef..3e5496da78 100644 --- a/src/SIL.XForge.Scripture/ClientApp/src/app/checking/checking/checking-answers/checking-comments/checking-comments.component.html +++ b/src/SIL.XForge.Scripture/ClientApp/src/app/checking/checking/checking-answers/checking-comments/checking-comments.component.html @@ -16,7 +16,7 @@ {{ audio.playing ? "stop" : "play_arrow" }} } - {{ comment.text }} + {{ comment.text ? comment.text : "- " + t("audio_comment") + " -" }}
- diff --git a/src/SIL.XForge.Scripture/ClientApp/src/app/checking/checking/checking.component.spec.ts b/src/SIL.XForge.Scripture/ClientApp/src/app/checking/checking/checking.component.spec.ts index db10e0fcd0..831c6730c4 100644 --- a/src/SIL.XForge.Scripture/ClientApp/src/app/checking/checking/checking.component.spec.ts +++ b/src/SIL.XForge.Scripture/ClientApp/src/app/checking/checking/checking.component.spec.ts @@ -1768,12 +1768,13 @@ describe('CheckingComponent', () => { env.selectQuestion(1); env.answerQuestion('Answer question to be commented on'); const resolveUpload$: Subject = env.resolveFileUploadSubject('blob://audio'); - env.commentOnAnswer(0, 'comment with audio', 'audioFile.mp3'); + env.commentOnAnswer(0, '', 'audioFile.mp3'); resolveUpload$.next(); env.waitForSliderUpdate(); expect(env.component.answersPanel!.answers[0].comments[0].audioUrl).toEqual('blob://audio'); env.waitForSliderUpdate(); expect(env.getAnswerCommentAudio(0, 0)).not.toBeNull(); + expect(env.getAnswerCommentText(0, 0)).toContain('Audio comment'); })); it('can remove audio from a comment', fakeAsync(() => { @@ -1786,6 +1787,7 @@ describe('CheckingComponent', () => { env.waitForSliderUpdate(); expect(env.component.answersPanel!.answers[0].comments[0].audioUrl).toEqual('blob://audio'); env.waitForSliderUpdate(); + expect(env.getAnswerCommentText(0, 0)).toContain('comment with audio'); expect(env.getAnswerCommentAudio(0, 0)).not.toBeNull(); env.clickButton(env.getEditCommentButton(0, 0)); env.clickButton(env.removeAudioButton); diff --git a/src/SIL.XForge.Scripture/ClientApp/src/assets/i18n/checking_en.json b/src/SIL.XForge.Scripture/ClientApp/src/assets/i18n/checking_en.json index 05e25a6fe7..fe6a7c214c 100644 --- a/src/SIL.XForge.Scripture/ClientApp/src/assets/i18n/checking_en.json +++ b/src/SIL.XForge.Scripture/ClientApp/src/assets/i18n/checking_en.json @@ -271,6 +271,7 @@ }, "checking_comments": { "add_a_comment": "Add a comment", + "audio_comment": "Audio comment", "delete": "Delete", "confirm_delete": "Are you sure you want to delete this comment?", "edit": "Edit", From a8e4ddb5c004af93706b889374cf77baa3d9a408 Mon Sep 17 00:00:00 2001 From: Raymond Luong Date: Fri, 18 Oct 2024 12:59:51 -0600 Subject: [PATCH 04/10] Wait for button to appear --- .../audio-recorder-dialog.component.spec.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/SIL.XForge.Scripture/ClientApp/src/app/shared/audio-recorder-dialog/audio-recorder-dialog.component.spec.ts b/src/SIL.XForge.Scripture/ClientApp/src/app/shared/audio-recorder-dialog/audio-recorder-dialog.component.spec.ts index 953083860d..626a5b7221 100644 --- a/src/SIL.XForge.Scripture/ClientApp/src/app/shared/audio-recorder-dialog/audio-recorder-dialog.component.spec.ts +++ b/src/SIL.XForge.Scripture/ClientApp/src/app/shared/audio-recorder-dialog/audio-recorder-dialog.component.spec.ts @@ -61,6 +61,7 @@ describe('AudioRecorderDialogComponent', () => { it('can record', async () => { const env = new TestEnvironment(); + env.waitForRecorder(300); expect(env.recordButton).toBeTruthy(); expect(env.stopRecordingButton).toBeFalsy(); expect(env.recordingIndicator).toBeNull(); From 9097e289a0d028f5f2977e3dc60ca916a722d9c6 Mon Sep 17 00:00:00 2001 From: Raymond Luong Date: Mon, 21 Oct 2024 14:22:26 -0600 Subject: [PATCH 05/10] Convert button text to icons --- .../checking-answers.component.html | 54 ++++++++------- .../checking-answers.component.scss | 16 ++--- .../checking-comments.component.html | 66 ++++++++++++------- .../checking-comments.component.scss | 24 ++++--- .../single-button-audio-player.component.ts | 8 +++ 5 files changed, 101 insertions(+), 67 deletions(-) diff --git a/src/SIL.XForge.Scripture/ClientApp/src/app/checking/checking/checking-answers/checking-answers.component.html b/src/SIL.XForge.Scripture/ClientApp/src/app/checking/checking/checking-answers/checking-answers.component.html index 433e71314e..33baad1254 100644 --- a/src/SIL.XForge.Scripture/ClientApp/src/app/checking/checking/checking-answers/checking-answers.component.html +++ b/src/SIL.XForge.Scripture/ClientApp/src/app/checking/checking/checking-answers/checking-answers.component.html @@ -159,12 +159,6 @@

} - @if (comment.audioUrl != null) { - - {{ audio.playing ? "stop" : "play_arrow" }} - - } - {{ comment.text ? comment.text : "- " + t("audio_comment") + " -" }} -
- - - -
-
- @if (canEditComment(comment)) { - +
+ @if (comment.audioUrl != null) { + + {{ audio.playing ? "stop" : "play_arrow" }} + } - @if (canDeleteComment(comment)) { - + @if (comment.text != null) { + {{ comment.text }} }
+
} @if (commentFormVisible && activeComment?.dataId === comment.dataId) { @@ -55,6 +72,7 @@ @if (!commentFormVisible && (commentCount <= maxCommentsToShow || showAllComments) && canAddComment) { } diff --git a/src/SIL.XForge.Scripture/ClientApp/src/app/checking/checking/checking-answers/checking-comments/checking-comments.component.scss b/src/SIL.XForge.Scripture/ClientApp/src/app/checking/checking/checking-answers/checking-comments/checking-comments.component.scss index c9d9f9fb9a..a41b426e72 100644 --- a/src/SIL.XForge.Scripture/ClientApp/src/app/checking/checking/checking-answers/checking-comments/checking-comments.component.scss +++ b/src/SIL.XForge.Scripture/ClientApp/src/app/checking/checking/checking-answers/checking-comments/checking-comments.component.scss @@ -23,22 +23,20 @@ } } - .comment-text { - flex: 1 0 50%; + .comment-content { + display: flex; + align-items: center; + column-gap: 8px; } - .actions { - flex: 1; + .comment-footer { display: flex; - justify-content: flex-end; - - button { - height: auto; - } + justify-content: space-between; + flex-grow: 1; + } - .mat-mdc-button { - line-height: unset; - padding: 0 8px; - } + .owner { + display: flex; + align-items: center; } } diff --git a/src/SIL.XForge.Scripture/ClientApp/src/app/checking/checking/single-button-audio-player/single-button-audio-player.component.ts b/src/SIL.XForge.Scripture/ClientApp/src/app/checking/checking/single-button-audio-player/single-button-audio-player.component.ts index c1acc57014..200ebcea9c 100644 --- a/src/SIL.XForge.Scripture/ClientApp/src/app/checking/checking/single-button-audio-player/single-button-audio-player.component.ts +++ b/src/SIL.XForge.Scripture/ClientApp/src/app/checking/checking/single-button-audio-player/single-button-audio-player.component.ts @@ -49,6 +49,14 @@ export class SingleButtonAudioPlayerComponent extends AudioPlayerBaseComponent i this.audio?.stop(); } + togglePlay(): void { + if (this.audio?.isPlaying) { + this.stop(); + } else { + this.play(); + } + } + ngOnChanges(): void { this.isAudioAvailable$.next(false); this.hasFinishedPlayingOnce$.next(false); From 25c98704dde99262f477c1266168f6ed0522c812 Mon Sep 17 00:00:00 2001 From: Raymond Luong Date: Mon, 21 Oct 2024 14:49:03 -0600 Subject: [PATCH 06/10] Fix tests --- .../checking-comments.component.html | 2 ++ .../checking-comments/checking-comments.stories.ts | 12 ++++++------ .../app/checking/checking/checking.component.spec.ts | 2 +- .../ClientApp/src/assets/i18n/checking_en.json | 1 - 4 files changed, 9 insertions(+), 8 deletions(-) diff --git a/src/SIL.XForge.Scripture/ClientApp/src/app/checking/checking/checking-answers/checking-comments/checking-comments.component.html b/src/SIL.XForge.Scripture/ClientApp/src/app/checking/checking/checking-answers/checking-comments/checking-comments.component.html index 7bc2d4f06f..8cf52dfa33 100644 --- a/src/SIL.XForge.Scripture/ClientApp/src/app/checking/checking/checking-answers/checking-comments/checking-comments.component.html +++ b/src/SIL.XForge.Scripture/ClientApp/src/app/checking/checking/checking-answers/checking-comments/checking-comments.component.html @@ -29,6 +29,7 @@ type="button" (click)="editComment(comment)" class="comment-edit" + data-testid="comment-edit" [matTooltip]="t('edit')" > edit @@ -40,6 +41,7 @@ type="button" (click)="deleteCommentClicked(comment)" class="comment-delete" + data-testid="answer-delete" [matTooltip]="t('delete')" > delete diff --git a/src/SIL.XForge.Scripture/ClientApp/src/app/checking/checking/checking-answers/checking-comments/checking-comments.stories.ts b/src/SIL.XForge.Scripture/ClientApp/src/app/checking/checking/checking-answers/checking-comments/checking-comments.stories.ts index 3b5f0338c9..0d892d2492 100644 --- a/src/SIL.XForge.Scripture/ClientApp/src/app/checking/checking/checking-answers/checking-comments/checking-comments.stories.ts +++ b/src/SIL.XForge.Scripture/ClientApp/src/app/checking/checking/checking-answers/checking-comments/checking-comments.stories.ts @@ -91,10 +91,10 @@ export const OwnComments: Story = { play: async ({ canvasElement }) => { const root = within(canvasElement); - const editButtons = root.getAllByRole('button', { name: /Edit/i }); + const editButtons = root.getAllByTestId('comment-edit'); expect(editButtons.length).toBe(3); - const deleteButtons = root.getAllByRole('button', { name: /Delete/i }); + const deleteButtons = root.getAllByTestId('comment-delete'); expect(deleteButtons.length).toBe(3); } }; @@ -109,10 +109,10 @@ export const OthersComments: Story = { play: async ({ canvasElement }) => { const root = within(canvasElement); - const editButtons = root.queryAllByRole('button', { name: /Edit/i }); + const editButtons = root.queryAllByTestId('comment-edit'); expect(editButtons.length).toBe(0); - const deleteButtons = root.queryAllByRole('button', { name: /Delete/i }); + const deleteButtons = root.queryAllByTestId('comment-delete'); expect(deleteButtons.length).toBe(0); } }; @@ -133,10 +133,10 @@ export const Admin: Story = { play: async ({ canvasElement }) => { const root = within(canvasElement); - const editButtons = root.getAllByRole('button', { name: /Edit/i }); + const editButtons = root.getAllByTestId('comment-edit'); expect(editButtons.length).toBe(1); - const deleteButtons = root.getAllByRole('button', { name: /Delete/i }); + const deleteButtons = root.getAllByTestId('comment-delete'); expect(deleteButtons.length).toBe(3); } }; diff --git a/src/SIL.XForge.Scripture/ClientApp/src/app/checking/checking/checking.component.spec.ts b/src/SIL.XForge.Scripture/ClientApp/src/app/checking/checking/checking.component.spec.ts index 831c6730c4..8de69afb96 100644 --- a/src/SIL.XForge.Scripture/ClientApp/src/app/checking/checking/checking.component.spec.ts +++ b/src/SIL.XForge.Scripture/ClientApp/src/app/checking/checking/checking.component.spec.ts @@ -1774,7 +1774,7 @@ describe('CheckingComponent', () => { expect(env.component.answersPanel!.answers[0].comments[0].audioUrl).toEqual('blob://audio'); env.waitForSliderUpdate(); expect(env.getAnswerCommentAudio(0, 0)).not.toBeNull(); - expect(env.getAnswerCommentText(0, 0)).toContain('Audio comment'); + expect(env.getAnswerCommentText(0, 0)).toBe(''); })); it('can remove audio from a comment', fakeAsync(() => { diff --git a/src/SIL.XForge.Scripture/ClientApp/src/assets/i18n/checking_en.json b/src/SIL.XForge.Scripture/ClientApp/src/assets/i18n/checking_en.json index fe6a7c214c..05e25a6fe7 100644 --- a/src/SIL.XForge.Scripture/ClientApp/src/assets/i18n/checking_en.json +++ b/src/SIL.XForge.Scripture/ClientApp/src/assets/i18n/checking_en.json @@ -271,7 +271,6 @@ }, "checking_comments": { "add_a_comment": "Add a comment", - "audio_comment": "Audio comment", "delete": "Delete", "confirm_delete": "Are you sure you want to delete this comment?", "edit": "Edit", From b8343929684dfbd8c37bcd116dff9b444c625af7 Mon Sep 17 00:00:00 2001 From: Raymond Luong Date: Mon, 21 Oct 2024 15:04:32 -0600 Subject: [PATCH 07/10] Fix storybook tests --- .../checking-comments/checking-comments.component.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/SIL.XForge.Scripture/ClientApp/src/app/checking/checking/checking-answers/checking-comments/checking-comments.component.html b/src/SIL.XForge.Scripture/ClientApp/src/app/checking/checking/checking-answers/checking-comments/checking-comments.component.html index 8cf52dfa33..4f8f16f08f 100644 --- a/src/SIL.XForge.Scripture/ClientApp/src/app/checking/checking/checking-answers/checking-comments/checking-comments.component.html +++ b/src/SIL.XForge.Scripture/ClientApp/src/app/checking/checking/checking-answers/checking-comments/checking-comments.component.html @@ -41,7 +41,7 @@ type="button" (click)="deleteCommentClicked(comment)" class="comment-delete" - data-testid="answer-delete" + data-testid="comment-delete" [matTooltip]="t('delete')" > delete From 28be54aa62133de79cd4c6399a7038c0f7664ac0 Mon Sep 17 00:00:00 2001 From: Raymond Luong Date: Tue, 22 Oct 2024 15:02:39 -0600 Subject: [PATCH 08/10] Improve comment layout --- .../checking-comments.component.html | 30 +++++++++---------- .../checking-comments.component.scss | 22 ++++++++++---- .../checking/checking-global-vars.scss | 1 + .../xforge-common/owner/owner.component.scss | 1 + 4 files changed, 32 insertions(+), 22 deletions(-) diff --git a/src/SIL.XForge.Scripture/ClientApp/src/app/checking/checking/checking-answers/checking-comments/checking-comments.component.html b/src/SIL.XForge.Scripture/ClientApp/src/app/checking/checking/checking-answers/checking-comments/checking-comments.component.html index 4f8f16f08f..aa89efb57e 100644 --- a/src/SIL.XForge.Scripture/ClientApp/src/app/checking/checking/checking-answers/checking-comments/checking-comments.component.html +++ b/src/SIL.XForge.Scripture/ClientApp/src/app/checking/checking/checking-answers/checking-comments/checking-comments.component.html @@ -5,22 +5,20 @@ (activeComment == null || (commentFormVisible && activeComment.dataId !== comment.dataId)) && (i + 1 < maxCommentsToShow || commentCount === maxCommentsToShow || showAllComments) ) { -
-
- @if (comment.audioUrl != null) { - - {{ audio.playing ? "stop" : "play_arrow" }} - - } - @if (comment.text != null) { - {{ comment.text }} - } -
+
+ @if (comment.audioUrl != null) { + + {{ audio.playing ? "stop" : "play_arrow" }} + + } + @if (comment.text != null) { + {{ comment.text }} + }