MOBILE-2327 messages: Add messages discussions page
parent
67f8b32c42
commit
8fbed26e98
|
@ -0,0 +1,42 @@
|
|||
// (C) Copyright 2015 Martin Dougiamas
|
||||
//
|
||||
// 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 { NgModule } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { IonicModule } from 'ionic-angular';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { CoreComponentsModule } from '../../../components/components.module';
|
||||
import { CoreDirectivesModule } from '../../../directives/directives.module';
|
||||
import { CorePipesModule } from '../../../pipes/pipes.module';
|
||||
import { AddonMessagesDiscussionsComponent } from '../components/discussions/discussions';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
AddonMessagesDiscussionsComponent
|
||||
],
|
||||
imports: [
|
||||
CommonModule,
|
||||
IonicModule,
|
||||
TranslateModule.forChild(),
|
||||
CoreComponentsModule,
|
||||
CoreDirectivesModule,
|
||||
CorePipesModule
|
||||
],
|
||||
providers: [
|
||||
],
|
||||
exports: [
|
||||
AddonMessagesDiscussionsComponent
|
||||
]
|
||||
})
|
||||
export class AddonMessagesComponentsModule {}
|
|
@ -0,0 +1,45 @@
|
|||
<ion-content>
|
||||
<ion-refresher [enabled]="loaded" (ionRefresh)="refreshData($event)">
|
||||
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
|
||||
</ion-refresher>
|
||||
|
||||
<core-search-box *ngIf="search.enabled" (onSubmit)="searchMessage($event)" [placeholder]=" 'addon.messages.message' | translate" [initialValue]="search.text" autocorrect="off" spellcheck="false" lengthCheck="2"></core-search-box>
|
||||
|
||||
<core-loading [hideUntil]="loaded" [message]="loadingMessage">
|
||||
<!-- Message telling there are no files. -->
|
||||
<core-empty-box *ngIf="(!discussions || discussions.length <= 0) && !search.showResults" icon="chatbubbles" [message]="'addon.messages.nomessages' | translate"></core-empty-box>
|
||||
|
||||
<core-empty-box *ngIf="(!search.results || search.results.length <= 0) && search.showResults" icon="search" [message]="'core.noresults' | translate"></core-empty-box>
|
||||
|
||||
<ion-list *ngIf="search.showResults" no-margin>
|
||||
<ion-item-divider color="light">
|
||||
<h2>{{ 'core.searchresults' | translate }}</h2>
|
||||
<ion-note item-end>{{ search.results.length }}</ion-note>
|
||||
<button item-end ion-button icon-only clear="true" class="addon-messages-clear-search" (click)="clearSearch()" [attr.aria-label]="'core.clearsearch' | translate">
|
||||
<ion-icon name="close"></ion-icon>
|
||||
</button>
|
||||
</ion-item-divider>
|
||||
<ion-item text-wrap *ngFor="let result of search.results" [title]="result.fullname" (click)="gotoDiscussion(result.userid, result.messageid)" [class.core-split-item-selected]="result.userid == discussionUserId" detail-none>
|
||||
<ion-avatar item-start>
|
||||
<img src="{{result.profileimageurl}}" [alt]="'core.pictureof' | translate:{$a: result.fullname}" core-external-content onError="this.src='assets/img/user-avatar.png'">
|
||||
</ion-avatar>
|
||||
<h2><core-format-text [text]="result.fullname"></core-format-text></h2>
|
||||
<p><core-format-text clean="true" singleLine="true" [text]="result.lastmessage"></core-format-text></p>
|
||||
</ion-item>
|
||||
</ion-list>
|
||||
|
||||
<ion-list *ngIf="!search.showResults" no-margin>
|
||||
<ion-item text-wrap *ngFor="let discussion of discussions" [title]="discussion.fullname" (click)="gotoDiscussion(discussion.message.user)" [class.core-split-item-selected]="discussion.message.user == discussionUserId" detail-none>
|
||||
<ion-avatar item-start>
|
||||
<img src="{{discussion.profileimageurl}}" [alt]="'core.pictureof' | translate:{$a: discussion.fullname}" core-external-content onError="this.src='assets/img/user-avatar.png'">
|
||||
</ion-avatar>
|
||||
<h2><core-format-text [text]="discussion.fullname"></core-format-text></h2>
|
||||
<ion-note item-end *ngIf="discussion.message.timecreated > 0 || discussion.unread">
|
||||
<div *ngIf="discussion.message.timecreated> 0">{{discussion.message.timecreated / 1000 | coreDateDayOrTime}}</div>
|
||||
<div text-right *ngIf="discussion.unread" class="core-primary-circle"></div>
|
||||
</ion-note>
|
||||
<p><core-format-text clean="true" singleLine="true" [text]="discussion.message.message"></core-format-text></p>
|
||||
</ion-item>
|
||||
</ion-list>
|
||||
</core-loading>
|
||||
</ion-content>
|
|
@ -0,0 +1,234 @@
|
|||
// (C) Copyright 2015 Martin Dougiamas
|
||||
//
|
||||
// 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 { Component, OnDestroy, ViewChild } from '@angular/core';
|
||||
import { Platform, NavParams } from 'ionic-angular';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { CoreEventsProvider } from '../../../../providers/events';
|
||||
import { CoreSitesProvider } from '../../../../providers/sites';
|
||||
import { AddonMessagesProvider } from '../../providers/messages';
|
||||
import { CoreDomUtilsProvider } from '../../../../providers/utils/dom';
|
||||
import { CoreAppProvider } from '../../../../providers/app';
|
||||
|
||||
/**
|
||||
* Component that displays the list of discussions.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'addon-messages-discussions',
|
||||
templateUrl: 'discussions.html',
|
||||
})
|
||||
export class AddonMessagesDiscussionsComponent implements OnDestroy {
|
||||
protected newMessagesObserver: any;
|
||||
protected readChangedObserver: any;
|
||||
protected cronObserver: any;
|
||||
protected appResumeSubscription: any;
|
||||
protected loadingMessages: string;
|
||||
protected siteId: string;
|
||||
|
||||
loaded = false;
|
||||
loadingMessage: string;
|
||||
discussions: any;
|
||||
discussionUserId: number;
|
||||
messageId: number;
|
||||
search = {
|
||||
enabled: false,
|
||||
showResults: false,
|
||||
results: [],
|
||||
loading: '',
|
||||
text: ''
|
||||
};
|
||||
|
||||
constructor(private eventsProvider: CoreEventsProvider, sitesProvider: CoreSitesProvider, translate: TranslateService,
|
||||
private messagesProvider: AddonMessagesProvider, private domUtils: CoreDomUtilsProvider, navParams: NavParams,
|
||||
private appProvider: CoreAppProvider, platform: Platform) {
|
||||
|
||||
this.search.loading = translate.instant('core.searching');
|
||||
this.loadingMessages = translate.instant('core.loading');
|
||||
this.siteId = sitesProvider.getCurrentSiteId();
|
||||
|
||||
// Update discussions when new message is received.
|
||||
this.newMessagesObserver = eventsProvider.on(AddonMessagesProvider.NEW_MESSAGE_EVENT, (data) => {
|
||||
if (data.userId) {
|
||||
const discussion = this.discussions[data.userId];
|
||||
|
||||
if (typeof discussion == 'undefined') {
|
||||
this.loaded = false;
|
||||
this.refreshData().finally(() => {
|
||||
this.loaded = true;
|
||||
});
|
||||
} else {
|
||||
// An existing discussion has a new message, update the last message.
|
||||
discussion.message.message = data.message;
|
||||
discussion.message.timecreated = data.timecreated;
|
||||
}
|
||||
}
|
||||
}, this.siteId);
|
||||
|
||||
// Update discussions when a message is read.
|
||||
this.readChangedObserver = eventsProvider.on(AddonMessagesProvider.READ_CHANGED_EVENT, (data) => {
|
||||
if (data.userId) {
|
||||
const discussion = this.discussions[data.userId];
|
||||
|
||||
if (typeof discussion != 'undefined') {
|
||||
// A discussion has been read reset counter.
|
||||
discussion.unread = false;
|
||||
|
||||
// Discussions changed, invalidate them.
|
||||
this.messagesProvider.invalidateDiscussionsCache();
|
||||
}
|
||||
}
|
||||
}, this.siteId);
|
||||
|
||||
// Update discussions when cron read is executed.
|
||||
this.cronObserver = eventsProvider.on(AddonMessagesProvider.READ_CRON_EVENT, (data) => {
|
||||
this.refreshData();
|
||||
}, this.siteId);
|
||||
|
||||
// Refresh the view when the app is resumed.
|
||||
this.appResumeSubscription = platform.resume.subscribe(() => {
|
||||
if (!this.loaded) {
|
||||
return;
|
||||
}
|
||||
this.loaded = false;
|
||||
this.refreshData();
|
||||
});
|
||||
|
||||
this.discussionUserId = navParams.get('discussionUserId') || false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Component loaded.
|
||||
*/
|
||||
ngOnInit(): void {
|
||||
if (this.discussionUserId) {
|
||||
// There is a discussion to load, open the discussion in a new state.
|
||||
this.gotoDiscussion(this.discussionUserId);
|
||||
}
|
||||
|
||||
this.fetchData().then(() => {
|
||||
if (!this.discussionUserId && this.discussions.length > 0) {
|
||||
// Take first and load it.
|
||||
this.gotoDiscussion(this.discussions[0].message.user, undefined, true);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh the data.
|
||||
*
|
||||
* @param {any} [refresher] Refresher.
|
||||
* @return {Promise<any>} Promise resolved when done.
|
||||
*/
|
||||
refreshData(refresher?: any): Promise<any> {
|
||||
return this.messagesProvider.invalidateDiscussionsCache().then(() => {
|
||||
return this.fetchData().finally(() => {
|
||||
if (refresher) {
|
||||
// Actions to take if refresh comes from the user.
|
||||
this.eventsProvider.trigger(AddonMessagesProvider.READ_CHANGED_EVENT, undefined, this.siteId);
|
||||
refresher.complete();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch discussions.
|
||||
*
|
||||
* @return {Promise<any>} Promise resolved when done.
|
||||
*/
|
||||
protected fetchData(): Promise<any> {
|
||||
this.loadingMessage = this.loadingMessages;
|
||||
this.search.enabled = this.messagesProvider.isSearchMessagesEnabled();
|
||||
|
||||
return this.messagesProvider.getDiscussions().then((discussions) => {
|
||||
// Convert to an array for sorting.
|
||||
const discussionsSorted = [];
|
||||
for (const userId in discussions) {
|
||||
discussions[userId].unread = !!discussions[userId].unread;
|
||||
discussionsSorted.push(discussions[userId]);
|
||||
}
|
||||
|
||||
this.discussions = discussionsSorted.sort((a, b) => {
|
||||
return a.message.timecreated - b.message.timecreated;
|
||||
});
|
||||
}).catch((error) => {
|
||||
this.domUtils.showErrorModalDefault(error, 'addon.messages.errorwhileretrievingdiscussions', true);
|
||||
}).finally(() => {
|
||||
this.loaded = true;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear search and show discussions again.
|
||||
*/
|
||||
clearSearch(): void {
|
||||
this.loaded = false;
|
||||
this.search.showResults = false;
|
||||
this.fetchData().finally(() => {
|
||||
this.loaded = true;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Search messages cotaining text.
|
||||
*
|
||||
* @param {string} query Text to search for.
|
||||
* @return {Promise<any>} Resolved when done.
|
||||
*/
|
||||
searchMessage(query: string): Promise<any> {
|
||||
this.appProvider.closeKeyboard();
|
||||
this.loaded = false;
|
||||
this.loadingMessage = this.search.loading;
|
||||
|
||||
return this.messagesProvider.searchMessages(query).then((searchResults) => {
|
||||
this.search.showResults = true;
|
||||
this.search.results = searchResults;
|
||||
}).catch((error) => {
|
||||
this.domUtils.showErrorModalDefault(error, 'mma.messages.errorwhileretrievingmessages', true);
|
||||
}).finally(() => {
|
||||
this.loaded = true;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigate to a particular discussion.
|
||||
*
|
||||
* @param {number} discussionUserId Discussion Id to load.
|
||||
* @param {number} [messageId] Message to scroll after loading the discussion. Used when searching.
|
||||
* @param {boolean} [onlyWithSplitView=false] Only go to Discussion if split view is on.
|
||||
*/
|
||||
gotoDiscussion(discussionUserId: number, messageId?: number, onlyWithSplitView: boolean = false): void {
|
||||
this.discussionUserId = discussionUserId;
|
||||
|
||||
const params = {
|
||||
discussion: discussionUserId,
|
||||
onlyWithSplitView: onlyWithSplitView
|
||||
};
|
||||
if (messageId) {
|
||||
params['message'] = messageId;
|
||||
this.messageId = messageId;
|
||||
}
|
||||
this.eventsProvider.trigger(AddonMessagesProvider.SPLIT_VIEW_LOAD_EVENT, params, this.siteId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Component destroyed.
|
||||
*/
|
||||
ngOnDestroy(): void {
|
||||
this.newMessagesObserver && this.newMessagesObserver.off();
|
||||
this.readChangedObserver && this.readChangedObserver.off();
|
||||
this.cronObserver && this.cronObserver.off();
|
||||
this.appResumeSubscription && this.appResumeSubscription.unsubscribe();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"errorwhileretrievingdiscussions": "Error while retrieving discussions from the server.",
|
||||
"message": "Message",
|
||||
"messages": "Messages",
|
||||
"nomessages": "No messages"
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
// (C) Copyright 2015 Martin Dougiamas
|
||||
//
|
||||
// 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 { NgModule } from '@angular/core';
|
||||
import { AddonMessagesProvider } from './providers/messages';
|
||||
import { AddonMessagesOfflineProvider } from './providers/messages-offline';
|
||||
import { AddonMessagesMainMenuHandler } from './providers/mainmenu-handler';
|
||||
import { CoreMainMenuDelegate } from '../../core/mainmenu/providers/delegate';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
],
|
||||
imports: [
|
||||
],
|
||||
providers: [
|
||||
AddonMessagesProvider,
|
||||
AddonMessagesOfflineProvider,
|
||||
AddonMessagesMainMenuHandler
|
||||
]
|
||||
})
|
||||
export class AddonMessagesModule {
|
||||
constructor(mainMenuDelegate: CoreMainMenuDelegate, mainmenuHandler: AddonMessagesMainMenuHandler) {
|
||||
// Register handlers.
|
||||
mainMenuDelegate.registerHandler(mainmenuHandler);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
<ion-header>
|
||||
<ion-navbar>
|
||||
<ion-title>{{ 'addon.messages.messages' | translate }}</ion-title>
|
||||
</ion-navbar>
|
||||
</ion-header>
|
||||
<core-split-view>
|
||||
<ion-content>
|
||||
<core-tabs>
|
||||
<core-tab [title]="'addon.messages.messages' | translate" icon="chatbubbles">
|
||||
<ng-template>
|
||||
<addon-messages-discussions></addon-messages-discussions>
|
||||
</ng-template>
|
||||
</core-tab>
|
||||
<!-- <core-tab [title]="'addon.messages.contacts' | translate" icon="person">
|
||||
<ng-template>
|
||||
<addon-messages-contacts></addon-messages-contacts>
|
||||
</ng-template>
|
||||
</core-tab>-->
|
||||
</core-tabs>
|
||||
</ion-content>
|
||||
</core-split-view>
|
|
@ -0,0 +1,37 @@
|
|||
// (C) Copyright 2015 Martin Dougiamas
|
||||
//
|
||||
// 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 { NgModule } from '@angular/core';
|
||||
import { IonicPageModule } from 'ionic-angular';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { AddonMessagesIndexPage } from './index';
|
||||
import { CoreComponentsModule } from '../../../../components/components.module';
|
||||
import { CoreDirectivesModule } from '../../../../directives/directives.module';
|
||||
import { CorePipesModule } from '../../../../pipes/pipes.module';
|
||||
import { AddonMessagesComponentsModule } from '../../components/components.module';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
AddonMessagesIndexPage,
|
||||
],
|
||||
imports: [
|
||||
CoreComponentsModule,
|
||||
CoreDirectivesModule,
|
||||
CorePipesModule,
|
||||
AddonMessagesComponentsModule,
|
||||
IonicPageModule.forChild(AddonMessagesIndexPage),
|
||||
TranslateModule.forChild()
|
||||
],
|
||||
})
|
||||
export class AddonMessagesIndexPageModule {}
|
|
@ -0,0 +1,71 @@
|
|||
// (C) Copyright 2015 Martin Dougiamas
|
||||
//
|
||||
// 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 { Component, OnDestroy, ViewChild } from '@angular/core';
|
||||
import { IonicPage } from 'ionic-angular';
|
||||
import { CoreEventsProvider } from '../../../../providers/events';
|
||||
import { CoreSitesProvider } from '../../../../providers/sites';
|
||||
import { AddonMessagesProvider } from '../../providers/messages';
|
||||
import { CoreSplitViewComponent } from '../../../../components/split-view/split-view';
|
||||
|
||||
/**
|
||||
* Page that displays the messages index page.
|
||||
*/
|
||||
@IonicPage({ segment: 'addon-messages-index' })
|
||||
@Component({
|
||||
selector: 'page-addon-messages-index',
|
||||
templateUrl: 'index.html',
|
||||
})
|
||||
export class AddonMessagesIndexPage implements OnDestroy {
|
||||
@ViewChild(CoreSplitViewComponent) splitviewCtrl: CoreSplitViewComponent;
|
||||
|
||||
protected loadSplitViewObserver: any;
|
||||
protected siteId: string;
|
||||
|
||||
constructor(private eventsProvider: CoreEventsProvider, sitesProvider: CoreSitesProvider,
|
||||
private messagesProvider: AddonMessagesProvider) {
|
||||
|
||||
this.siteId = sitesProvider.getCurrentSiteId();
|
||||
|
||||
// Update split view or navigate.
|
||||
this.loadSplitViewObserver = eventsProvider.on(AddonMessagesProvider.SPLIT_VIEW_LOAD_EVENT, (data) => {
|
||||
if (data.discussion && (this.splitviewCtrl.isOn() || !data.onlyWithSplitView)) {
|
||||
this.gotoDiscussion(data.discussion, data.message);
|
||||
}
|
||||
}, this.siteId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigate to a particular discussion.
|
||||
*
|
||||
* @param {number} discussionUserId Discussion Id to load.
|
||||
* @param {number} [messageId] Message to scroll after loading the discussion. Used when searching.
|
||||
*/
|
||||
gotoDiscussion(discussionUserId: number, messageId?: number): void {
|
||||
const params = {
|
||||
id: discussionUserId
|
||||
};
|
||||
if (messageId) {
|
||||
params['message'] = messageId;
|
||||
}
|
||||
this.splitviewCtrl.push('AddonMessagesDiscussionPage', params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Page destroyed.
|
||||
*/
|
||||
ngOnDestroy(): void {
|
||||
this.loadSplitViewObserver && this.loadSplitViewObserver.off();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,51 @@
|
|||
// (C) Copyright 2015 Martin Dougiamas
|
||||
//
|
||||
// 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 { Injectable } from '@angular/core';
|
||||
import { AddonMessagesProvider } from './messages';
|
||||
import { CoreMainMenuHandler, CoreMainMenuHandlerData } from '../../../core/mainmenu/providers/delegate';
|
||||
|
||||
/**
|
||||
* Handler to inject an option into main menu.
|
||||
*/
|
||||
@Injectable()
|
||||
export class AddonMessagesMainMenuHandler implements CoreMainMenuHandler {
|
||||
name = 'AddonMessages';
|
||||
priority = 600;
|
||||
|
||||
constructor(private messagesProvider: AddonMessagesProvider) { }
|
||||
|
||||
/**
|
||||
* Check if the handler is enabled on a site level.
|
||||
*
|
||||
* @return {boolean} Whether or not the handler is enabled on a site level.
|
||||
*/
|
||||
isEnabled(): boolean | Promise<boolean> {
|
||||
return this.messagesProvider.isPluginEnabled();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the data needed to render the handler.
|
||||
*
|
||||
* @return {CoreMainMenuHandlerData} Data needed to render the handler.
|
||||
*/
|
||||
getDisplayData(): CoreMainMenuHandlerData {
|
||||
return {
|
||||
icon: 'chatbubbles',
|
||||
title: 'addon.messages.messages',
|
||||
page: 'AddonMessagesIndexPage',
|
||||
class: 'addon-messages-handler'
|
||||
};
|
||||
}
|
||||
}
|
|
@ -0,0 +1,74 @@
|
|||
// (C) Copyright 2015 Martin Dougiamas
|
||||
//
|
||||
// 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 { Injectable } from '@angular/core';
|
||||
import { CoreLoggerProvider } from '../../../providers/logger';
|
||||
import { CoreSitesProvider } from '../../../providers/sites';
|
||||
|
||||
/**
|
||||
* Service to handle Offline messages.
|
||||
*/
|
||||
@Injectable()
|
||||
export class AddonMessagesOfflineProvider {
|
||||
|
||||
protected logger;
|
||||
|
||||
// Variables for database.
|
||||
protected MESSAGES_TABLE = 'mma_messages_offline_messages';
|
||||
protected tablesSchema = [
|
||||
{
|
||||
name: this.MESSAGES_TABLE,
|
||||
columns: [
|
||||
{
|
||||
name: 'touserid',
|
||||
type: 'INTEGER'
|
||||
},
|
||||
{
|
||||
name: 'useridfrom',
|
||||
type: 'INTEGER'
|
||||
},
|
||||
{
|
||||
name: 'smallmessage',
|
||||
type: 'TEXT'
|
||||
},
|
||||
{
|
||||
name: 'timecreated',
|
||||
type: 'INTEGER'
|
||||
},
|
||||
{
|
||||
name: 'deviceoffline', // If message was stored because device was offline.
|
||||
type: 'INTEGER'
|
||||
}
|
||||
],
|
||||
primaryKeys: ['touserid', 'smallmessage', 'timecreated']
|
||||
}
|
||||
];
|
||||
|
||||
constructor(logger: CoreLoggerProvider, private sitesProvider: CoreSitesProvider) {
|
||||
this.logger = logger.getInstance('AddonMessagesOfflineProvider');
|
||||
this.sitesProvider.createTablesFromSchema(this.tablesSchema);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all offline messages.
|
||||
*
|
||||
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||
* @return {Promise<any>} Promise resolved with messages.
|
||||
*/
|
||||
getAllMessages(siteId?: string): Promise<any> {
|
||||
return this.sitesProvider.getSite(siteId).then((site) => {
|
||||
return site.getDb().getAllRecords(this.MESSAGES_TABLE);
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,354 @@
|
|||
// (C) Copyright 2015 Martin Dougiamas
|
||||
//
|
||||
// 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 { Injectable } from '@angular/core';
|
||||
import { CoreLoggerProvider } from '../../../providers/logger';
|
||||
import { CoreSitesProvider } from '../../../providers/sites';
|
||||
import { CoreAppProvider } from '../../../providers/app';
|
||||
import { CoreUserProvider } from '../../../core/user/providers/user';
|
||||
import { AddonMessagesOfflineProvider } from './messages-offline';
|
||||
|
||||
/**
|
||||
* Service to handle messages.
|
||||
*/
|
||||
@Injectable()
|
||||
export class AddonMessagesProvider {
|
||||
protected ROOT_CACHE_KEY = 'mmaMessages:';
|
||||
protected LIMIT_MESSAGES = 50;
|
||||
static NEW_MESSAGE_EVENT = 'new_message_event';
|
||||
static READ_CHANGED_EVENT = 'read_changed_event';
|
||||
static READ_CRON_EVENT = 'read_cron_event';
|
||||
static SPLIT_VIEW_LOAD_EVENT = 'split_view_load_event';
|
||||
|
||||
protected logger;
|
||||
|
||||
constructor(logger: CoreLoggerProvider, private sitesProvider: CoreSitesProvider, private appProvider: CoreAppProvider,
|
||||
private userProvider: CoreUserProvider, private messagesOffline: AddonMessagesOfflineProvider) {
|
||||
this.logger = logger.getInstance('AddonMessagesProvider');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the cache key for contacts.
|
||||
*
|
||||
* @return {string} Cache key.
|
||||
*/
|
||||
protected getCacheKeyForContacts(): string {
|
||||
return this.ROOT_CACHE_KEY + 'contacts';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the cache key for the list of discussions.
|
||||
*
|
||||
* @return {string} Cache key.
|
||||
*/
|
||||
protected getCacheKeyForDiscussions(): string {
|
||||
return this.ROOT_CACHE_KEY + 'discussions';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the discussions of the current user.
|
||||
*
|
||||
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||
* @return {Promise<any>} Resolved with an object where the keys are the user ID of the other user.
|
||||
*/
|
||||
getDiscussions(siteId?: string): Promise<any> {
|
||||
return this.sitesProvider.getSite(siteId).then((site) => {
|
||||
const discussions = {},
|
||||
currentUserId = site.getUserId(),
|
||||
params = {
|
||||
useridto: currentUserId,
|
||||
useridfrom: 0,
|
||||
limitnum: this.LIMIT_MESSAGES
|
||||
},
|
||||
preSets = {
|
||||
cacheKey: this.getCacheKeyForDiscussions()
|
||||
};
|
||||
|
||||
/**
|
||||
* Convenience function to treat a recent message, adding it to discussions list if needed.
|
||||
*/
|
||||
const treatRecentMessage = (message: any, userId: number, userFullname: string): void => {
|
||||
if (typeof discussions[userId] === 'undefined') {
|
||||
discussions[userId] = {
|
||||
fullname: userFullname,
|
||||
profileimageurl: ''
|
||||
};
|
||||
|
||||
if (!message.timeread && !message.pending && message.useridfrom != currentUserId) {
|
||||
discussions[userId].unread = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Extract the most recent message. Pending messages are considered more recent than messages already sent.
|
||||
const discMessage = discussions[userId].message;
|
||||
if (typeof discMessage === 'undefined' || (!discMessage.pending && message.pending) ||
|
||||
(discMessage.pending == message.pending && (discMessage.timecreated < message.timecreated ||
|
||||
(discMessage.timecreated == message.timecreated && discMessage.id < message.id)))) {
|
||||
|
||||
discussions[userId].message = {
|
||||
id: message.id,
|
||||
user: userId,
|
||||
message: message.text,
|
||||
timecreated: message.timecreated,
|
||||
pending: message.pending
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
// Get recent messages sent to current user.
|
||||
return this.getRecentMessages(params, preSets, undefined, undefined, undefined, site.getId()).then((messages) => {
|
||||
|
||||
// Extract the discussions by filtering same senders.
|
||||
messages.forEach((message) => {
|
||||
treatRecentMessage(message, message.useridfrom, message.userfromfullname);
|
||||
});
|
||||
|
||||
// Now get the last messages sent by the current user.
|
||||
params.useridfrom = params.useridto;
|
||||
params.useridto = 0;
|
||||
|
||||
return this.getRecentMessages(params, preSets);
|
||||
}).then((messages) => {
|
||||
|
||||
// Extract the discussions by filtering same senders.
|
||||
messages.forEach((message) => {
|
||||
treatRecentMessage(message, message.useridto, message.usertofullname);
|
||||
});
|
||||
|
||||
// Now get unsent messages.
|
||||
return this.messagesOffline.getAllMessages(site.getId());
|
||||
}).then((offlineMessages) => {
|
||||
offlineMessages.forEach((message) => {
|
||||
message.pending = true;
|
||||
message.text = message.smallmessage;
|
||||
treatRecentMessage(message, message.touserid, '');
|
||||
});
|
||||
|
||||
return this.getDiscussionsUserImg(discussions, site.getId()).then((discussions) => {
|
||||
this.storeUsersFromDiscussions(discussions);
|
||||
|
||||
return discussions;
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user images for all the discussions that don't have one already.
|
||||
*
|
||||
* @param {any} discussions List of discussions.
|
||||
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||
* @return {Promise<any>} Promise always resolved. Resolve param is the formatted discussions.
|
||||
*/
|
||||
protected getDiscussionsUserImg(discussions: any, siteId?: string): Promise<any> {
|
||||
const promises = [];
|
||||
|
||||
for (const userId in discussions) {
|
||||
if (!discussions[userId].profileimageurl) {
|
||||
// We don't have the user image. Try to retrieve it.
|
||||
promises.push(this.userProvider.getProfile(discussions[userId].message.user, 0, true, siteId).then((user) => {
|
||||
discussions[userId].profileimageurl = user.profileimageurl;
|
||||
}).catch(() => {
|
||||
// Error getting profile, resolve promise without adding any extra data.
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
return Promise.all(promises).then(() => {
|
||||
return discussions;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get messages according to the params.
|
||||
*
|
||||
* @param {any} params Parameters to pass to the WS.
|
||||
* @param {any} preSets Set of presets for the WS.
|
||||
* @param {boolean} [toDisplay=true] True if messages will be displayed to the user, either in view or in a notification.
|
||||
* @param {string} [siteId] Site ID. If not defined, use current site.
|
||||
* @return {Promise<any>}
|
||||
*/
|
||||
protected getMessages(params: any, preSets: any, toDisplay: boolean = true, siteId?: string): Promise<any> {
|
||||
params['type'] = 'conversations';
|
||||
params['newestfirst'] = 1;
|
||||
|
||||
return this.sitesProvider.getSite(siteId).then((site) => {
|
||||
const userId = site.getUserId();
|
||||
|
||||
return site.read('core_message_get_messages', params, preSets).then((response) => {
|
||||
response.messages.forEach((message) => {
|
||||
message.read = params.read == 0 ? 0 : 1;
|
||||
// Convert times to milliseconds.
|
||||
message.timecreated = message.timecreated ? message.timecreated * 1000 : 0;
|
||||
message.timeread = message.timeread ? message.timeread * 1000 : 0;
|
||||
});
|
||||
|
||||
if (toDisplay && this.appProvider.isDesktop() && !params.read && params.useridto == userId &&
|
||||
params.limitfrom === 0) {
|
||||
// Store the last unread received messages. Don't block the user for this.
|
||||
// @todo
|
||||
// this.storeLastReceivedMessageIfNeeded(params.useridfrom, response.messages[0], site.getId());
|
||||
}
|
||||
|
||||
return response;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the most recent messages.
|
||||
*
|
||||
* @param {any} params Parameters to pass to the WS.
|
||||
* @param {any} preSets Set of presets for the WS.
|
||||
* @param {number} [limitFromUnread=0] Number of read messages already fetched, so fetch will be done from this number.
|
||||
* @param {number} [limitFromRead=0] Number of unread messages already fetched, so fetch will be done from this number.
|
||||
* @param {boolean} [toDisplay=true] True if messages will be displayed to the user, either in view or in a notification.
|
||||
* @param {string} [siteId] Site ID. If not defined, use current site.
|
||||
* @return {Promise<any>}
|
||||
*/
|
||||
protected getRecentMessages(params: any, preSets: any, limitFromUnread: number = 0, limitFromRead: number = 0,
|
||||
toDisplay: boolean = true, siteId?: string): Promise<any> {
|
||||
limitFromUnread = limitFromUnread || 0;
|
||||
limitFromRead = limitFromRead || 0;
|
||||
|
||||
params['read'] = 0;
|
||||
params['limitfrom'] = limitFromUnread;
|
||||
|
||||
return this.getMessages(params, preSets, toDisplay, siteId).then((response) => {
|
||||
let messages = response.messages;
|
||||
if (messages) {
|
||||
if (messages.length >= params.limitnum) {
|
||||
return messages;
|
||||
}
|
||||
|
||||
// We need to fetch more messages.
|
||||
params.limitnum = params.limitnum - messages.length;
|
||||
params.read = 1;
|
||||
params.limitfrom = limitFromRead;
|
||||
|
||||
return this.getMessages(params, preSets, toDisplay, siteId).then((response) => {
|
||||
if (response.messages) {
|
||||
messages = messages.concat(response.messages);
|
||||
}
|
||||
|
||||
return messages;
|
||||
}).catch(() => {
|
||||
return messages;
|
||||
});
|
||||
|
||||
} else {
|
||||
return Promise.reject(null);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidate contacts cache.
|
||||
*
|
||||
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||
* @return {Promise<any>} Resolved when done.
|
||||
*/
|
||||
invalidateContactsCache(siteId?: string): Promise<any> {
|
||||
return this.sitesProvider.getSite(siteId).then((site) => {
|
||||
return site.invalidateWsCacheForKey(this.getCacheKeyForContacts());
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidate discussions cache.
|
||||
*
|
||||
* Note that {@link this.getDiscussions} uses the contacts, so we need to invalidate contacts too.
|
||||
*
|
||||
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||
* @return {Promise<any>} Resolved when done.
|
||||
*/
|
||||
invalidateDiscussionsCache(siteId?: string): Promise<any> {
|
||||
return this.sitesProvider.getSite(siteId).then((site) => {
|
||||
return site.invalidateWsCacheForKey(this.getCacheKeyForDiscussions()).then(() => {
|
||||
return this.invalidateContactsCache(site.getId());
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether or not the plugin is enabled in a certain site.
|
||||
*
|
||||
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||
* @return {Promise<any>} Promise resolved with true if enabled, rejected or resolved with false otherwise.
|
||||
*/
|
||||
isPluginEnabled(siteId?: string): Promise<any> {
|
||||
return this.sitesProvider.getSite(siteId).then((site) => {
|
||||
return site.canUseAdvancedFeature('messaging');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether or not we can search messages.
|
||||
*
|
||||
* @return {boolean}
|
||||
* @since 3.2
|
||||
*/
|
||||
isSearchMessagesEnabled(): boolean {
|
||||
return this.sitesProvider.getCurrentSite().wsAvailable('core_message_data_for_messagearea_search_messages');
|
||||
}
|
||||
|
||||
/**
|
||||
* Search for all the messges with a specific text.
|
||||
*
|
||||
* @param {string} query The query string
|
||||
* @param {number} [userId] The user ID. If not defined, current user.
|
||||
* @param {number} [from=0] Position of the first result to get. Defaults to 0.
|
||||
* @param {number} [limit] Number of results to get. Defaults to LIMIT_MESSAGES.
|
||||
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||
* @return {Promise<any>} Promise resolved with the results.
|
||||
*/
|
||||
searchMessages(query: string, userId?: number, from: number = 0, limit: number = this.LIMIT_MESSAGES, siteId?: string):
|
||||
Promise<any> {
|
||||
return this.sitesProvider.getSite(siteId).then((site) => {
|
||||
const param = {
|
||||
userid: userId || site.getUserId(),
|
||||
search: query,
|
||||
limitfrom: from,
|
||||
limitnum: limit
|
||||
},
|
||||
preSets = {
|
||||
getFromCache: false // Always try to get updated data. If it fails, it will get it from cache.
|
||||
};
|
||||
|
||||
return site.read('core_message_data_for_messagearea_search_messages', param, preSets).then((searchResults) => {
|
||||
return searchResults.contacts;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Store user data from discussions in local DB.
|
||||
*
|
||||
* @param {any} discussions List of discussions.
|
||||
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||
*/
|
||||
protected storeUsersFromDiscussions(discussions: any, siteId?: string): void {
|
||||
const users = [];
|
||||
for (const userId in discussions) {
|
||||
if (typeof userId != 'undefined' && !isNaN(parseInt(userId))) {
|
||||
users.push({
|
||||
id: userId,
|
||||
fullname: discussions[userId].fullname,
|
||||
profileimageurl: discussions[userId].profileimageurl
|
||||
});
|
||||
}
|
||||
}
|
||||
this.userProvider.storeUsers(users, siteId);
|
||||
}
|
||||
}
|
|
@ -69,6 +69,7 @@ import { AddonUserProfileFieldModule } from '../addon/userprofilefield/userprofi
|
|||
import { AddonFilesModule } from '../addon/files/files.module';
|
||||
import { AddonModBookModule } from '../addon/mod/book/book.module';
|
||||
import { AddonModLabelModule } from '../addon/mod/label/label.module';
|
||||
import { AddonMessagesModule } from '../addon/messages/messages.module';
|
||||
|
||||
// For translate loader. AoT requires an exported function for factories.
|
||||
export function createTranslateLoader(http: HttpClient): TranslateHttpLoader {
|
||||
|
@ -109,7 +110,8 @@ export function createTranslateLoader(http: HttpClient): TranslateHttpLoader {
|
|||
AddonUserProfileFieldModule,
|
||||
AddonFilesModule,
|
||||
AddonModBookModule,
|
||||
AddonModLabelModule
|
||||
AddonModLabelModule,
|
||||
AddonMessagesModule
|
||||
],
|
||||
bootstrap: [IonicApp],
|
||||
entryComponents: [
|
||||
|
|
|
@ -439,3 +439,15 @@ ion-toast.core-toast-alert .toast-wrapper{
|
|||
border-bottom: 3px solid $color-base;
|
||||
}
|
||||
}
|
||||
|
||||
.core-circle:before {
|
||||
content: ' \25CF';
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
@each $color-name, $color-base, $color-contrast in get-colors($colors) {
|
||||
.core-#{$color-name}-circle:before {
|
||||
@extend .core-circle:before;
|
||||
color: $color-base;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -77,3 +77,10 @@ core-tabs {
|
|||
@extend .header-md::after;
|
||||
}
|
||||
}
|
||||
|
||||
.ios, .md, .wp {
|
||||
.core-avoid-header ion-content core-tabs core-tab ion-content {
|
||||
top: 0;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -93,6 +93,7 @@ export class SQLiteDBMock extends SQLiteDB {
|
|||
tx.executeSql(sql, params, (tx, results) => {
|
||||
resolve(results);
|
||||
}, (tx, error) => {
|
||||
console.error(sql, params, error);
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
<div *ngIf="site" text-wrap text-center [ngClass]="{'item-avatar-center': site.avatar}">
|
||||
<ion-avatar *ngIf="site.avatar">
|
||||
<!-- Show user avatar. -->
|
||||
<img [src]="site.avatar" class="avatar" core-external-content [siteId]="site.id" alt="{{ 'core.pictureof' | translate:{$a: site.fullname} }}" role="presentation">
|
||||
<img [src]="site.avatar" class="avatar" core-external-content [siteId]="site.id" alt="{{ 'core.pictureof' | translate:{$a: site.fullname} }}" role="presentation" onError="this.src='assets/img/user-avatar.png'">
|
||||
</ion-avatar>
|
||||
|
||||
<!-- Show site logo or a default image. -->
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
<ion-list>
|
||||
<ion-item (click)="login(site.id)" *ngFor="let site of sites; let idx = index">
|
||||
<ion-avatar item-start>
|
||||
<img [src]="site.avatar" core-external-content [siteId]="site.id" alt="{{ 'core.pictureof' | translate:{$a: site.fullname} }}" role="presentation">
|
||||
<img [src]="site.avatar" core-external-content [siteId]="site.id" alt="{{ 'core.pictureof' | translate:{$a: site.fullname} }}" role="presentation" onError="this.src='assets/img/user-avatar.png'">
|
||||
</ion-avatar>
|
||||
<h2>{{site.fullName}}</h2>
|
||||
<p><core-format-text [text]="site.siteName" clean="true" watch="true" [siteId]="site.id"></core-format-text></p>
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
<ion-item text-center *ngIf="(!handlers || !handlers.length) && !handlersLoaded">
|
||||
<ion-spinner></ion-spinner>
|
||||
</ion-item>
|
||||
<ion-item *ngFor="let handler of handlers" class="core-moremenu-handler {{handler.class}}" (click)="openHandler(handler)" title="{{ handler.title | translate }}">
|
||||
<ion-item *ngFor="let handler of handlers" [ngClass]="['core-moremenu-handler', handler.class]" (click)="openHandler(handler)" title="{{ handler.title | translate }}">
|
||||
<ion-icon [name]="handler.icon" item-start></ion-icon>
|
||||
<p>{{ handler.title | translate}}</p>
|
||||
<!-- @todo: Badge. -->
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
<ion-list *ngIf="participants && participants.length > 0" no-margin>
|
||||
<a ion-item text-wrap *ngFor="let participant of participants" [title]="participant.fullname" (click)="gotoParticipant(participant.id)" [class.core-split-item-selected]="participant.id == participantId">
|
||||
<ion-avatar item-start>
|
||||
<img src="{{participant.profileimageurl}}" [alt]="'core.pictureof' | translate:{$a: participant.fullname}" core-external-content>
|
||||
<img src="{{participant.profileimageurl}}" [alt]="'core.pictureof' | translate:{$a: participant.fullname}" core-external-content onError="this.src='assets/img/user-avatar.png'">
|
||||
</ion-avatar>
|
||||
<h2><core-format-text [text]="participant.fullname"></core-format-text></h2>
|
||||
<p *ngIf="participant.lastaccess"><strong>{{ 'core.lastaccess' | translate }}: </strong>{{ participant.lastaccess * 1000 | coreFormatDate:"dfmediumdate"}}</p>
|
||||
|
|
|
@ -11,8 +11,7 @@
|
|||
<ion-list *ngIf="user && !isDeleted">
|
||||
<ion-item text-center>
|
||||
<div class="item-avatar-center">
|
||||
<img *ngIf="user.profileimageurl" class="avatar" [src]="user.profileimageurl" core-external-content alt="{{ 'core.pictureof' | translate:{$a: user.fullname} }}" role="presentation">
|
||||
<img *ngIf="!user.profileimageurl" class="avatar" src="assets/img/user-avatar.png" alt="{{ 'core.pictureof' | translate:{$a: user.fullname} }}" role="presentation">
|
||||
<img class="avatar" [src]="user.profileimageurl" core-external-content alt="{{ 'core.pictureof' | translate:{$a: user.fullname} }}" role="presentation" onError="this.src='assets/img/user-avatar.png'">
|
||||
<ion-icon name="create" class="core-icon-foreground" *ngIf="canChangeProfilePicture" (click)="changeProfilePicture()"></ion-icon>
|
||||
</div>
|
||||
<h2><core-format-text [text]="user.fullname"></core-format-text></h2>
|
||||
|
|
Loading…
Reference in New Issue