commit
ac574a76f3
|
@ -346,6 +346,7 @@
|
||||||
"addon.mod_book.errorchapter": "book",
|
"addon.mod_book.errorchapter": "book",
|
||||||
"addon.mod_book.modulenameplural": "book",
|
"addon.mod_book.modulenameplural": "book",
|
||||||
"addon.mod_chat.beep": "chat",
|
"addon.mod_chat.beep": "chat",
|
||||||
|
"addon.mod_chat.chatreport": "chat",
|
||||||
"addon.mod_chat.currentusers": "chat",
|
"addon.mod_chat.currentusers": "chat",
|
||||||
"addon.mod_chat.enterchat": "chat",
|
"addon.mod_chat.enterchat": "chat",
|
||||||
"addon.mod_chat.entermessage": "chat",
|
"addon.mod_chat.entermessage": "chat",
|
||||||
|
@ -357,12 +358,16 @@
|
||||||
"addon.mod_chat.messagebeepsyou": "chat",
|
"addon.mod_chat.messagebeepsyou": "chat",
|
||||||
"addon.mod_chat.messageenter": "chat",
|
"addon.mod_chat.messageenter": "chat",
|
||||||
"addon.mod_chat.messageexit": "chat",
|
"addon.mod_chat.messageexit": "chat",
|
||||||
|
"addon.mod_chat.messages": "chat",
|
||||||
"addon.mod_chat.modulenameplural": "chat",
|
"addon.mod_chat.modulenameplural": "chat",
|
||||||
"addon.mod_chat.mustbeonlinetosendmessages": "local_moodlemobileapp",
|
"addon.mod_chat.mustbeonlinetosendmessages": "local_moodlemobileapp",
|
||||||
"addon.mod_chat.nomessages": "chat",
|
"addon.mod_chat.nomessages": "chat",
|
||||||
|
"addon.mod_chat.nosessionsfound": "local_moodlemobileapp",
|
||||||
"addon.mod_chat.send": "chat",
|
"addon.mod_chat.send": "chat",
|
||||||
"addon.mod_chat.sessionstart": "chat",
|
"addon.mod_chat.sessionstart": "chat",
|
||||||
|
"addon.mod_chat.showincompletesessions": "local_moodlemobileapp",
|
||||||
"addon.mod_chat.talk": "chat",
|
"addon.mod_chat.talk": "chat",
|
||||||
|
"addon.mod_chat.viewreport": "chat",
|
||||||
"addon.mod_choice.cannotsubmit": "choice",
|
"addon.mod_choice.cannotsubmit": "choice",
|
||||||
"addon.mod_choice.choiceoptions": "choice",
|
"addon.mod_choice.choiceoptions": "choice",
|
||||||
"addon.mod_choice.errorgetchoice": "local_moodlemobileapp",
|
"addon.mod_choice.errorgetchoice": "local_moodlemobileapp",
|
||||||
|
@ -1295,6 +1300,7 @@
|
||||||
"core.defaultvalue": "tool_usertours",
|
"core.defaultvalue": "tool_usertours",
|
||||||
"core.delete": "moodle",
|
"core.delete": "moodle",
|
||||||
"core.deletedoffline": "local_moodlemobileapp",
|
"core.deletedoffline": "local_moodlemobileapp",
|
||||||
|
"core.deleteduser": "bulkusers",
|
||||||
"core.deleting": "local_moodlemobileapp",
|
"core.deleting": "local_moodlemobileapp",
|
||||||
"core.description": "moodle",
|
"core.description": "moodle",
|
||||||
"core.dfdaymonthyear": "local_moodlemobileapp",
|
"core.dfdaymonthyear": "local_moodlemobileapp",
|
||||||
|
@ -1545,6 +1551,7 @@
|
||||||
"core.noresults": "moodle",
|
"core.noresults": "moodle",
|
||||||
"core.notapplicable": "local_moodlemobileapp",
|
"core.notapplicable": "local_moodlemobileapp",
|
||||||
"core.notice": "moodle",
|
"core.notice": "moodle",
|
||||||
|
"core.notingroup": "moodle",
|
||||||
"core.notsent": "local_moodlemobileapp",
|
"core.notsent": "local_moodlemobileapp",
|
||||||
"core.now": "moodle",
|
"core.now": "moodle",
|
||||||
"core.numwords": "moodle",
|
"core.numwords": "moodle",
|
||||||
|
|
|
@ -15,11 +15,13 @@
|
||||||
import { NgModule } from '@angular/core';
|
import { NgModule } from '@angular/core';
|
||||||
import { CoreContentLinksDelegate } from '@core/contentlinks/providers/delegate';
|
import { CoreContentLinksDelegate } from '@core/contentlinks/providers/delegate';
|
||||||
import { CoreCourseModuleDelegate } from '@core/course/providers/module-delegate';
|
import { CoreCourseModuleDelegate } from '@core/course/providers/module-delegate';
|
||||||
|
import { CoreCourseModulePrefetchDelegate } from '@core/course/providers/module-prefetch-delegate';
|
||||||
import { AddonModChatComponentsModule } from './components/components.module';
|
import { AddonModChatComponentsModule } from './components/components.module';
|
||||||
import { AddonModChatProvider } from './providers/chat';
|
import { AddonModChatProvider } from './providers/chat';
|
||||||
import { AddonModChatLinkHandler } from './providers/link-handler';
|
import { AddonModChatLinkHandler } from './providers/link-handler';
|
||||||
import { AddonModChatListLinkHandler } from './providers/list-link-handler';
|
import { AddonModChatListLinkHandler } from './providers/list-link-handler';
|
||||||
import { AddonModChatModuleHandler } from './providers/module-handler';
|
import { AddonModChatModuleHandler } from './providers/module-handler';
|
||||||
|
import { AddonModChatPrefetchHandler } from './providers/prefetch-handler';
|
||||||
|
|
||||||
// List of providers (without handlers).
|
// List of providers (without handlers).
|
||||||
export const ADDON_MOD_CHAT_PROVIDERS: any[] = [
|
export const ADDON_MOD_CHAT_PROVIDERS: any[] = [
|
||||||
|
@ -37,15 +39,18 @@ export const ADDON_MOD_CHAT_PROVIDERS: any[] = [
|
||||||
AddonModChatLinkHandler,
|
AddonModChatLinkHandler,
|
||||||
AddonModChatListLinkHandler,
|
AddonModChatListLinkHandler,
|
||||||
AddonModChatModuleHandler,
|
AddonModChatModuleHandler,
|
||||||
|
AddonModChatPrefetchHandler
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
export class AddonModChatModule {
|
export class AddonModChatModule {
|
||||||
constructor(moduleDelegate: CoreCourseModuleDelegate, moduleHandler: AddonModChatModuleHandler,
|
constructor(moduleDelegate: CoreCourseModuleDelegate, moduleHandler: AddonModChatModuleHandler,
|
||||||
contentLinksDelegate: CoreContentLinksDelegate, linkHandler: AddonModChatLinkHandler,
|
contentLinksDelegate: CoreContentLinksDelegate, linkHandler: AddonModChatLinkHandler,
|
||||||
|
prefetchDelegate: CoreCourseModulePrefetchDelegate, prefetchHandler: AddonModChatPrefetchHandler,
|
||||||
listLinkHandler: AddonModChatListLinkHandler) {
|
listLinkHandler: AddonModChatListLinkHandler) {
|
||||||
|
|
||||||
moduleDelegate.registerHandler(moduleHandler);
|
moduleDelegate.registerHandler(moduleHandler);
|
||||||
contentLinksDelegate.registerHandler(linkHandler);
|
contentLinksDelegate.registerHandler(linkHandler);
|
||||||
contentLinksDelegate.registerHandler(listLinkHandler);
|
contentLinksDelegate.registerHandler(listLinkHandler);
|
||||||
|
prefetchDelegate.registerHandler(prefetchHandler);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,6 +5,7 @@
|
||||||
<core-context-menu-item *ngIf="description" [priority]="800" [content]="'core.moduleintro' | translate" (action)="expandDescription()" [iconAction]="'arrow-forward'"></core-context-menu-item>
|
<core-context-menu-item *ngIf="description" [priority]="800" [content]="'core.moduleintro' | translate" (action)="expandDescription()" [iconAction]="'arrow-forward'"></core-context-menu-item>
|
||||||
<core-context-menu-item *ngIf="blog" [priority]="750" content="{{'addon.blog.blog' | translate}}" [iconAction]="'fa-newspaper-o'" (action)="gotoBlog($event)"></core-context-menu-item>
|
<core-context-menu-item *ngIf="blog" [priority]="750" content="{{'addon.blog.blog' | translate}}" [iconAction]="'fa-newspaper-o'" (action)="gotoBlog($event)"></core-context-menu-item>
|
||||||
<core-context-menu-item *ngIf="loaded && !hasOffline && isOnline" [priority]="700" [content]="'core.refresh' | translate" (action)="doRefresh(null, $event)" [iconAction]="refreshIcon" [closeOnClick]="false"></core-context-menu-item>
|
<core-context-menu-item *ngIf="loaded && !hasOffline && isOnline" [priority]="700" [content]="'core.refresh' | translate" (action)="doRefresh(null, $event)" [iconAction]="refreshIcon" [closeOnClick]="false"></core-context-menu-item>
|
||||||
|
<core-context-menu-item *ngIf="prefetchStatusIcon" [priority]="500" [content]="prefetchText" (action)="prefetch()" [iconAction]="prefetchStatusIcon" [closeOnClick]="false"></core-context-menu-item>
|
||||||
</core-context-menu>
|
</core-context-menu>
|
||||||
</core-navbar-buttons>
|
</core-navbar-buttons>
|
||||||
|
|
||||||
|
@ -17,7 +18,8 @@
|
||||||
<ion-icon name="time"></ion-icon> {{ 'addon.mod_chat.sessionstart' | translate:{$a: chatInfo} }}
|
<ion-icon name="time"></ion-icon> {{ 'addon.mod_chat.sessionstart' | translate:{$a: chatInfo} }}
|
||||||
</ion-card>
|
</ion-card>
|
||||||
|
|
||||||
<div padding-horizontal>
|
<div padding>
|
||||||
<a ion-button block color="primary" (click)="enterChat()">{{ 'addon.mod_chat.enterchat' | translate }}</a>
|
<a ion-button block color="primary" (click)="enterChat()">{{ 'addon.mod_chat.enterchat' | translate }}</a>
|
||||||
|
<a ion-button block color="light" margin-top *ngIf="sessionsAvailable" (click)="viewSessions()">{{ 'addon.mod_chat.viewreport' | translate }}</a>
|
||||||
</div>
|
</div>
|
||||||
</core-loading>
|
</core-loading>
|
||||||
|
|
|
@ -33,6 +33,7 @@ export class AddonModChatIndexComponent extends CoreCourseModuleMainActivityComp
|
||||||
chatInfo: any;
|
chatInfo: any;
|
||||||
|
|
||||||
protected title: string;
|
protected title: string;
|
||||||
|
protected sessionsAvailable = false;
|
||||||
|
|
||||||
constructor(injector: Injector, private chatProvider: AddonModChatProvider, private timeUtils: CoreTimeUtilsProvider,
|
constructor(injector: Injector, private chatProvider: AddonModChatProvider, private timeUtils: CoreTimeUtilsProvider,
|
||||||
protected navCtrl: NavController) {
|
protected navCtrl: NavController) {
|
||||||
|
@ -83,6 +84,10 @@ export class AddonModChatIndexComponent extends CoreCourseModuleMainActivityComp
|
||||||
|
|
||||||
// All data obtained, now fill the context menu.
|
// All data obtained, now fill the context menu.
|
||||||
this.fillContextMenu(refresh);
|
this.fillContextMenu(refresh);
|
||||||
|
|
||||||
|
return this.chatProvider.areSessionsAvailable().then((available) => {
|
||||||
|
this.sessionsAvailable = available;
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -93,4 +98,11 @@ export class AddonModChatIndexComponent extends CoreCourseModuleMainActivityComp
|
||||||
const title = this.chat.name || this.moduleName;
|
const title = this.chat.name || this.moduleName;
|
||||||
this.navCtrl.push('AddonModChatChatPage', {chatId: this.chat.id, courseId: this.courseId, title: title });
|
this.navCtrl.push('AddonModChatChatPage', {chatId: this.chat.id, courseId: this.courseId, title: title });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* View past sessions.
|
||||||
|
*/
|
||||||
|
viewSessions(): void {
|
||||||
|
this.navCtrl.push('AddonModChatSessionsPage', {courseId: this.courseId, chatId: this.chat.id, cmId: this.module.id});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
{
|
{
|
||||||
"beep": "Beep",
|
"beep": "Beep",
|
||||||
|
"chatreport": "Chat sessions",
|
||||||
"currentusers": "Current users",
|
"currentusers": "Current users",
|
||||||
"enterchat": "Click here to enter the chat now",
|
"enterchat": "Click here to enter the chat now",
|
||||||
"entermessage": "Enter your message",
|
"entermessage": "Enter your message",
|
||||||
|
@ -11,10 +12,14 @@
|
||||||
"messagebeepsyou": "{{$a}} has just beeped you!",
|
"messagebeepsyou": "{{$a}} has just beeped you!",
|
||||||
"messageenter": "{{$a}} has just entered this chat",
|
"messageenter": "{{$a}} has just entered this chat",
|
||||||
"messageexit": "{{$a}} has left this chat",
|
"messageexit": "{{$a}} has left this chat",
|
||||||
|
"messages": "Messages",
|
||||||
"modulenameplural": "Chats",
|
"modulenameplural": "Chats",
|
||||||
"mustbeonlinetosendmessages": "You must be online to send messages.",
|
"mustbeonlinetosendmessages": "You must be online to send messages.",
|
||||||
"nomessages": "No messages yet",
|
"nomessages": "No messages yet",
|
||||||
|
"nosessionsfound": "No sessions found",
|
||||||
"send": "Send",
|
"send": "Send",
|
||||||
"sessionstart": "The next chat session will start on {{$a.date}}, ({{$a.fromnow}} from now)",
|
"sessionstart": "The next chat session will start on {{$a.date}}, ({{$a.fromnow}} from now)",
|
||||||
"talk": "Talk"
|
"showincompletesessions": "Show incomplete sessions",
|
||||||
|
"talk": "Talk",
|
||||||
|
"viewreport": "View past chat sessions"
|
||||||
}
|
}
|
|
@ -0,0 +1,40 @@
|
||||||
|
<ion-header>
|
||||||
|
<ion-navbar core-back-button>
|
||||||
|
<ion-title>{{ 'addon.mod_chat.messages' | translate }}</ion-title>
|
||||||
|
</ion-navbar>
|
||||||
|
</ion-header>
|
||||||
|
<ion-content>
|
||||||
|
<ion-refresher [enabled]="loaded" (ionRefresh)="refreshMessages($event)">
|
||||||
|
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
|
||||||
|
</ion-refresher>
|
||||||
|
<core-loading [hideUntil]="loaded">
|
||||||
|
<div *ngFor="let message of messages; index as index; last as last">
|
||||||
|
<div text-center *ngIf="showDate(messages[index], messages[index - 1])" class="addon-mod-chat-notice">
|
||||||
|
<ion-badge text-wrap color="light">
|
||||||
|
<span>{{ message.timestamp * 1000 | coreFormatDate:"strftimedayshort" }}</span>
|
||||||
|
</ion-badge>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div text-center *ngIf="message.issystem && message.message == 'enter'" class="addon-mod-chat-notice">
|
||||||
|
<ion-badge text-wrap color="light">
|
||||||
|
<span>{{ message.timestamp * 1000 | coreFormatDate:"strftimetime" }} {{ 'addon.mod_chat.messageenter' | translate:{$a: message.userfullname} }}</span>
|
||||||
|
</ion-badge>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div text-center *ngIf="message.issystem && message.message == 'exit'" class="addon-mod-chat-notice">
|
||||||
|
<ion-badge text-wrap color="light">
|
||||||
|
<span>{{ message.timestamp * 1000 | coreFormatDate:"strftimetime" }} {{ 'addon.mod_chat.messageexit' | translate:{$a: message.userfullname} }}</span>
|
||||||
|
</ion-badge>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ion-item text-wrap *ngIf="!message.issystem && message.message.substr(0, 4) != 'beep'" class="addon-mod-chat-message">
|
||||||
|
<ion-avatar core-user-avatar [user]="message" item-start></ion-avatar>
|
||||||
|
<h2>
|
||||||
|
<p float-end>{{ message.timestamp * 1000 | coreFormatDate:"strftimetime" }}</p>
|
||||||
|
<core-format-text [text]="message.userfullname"></core-format-text>
|
||||||
|
</h2>
|
||||||
|
<core-format-text [text]="message.message"></core-format-text>
|
||||||
|
</ion-item>
|
||||||
|
</div>
|
||||||
|
</core-loading>
|
||||||
|
</ion-content>
|
|
@ -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 { CoreComponentsModule } from '@components/components.module';
|
||||||
|
import { CoreDirectivesModule } from '@directives/directives.module';
|
||||||
|
import { CorePipesModule } from '@pipes/pipes.module';
|
||||||
|
import { AddonModChatComponentsModule } from '../../components/components.module';
|
||||||
|
import { AddonModChatSessionMessagesPage } from './session-messages';
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
declarations: [
|
||||||
|
AddonModChatSessionMessagesPage,
|
||||||
|
],
|
||||||
|
imports: [
|
||||||
|
CoreComponentsModule,
|
||||||
|
CoreDirectivesModule,
|
||||||
|
CorePipesModule,
|
||||||
|
AddonModChatComponentsModule,
|
||||||
|
IonicPageModule.forChild(AddonModChatSessionMessagesPage),
|
||||||
|
TranslateModule.forChild()
|
||||||
|
],
|
||||||
|
})
|
||||||
|
export class AddonModChatSessionMessagesPageModule {}
|
|
@ -0,0 +1,9 @@
|
||||||
|
ion-app.app-root page-addon-mod-chat-session-messages {
|
||||||
|
.addon-mod-chat-notice {
|
||||||
|
margin-top: 10px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
.addon-mod-chat-message {
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,95 @@
|
||||||
|
// (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 } from '@angular/core';
|
||||||
|
import { IonicPage, NavParams } from 'ionic-angular';
|
||||||
|
import { CoreDomUtilsProvider } from '@providers/utils/dom';
|
||||||
|
import { AddonModChatProvider } from '../../providers/chat';
|
||||||
|
import * as moment from 'moment';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Page that displays list of chat session messages.
|
||||||
|
*/
|
||||||
|
@IonicPage({ segment: 'addon-mod-chat-session-messages' })
|
||||||
|
@Component({
|
||||||
|
selector: 'page-addon-mod-chat-session-messages',
|
||||||
|
templateUrl: 'session-messages.html',
|
||||||
|
})
|
||||||
|
export class AddonModChatSessionMessagesPage {
|
||||||
|
|
||||||
|
protected courseId: number;
|
||||||
|
protected chatId: number;
|
||||||
|
protected sessionStart: number;
|
||||||
|
protected sessionEnd: number;
|
||||||
|
protected groupId: number;
|
||||||
|
protected loaded = false;
|
||||||
|
protected messages = [];
|
||||||
|
|
||||||
|
constructor(navParams: NavParams, private domUtils: CoreDomUtilsProvider, private chatProvider: AddonModChatProvider) {
|
||||||
|
this.courseId = navParams.get('courseId');
|
||||||
|
this.chatId = navParams.get('chatId');
|
||||||
|
this.groupId = navParams.get('groupId');
|
||||||
|
this.sessionStart = navParams.get('sessionStart');
|
||||||
|
this.sessionEnd = navParams.get('sessionEnd');
|
||||||
|
|
||||||
|
this.fetchMessages();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch session messages.
|
||||||
|
*
|
||||||
|
* @return {Promise<any>} Promise resolved when done.
|
||||||
|
*/
|
||||||
|
protected fetchMessages(): Promise<any> {
|
||||||
|
return this.chatProvider.getSessionMessages(this.chatId, this.sessionStart, this.sessionEnd, this.groupId)
|
||||||
|
.then((messages) => {
|
||||||
|
return this.chatProvider.getMessagesUserData(messages, this.courseId).then((messages) => {
|
||||||
|
this.messages = messages;
|
||||||
|
});
|
||||||
|
}).catch((error) => {
|
||||||
|
this.domUtils.showErrorModalDefault(error, 'core.errorloadingcontent', true);
|
||||||
|
}).finally(() => {
|
||||||
|
this.loaded = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Refresh session messages.
|
||||||
|
*
|
||||||
|
* @param {any} refresher Refresher.
|
||||||
|
*/
|
||||||
|
refreshMessages(refresher: any): void {
|
||||||
|
this.chatProvider.invalidateSessionMessages(this.chatId, this.sessionStart, this.groupId).finally(() => {
|
||||||
|
this.fetchMessages().finally(() => {
|
||||||
|
refresher.complete();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the date should be displayed between messages (when the day changes at midnight for example).
|
||||||
|
*
|
||||||
|
* @param {any} message New message object.
|
||||||
|
* @param {any} prevMessage Previous message object.
|
||||||
|
* @return {boolean} True if messages are from diferent days, false othetwise.
|
||||||
|
*/
|
||||||
|
showDate(message: any, prevMessage: any): boolean {
|
||||||
|
if (!prevMessage) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if day has changed.
|
||||||
|
return !moment(message.timestamp * 1000).isSame(prevMessage.timestamp * 1000, 'day');
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,45 @@
|
||||||
|
<ion-header>
|
||||||
|
<ion-navbar core-back-button>
|
||||||
|
<ion-title>{{ 'addon.mod_chat.chatreport' | translate }}</ion-title>
|
||||||
|
</ion-navbar>
|
||||||
|
</ion-header>
|
||||||
|
<core-split-view>
|
||||||
|
<ion-content>
|
||||||
|
<ion-refresher [enabled]="loaded" (ionRefresh)="refreshSessions($event)">
|
||||||
|
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
|
||||||
|
</ion-refresher>
|
||||||
|
<core-loading [hideUntil]="loaded">
|
||||||
|
<ion-item text-wrap *ngIf="groupInfo && (groupInfo.separateGroups || groupInfo.visibleGroups)">
|
||||||
|
<ion-label id="addon-chat-groupslabel" *ngIf="groupInfo.separateGroups">{{ 'core.groupsseparate' | translate }}</ion-label>
|
||||||
|
<ion-label id="addon-chat-groupslabel" *ngIf="groupInfo.visibleGroups">{{ 'core.groupsvisible' | translate }}</ion-label>
|
||||||
|
<ion-select [(ngModel)]="groupId" (ionChange)="fetchSessions(true)" aria-labelledby="addon-chat-groupslabel" interface="action-sheet">
|
||||||
|
<ion-option *ngFor="let groupOpt of groupInfo.groups" [value]="groupOpt.id">{{groupOpt.name}}</ion-option>
|
||||||
|
</ion-select>
|
||||||
|
</ion-item>
|
||||||
|
<ion-item>
|
||||||
|
<ion-label id="addon-chat-showalllabel">{{ 'addon.mod_chat.showincompletesessions' | translate }}</ion-label>
|
||||||
|
<ion-toggle [(ngModel)]="showAll" (ionChange)="fetchSessions(true)" aria-labelledby="addon-chat-showalllabel"></ion-toggle>
|
||||||
|
</ion-item>
|
||||||
|
<ion-card *ngFor="let session of sessions" (click)="openSession(session)"
|
||||||
|
[class.addon-mod-chat-session-selected]="session.sessionstart == selectedSessionStart && groupId == selectedSessionGroupId"
|
||||||
|
[class.addon-mod-chat-session-show-more]="session.sessionusers.length < session.allsessionusers.length">
|
||||||
|
<ion-item text-wrap>
|
||||||
|
<h2>{{ session.sessionstart * 1000 | coreFormatDate }}</h2>
|
||||||
|
<p *ngIf="session.duration">{{ session.duration | coreDuration }}</p>
|
||||||
|
</ion-item>
|
||||||
|
<ion-card-content>
|
||||||
|
<p *ngFor="let user of session.sessionusers">
|
||||||
|
{{ user.userfullname }} <span *ngIf="user.messagecount">({{ user.messagecount }})</span>
|
||||||
|
</p>
|
||||||
|
</ion-card-content>
|
||||||
|
<div *ngIf="session.sessionusers.length < session.allsessionusers.length">
|
||||||
|
<button ion-button clear (click)="showMoreUsers(session, $event)">
|
||||||
|
{{ 'core.showmore' | translate }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</ion-card>
|
||||||
|
<core-empty-box *ngIf="sessions.length == 0" icon="chatbubbles" [message]="'addon.mod_chat.nosessionsfound' | translate">
|
||||||
|
</core-empty-box>
|
||||||
|
</core-loading>
|
||||||
|
</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 { CoreComponentsModule } from '@components/components.module';
|
||||||
|
import { CoreDirectivesModule } from '@directives/directives.module';
|
||||||
|
import { CorePipesModule } from '@pipes/pipes.module';
|
||||||
|
import { AddonModChatComponentsModule } from '../../components/components.module';
|
||||||
|
import { AddonModChatSessionsPage } from './sessions';
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
declarations: [
|
||||||
|
AddonModChatSessionsPage,
|
||||||
|
],
|
||||||
|
imports: [
|
||||||
|
CoreComponentsModule,
|
||||||
|
CoreDirectivesModule,
|
||||||
|
CorePipesModule,
|
||||||
|
AddonModChatComponentsModule,
|
||||||
|
IonicPageModule.forChild(AddonModChatSessionsPage),
|
||||||
|
TranslateModule.forChild()
|
||||||
|
],
|
||||||
|
})
|
||||||
|
export class AddonModChatSessionsPageModule {}
|
|
@ -0,0 +1,8 @@
|
||||||
|
ion-app.app-root page-addon-mod-chat-sessions {
|
||||||
|
.addon-mod-chat-session-show-more .card-content{
|
||||||
|
padding-bottom: 0;
|
||||||
|
}
|
||||||
|
.addon-mod-chat-session-selected {
|
||||||
|
border-top: 5px solid $core-splitview-selected;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,165 @@
|
||||||
|
// (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, ViewChild } from '@angular/core';
|
||||||
|
import { IonicPage, NavParams } from 'ionic-angular';
|
||||||
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
|
import { CoreSplitViewComponent } from '@components/split-view/split-view';
|
||||||
|
import { CoreUserProvider } from '@core/user/providers/user';
|
||||||
|
import { CoreGroupsProvider, CoreGroupInfo } from '@providers/groups';
|
||||||
|
import { CoreDomUtilsProvider } from '@providers/utils/dom';
|
||||||
|
import { CoreUtilsProvider } from '@providers/utils/utils';
|
||||||
|
import { AddonModChatProvider } from '../../providers/chat';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Page that displays list of chat sessions.
|
||||||
|
*/
|
||||||
|
@IonicPage({ segment: 'addon-mod-chat-sessions' })
|
||||||
|
@Component({
|
||||||
|
selector: 'page-addon-mod-chat-sessions',
|
||||||
|
templateUrl: 'sessions.html',
|
||||||
|
})
|
||||||
|
export class AddonModChatSessionsPage {
|
||||||
|
|
||||||
|
@ViewChild(CoreSplitViewComponent) splitviewCtrl: CoreSplitViewComponent;
|
||||||
|
|
||||||
|
protected courseId: number;
|
||||||
|
protected cmId: number;
|
||||||
|
protected chatId: number;
|
||||||
|
protected loaded = false;
|
||||||
|
protected showAll = false;
|
||||||
|
protected groupId = 0;
|
||||||
|
protected groupInfo: CoreGroupInfo;
|
||||||
|
protected sessions = [];
|
||||||
|
protected selectedSessionStart: number;
|
||||||
|
protected selectedSessionGroupId: number;
|
||||||
|
|
||||||
|
constructor(navParams: NavParams, private chatProvider: AddonModChatProvider, private domUtils: CoreDomUtilsProvider,
|
||||||
|
private userProvider: CoreUserProvider, private groupsProvider: CoreGroupsProvider,
|
||||||
|
private translate: TranslateService, private utils: CoreUtilsProvider) {
|
||||||
|
this.courseId = navParams.get('courseId');
|
||||||
|
this.cmId = navParams.get('cmId');
|
||||||
|
this.chatId = navParams.get('chatId');
|
||||||
|
|
||||||
|
this.fetchSessions().then(() => {
|
||||||
|
if (this.splitviewCtrl.isOn() && this.sessions.length > 0) {
|
||||||
|
this.openSession(this.sessions[0]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch chat sessions.
|
||||||
|
*
|
||||||
|
* @param {number} [showLoading] Display a loading modal.
|
||||||
|
* @return {Promise<any>} Promise resolved when done.
|
||||||
|
*/
|
||||||
|
fetchSessions(showLoading?: boolean): Promise<any> {
|
||||||
|
const modal = showLoading ? this.domUtils.showModalLoading() : null;
|
||||||
|
|
||||||
|
return this.groupsProvider.getActivityGroupInfo(this.cmId, false).then((groupInfo) => {
|
||||||
|
this.groupInfo = groupInfo;
|
||||||
|
|
||||||
|
if (groupInfo.groups && groupInfo.groups.length > 0) {
|
||||||
|
if (!groupInfo.groups.find((group) => group.id === this.groupId)) {
|
||||||
|
this.groupId = groupInfo.groups[0].id;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.groupId = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.chatProvider.getSessions(this.chatId, this.groupId, this.showAll);
|
||||||
|
}).then((sessions) => {
|
||||||
|
// Fetch user profiles.
|
||||||
|
const promises = [];
|
||||||
|
|
||||||
|
sessions.forEach((session) => {
|
||||||
|
session.duration = session.sessionend - session.sessionstart;
|
||||||
|
session.sessionusers.forEach((sessionUser) => {
|
||||||
|
if (!sessionUser.userfullname) {
|
||||||
|
// The WS does not return the user name, fetch user profile.
|
||||||
|
promises.push(this.userProvider.getProfile(sessionUser.userid, this.courseId, true).then((user) => {
|
||||||
|
sessionUser.userfullname = user.fullname;
|
||||||
|
}).catch(() => {
|
||||||
|
// Error getting profile, most probably the user is deleted.
|
||||||
|
sessionUser.userfullname = this.translate.instant('core.deleteduser') + ' ' + sessionUser.userid;
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// If session has more than 4 users we display a "Show more" link.
|
||||||
|
session.allsessionusers = session.sessionusers;
|
||||||
|
if (session.sessionusers.length > 4) {
|
||||||
|
session.sessionusers = session.allsessionusers.slice(0, 3);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return Promise.all(promises).then(() => {
|
||||||
|
this.sessions = sessions;
|
||||||
|
});
|
||||||
|
}).catch((error) => {
|
||||||
|
this.domUtils.showErrorModalDefault(error, 'core.errorloadingcontent', true);
|
||||||
|
}).finally(() => {
|
||||||
|
this.loaded = true;
|
||||||
|
modal && modal.dismiss();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Refresh chat sessions.
|
||||||
|
*
|
||||||
|
* @param {any} refresher Refresher.
|
||||||
|
*/
|
||||||
|
refreshSessions(refresher: any): void {
|
||||||
|
const promises = [
|
||||||
|
this.groupsProvider.invalidateActivityGroupInfo(this.cmId),
|
||||||
|
this.chatProvider.invalidateSessions(this.chatId, this.groupId, this.showAll)
|
||||||
|
];
|
||||||
|
|
||||||
|
this.utils.allPromises(promises).finally(() => {
|
||||||
|
this.fetchSessions().finally(() => {
|
||||||
|
refresher.complete();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Navigate to a session.
|
||||||
|
*
|
||||||
|
* @param {any} session Chat session.
|
||||||
|
*/
|
||||||
|
openSession(session: any): void {
|
||||||
|
this.selectedSessionStart = session.sessionstart;
|
||||||
|
this.selectedSessionGroupId = this.groupId;
|
||||||
|
const params = {
|
||||||
|
courseId: this.courseId,
|
||||||
|
chatId: this.chatId,
|
||||||
|
groupId: this.groupId,
|
||||||
|
sessionStart: session.sessionstart,
|
||||||
|
sessionEnd: session.sessionend
|
||||||
|
};
|
||||||
|
this.splitviewCtrl.push('AddonModChatSessionMessagesPage', params);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show more session users.
|
||||||
|
*
|
||||||
|
* @param {any} session Chat session.
|
||||||
|
* @param {Event} $event The event.
|
||||||
|
*/
|
||||||
|
showMoreUsers(session: any, $event: Event): void {
|
||||||
|
session.sessionusers = session.allsessionusers;
|
||||||
|
$event.stopPropagation();
|
||||||
|
}
|
||||||
|
}
|
|
@ -13,9 +13,12 @@
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
import { CoreSitesProvider } from '@providers/sites';
|
import { CoreSitesProvider } from '@providers/sites';
|
||||||
import { CoreUserProvider } from '@core/user/providers/user';
|
import { CoreUserProvider } from '@core/user/providers/user';
|
||||||
import { CoreCourseLogHelperProvider } from '@core/course/providers/log-helper';
|
import { CoreCourseLogHelperProvider } from '@core/course/providers/log-helper';
|
||||||
|
import { CoreUtilsProvider } from '@providers/utils/utils';
|
||||||
|
import { CoreSiteWSPreSets } from '@classes/site';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Service that provides some features for chats.
|
* Service that provides some features for chats.
|
||||||
|
@ -25,26 +28,29 @@ export class AddonModChatProvider {
|
||||||
static COMPONENT = 'mmaModChat';
|
static COMPONENT = 'mmaModChat';
|
||||||
static POLL_INTERVAL = 4000;
|
static POLL_INTERVAL = 4000;
|
||||||
|
|
||||||
|
protected ROOT_CACHE_KEY = 'AddonModChat:';
|
||||||
|
|
||||||
constructor(private sitesProvider: CoreSitesProvider, private userProvider: CoreUserProvider,
|
constructor(private sitesProvider: CoreSitesProvider, private userProvider: CoreUserProvider,
|
||||||
private logHelper: CoreCourseLogHelperProvider) {}
|
private logHelper: CoreCourseLogHelperProvider, protected utils: CoreUtilsProvider, private translate: TranslateService) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get a chat.
|
* Get a chat.
|
||||||
*
|
*
|
||||||
* @param {number} courseId Course ID.
|
* @param {number} courseId Course ID.
|
||||||
* @param {number} cmId Course module ID.
|
* @param {number} cmId Course module ID.
|
||||||
* @param {boolean} [refresh=false] True when we should not get the value from the cache.
|
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||||
* @return {Promise<any>} Promise resolved when the chat is retrieved.
|
* @return {Promise<any>} Promise resolved when the chat is retrieved.
|
||||||
*/
|
*/
|
||||||
getChat(courseId: number, cmId: number, refresh: boolean = false): Promise<any> {
|
getChat(courseId: number, cmId: number, siteId?: string): Promise<any> {
|
||||||
|
return this.sitesProvider.getSite(siteId).then((site) => {
|
||||||
const params = {
|
const params = {
|
||||||
courseids: [courseId]
|
courseids: [courseId]
|
||||||
};
|
};
|
||||||
const preSets = {
|
const preSets: CoreSiteWSPreSets = {
|
||||||
getFromCache: refresh ? false : undefined,
|
cacheKey: this.getChatsCacheKey(courseId)
|
||||||
};
|
};
|
||||||
|
|
||||||
return this.sitesProvider.getCurrentSite().read('mod_chat_get_chats_by_courses', params, preSets).then((response) => {
|
return site.read('mod_chat_get_chats_by_courses', params, preSets).then((response) => {
|
||||||
if (response.chats) {
|
if (response.chats) {
|
||||||
const chat = response.chats.find((chat) => chat.coursemodule == cmId);
|
const chat = response.chats.find((chat) => chat.coursemodule == cmId);
|
||||||
if (chat) {
|
if (chat) {
|
||||||
|
@ -54,6 +60,7 @@ export class AddonModChatProvider {
|
||||||
|
|
||||||
return Promise.reject(null);
|
return Promise.reject(null);
|
||||||
});
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -146,8 +153,8 @@ export class AddonModChatProvider {
|
||||||
message.userfullname = user.fullname;
|
message.userfullname = user.fullname;
|
||||||
message.userprofileimageurl = user.profileimageurl;
|
message.userprofileimageurl = user.profileimageurl;
|
||||||
}).catch(() => {
|
}).catch(() => {
|
||||||
// Error getting profile. Set default data.
|
// Error getting profile, most probably the user is deleted.
|
||||||
message.userfullname = message.userid;
|
message.userfullname = this.translate.instant('core.deleteduser') + ' ' + message.userid;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -172,4 +179,210 @@ export class AddonModChatProvider {
|
||||||
|
|
||||||
return this.sitesProvider.getCurrentSite().read('mod_chat_get_chat_users', params, preSets);
|
return this.sitesProvider.getCurrentSite().read('mod_chat_get_chat_users', params, preSets);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return whether WS for passed sessions are available.
|
||||||
|
*
|
||||||
|
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||||
|
* @return {Promise<boolean>} Promise resolved with a boolean.
|
||||||
|
*/
|
||||||
|
areSessionsAvailable(siteId?: string): Promise<boolean> {
|
||||||
|
return this.sitesProvider.getSite(siteId).then((site) => {
|
||||||
|
return site.wsAvailable('mod_chat_get_sessions') && site.wsAvailable('mod_chat_get_session_messages');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get chat sessions.
|
||||||
|
*
|
||||||
|
* @param {number} chatId Chat ID.
|
||||||
|
* @param {number} [groupId=0] Group ID, 0 means that the function will determine the user group.
|
||||||
|
* @param {boolean} [showAll=false] Whether to include incomplete sessions or not.
|
||||||
|
* @param {boolean} [ignoreCache=false] True if it should ignore cached data (it will always fail in offline or server down).
|
||||||
|
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||||
|
* @return {Promise<any[]>} Promise resolved with the list of sessions.
|
||||||
|
* @since 3.5
|
||||||
|
*/
|
||||||
|
getSessions(chatId: number, groupId: number = 0, showAll: boolean = false, ignoreCache: boolean = false, siteId?: string):
|
||||||
|
Promise<any[]> {
|
||||||
|
return this.sitesProvider.getSite(siteId).then((site) => {
|
||||||
|
const params = {
|
||||||
|
chatid: chatId,
|
||||||
|
groupid: groupId,
|
||||||
|
showall: showAll ? 1 : 0
|
||||||
|
};
|
||||||
|
const preSets: CoreSiteWSPreSets = {
|
||||||
|
cacheKey: this.getSessionsCacheKey(chatId, groupId, showAll),
|
||||||
|
};
|
||||||
|
if (ignoreCache) {
|
||||||
|
preSets.getFromCache = false;
|
||||||
|
preSets.emergencyCache = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return site.read('mod_chat_get_sessions', params, preSets).then((response) => {
|
||||||
|
if (!response || !response.sessions) {
|
||||||
|
return Promise.reject(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.sessions;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get chat session messages.
|
||||||
|
*
|
||||||
|
* @param {number} chatId Chat ID.
|
||||||
|
* @param {number} sessionStart Session start time.
|
||||||
|
* @param {number} sessionEnd Session end time.
|
||||||
|
* @param {number} [groupId=0] Group ID, 0 means that the function will determine the user group.
|
||||||
|
* @param {boolean} [ignoreCache=false] True if it should ignore cached data (it will always fail in offline or server down).
|
||||||
|
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||||
|
* @return {Promise<any[]>} Promise resolved with the list of messages.
|
||||||
|
* @since 3.5
|
||||||
|
*/
|
||||||
|
getSessionMessages(chatId: number, sessionStart: number, sessionEnd: number, groupId: number = 0, ignoreCache: boolean = false,
|
||||||
|
siteId?: string): Promise<any[]> {
|
||||||
|
return this.sitesProvider.getSite(siteId).then((site) => {
|
||||||
|
const params = {
|
||||||
|
chatid: chatId,
|
||||||
|
sessionstart: sessionStart,
|
||||||
|
sessionend: sessionEnd,
|
||||||
|
groupid: groupId
|
||||||
|
};
|
||||||
|
const preSets: CoreSiteWSPreSets = {
|
||||||
|
cacheKey: this.getSessionMessagesCacheKey(chatId, sessionStart, groupId)
|
||||||
|
};
|
||||||
|
if (ignoreCache) {
|
||||||
|
preSets.getFromCache = false;
|
||||||
|
preSets.emergencyCache = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return site.read('mod_chat_get_session_messages', params, preSets).then((response) => {
|
||||||
|
if (!response || !response.messages) {
|
||||||
|
return Promise.reject(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.messages;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invalidate chats.
|
||||||
|
*
|
||||||
|
* @param {number} courseId Course ID.
|
||||||
|
* @return {Promise<any>} Promise resolved when the data is invalidated.
|
||||||
|
*/
|
||||||
|
invalidateChats(courseId: number): Promise<any> {
|
||||||
|
const site = this.sitesProvider.getCurrentSite();
|
||||||
|
|
||||||
|
return site.invalidateWsCacheForKey(this.getChatsCacheKey(courseId));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invalidate chat sessions.
|
||||||
|
*
|
||||||
|
* @param {number} chatId Chat ID.
|
||||||
|
* @param {number} [groupId=0] Group ID, 0 means that the function will determine the user group.
|
||||||
|
* @param {boolean} [showAll=false] Whether to include incomplete sessions or not.
|
||||||
|
* @return {Promise<any>} Promise resolved when the data is invalidated.
|
||||||
|
*/
|
||||||
|
invalidateSessions(chatId: number, groupId: number = 0, showAll: boolean = false): Promise<any> {
|
||||||
|
const site = this.sitesProvider.getCurrentSite();
|
||||||
|
|
||||||
|
return site.invalidateWsCacheForKey(this.getSessionsCacheKey(chatId, groupId, showAll));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invalidate all chat sessions.
|
||||||
|
*
|
||||||
|
* @param {number} chatId Chat ID.
|
||||||
|
* @return {Promise<any>} Promise resolved when the data is invalidated.
|
||||||
|
*/
|
||||||
|
invalidateAllSessions(chatId: number): Promise<any> {
|
||||||
|
const site = this.sitesProvider.getCurrentSite();
|
||||||
|
|
||||||
|
return site.invalidateWsCacheForKeyStartingWith(this.getSessionsCacheKeyPrefix(chatId));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invalidate chat session messages.
|
||||||
|
*
|
||||||
|
* @param {number} chatId Chat ID.
|
||||||
|
* @param {number} sessionStart Session start time.
|
||||||
|
* @param {number} [groupId=0] Group ID, 0 means that the function will determine the user group.
|
||||||
|
* @return {Promise<any>} Promise resolved when the data is invalidated.
|
||||||
|
*/
|
||||||
|
invalidateSessionMessages(chatId: number, sessionStart: number, groupId: number = 0): Promise<any> {
|
||||||
|
const site = this.sitesProvider.getCurrentSite();
|
||||||
|
|
||||||
|
return site.invalidateWsCacheForKey(this.getSessionMessagesCacheKey(chatId, sessionStart, groupId));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invalidate all chat session messages.
|
||||||
|
*
|
||||||
|
* @param {number} chatId Chat ID.
|
||||||
|
* @return {Promise<any>} Promise resolved when the data is invalidated.
|
||||||
|
*/
|
||||||
|
invalidateAllSessionMessages(chatId: number): Promise<any> {
|
||||||
|
const site = this.sitesProvider.getCurrentSite();
|
||||||
|
|
||||||
|
return site.invalidateWsCacheForKeyStartingWith(this.getSessionMessagesCacheKeyPrefix(chatId));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get cache key for chats WS call.
|
||||||
|
*
|
||||||
|
* @param {number} courseId Course ID.
|
||||||
|
* @return {string} Cache key.
|
||||||
|
*/
|
||||||
|
protected getChatsCacheKey(courseId: number): string {
|
||||||
|
return this.ROOT_CACHE_KEY + 'chats:' + courseId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get cache key for sessions WS call.
|
||||||
|
*
|
||||||
|
* @param {number} chatId Chat ID.
|
||||||
|
* @param {number} groupId Goup ID, 0 means that the function will determine the user group.
|
||||||
|
* @param {boolean} showAll Whether to include incomplete sessions or not.
|
||||||
|
* @return {string} Cache key.
|
||||||
|
*/
|
||||||
|
protected getSessionsCacheKey(chatId: number, groupId: number, showAll: boolean): string {
|
||||||
|
return this.getSessionsCacheKeyPrefix(chatId) + groupId + ':' + (showAll ? 1 : 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get cache key prefix for sessions WS call.
|
||||||
|
*
|
||||||
|
* @param {number} chatId Chat ID.
|
||||||
|
* @return {string} Cache key prefix.
|
||||||
|
*/
|
||||||
|
protected getSessionsCacheKeyPrefix(chatId: number): string {
|
||||||
|
return this.ROOT_CACHE_KEY + 'sessions:' + chatId + ':';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get cache key for session messages WS call.
|
||||||
|
*
|
||||||
|
* @param {number} chatId Chat ID.
|
||||||
|
* @param {number} sessionStart Session start time.
|
||||||
|
* @param {number} groupId Group ID, 0 means that the function will determine the user group.
|
||||||
|
* @return {string} Cache key.
|
||||||
|
*/
|
||||||
|
protected getSessionMessagesCacheKey(chatId: number, sessionStart: number, groupId: number): string {
|
||||||
|
return this.getSessionMessagesCacheKeyPrefix(chatId) + sessionStart + ':' + groupId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get cache key prefix for session messages WS call.
|
||||||
|
*
|
||||||
|
* @param {number} chatId Chat ID.
|
||||||
|
* @return {string} Cache key prefix.
|
||||||
|
*/
|
||||||
|
protected getSessionMessagesCacheKeyPrefix(chatId: number): string {
|
||||||
|
return this.ROOT_CACHE_KEY + 'sessionsMessages:' + chatId + ':';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,6 +18,7 @@ import { AddonModChatIndexComponent } from '../components/index/index';
|
||||||
import { CoreCourseModuleHandler, CoreCourseModuleHandlerData } from '@core/course/providers/module-delegate';
|
import { CoreCourseModuleHandler, CoreCourseModuleHandlerData } from '@core/course/providers/module-delegate';
|
||||||
import { CoreCourseProvider } from '@core/course/providers/course';
|
import { CoreCourseProvider } from '@core/course/providers/course';
|
||||||
import { CoreConstants } from '@core/constants';
|
import { CoreConstants } from '@core/constants';
|
||||||
|
import { AddonModChatProvider } from './chat';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handler to support chat modules.
|
* Handler to support chat modules.
|
||||||
|
@ -38,7 +39,7 @@ export class AddonModChatModuleHandler implements CoreCourseModuleHandler {
|
||||||
[CoreConstants.FEATURE_SHOW_DESCRIPTION]: true
|
[CoreConstants.FEATURE_SHOW_DESCRIPTION]: true
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor(private courseProvider: CoreCourseProvider) { }
|
constructor(private courseProvider: CoreCourseProvider, private chatProvider: AddonModChatProvider) { }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if the handler is enabled on a site level.
|
* Check if the handler is enabled on a site level.
|
||||||
|
@ -58,7 +59,7 @@ export class AddonModChatModuleHandler implements CoreCourseModuleHandler {
|
||||||
* @return {CoreCourseModuleHandlerData} Data to render the module.
|
* @return {CoreCourseModuleHandlerData} Data to render the module.
|
||||||
*/
|
*/
|
||||||
getData(module: any, courseId: number, sectionId: number): CoreCourseModuleHandlerData {
|
getData(module: any, courseId: number, sectionId: number): CoreCourseModuleHandlerData {
|
||||||
return {
|
const data: CoreCourseModuleHandlerData = {
|
||||||
icon: this.courseProvider.getModuleIconSrc(this.modName, module.modicon),
|
icon: this.courseProvider.getModuleIconSrc(this.modName, module.modicon),
|
||||||
title: module.name,
|
title: module.name,
|
||||||
class: 'addon-mod_chat-handler',
|
class: 'addon-mod_chat-handler',
|
||||||
|
@ -70,6 +71,12 @@ export class AddonModChatModuleHandler implements CoreCourseModuleHandler {
|
||||||
navCtrl.push('AddonModChatIndexPage', pageParams, options);
|
navCtrl.push('AddonModChatIndexPage', pageParams, options);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
this.chatProvider.areSessionsAvailable().then((available) => {
|
||||||
|
data.showDownloadButton = available;
|
||||||
|
});
|
||||||
|
|
||||||
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -0,0 +1,185 @@
|
||||||
|
// (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 { TranslateService } from '@ngx-translate/core';
|
||||||
|
import { CoreAppProvider } from '@providers/app';
|
||||||
|
import { CoreFilepoolProvider } from '@providers/filepool';
|
||||||
|
import { CoreGroupsProvider, CoreGroupInfo } from '@providers/groups';
|
||||||
|
import { CoreSitesProvider } from '@providers/sites';
|
||||||
|
import { CoreDomUtilsProvider } from '@providers/utils/dom';
|
||||||
|
import { CoreUtilsProvider } from '@providers/utils/utils';
|
||||||
|
import { CoreCourseProvider } from '@core/course/providers/course';
|
||||||
|
import { CoreCourseActivityPrefetchHandlerBase } from '@core/course/classes/activity-prefetch-handler';
|
||||||
|
import { CoreUserProvider } from '@core/user/providers/user';
|
||||||
|
import { AddonModChatProvider } from './chat';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handler to prefetch chats.
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class AddonModChatPrefetchHandler extends CoreCourseActivityPrefetchHandlerBase {
|
||||||
|
name = 'AddonModChat';
|
||||||
|
modName = 'chat';
|
||||||
|
component = AddonModChatProvider.COMPONENT;
|
||||||
|
|
||||||
|
constructor(translate: TranslateService,
|
||||||
|
appProvider: CoreAppProvider,
|
||||||
|
utils: CoreUtilsProvider,
|
||||||
|
courseProvider: CoreCourseProvider,
|
||||||
|
filepoolProvider: CoreFilepoolProvider,
|
||||||
|
sitesProvider: CoreSitesProvider,
|
||||||
|
domUtils: CoreDomUtilsProvider,
|
||||||
|
private groupsProvider: CoreGroupsProvider,
|
||||||
|
private userProvider: CoreUserProvider,
|
||||||
|
private chatProvider: AddonModChatProvider) {
|
||||||
|
|
||||||
|
super(translate, appProvider, utils, courseProvider, filepoolProvider, sitesProvider, domUtils);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether or not the handler is enabled on a site level.
|
||||||
|
*
|
||||||
|
* @return {boolean|Promise<boolean>} A boolean, or a promise resolved with a boolean, indicating if the handler is enabled.
|
||||||
|
*/
|
||||||
|
isEnabled(): boolean | Promise<boolean> {
|
||||||
|
return this.chatProvider.areSessionsAvailable();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invalidate the prefetched content.
|
||||||
|
*
|
||||||
|
* @param {number} moduleId The module ID.
|
||||||
|
* @param {number} courseId The course ID the module belongs to.
|
||||||
|
* @return {Promise<any>} Promise resolved when the data is invalidated.
|
||||||
|
*/
|
||||||
|
invalidateContent(moduleId: number, courseId: number): Promise<any> {
|
||||||
|
return this.chatProvider.getChat(courseId, moduleId).then((chat) => {
|
||||||
|
const promises = [
|
||||||
|
this.chatProvider.invalidateAllSessions(chat.id),
|
||||||
|
this.chatProvider.invalidateAllSessionMessages(chat.id)
|
||||||
|
];
|
||||||
|
|
||||||
|
return this.utils.allPromises(promises);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invalidate WS calls needed to determine module status (usually, to check if module is downloadable).
|
||||||
|
* It doesn't need to invalidate check updates. It should NOT invalidate files nor all the prefetched data.
|
||||||
|
*
|
||||||
|
* @param {any} module Module.
|
||||||
|
* @param {number} courseId Course ID the module belongs to.
|
||||||
|
* @return {Promise<any>} Promise resolved when invalidated.
|
||||||
|
*/
|
||||||
|
invalidateModule(module: any, courseId: number): Promise<any> {
|
||||||
|
const promises = [
|
||||||
|
this.chatProvider.invalidateChats(courseId),
|
||||||
|
this.courseProvider.invalidateModule(module.id)
|
||||||
|
];
|
||||||
|
|
||||||
|
return this.utils.allPromises(promises);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prefetch a module.
|
||||||
|
*
|
||||||
|
* @param {any} module Module.
|
||||||
|
* @param {number} courseId Course ID the module belongs to.
|
||||||
|
* @param {boolean} [single] True if we're downloading a single module, false if we're downloading a whole section.
|
||||||
|
* @param {string} [dirPath] Path of the directory where to store all the content files.
|
||||||
|
* @return {Promise<any>} Promise resolved when done.
|
||||||
|
*/
|
||||||
|
prefetch(module: any, courseId?: number, single?: boolean, dirPath?: string): Promise<any> {
|
||||||
|
return this.prefetchPackage(module, courseId, single, this.prefetchChat.bind(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prefetch a chat.
|
||||||
|
*
|
||||||
|
* @param {any} module The module object returned by WS.
|
||||||
|
* @param {number} courseId Course ID the module belongs to.
|
||||||
|
* @param {boolean} single True if we're downloading a single module, false if we're downloading a whole section.
|
||||||
|
* @param {string} siteId Site ID.
|
||||||
|
* @return {Promise<any>} Promise resolved when done.
|
||||||
|
*/
|
||||||
|
protected prefetchChat(module: any, courseId: number, single: boolean, siteId: string): Promise<any> {
|
||||||
|
// Prefetch chat and group info.
|
||||||
|
const promises = [
|
||||||
|
this.chatProvider.getChat(courseId, module.id, siteId),
|
||||||
|
this.groupsProvider.getActivityGroupInfo(module.id, false, undefined, siteId)
|
||||||
|
];
|
||||||
|
|
||||||
|
return Promise.all(promises).then(([chat, groupInfo]: [any, CoreGroupInfo]) => {
|
||||||
|
const promises = [];
|
||||||
|
|
||||||
|
let groupIds = [0];
|
||||||
|
if (groupInfo.groups && groupInfo.groups.length > 0) {
|
||||||
|
groupIds = groupInfo.groups.map((group) => group.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
groupIds.forEach((groupId) => {
|
||||||
|
// Prefetch complete sessions.
|
||||||
|
promises.push(this.chatProvider.getSessions(chat.id, groupId, false, true, siteId).catch((error) => {
|
||||||
|
// Ignore group error.
|
||||||
|
if (error.errorcode != 'notingroup') {
|
||||||
|
return Promise.reject(error);
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Prefetch all sessions.
|
||||||
|
promises.push(this.chatProvider.getSessions(chat.id, groupId, true, true, siteId).then((sessions) => {
|
||||||
|
const promises = sessions.map((session) => this.prefetchSession(chat.id, session, 0, courseId, siteId));
|
||||||
|
|
||||||
|
return Promise.all(promises);
|
||||||
|
}).catch((error) => {
|
||||||
|
// Ignore group error.
|
||||||
|
if (error.errorcode != 'notingroup') {
|
||||||
|
return Promise.reject(error);
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
return Promise.all(promises);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prefetch chat session messages and user profiles.
|
||||||
|
*
|
||||||
|
* @param {number} chatId Chat ID.
|
||||||
|
* @param {any} session Session object.
|
||||||
|
* @param {number} groupId Group ID.
|
||||||
|
* @param {number} courseId Course ID the module belongs to.
|
||||||
|
* @param {string} siteId Site ID.
|
||||||
|
* @return {Promise<any>} Promise resolved when done.
|
||||||
|
*/
|
||||||
|
protected prefetchSession(chatId: number, session: any, groupId: number, courseId: number, siteId: string): Promise<any> {
|
||||||
|
return this.chatProvider.getSessionMessages(chatId, session.sessionstart, session.sessionend, groupId, true, siteId)
|
||||||
|
.then((messages) => {
|
||||||
|
const users = {};
|
||||||
|
session.sessionusers.forEach((user) => {
|
||||||
|
users[user.userid] = true;
|
||||||
|
});
|
||||||
|
messages.forEach((message) => {
|
||||||
|
users[message.userid] = true;
|
||||||
|
});
|
||||||
|
const userIds = Object.keys(users).map(Number);
|
||||||
|
|
||||||
|
return this.userProvider.prefetchProfiles(userIds, courseId, siteId).catch(() => {
|
||||||
|
// Ignore errors, some users might not exist.
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -345,6 +345,7 @@
|
||||||
"addon.mod_book.errorchapter": "Error reading chapter of book.",
|
"addon.mod_book.errorchapter": "Error reading chapter of book.",
|
||||||
"addon.mod_book.modulenameplural": "Books",
|
"addon.mod_book.modulenameplural": "Books",
|
||||||
"addon.mod_chat.beep": "Beep",
|
"addon.mod_chat.beep": "Beep",
|
||||||
|
"addon.mod_chat.chatreport": "Chat sessions",
|
||||||
"addon.mod_chat.currentusers": "Current users",
|
"addon.mod_chat.currentusers": "Current users",
|
||||||
"addon.mod_chat.enterchat": "Click here to enter the chat now",
|
"addon.mod_chat.enterchat": "Click here to enter the chat now",
|
||||||
"addon.mod_chat.entermessage": "Enter your message",
|
"addon.mod_chat.entermessage": "Enter your message",
|
||||||
|
@ -356,12 +357,16 @@
|
||||||
"addon.mod_chat.messagebeepsyou": "{{$a}} has just beeped you!",
|
"addon.mod_chat.messagebeepsyou": "{{$a}} has just beeped you!",
|
||||||
"addon.mod_chat.messageenter": "{{$a}} has just entered this chat",
|
"addon.mod_chat.messageenter": "{{$a}} has just entered this chat",
|
||||||
"addon.mod_chat.messageexit": "{{$a}} has left this chat",
|
"addon.mod_chat.messageexit": "{{$a}} has left this chat",
|
||||||
|
"addon.mod_chat.messages": "Messages",
|
||||||
"addon.mod_chat.modulenameplural": "Chats",
|
"addon.mod_chat.modulenameplural": "Chats",
|
||||||
"addon.mod_chat.mustbeonlinetosendmessages": "You must be online to send messages.",
|
"addon.mod_chat.mustbeonlinetosendmessages": "You must be online to send messages.",
|
||||||
"addon.mod_chat.nomessages": "No messages yet",
|
"addon.mod_chat.nomessages": "No messages yet",
|
||||||
|
"addon.mod_chat.nosessionsfound": "No sessions found",
|
||||||
"addon.mod_chat.send": "Send",
|
"addon.mod_chat.send": "Send",
|
||||||
"addon.mod_chat.sessionstart": "The next chat session will start on {{$a.date}}, ({{$a.fromnow}} from now)",
|
"addon.mod_chat.sessionstart": "The next chat session will start on {{$a.date}}, ({{$a.fromnow}} from now)",
|
||||||
|
"addon.mod_chat.showincompletesessions": "Show incomplete sessions",
|
||||||
"addon.mod_chat.talk": "Talk",
|
"addon.mod_chat.talk": "Talk",
|
||||||
|
"addon.mod_chat.viewreport": "View past chat sessions",
|
||||||
"addon.mod_choice.cannotsubmit": "Sorry, there was a problem submitting your choice. Please try again.",
|
"addon.mod_choice.cannotsubmit": "Sorry, there was a problem submitting your choice. Please try again.",
|
||||||
"addon.mod_choice.choiceoptions": "Choice options",
|
"addon.mod_choice.choiceoptions": "Choice options",
|
||||||
"addon.mod_choice.errorgetchoice": "Error getting choice data.",
|
"addon.mod_choice.errorgetchoice": "Error getting choice data.",
|
||||||
|
@ -1294,6 +1299,7 @@
|
||||||
"core.defaultvalue": "Default ({{$a}})",
|
"core.defaultvalue": "Default ({{$a}})",
|
||||||
"core.delete": "Delete",
|
"core.delete": "Delete",
|
||||||
"core.deletedoffline": "Deleted offline",
|
"core.deletedoffline": "Deleted offline",
|
||||||
|
"core.deleteduser": "Deleted user",
|
||||||
"core.deleting": "Deleting",
|
"core.deleting": "Deleting",
|
||||||
"core.description": "Description",
|
"core.description": "Description",
|
||||||
"core.dfdaymonthyear": "MM-DD-YYYY",
|
"core.dfdaymonthyear": "MM-DD-YYYY",
|
||||||
|
@ -1544,6 +1550,7 @@
|
||||||
"core.noresults": "No results",
|
"core.noresults": "No results",
|
||||||
"core.notapplicable": "n/a",
|
"core.notapplicable": "n/a",
|
||||||
"core.notice": "Notice",
|
"core.notice": "Notice",
|
||||||
|
"core.notingroup": "Sorry, but you need to be part of a group to see this page.",
|
||||||
"core.notsent": "Not sent",
|
"core.notsent": "Not sent",
|
||||||
"core.now": "now",
|
"core.now": "now",
|
||||||
"core.numwords": "{{$a}} words",
|
"core.numwords": "{{$a}} words",
|
||||||
|
|
|
@ -632,10 +632,13 @@ export class CoreSite {
|
||||||
error.message = this.translate.instant('core.unicodenotsupported');
|
error.message = this.translate.instant('core.unicodenotsupported');
|
||||||
|
|
||||||
return Promise.reject(error);
|
return Promise.reject(error);
|
||||||
} else if (error.exception === 'required_capability_exception' || error.errorcode === 'nopermission') {
|
} else if (error.exception === 'required_capability_exception' || error.errorcode === 'nopermission' ||
|
||||||
|
error.errorcode === 'notingroup') {
|
||||||
|
// Translate error messages with missing strings.
|
||||||
if (error.message === 'error/nopermission') {
|
if (error.message === 'error/nopermission') {
|
||||||
// This error message is returned by some web services but the string does not exist.
|
|
||||||
error.message = this.translate.instant('core.nopermissionerror');
|
error.message = this.translate.instant('core.nopermissionerror');
|
||||||
|
} else if (error.message === 'error/notingroup') {
|
||||||
|
error.message = this.translate.instant('core.notingroup');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save the error instead of deleting the cache entry so the same content is displayed in offline.
|
// Save the error instead of deleting the cache entry so the same content is displayed in offline.
|
||||||
|
|
|
@ -57,6 +57,7 @@
|
||||||
"defaultvalue": "Default ({{$a}})",
|
"defaultvalue": "Default ({{$a}})",
|
||||||
"delete": "Delete",
|
"delete": "Delete",
|
||||||
"deletedoffline": "Deleted offline",
|
"deletedoffline": "Deleted offline",
|
||||||
|
"deleteduser": "Deleted user",
|
||||||
"deleting": "Deleting",
|
"deleting": "Deleting",
|
||||||
"description": "Description",
|
"description": "Description",
|
||||||
"dfdaymonthyear": "MM-DD-YYYY",
|
"dfdaymonthyear": "MM-DD-YYYY",
|
||||||
|
@ -169,6 +170,7 @@
|
||||||
"noresults": "No results",
|
"noresults": "No results",
|
||||||
"notapplicable": "n/a",
|
"notapplicable": "n/a",
|
||||||
"notice": "Notice",
|
"notice": "Notice",
|
||||||
|
"notingroup": "Sorry, but you need to be part of a group to see this page.",
|
||||||
"notsent": "Not sent",
|
"notsent": "Not sent",
|
||||||
"now": "now",
|
"now": "now",
|
||||||
"numwords": "{{$a}} words",
|
"numwords": "{{$a}} words",
|
||||||
|
|
Loading…
Reference in New Issue