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-icon name="fas-arrow-down" aria-hidden="true"></ion-icon>
|
||||||
</ion-chip>
|
</ion-chip>
|
||||||
|
|
||||||
<ion-item class="ion-text-wrap addon-message" (longPress)="copyMessage(message)"
|
<core-message [message]="message" [user]="members[message.useridfrom]" (afterRender)="last && scrollToBottom()"
|
||||||
[class.addon-message-mine]="message.useridfrom == currentUserId"
|
[text]="message.text" (onDeleteMessage)="deleteMessage(message, index)" [showDelete]="showDelete"
|
||||||
[class.addon-message-not-mine]="message.useridfrom != currentUserId"
|
[time]="message.timecreated">
|
||||||
[class.addon-message-no-user]="!message.showUserData"
|
</core-message>
|
||||||
[@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>
|
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</ion-list>
|
</ion-list>
|
||||||
|
|
||||||
<core-empty-box *ngIf="!messages || messages.length <= 0" icon="far-comments"
|
<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>
|
</core-loading>
|
||||||
<!-- Scroll bottom. -->
|
<!-- Scroll bottom. -->
|
||||||
<ion-fab slot="fixed" core-fab vertical="bottom" horizontal="end" *ngIf="loaded && newMessages > 0">
|
<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 { CoreInfiniteLoadingComponent } from '@components/infinite-loading/infinite-loading';
|
||||||
import { Md5 } from 'ts-md5/dist/md5';
|
import { Md5 } from 'ts-md5/dist/md5';
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import { CoreAnimations } from '@components/animations';
|
|
||||||
import { CoreError } from '@classes/errors/error';
|
import { CoreError } from '@classes/errors/error';
|
||||||
import { Translate } from '@singletons';
|
import { Translate } from '@singletons';
|
||||||
import { CoreNavigator } from '@services/navigator';
|
import { CoreNavigator } from '@services/navigator';
|
||||||
|
@ -53,7 +52,6 @@ import { CoreDom } from '@singletons/dom';
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'page-addon-messages-discussion',
|
selector: 'page-addon-messages-discussion',
|
||||||
templateUrl: 'discussion.html',
|
templateUrl: 'discussion.html',
|
||||||
animations: [CoreAnimations.SLIDE_IN_OUT],
|
|
||||||
styleUrls: ['discussion.scss'],
|
styleUrls: ['discussion.scss'],
|
||||||
})
|
})
|
||||||
export class AddonMessagesDiscussionPage implements OnInit, OnDestroy, AfterViewInit {
|
export class AddonMessagesDiscussionPage implements OnInit, OnDestroy, AfterViewInit {
|
||||||
|
@ -305,7 +303,7 @@ export class AddonMessagesDiscussionPage implements OnInit, OnDestroy, AfterView
|
||||||
} else {
|
} else {
|
||||||
if (this.userId) {
|
if (this.userId) {
|
||||||
// Fake the user member info.
|
// 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 = {
|
this.otherMember = {
|
||||||
id: user.id,
|
id: user.id,
|
||||||
fullname: user.fullname,
|
fullname: user.fullname,
|
||||||
|
@ -524,7 +522,7 @@ export class AddonMessagesDiscussionPage implements OnInit, OnDestroy, AfterView
|
||||||
return;
|
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)
|
.slice(-this.newMessages)
|
||||||
.reverse();
|
.reverse();
|
||||||
|
|
||||||
|
@ -555,7 +553,7 @@ export class AddonMessagesDiscussionPage implements OnInit, OnDestroy, AfterView
|
||||||
// Try to get the conversationId if we don't have it.
|
// Try to get the conversationId if we don't have it.
|
||||||
if (!conversationId && userId) {
|
if (!conversationId && userId) {
|
||||||
try {
|
try {
|
||||||
if (userId == this.currentUserId && AddonMessages.isSelfConversationEnabled()) {
|
if (userId === this.currentUserId && AddonMessages.isSelfConversationEnabled()) {
|
||||||
fallbackConversation = await AddonMessages.getSelfConversation();
|
fallbackConversation = await AddonMessages.getSelfConversation();
|
||||||
} else {
|
} else {
|
||||||
fallbackConversation = await AddonMessages.getConversationBetweenUsers(userId, undefined, true);
|
fallbackConversation = await AddonMessages.getConversationBetweenUsers(userId, undefined, true);
|
||||||
|
@ -563,7 +561,7 @@ export class AddonMessagesDiscussionPage implements OnInit, OnDestroy, AfterView
|
||||||
conversationId = fallbackConversation.id;
|
conversationId = fallbackConversation.id;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Probably conversation does not exist or user is offline. Try to load offline messages.
|
// 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);
|
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.
|
// Retrieve the conversation. Invalidate data first to get the right unreadcount.
|
||||||
await AddonMessages.invalidateConversation(conversationId!);
|
await AddonMessages.invalidateConversation(conversationId);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
this.conversation = await AddonMessages.getConversation(conversationId!, undefined, true);
|
this.conversation = await AddonMessages.getConversation(conversationId, undefined, true);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Get conversation failed, use the fallback one if we have it.
|
// Get conversation failed, use the fallback one if we have it.
|
||||||
if (fallbackConversation) {
|
if (fallbackConversation) {
|
||||||
|
@ -947,7 +949,6 @@ export class AddonMessagesDiscussionPage implements OnInit, OnDestroy, AfterView
|
||||||
message: AddonMessagesConversationMessageFormatted,
|
message: AddonMessagesConversationMessageFormatted,
|
||||||
index: number,
|
index: number,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
|
|
||||||
const canDeleteAll = this.conversation && this.conversation.candeletemessagesforallusers;
|
const canDeleteAll = this.conversation && this.conversation.candeletemessagesforallusers;
|
||||||
const langKey = message.pending || canDeleteAll || this.isSelf ? 'core.areyousure' :
|
const langKey = message.pending || canDeleteAll || this.isSelf ? 'core.areyousure' :
|
||||||
'addon.messages.deletemessageconfirmation';
|
'addon.messages.deletemessageconfirmation';
|
||||||
|
@ -1099,7 +1100,7 @@ export class AddonMessagesDiscussionPage implements OnInit, OnDestroy, AfterView
|
||||||
*/
|
*/
|
||||||
scrollToFirstUnreadMessage(): void {
|
scrollToFirstUnreadMessage(): void {
|
||||||
if (this.newMessages > 0) {
|
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]);
|
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 "hi" in the app
|
||||||
And I should find "byee" 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
|
Scenario: User profile: send message, add/remove contact
|
||||||
Given I entered the app as "teacher1"
|
Given I entered the app as "teacher1"
|
||||||
When I press "Messages" in the app
|
When I press "Messages" in the app
|
||||||
|
|
|
@ -81,27 +81,10 @@
|
||||||
</ion-badge>
|
</ion-badge>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ion-item *ngIf="!message.special" class="ion-text-wrap addon-message"
|
<core-message *ngIf="!message.special" [message]="message" [user]="message" [text]="message.message"
|
||||||
[class.addon-message-mine]="message.userid == currentUserId"
|
[time]="message.timestamp * 1000" (afterRender)="last && scrollToBottom()" contextLevel="module" [instanceId]="cmId"
|
||||||
[class.addon-message-not-mine]="message.userid != currentUserId" [class.addon-message-no-user]="!message.showUserData"
|
[courseId]="courseId">
|
||||||
[@coreSlideInOut]="message.userid == currentUserId ? '' : 'fromLeft'">
|
</core-message>
|
||||||
<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>
|
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</ion-list>
|
</ion-list>
|
||||||
|
|
||||||
|
|
|
@ -13,7 +13,6 @@
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
import { Component, ViewChild, OnInit, OnDestroy } from '@angular/core';
|
import { Component, ViewChild, OnInit, OnDestroy } from '@angular/core';
|
||||||
import { CoreAnimations } from '@components/animations';
|
|
||||||
import { CoreSendMessageFormComponent } from '@components/send-message-form/send-message-form';
|
import { CoreSendMessageFormComponent } from '@components/send-message-form/send-message-form';
|
||||||
import { CanLeave } from '@guards/can-leave';
|
import { CanLeave } from '@guards/can-leave';
|
||||||
import { IonContent } from '@ionic/angular';
|
import { IonContent } from '@ionic/angular';
|
||||||
|
@ -36,7 +35,6 @@ import { AddonModChatFormattedMessage, AddonModChatHelper } from '../../services
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'page-addon-mod-chat-chat',
|
selector: 'page-addon-mod-chat-chat',
|
||||||
templateUrl: 'chat.html',
|
templateUrl: 'chat.html',
|
||||||
animations: [CoreAnimations.SLIDE_IN_OUT],
|
|
||||||
styleUrls: ['chat.scss'],
|
styleUrls: ['chat.scss'],
|
||||||
})
|
})
|
||||||
export class AddonModChatChatPage implements OnInit, OnDestroy, CanLeave {
|
export class AddonModChatChatPage implements OnInit, OnDestroy, CanLeave {
|
||||||
|
|
|
@ -75,26 +75,9 @@
|
||||||
</ion-badge>
|
</ion-badge>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ion-item *ngIf="!message.special" class="ion-text-wrap addon-message"
|
<core-message *ngIf="!message.special" [message]="message" [user]="message" [text]="message.message"
|
||||||
[class.addon-message-mine]="message.userid == currentUserId"
|
[time]="message.timestamp * 1000" contextLevel="module" [instanceId]="cmId" [courseId]="courseId">
|
||||||
[class.addon-message-not-mine]="message.userid != currentUserId" [class.addon-message-no-user]="!message.showUserData">
|
</core-message>
|
||||||
<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>
|
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</ion-list>
|
</ion-list>
|
||||||
</core-loading>
|
</core-loading>
|
||||||
|
|
|
@ -36,7 +36,7 @@ export class CoreAnimations {
|
||||||
animate(300, keyframes([
|
animate(300, keyframes([
|
||||||
style({ opacity: 0, transform: 'translateX(-100%)', offset: 0 }),
|
style({ opacity: 0, transform: 'translateX(-100%)', offset: 0 }),
|
||||||
style({ opacity: 1, transform: 'translateX(5%)', offset: 0.7 }),
|
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.
|
// Leave animation.
|
||||||
|
@ -44,7 +44,7 @@ export class CoreAnimations {
|
||||||
animate(300, keyframes([
|
animate(300, keyframes([
|
||||||
style({ opacity: 1, transform: 'translateX(0)', offset: 0 }),
|
style({ opacity: 1, transform: 'translateX(0)', offset: 0 }),
|
||||||
style({ opacity: 1, transform: 'translateX(5%)', offset: 0.3 }),
|
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.
|
// Enter animation.
|
||||||
|
@ -52,7 +52,7 @@ export class CoreAnimations {
|
||||||
animate(300, keyframes([
|
animate(300, keyframes([
|
||||||
style({ opacity: 0, transform: 'translateX(100%)', offset: 0 }),
|
style({ opacity: 0, transform: 'translateX(100%)', offset: 0 }),
|
||||||
style({ opacity: 1, transform: 'translateX(-5%)', offset: 0.7 }),
|
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.
|
// Leave animation.
|
||||||
|
@ -60,7 +60,7 @@ export class CoreAnimations {
|
||||||
animate(300, keyframes([
|
animate(300, keyframes([
|
||||||
style({ opacity: 1, transform: 'translateX(0)', offset: 0 }),
|
style({ opacity: 1, transform: 'translateX(0)', offset: 0 }),
|
||||||
style({ opacity: 1, transform: 'translateX(-5%)', offset: 0.3 }),
|
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 { CoreButtonWithSpinnerComponent } from './button-with-spinner/button-with-spinner';
|
||||||
import { CoreSwipeSlidesComponent } from './swipe-slides/swipe-slides';
|
import { CoreSwipeSlidesComponent } from './swipe-slides/swipe-slides';
|
||||||
import { CoreSwipeNavigationTourComponent } from './swipe-navigation-tour/swipe-navigation-tour';
|
import { CoreSwipeNavigationTourComponent } from './swipe-navigation-tour/swipe-navigation-tour';
|
||||||
|
import { CoreMessageComponent } from './message/message';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
declarations: [
|
declarations: [
|
||||||
|
@ -84,6 +85,7 @@ import { CoreSwipeNavigationTourComponent } from './swipe-navigation-tour/swipe-
|
||||||
CoreLoadingComponent,
|
CoreLoadingComponent,
|
||||||
CoreLocalFileComponent,
|
CoreLocalFileComponent,
|
||||||
CoreMarkRequiredComponent,
|
CoreMarkRequiredComponent,
|
||||||
|
CoreMessageComponent,
|
||||||
CoreModIconComponent,
|
CoreModIconComponent,
|
||||||
CoreNavBarButtonsComponent,
|
CoreNavBarButtonsComponent,
|
||||||
CoreNavigationBarComponent,
|
CoreNavigationBarComponent,
|
||||||
|
@ -134,6 +136,7 @@ import { CoreSwipeNavigationTourComponent } from './swipe-navigation-tour/swipe-
|
||||||
CoreLoadingComponent,
|
CoreLoadingComponent,
|
||||||
CoreLocalFileComponent,
|
CoreLocalFileComponent,
|
||||||
CoreMarkRequiredComponent,
|
CoreMarkRequiredComponent,
|
||||||
|
CoreMessageComponent,
|
||||||
CoreModIconComponent,
|
CoreModIconComponent,
|
||||||
CoreNavBarButtonsComponent,
|
CoreNavBarButtonsComponent,
|
||||||
CoreNavigationBarComponent,
|
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 with all possible formats of user.
|
||||||
*/
|
*/
|
||||||
type CoreUserWithAvatar = CoreUserBasicData & {
|
export type CoreUserWithAvatar = CoreUserBasicData & {
|
||||||
userpictureurl?: string;
|
userpictureurl?: string;
|
||||||
userprofileimageurl?: string;
|
userprofileimageurl?: string;
|
||||||
profileimageurlsmall?: string;
|
profileimageurlsmall?: string;
|
||||||
|
|
|
@ -45,71 +45,20 @@
|
||||||
{{ comment.timecreated * 1000 | coreFormatDate: "strftimedayshort" }}
|
{{ comment.timecreated * 1000 | coreFormatDate: "strftimedayshort" }}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<ion-item class="ion-text-wrap addon-message" [class.addon-message-mine]="comment.userid == currentUserId"
|
<core-message [message]="comment" [text]="comment.content" [time]="comment.timecreated * 1000" [user]="comment"
|
||||||
[class.addon-message-not-mine]="comment.userid != currentUserId" [class.addon-message-no-user]="!comment.showUserData"
|
[showDelete]="showDelete" [contextLevel]="contextLevel" [instanceId]="instanceId" [courseId]="courseId"
|
||||||
[@coreSlideInOut]="comment.userid == currentUserId ? '' : 'fromLeft'">
|
(onDeleteMessage)="deleteComment(comment)" (onUndoDeleteMessage)="undoDeleteComment(comment)">
|
||||||
<ion-label>
|
</core-message>
|
||||||
<!-- 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>
|
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<ion-item *ngIf="hasOffline" class="ion-text-wrap addon-message addon-message-mine">
|
<ion-badge class="ion-text-wrap" color="info" *ngIf="hasOffline">
|
||||||
<ion-label>
|
<ion-icon name="fas-exclamation-triangle" aria-hidden="true"></ion-icon>
|
||||||
<!-- User data. -->
|
{{ 'core.thereisdatatosync' | translate:{$a: 'core.comments.comments' | translate | lowercase } }}
|
||||||
<p class="ion-text-center">
|
</ion-badge>
|
||||||
<ion-icon name="fas-exclamation-triangle" aria-hidden="true"></ion-icon>
|
<core-message *ngIf="hasOffline && offlineComment" [message]="offlineComment" [text]="offlineComment.content"
|
||||||
{{ 'core.thereisdatatosync' | translate:{$a: 'core.comments.comments' | translate | lowercase } }}
|
[user]="offlineComment" [showDelete]="showDelete" [contextLevel]="contextLevel" [instanceId]="instanceId"
|
||||||
</p>
|
[courseId]="courseId" (onDeleteMessage)="deleteComment(offlineComment)">
|
||||||
|
</core-message>
|
||||||
<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-list>
|
</ion-list>
|
||||||
|
|
||||||
</core-loading>
|
</core-loading>
|
||||||
|
|
|
@ -14,7 +14,6 @@
|
||||||
|
|
||||||
import { Component, OnDestroy, OnInit, ViewChild } from '@angular/core';
|
import { Component, OnDestroy, OnInit, ViewChild } from '@angular/core';
|
||||||
import { CoreEventObserver, CoreEvents } from '@singletons/events';
|
import { CoreEventObserver, CoreEvents } from '@singletons/events';
|
||||||
import { CoreAnimations } from '@components/animations';
|
|
||||||
import { ActivatedRoute } from '@angular/router';
|
import { ActivatedRoute } from '@angular/router';
|
||||||
import { CoreSites } from '@services/sites';
|
import { CoreSites } from '@services/sites';
|
||||||
import {
|
import {
|
||||||
|
@ -43,6 +42,7 @@ import { CoreApp } from '@services/app';
|
||||||
import { CoreNetwork } from '@services/network';
|
import { CoreNetwork } from '@services/network';
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import { Subscription } from 'rxjs';
|
import { Subscription } from 'rxjs';
|
||||||
|
import { CoreAnimations } from '@components/animations';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Page that displays comments.
|
* Page that displays comments.
|
||||||
|
@ -75,7 +75,7 @@ export class CoreCommentsViewerPage implements OnInit, OnDestroy {
|
||||||
hasOffline = false;
|
hasOffline = false;
|
||||||
refreshIcon = CoreConstants.ICON_LOADING;
|
refreshIcon = CoreConstants.ICON_LOADING;
|
||||||
syncIcon = CoreConstants.ICON_LOADING;
|
syncIcon = CoreConstants.ICON_LOADING;
|
||||||
offlineComment?: CoreCommentsOfflineWithUser;
|
offlineComment?: CoreCommentsOfflineWithUser & { pending?: boolean };
|
||||||
currentUserId: number;
|
currentUserId: number;
|
||||||
sending = false;
|
sending = false;
|
||||||
newComment = '';
|
newComment = '';
|
||||||
|
@ -361,13 +361,9 @@ export class CoreCommentsViewerPage implements OnInit, OnDestroy {
|
||||||
/**
|
/**
|
||||||
* Delete a comment.
|
* Delete a comment.
|
||||||
*
|
*
|
||||||
* @param e Click event.
|
|
||||||
* @param comment Comment to delete.
|
* @param comment Comment to delete.
|
||||||
*/
|
*/
|
||||||
async deleteComment(e: Event, comment: CoreCommentsDataToDisplay | CoreCommentsOfflineWithUser): Promise<void> {
|
async deleteComment(comment: CoreCommentsDataToDisplay | CoreCommentsOfflineWithUser): Promise<void> {
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
|
|
||||||
const modified = 'lastmodified' in comment
|
const modified = 'lastmodified' in comment
|
||||||
? comment.lastmodified
|
? comment.lastmodified
|
||||||
: comment.timecreated;
|
: comment.timecreated;
|
||||||
|
@ -529,15 +525,16 @@ export class CoreCommentsViewerPage implements OnInit, OnDestroy {
|
||||||
).then(async (offlineComment) => {
|
).then(async (offlineComment) => {
|
||||||
this.offlineComment = offlineComment;
|
this.offlineComment = offlineComment;
|
||||||
|
|
||||||
if (!offlineComment) {
|
if (!this.offlineComment) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.newComment == '') {
|
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;
|
return;
|
||||||
}));
|
}));
|
||||||
|
@ -573,13 +570,9 @@ export class CoreCommentsViewerPage implements OnInit, OnDestroy {
|
||||||
/**
|
/**
|
||||||
* Restore a comment.
|
* Restore a comment.
|
||||||
*
|
*
|
||||||
* @param e Click event.
|
|
||||||
* @param comment Comment to delete.
|
* @param comment Comment to delete.
|
||||||
*/
|
*/
|
||||||
async undoDeleteComment(e: Event, comment: CoreCommentsDataToDisplay): Promise<void> {
|
async undoDeleteComment(comment: CoreCommentsDataToDisplay): Promise<void> {
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
|
|
||||||
await CoreCommentsOffline.undoDeleteComment(comment.id);
|
await CoreCommentsOffline.undoDeleteComment(comment.id);
|
||||||
|
|
||||||
comment.deleted = false;
|
comment.deleted = false;
|
||||||
|
|
|
@ -1 +1,5 @@
|
||||||
@import "~theme/components/discussion.scss";
|
@import "~theme/components/discussion.scss";
|
||||||
|
|
||||||
|
ion-badge {
|
||||||
|
margin: 8px auto;
|
||||||
|
}
|
||||||
|
|
|
@ -27,148 +27,3 @@ ion-content {
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
font-size: 0.9rem;
|
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);
|
--core-collapsible-footer-background: var(--contrast-background);
|
||||||
|
|
||||||
--addon-messages-message-bg: var(--gray-800);
|
--core-messages-message-bg: var(--gray-800);
|
||||||
--addon-messages-message-activated-bg: var(--gray-700);
|
--core-messages-message-activated-bg: var(--gray-700);
|
||||||
--addon-messages-message-note-text: var(--subdued-text-color);
|
--core-messages-message-note-text: var(--subdued-text-color);
|
||||||
--addon-messages-message-mine-bg: var(--gray-700);
|
--core-messages-message-mine-bg: var(--gray-700);
|
||||||
--addon-messages-message-mine-activated-bg: var(--gray-600);
|
--core-messages-message-mine-activated-bg: var(--gray-600);
|
||||||
--addon-messages-discussion-badge: var(--primary);
|
--core-messages-discussion-badge: var(--primary);
|
||||||
--addon-messages-discussion-badge-text: var(--gray-100);
|
--core-messages-discussion-badge-text: var(--gray-100);
|
||||||
|
|
||||||
--addon-forum-border-color: var(--gray-500);
|
--addon-forum-border-color: var(--gray-500);
|
||||||
--addon-forum-highlight-color: var(--gray-200);
|
--addon-forum-highlight-color: var(--gray-200);
|
||||||
|
|
|
@ -344,15 +344,15 @@
|
||||||
--addon-calendar-today-border-color: var(--primary);
|
--addon-calendar-today-border-color: var(--primary);
|
||||||
--addon-calendar-border-color: var(--stroke);
|
--addon-calendar-border-color: var(--stroke);
|
||||||
|
|
||||||
--addon-messages-message-bg: var(--white);
|
--core-messages-message-bg: var(--white);
|
||||||
--addon-messages-message-activated-bg: var(--gray-200);
|
--core-messages-message-activated-bg: var(--gray-200);
|
||||||
--addon-messages-message-note-text: var(--gray-500);
|
--core-messages-message-note-text: var(--gray-500);
|
||||||
--addon-messages-message-note-font-size: 75%;
|
--core-messages-message-note-font-size: 75%;
|
||||||
--addon-messages-message-mine-bg: var(--gray-300);
|
--core-messages-message-mine-bg: var(--gray-300);
|
||||||
--addon-messages-message-mine-activated-bg: var(--gray-400);
|
--core-messages-message-mine-activated-bg: var(--gray-400);
|
||||||
--addon-messages-avatar-size: 30px;
|
--core-messages-avatar-size: 30px;
|
||||||
--addon-messages-discussion-badge: var(--primary);
|
--core-messages-discussion-badge: var(--primary);
|
||||||
--addon-messages-discussion-badge-text: var(--white);
|
--core-messages-discussion-badge-text: var(--white);
|
||||||
|
|
||||||
--addon-forum-avatar-size: var(--core-avatar-size);
|
--addon-forum-avatar-size: var(--core-avatar-size);
|
||||||
--addon-forum-border-color: var(--stroke);
|
--addon-forum-border-color: var(--stroke);
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
This files describes API changes in the Moodle Mobile app,
|
This files describes API changes in the Moodle Mobile app,
|
||||||
information provided here is intended especially for developers.
|
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".
|
- Zoom levels changed from "normal / low / high" to " none / medium / high".
|
||||||
|
- --addon-messages-* CSS3 variables have been renamed to --core-messages-*
|
||||||
|
|
||||||
=== 4.0.0 ===
|
=== 4.0.0 ===
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue