MOBILE-4061 core: Create a new message component to fix animations

main
Pau Ferrer Ocaña 2022-06-09 11:01:59 +02:00
parent ef574e7e63
commit e337bc64d5
19 changed files with 378 additions and 336 deletions

View File

@ -81,47 +81,16 @@
<ion-icon name="fas-arrow-down" aria-hidden="true"></ion-icon>
</ion-chip>
<ion-item class="ion-text-wrap addon-message" (longPress)="copyMessage(message)"
[class.addon-message-mine]="message.useridfrom == currentUserId"
[class.addon-message-not-mine]="message.useridfrom != currentUserId"
[class.addon-message-no-user]="!message.showUserData"
[@coreSlideInOut]="message.useridfrom == currentUserId ? '' : 'fromLeft'">
<ion-label>
<!-- User data. -->
<div *ngIf="message.showUserData" class="item-heading addon-message-user">
<core-user-avatar slot="start" [user]="members[message.useridfrom]" [linkProfile]="false" aria-hidden="true">
</core-user-avatar>
<div>{{ members[message.useridfrom].fullname }}</div>
</div>
<div *ngIf="!message.showUserData" class="sr-only">
{{ message.useridfrom == currentUserId
? ('addon.messages.you' | translate)
: members[message.useridfrom].fullname }}
</div>
<!-- Some messages have <p> and some others don't. Add a <p> so they all have same styles. -->
<div class="addon-message-text">
<core-format-text (afterRender)="last && scrollToBottom()" [text]="message.text" contextLevel="system"
[contextInstanceId]="0"></core-format-text>
</div>
</ion-label>
<ion-note *ngIf="!message.pending" slot="end">{{ message.timecreated | coreFormatDate: "strftimetime" }}</ion-note>
<ion-note *ngIf="message.pending" slot="end">
<ion-icon name="fas-clock" [attr.aria-label]="'core.notsent' | translate" role="status"></ion-icon>
</ion-note>
<ion-button fill="clear" *ngIf="!message.sending && showDelete" (click)="deleteMessage(message, index)"
class="addon-messages-delete-button" [@coreSlideInOut]="'fromRight'"
[attr.aria-label]=" 'addon.messages.deletemessage' | translate" slot="end">
<ion-icon name="fas-trash" color="danger" slot="icon-only" aria-hidden="true"></ion-icon>
</ion-button>
<div class="tail" *ngIf="message.showTail"></div>
</ion-item>
<core-message [message]="message" [user]="members[message.useridfrom]" (afterRender)="last && scrollToBottom()"
[text]="message.text" (onDeleteMessage)="deleteMessage(message, index)" [showDelete]="showDelete"
[time]="message.timecreated">
</core-message>
</ng-container>
</ion-list>
<core-empty-box *ngIf="!messages || messages.length <= 0" icon="far-comments"
[message]="'addon.messages.nomessagesfound' | translate"></core-empty-box>
[message]="'addon.messages.nomessagesfound' | translate">
</core-empty-box>
</core-loading>
<!-- Scroll bottom. -->
<ion-fab slot="fixed" core-fab vertical="bottom" horizontal="end" *ngIf="loaded && newMessages > 0">

View File

