MOBILE-4061 core: Create a new message component to fix animations
parent
ef574e7e63
commit
e337bc64d5
|
@ -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">
|
||||
|
|
|
@ -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]);
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 }),
|
||||
])),
|
||||
]),
|
||||
]);
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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>
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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.
|
||||
};
|
|
@ -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;
|
||||
|
|
|
@ -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-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 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 } }}
|
||||
</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>
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -1 +1,5 @@
|
|||
@import "~theme/components/discussion.scss";
|
||||
|
||||
ion-badge {
|
||||
margin: 8px auto;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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 ===
|
||||
|
||||
|
|
Loading…
Reference in New Issue