@ -37,7 +37,6 @@ import { CoreApp } from '@services/app';
import { CoreInfiniteLoadingComponent } from '@components/infinite-loading/infinite-loading';
import { Md5 } from 'ts-md5/dist/md5';
import moment from 'moment';
import { CoreAnimations } from '@components/animations';
import { CoreError } from '@classes/errors/error';
import { Translate } from '@singletons';
import { CoreNavigator } from '@services/navigator';
@ -53,7 +52,6 @@ import { CoreDom } from '@singletons/dom';
@Component({
selector: 'page-addon-messages-discussion',
templateUrl: 'discussion.html',
animations: [CoreAnimations.SLIDE_IN_OUT],
styleUrls: ['discussion.scss'],
})
export class AddonMessagesDiscussionPage implements OnInit, OnDestroy, AfterViewInit {
@ -305,7 +303,7 @@ export class AddonMessagesDiscussionPage implements OnInit, OnDestroy, AfterView
} else {
if (this.userId) {
// Fake the user member info.
promises.push(CoreUser.getProfile(this.userId!).then(async (user) => {
promises.push(CoreUser.getProfile(this.userId).then(async (user) => {
this.otherMember = {
id: user.id,
fullname: user.fullname,
@ -524,7 +522,7 @@ export class AddonMessagesDiscussionPage implements OnInit, OnDestroy, AfterView
return;
}
const messages = Array.from(this.hostElement.querySelectorAll('.addon-message-not-mine'))
const messages = Array.from(this.hostElement.querySelectorAll('core-message:not(.is-mine)'))
.slice(-this.newMessages)
.reverse();
@ -555,7 +553,7 @@ export class AddonMessagesDiscussionPage implements OnInit, OnDestroy, AfterView
// Try to get the conversationId if we don't have it.
if (!conversationId && userId) {
try {
if (userId == this.currentUserId && AddonMessages.isSelfConversationEnabled()) {
if (userId === this.currentUserId && AddonMessages.isSelfConversationEnabled()) {
fallbackConversation = await AddonMessages.getSelfConversation();
} else {
fallbackConversation = await AddonMessages.getConversationBetweenUsers(userId, undefined, true);
@ -563,7 +561,7 @@ export class AddonMessagesDiscussionPage implements OnInit, OnDestroy, AfterView
conversationId = fallbackConversation.id;
} catch (error) {
// Probably conversation does not exist or user is offline. Try to load offline messages.
this.isSelf = userId == this.currentUserId;
this.isSelf = userId === this.currentUserId;
const messages = await AddonMessagesOffline.getMessages(userId);
@ -584,11 +582,15 @@ export class AddonMessagesDiscussionPage implements OnInit, OnDestroy, AfterView
}
}
if (!conversationId) {
return false;
}
// Retrieve the conversation. Invalidate data first to get the right unreadcount.
await AddonMessages.invalidateConversation(conversationId!);
await AddonMessages.invalidateConversation(conversationId);
try {
this.conversation = await AddonMessages.getConversation(conversationId!, undefined, true);
this.conversation = await AddonMessages.getConversation(conversationId, undefined, true);
} catch (error) {
// Get conversation failed, use the fallback one if we have it.
if (fallbackConversation) {
@ -947,7 +949,6 @@ export class AddonMessagesDiscussionPage implements OnInit, OnDestroy, AfterView
message: AddonMessagesConversationMessageFormatted,
index: number,
): Promise<void> {
const canDeleteAll = this.conversation && this.conversation.candeletemessagesforallusers;
const langKey = message.pending || canDeleteAll || this.isSelf ? 'core.areyousure' :
'addon.messages.deletemessageconfirmation';
@ -1099,7 +1100,7 @@ export class AddonMessagesDiscussionPage implements OnInit, OnDestroy, AfterView
*/
scrollToFirstUnreadMessage(): void {
if (this.newMessages > 0) {
const messages = Array.from(this.hostElement.querySelectorAll<HTMLElement>('.addon-message-not-mine'));
const messages = Array.from(this.hostElement.querySelectorAll<HTMLElement>('core-message:not(.is-mine)'));
CoreDom.scrollToElement(messages[messages.length - this.newMessages]);
}

View File

@ -106,7 +106,6 @@ Feature: Test basic usage of messages in app
And I should find "hi" in the app
And I should find "byee" in the app
# TODO Fix this test in all Moodle versions
Scenario: User profile: send message, add/remove contact
Given I entered the app as "teacher1"
When I press "Messages" in the app

View File

@ -81,27 +81,10 @@
</ion-badge>
</div>
<ion-item *ngIf="!message.special" class="ion-text-wrap addon-message"
[class.addon-message-mine]="message.userid == currentUserId"
[class.addon-message-not-mine]="message.userid != currentUserId" [class.addon-message-no-user]="!message.showUserData"
[@coreSlideInOut]="message.userid == currentUserId ? '' : 'fromLeft'">
<ion-label>
<!-- User data. -->
<h2 class="addon-message-user" *ngIf="message.showUserData">
<core-user-avatar slot="start" [user]="message" [linkProfile]="false">
</core-user-avatar>
<div>{{ message.userfullname }}</div>
</h2>
<div class="addon-message-text">
<core-format-text [text]="message.message" contextLevel="module" [contextInstanceId]="cmId"
[courseId]="courseId" (afterRender)="last && scrollToBottom()">
</core-format-text>
</div>
</ion-label>
<ion-note slot="end">{{ message.timestamp * 1000 | coreFormatDate: "strftimetime" }}</ion-note>
<div class="tail" *ngIf="message.showTail"></div>
</ion-item>
<core-message *ngIf="!message.special" [message]="message" [user]="message" [text]="message.message"
[time]="message.timestamp * 1000" (afterRender)="last && scrollToBottom()" contextLevel="module" [instanceId]="cmId"
[courseId]="courseId">
</core-message>
</ng-container>
</ion-list>

View File

@ -13,7 +13,6 @@
// limitations under the License.
import { Component, ViewChild, OnInit, OnDestroy } from '@angular/core';
import { CoreAnimations } from '@components/animations';
import { CoreSendMessageFormComponent } from '@components/send-message-form/send-message-form';
import { CanLeave } from '@guards/can-leave';
import { IonContent } from '@ionic/angular';
@ -36,7 +35,6 @@ import { AddonModChatFormattedMessage, AddonModChatHelper } from '../../services
@Component({
selector: 'page-addon-mod-chat-chat',
templateUrl: 'chat.html',
animations: [CoreAnimations.SLIDE_IN_OUT],
styleUrls: ['chat.scss'],
})
export class AddonModChatChatPage implements OnInit, OnDestroy, CanLeave {

View File

@ -75,26 +75,9 @@
</ion-badge>
</div>
<ion-item *ngIf="!message.special" class="ion-text-wrap addon-message"
[class.addon-message-mine]="message.userid == currentUserId"
[class.addon-message-not-mine]="message.userid != currentUserId" [class.addon-message-no-user]="!message.showUserData">
<ion-label>
<!-- User data. -->
<h2 class="addon-message-user">
<core-user-avatar slot="start" [user]="message" [linkProfile]="false" *ngIf="message.showUserData">
</core-user-avatar>
<div *ngIf="message.showUserData">{{ message.userfullname }}</div>
</h2>
<div class="addon-message-text">
<core-format-text [text]="message.message" contextLevel="module" [contextInstanceId]="cmId"
[courseId]="courseId">
</core-format-text>
</div>
</ion-label>
<ion-note slot="end">{{ message.timestamp * 1000 | coreFormatDate: "strftimetime" }}</ion-note>
<div class="tail" *ngIf="message.showTail"></div>
</ion-item>
<core-message *ngIf="!message.special" [message]="message" [user]="message" [text]="message.message"
[time]="message.timestamp * 1000" contextLevel="module" [instanceId]="cmId" [courseId]="courseId">
</core-message>
</ng-container>
</ion-list>
</core-loading>

View File

@ -36,7 +36,7 @@ export class CoreAnimations {
animate(300, keyframes([
style({ opacity: 0, transform: 'translateX(-100%)', offset: 0 }),
style({ opacity: 1, transform: 'translateX(5%)', offset: 0.7 }),
style({ opacity: 1, transform: 'translateX(0)', offset: 1.0 }),
style({ opacity: 1, transform: 'translateX(0)', offset: 1 }),
])),
]),
// Leave animation.
@ -44,7 +44,7 @@ export class CoreAnimations {
animate(300, keyframes([
style({ opacity: 1, transform: 'translateX(0)', offset: 0 }),
style({ opacity: 1, transform: 'translateX(5%)', offset: 0.3 }),
style({ opacity: 0, transform: 'translateX(-100%)', offset: 1.0 }),
style({ opacity: 0, transform: 'translateX(-100%)', offset: 1 }),
])),
]),
// Enter animation.
@ -52,7 +52,7 @@ export class CoreAnimations {
animate(300, keyframes([
style({ opacity: 0, transform: 'translateX(100%)', offset: 0 }),
style({ opacity: 1, transform: 'translateX(-5%)', offset: 0.7 }),
style({ opacity: 1, transform: 'translateX(0)', offset: 1.0 }),
style({ opacity: 1, transform: 'translateX(0)', offset: 1 }),
])),
]),
// Leave animation.
@ -60,7 +60,7 @@ export class CoreAnimations {
animate(300, keyframes([
style({ opacity: 1, transform: 'translateX(0)', offset: 0 }),
style({ opacity: 1, transform: 'translateX(-5%)', offset: 0.3 }),
style({ opacity: 0, transform: 'translateX(100%)', offset: 1.0 }),
style({ opacity: 0, transform: 'translateX(100%)', offset: 1 }),
])),
]),
]);

View File

@ -61,6 +61,7 @@ import { CoreHorizontalScrollControlsComponent } from './horizontal-scroll-contr
import { CoreButtonWithSpinnerComponent } from './button-with-spinner/button-with-spinner';
import { CoreSwipeSlidesComponent } from './swipe-slides/swipe-slides';
import { CoreSwipeNavigationTourComponent } from './swipe-navigation-tour/swipe-navigation-tour';
import { CoreMessageComponent } from './message/message';
@NgModule({
declarations: [
@ -84,6 +85,7 @@ import { CoreSwipeNavigationTourComponent } from './swipe-navigation-tour/swipe-
CoreLoadingComponent,
CoreLocalFileComponent,
CoreMarkRequiredComponent,
CoreMessageComponent,
CoreModIconComponent,
CoreNavBarButtonsComponent,
CoreNavigationBarComponent,
@ -134,6 +136,7 @@ import { CoreSwipeNavigationTourComponent } from './swipe-navigation-tour/swipe-
CoreLoadingComponent,
CoreLocalFileComponent,
CoreMarkRequiredComponent,
CoreMessageComponent,
CoreModIconComponent,
CoreNavBarButtonsComponent,
CoreNavigationBarComponent,

View File

@ -0,0 +1,45 @@
<div *ngIf="message" class="message-box" (longPress)="copyMessage()">
<div class="main">
<!-- User data. -->
<div class="message-user" *ngIf="message.showUserData">
<core-user-avatar slot="start" [user]="user" [courseId]="courseId" [linkProfile]="false" aria-hidden="true">
</core-user-avatar>
<div>{{ userFullname }}</div>
</div>
<div *ngIf="!message.showUserData" class="sr-only">
{{ isMine
? ('addon.messages.you' | translate)
: userFullname }}
</div>
<core-format-text class="message-text" [text]="text" (afterRender)="afterRender.emit()" [contextLevel]="contextLevel"
[contextInstanceId]="instanceId" [courseId]="courseId">
</core-format-text>
</div>
<div class="extra">
<div class="message-time">
<ng-container *ngIf="!message.pending && !message.deleted">
{{ time | coreFormatDate: 'strftimetime' }}
</ng-container>
<ng-container *ngIf="!message.pending && message.deleted">
<ion-icon name="fas-trash" aria-hidden="true"></ion-icon>
<span class="ion-text-wrap">
{{ 'core.deletedoffline' | translate }}
</span>
</ng-container>
<ion-icon *ngIf="message.pending" name="fas-clock" [attr.aria-label]="'core.notsent' | translate" role="status"></ion-icon>
</div>
<ion-button *ngIf="showDelete && !message.deleted && message.delete !== false" fill="clear" [@coreSlideInOut]="'fromRight'"
color="danger" (click)="delete($event)" [attr.aria-label]="'addon.messages.deletemessage' | translate" class="delete-button">
<ion-icon name="fas-trash" slot="icon-only" aria-hidden="true"></ion-icon>
</ion-button>
<ion-button *ngIf="showDelete && message.deleted" fill="clear" color="danger" (click)="undoDelete($event)"
[attr.aria-label]="'core.restore' | translate" class="delete-button">
<ion-icon name="fas-undo-alt" slot="icon-only" aria-hidden="true"></ion-icon>
</ion-button>
</div>
<div class="tail" *ngIf="message.showTail"></div>
</div>

View File

@ -0,0 +1,138 @@
@import "~theme/globals";
:host {
--message-background: var(--core-messages-message-bg);
--message-activated-background: var(--core-messages-message-activated-bg);
--message-alignment: flex-start;
display: flex;
justify-content: var(--message-alignment);
.message-box {
--background: var(--message-background);
--min-height: var(--a11y-min-target-size);
display: flex;
flex-direction: row;
position: relative;
border: 0;
border-radius: var(--medium-radius);
margin: 8px;
width: 90%;
max-width: var(--list-item-max-width);
min-height: 36px;
font-size: var(--text-size);
color: var(--ion-text-color);
background: var(--message-background);
@include core-transition(width);
// This is needed to display bubble tails.
overflow: visible;
&:hover {
-webkit-filter: drop-shadow(2px 2px 2px rgba(0,0,0,.3));
filter: drop-shadow(2px 2px 2px rgba(0,0,0,.3));
}
&[tappable]:active {
--message-background: var(--message-activated-background);
}
.main {
padding: 8px;
flex-grow: 1;
.message-user {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
margin-bottom: .5rem;
margin-top: 0;
color: var(--ion-text-color);
core-user-avatar {
display: block;
--core-avatar-size: var(--core-messages-avatar-size);
margin: 0;
}
div {
font-weight: 500;
flex-grow: 1;
padding-left: .5rem;
padding-right: .5rem;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: 16px;
}
}
.message-text {
::ng-deep > p:only-child {
display: inline;
margin: 0;
}
}
}
.extra {
flex-shrink: 1;
display: flex;
flex-direction: row;
padding-left: 8px;
padding-right: 8px;
.message-time {
padding-top: 8px;
color: var(--core-messages-message-note-text);
font-size: var(--core-messages-message-note-font-size);
}
.delete-button {
min-height: initial;
line-height: initial;
margin: 0px;
align-self: flex-end;
::ng-deep ion-icon {
font-size: 1.2em;
}
}
}
.tail {
content: '';
width: 0;
height: 0;
border: 0.5rem solid transparent;
position: absolute;
touch-action: none;
bottom: 0;
border-bottom-color: var(--message-background);
@include position(null, null, null, -8px);
}
}
&.no-user .message-box {
margin-top: 0px;
}
&.is-mine {
// Defined when a message is the user's.
--message-background: var(--core-messages-message-mine-bg);
--message-activated-background: var(--core-messages-message-mine-activated-bg);
--message-alignment: flex-end;
.message-box {
.tail {
@include position(null, -8px, null, unset);
}
}
}
}

View File

@ -0,0 +1,121 @@
// (C) Copyright 2015 Moodle Pty Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { ContextLevel } from '@/core/constants';
import { Component, EventEmitter, HostBinding, Input, OnInit, Output } from '@angular/core';
import { CoreAnimations } from '@components/animations';
import { CoreSites } from '@services/sites';
import { CoreUtils } from '@services/utils/utils';
import { CoreTextUtils } from '@services/utils/text';
import { CoreUserWithAvatar } from '@components/user-avatar/user-avatar';
/**
* Component to handle a message in a conversation.
*/
@Component({
selector: 'core-message',
templateUrl: 'message.html',
styleUrls: ['message.scss'],
animations: [CoreAnimations.SLIDE_IN_OUT],
})
export class CoreMessageComponent implements OnInit {
@Input() message?: CoreMessageData; // The message object.
@Input() user?: CoreUserWithAvatar; // The user object.
@Input() text = ''; // Message text.
@Input() time = 0; // Message time.
@Input() instanceId = 0;
@Input() courseId?: number;
@Input() contextLevel: ContextLevel = ContextLevel.SYSTEM;
@Input() showDelete = false;
@Output() onDeleteMessage = new EventEmitter<void>();
@Output() onUndoDeleteMessage = new EventEmitter<void>();
@Output() afterRender = new EventEmitter<void>();
protected deleted = false; // Needed to fix animation to void in Behat tests.
// @TODO Recover the animation using native css or wait for Angular 13.1
// where the bug https://github.com/angular/angular/issues/30693 is solved.
// @HostBinding('@coreSlideInOut') get animation(): string {
// return this.isMine ? '' : 'fromLeft';
// }
@HostBinding('class.is-mine') isMine = false;
@HostBinding('class.no-user') get showUser(): boolean {
return !this.message?.showUserData;
};
get userId(): number | undefined {
return this.user && (this.user.userid || this.user.id);
}
get userFullname(): string | undefined {
return this.user && (this.user.fullname || this.user.userfullname);
}
/**
* @inheritdoc
*/
async ngOnInit(): Promise<void> {
const currentUserId = CoreSites.getCurrentSiteUserId();
this.isMine = this.userId === currentUserId;
}
/**
* Emits the delete action.
*
* @param event Event.
*/
delete(event: Event): void {
event.preventDefault();
event.stopPropagation();
this.onDeleteMessage.emit();
}
/**
* Emits the undo delete action.
*
* @param event Event.
*/
undoDelete(event: Event): void {
event.preventDefault();
event.stopPropagation();
this.onUndoDeleteMessage.emit();
}
/**
* Copy message to clipboard.
*/
copyMessage(): void {
CoreUtils.copyToClipboard(CoreTextUtils.decodeHTMLEntities(this.text));
}
}
/**
* Conversation message with some calculated data.
*/
type CoreMessageData = {
pending?: boolean; // Whether the message is pending to be sent.
sending?: boolean; // Whether the message is being sent right now.
showDate?: boolean; // Whether to show the date before the message.
deleted?: boolean; // Whether the message has been deleted.
showUserData?: boolean; // Whether to show the user data in the message.
showTail?: boolean; // Whether to show a "tail" in the message.
delete?: boolean; // Permission to delete=true/false.
};

View File

@ -156,7 +156,7 @@ export class CoreUserAvatarComponent implements OnInit, OnChanges, OnDestroy {
/**
* Type with all possible formats of user.
*/
type CoreUserWithAvatar = CoreUserBasicData & {
export type CoreUserWithAvatar = CoreUserBasicData & {
userpictureurl?: string;
userprofileimageurl?: string;
profileimageurlsmall?: string;

View File

@ -45,71 +45,20 @@
{{ comment.timecreated * 1000 | coreFormatDate: "strftimedayshort" }}
</p>
<ion-item class="ion-text-wrap addon-message" [class.addon-message-mine]="comment.userid == currentUserId"
[class.addon-message-not-mine]="comment.userid != currentUserId" [class.addon-message-no-user]="!comment.showUserData"
[@coreSlideInOut]="comment.userid == currentUserId ? '' : 'fromLeft'">
<ion-label>
<!-- User data. -->
<h2 class="addon-message-user" *ngIf="comment.showUserData">
<core-user-avatar slot="start" [user]="comment" [linkProfile]="false">
</core-user-avatar>
<div>{{ comment.fullname }}</div>
</h2>
<div class="addon-message-text">
<core-format-text [text]="comment.content" [contextLevel]="contextLevel" [contextInstanceId]="instanceId"
[courseId]="courseId">
</core-format-text>
</div>
</ion-label>
<ion-note slot="end">
<ng-container *ngIf="!comment.deleted">
{{ comment.timecreated * 1000 | coreFormatDate: 'strftimetime' }}
</ng-container>
<ng-container *ngIf="comment.deleted">
<ion-icon name="fas-trash" aria-hidden="true"></ion-icon> <span class="ion-text-wrap">
{{ 'core.deletedoffline' | translate }}
</span>
</ng-container>
</ion-note>
<div class="tail" *ngIf="comment.showTail"></div>
<ion-button *ngIf="showDelete && !comment.deleted && comment.delete" slot="end" fill="clear"
[@coreSlideInOut]="'fromRight'" color="danger" (click)="deleteComment($event, comment)"
[attr.aria-label]="'core.delete' | translate" class="addon-messages-delete-button">
<ion-icon name="fas-trash" slot="icon-only" aria-hidden="true"></ion-icon>
</ion-button>
<ion-button *ngIf="showDelete && comment.deleted" slot="end" fill="clear" color="danger"
(click)="undoDeleteComment($event, comment)" [attr.aria-label]="'core.restore' | translate"
class="addon-messages-delete-button">
<ion-icon name="fas-undo-alt" slot="icon-only" aria-hidden="true"></ion-icon>
</ion-button>
</ion-item>
<core-message [message]="comment" [text]="comment.content" [time]="comment.timecreated * 1000" [user]="comment"
[showDelete]="showDelete" [contextLevel]="contextLevel" [instanceId]="instanceId" [courseId]="courseId"
(onDeleteMessage)="deleteComment(comment)" (onUndoDeleteMessage)="undoDeleteComment(comment)">
</core-message>
</ng-container>
<ion-item *ngIf="hasOffline" class="ion-text-wrap addon-message addon-message-mine">
<ion-label>
<!-- User data. -->
<p class="ion-text-center">
<ion-badge class="ion-text-wrap" color="info" *ngIf="hasOffline">
<ion-icon name="fas-exclamation-triangle" aria-hidden="true"></ion-icon>
{{ 'core.thereisdatatosync' | translate:{$a: 'core.comments.comments' | translate | lowercase } }}
</p>
<div class="addon-message-text" *ngIf="offlineComment">
<core-format-text [text]="offlineComment.content" [contextLevel]="contextLevel" [contextInstanceId]="instanceId"
[courseId]="courseId">
</core-format-text>
</div>
</ion-label>
<ion-note>
<ion-icon name="fas-clock" aria-hidden="true"></ion-icon> {{ 'core.notsent' | translate }}
</ion-note>
<div class="tail"></div>
<ion-button *ngIf="showDelete && offlineComment" slot="end" fill="clear" [@coreSlideInOut]="'fromRight'" color="danger"
(click)="deleteComment($event, offlineComment)" [attr.aria-label]="'core.delete' | translate"
class="addon-messages-delete-button">
<ion-icon name="fas-trash" slot="icon-only" aria-hidden="true"></ion-icon>
</ion-button>
</ion-item>
</ion-badge>
<core-message *ngIf="hasOffline && offlineComment" [message]="offlineComment" [text]="offlineComment.content"
[user]="offlineComment" [showDelete]="showDelete" [contextLevel]="contextLevel" [instanceId]="instanceId"
[courseId]="courseId" (onDeleteMessage)="deleteComment(offlineComment)">
</core-message>
</ion-list>
</core-loading>

View File

@ -14,7 +14,6 @@
import { Component, OnDestroy, OnInit, ViewChild } from '@angular/core';
import { CoreEventObserver, CoreEvents } from '@singletons/events';
import { CoreAnimations } from '@components/animations';
import { ActivatedRoute } from '@angular/router';
import { CoreSites } from '@services/sites';
import {
@ -43,6 +42,7 @@ import { CoreApp } from '@services/app';
import { CoreNetwork } from '@services/network';
import moment from 'moment';
import { Subscription } from 'rxjs';
import { CoreAnimations } from '@components/animations';
/**
* Page that displays comments.
@ -75,7 +75,7 @@ export class CoreCommentsViewerPage implements OnInit, OnDestroy {
hasOffline = false;
refreshIcon = CoreConstants.ICON_LOADING;
syncIcon = CoreConstants.ICON_LOADING;
offlineComment?: CoreCommentsOfflineWithUser;
offlineComment?: CoreCommentsOfflineWithUser & { pending?: boolean };
currentUserId: number;
sending = false;
newComment = '';
@ -361,13 +361,9 @@ export class CoreCommentsViewerPage implements OnInit, OnDestroy {
/**
* Delete a comment.
*
* @param e Click event.
* @param comment Comment to delete.
*/
async deleteComment(e: Event, comment: CoreCommentsDataToDisplay | CoreCommentsOfflineWithUser): Promise<void> {
e.preventDefault();
e.stopPropagation();
async deleteComment(comment: CoreCommentsDataToDisplay | CoreCommentsOfflineWithUser): Promise<void> {
const modified = 'lastmodified' in comment
? comment.lastmodified
: comment.timecreated;
@ -529,15 +525,16 @@ export class CoreCommentsViewerPage implements OnInit, OnDestroy {
).then(async (offlineComment) => {
this.offlineComment = offlineComment;
if (!offlineComment) {
if (!this.offlineComment) {
return;
}
if (this.newComment == '') {
this.newComment = this.offlineComment!.content;
this.newComment = this.offlineComment.content;
}
this.offlineComment!.userid = this.currentUserId;
this.offlineComment.userid = this.currentUserId;
this.offlineComment.pending = true;
return;
}));
@ -573,13 +570,9 @@ export class CoreCommentsViewerPage implements OnInit, OnDestroy {
/**
* Restore a comment.
*
* @param e Click event.
* @param comment Comment to delete.
*/
async undoDeleteComment(e: Event, comment: CoreCommentsDataToDisplay): Promise<void> {
e.preventDefault();
e.stopPropagation();
async undoDeleteComment(comment: CoreCommentsDataToDisplay): Promise<void> {
await CoreCommentsOffline.undoDeleteComment(comment.id);
comment.deleted = false;

View File

@ -1 +1,5 @@
@import "~theme/components/discussion.scss";
ion-badge {
margin: 8px auto;
}

View File

@ -27,148 +27,3 @@ ion-content {
font-weight: normal;
font-size: 0.9rem;
}
// Message item.
ion-item.addon-message {
--message-background: var(--addon-messages-message-bg);
--message-activated-background: var(--addon-messages-message-activated-bg);
--message-alignment: flex-start;
border: 0;
border-radius: var(--medium-radius);
padding: 0 8px 0 8px;
margin: 8px;
--background: var(--message-background);
background: var(--message-background);
align-self: var(--message-alignment);
width: 90%;
max-width: var(--list-item-max-width);
--min-height: var(--a11y-min-target-size);
position: relative;
@include core-transition(width);
// This is needed to display bubble tails.
overflow: visible;
&::part(native) {
--inner-border-width: 0px;
--inner-padding-end: 0px;
padding: 0;
margin: 0;
}
&:hover {
-webkit-filter: drop-shadow(2px 2px 2px rgba(0,0,0,.3));
filter: drop-shadow(2px 2px 2px rgba(0,0,0,.3));
}
core-format-text > p:only-child {
display: inline;
}
.addon-message-user {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
margin-bottom: .5rem;
margin-top: 0;
color: var(--ion-text-color);
core-user-avatar {
display: block;
--core-avatar-size: var(--addon-messages-avatar-size);
margin: 0;
}
div {
font-weight: 500;
flex-grow: 1;
padding-left: .5rem;
padding-right: .5rem;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
ion-note {
color: var(--addon-messages-message-note-text);
font-size: var(--addon-messages-message-note-font-size);
margin: 0;
padding: 8px 0;
align-self: flex-start;
}
&[tappable]:active {
--message-background: var(--message-activated-background);
}
ion-label {
margin: 0;
padding: 8px 0;
}
.addon-message-text {
display: inline-flex;
* {
color: var(--ion-text-color);
}
}
.tail {
content: '';
width: 0;
height: 0;
border: 0.5rem solid transparent;
position: absolute;
touch-action: none;
bottom: 0;
border-bottom-color: var(--message-background);
}
// Defines when an item-message is the user's.
&.addon-message-mine {
--message-background: var(--addon-messages-message-mine-bg);
--message-activated-background: var(--addon-messages-message-mine-activated-bg);
--message-alignment: flex-end;
.spinner {
@include float(end);
@include margin(2px, -3px, -2px, 5px);
svg {
width: 16px;
height: 16px;
}
}
.tail {
@include position(null, -8px, null, null);
@include margin-horizontal(null, -0.5rem);
}
}
&.addon-message-not-mine .tail {
@include position(null, null, null, -8px);
@include margin-horizontal(-0.5rem, null);
}
.addon-messages-delete-button {
min-height: initial;
line-height: initial;
margin-top: 0px;
margin-bottom: 0px;
height: var(--a11y-min-target-size) !important;
align-self: flex-end;
ion-icon {
font-size: 1.4em;
line-height: initial;
color: var(--danger);
}
}
&.addon-message-no-user {
margin-top: 0px;
}
}

View File

@ -146,13 +146,13 @@
--core-collapsible-footer-background: var(--contrast-background);
--addon-messages-message-bg: var(--gray-800);
--addon-messages-message-activated-bg: var(--gray-700);
--addon-messages-message-note-text: var(--subdued-text-color);
--addon-messages-message-mine-bg: var(--gray-700);
--addon-messages-message-mine-activated-bg: var(--gray-600);
--addon-messages-discussion-badge: var(--primary);
--addon-messages-discussion-badge-text: var(--gray-100);
--core-messages-message-bg: var(--gray-800);
--core-messages-message-activated-bg: var(--gray-700);
--core-messages-message-note-text: var(--subdued-text-color);
--core-messages-message-mine-bg: var(--gray-700);
--core-messages-message-mine-activated-bg: var(--gray-600);
--core-messages-discussion-badge: var(--primary);
--core-messages-discussion-badge-text: var(--gray-100);
--addon-forum-border-color: var(--gray-500);
--addon-forum-highlight-color: var(--gray-200);

View File

@ -344,15 +344,15 @@
--addon-calendar-today-border-color: var(--primary);
--addon-calendar-border-color: var(--stroke);
--addon-messages-message-bg: var(--white);
--addon-messages-message-activated-bg: var(--gray-200);
--addon-messages-message-note-text: var(--gray-500);
--addon-messages-message-note-font-size: 75%;
--addon-messages-message-mine-bg: var(--gray-300);
--addon-messages-message-mine-activated-bg: var(--gray-400);
--addon-messages-avatar-size: 30px;
--addon-messages-discussion-badge: var(--primary);
--addon-messages-discussion-badge-text: var(--white);
--core-messages-message-bg: var(--white);
--core-messages-message-activated-bg: var(--gray-200);
--core-messages-message-note-text: var(--gray-500);
--core-messages-message-note-font-size: 75%;
--core-messages-message-mine-bg: var(--gray-300);
--core-messages-message-mine-activated-bg: var(--gray-400);
--core-messages-avatar-size: 30px;
--core-messages-discussion-badge: var(--primary);
--core-messages-discussion-badge-text: var(--white);
--addon-forum-avatar-size: var(--core-avatar-size);
--addon-forum-border-color: var(--stroke);

View File

@ -1,9 +1,10 @@
This files describes API changes in the Moodle Mobile app,
information provided here is intended especially for developers.
=== 4.0.1 ===
=== 4.1.0 ===
- Zoom levels changed from "normal / low / high" to " none / medium / high".
- --addon-messages-* CSS3 variables have been renamed to --core-messages-*
=== 4.0.0 ===