Merge pull request #1657 from albertgasset/MOBILE-2620

Mobile 2620
main
Juan Leyva 2018-12-13 11:09:47 +01:00 committed by GitHub
commit b2c5bbc0c7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
33 changed files with 2007 additions and 214 deletions

View File

@ -147,8 +147,11 @@
"addon.files.privatefiles": "moodle",
"addon.files.sitefiles": "moodle",
"addon.messageoutput_airnotifier.processorsettingsdesc": "local_moodlemobileapp",
"addon.messages.acceptandaddcontact": "message",
"addon.messages.addcontact": "message",
"addon.messages.addcontactconfirm": "message",
"addon.messages.addtofavourites": "message",
"addon.messages.addtoyourcontacts": "message",
"addon.messages.blocknoncontacts": "message",
"addon.messages.blockuser": "message",
"addon.messages.blockuserconfirm": "message",
@ -159,7 +162,9 @@
"addon.messages.contactblocked": "message",
"addon.messages.contactlistempty": "local_moodlemobileapp",
"addon.messages.contactname": "local_moodlemobileapp",
"addon.messages.contactrequestsent": "message",
"addon.messages.contacts": "message",
"addon.messages.decline": "message",
"addon.messages.deleteallconfirm": "message",
"addon.messages.deleteconversationq": "message",
"addon.messages.deletemessage": "local_moodlemobileapp",
@ -168,34 +173,51 @@
"addon.messages.errorwhileretrievingcontacts": "local_moodlemobileapp",
"addon.messages.errorwhileretrievingdiscussions": "local_moodlemobileapp",
"addon.messages.errorwhileretrievingmessages": "local_moodlemobileapp",
"addon.messages.errorwhileretrievingusers": "local_moodlemobileapp",
"addon.messages.groupinfo": "message",
"addon.messages.groupmessages": "message",
"addon.messages.info": "message",
"addon.messages.isnotinyourcontacts": "message",
"addon.messages.message": "message",
"addon.messages.messagenotsent": "local_moodlemobileapp",
"addon.messages.messagepreferences": "message",
"addon.messages.messages": "message",
"addon.messages.newmessage": "message",
"addon.messages.newmessages": "local_moodlemobileapp",
"addon.messages.nocontactrequests": "message",
"addon.messages.noncontacts": "message",
"addon.messages.nocontactsgetstarted": "message",
"addon.messages.nofavourites": "message",
"addon.messages.nogroupmessages": "message",
"addon.messages.nomessages": "message",
"addon.messages.nousersfound": "local_moodlemobileapp",
"addon.messages.numparticipants": "message",
"addon.messages.removecontact": "message",
"addon.messages.removecontactconfirm": "local_moodlemobileapp",
"addon.messages.removecontactconfirm": "message",
"addon.messages.removefromyourcontacts": "message",
"addon.messages.removefromfavourites": "message",
"addon.messages.requests": "moodle",
"addon.messages.requirecontacttomessage": "message",
"addon.messages.searchcombined": "message",
"addon.messages.searchnocontactsfound": "message",
"addon.messages.searchnomessagesfound": "message",
"addon.messages.searchnononcontactsfound": "message",
"addon.messages.showdeletemessages": "local_moodlemobileapp",
"addon.messages.sendcontactrequest": "message",
"addon.messages.type_blocked": "local_moodlemobileapp",
"addon.messages.type_offline": "local_moodlemobileapp",
"addon.messages.type_online": "local_moodlemobileapp",
"addon.messages.type_search": "local_moodlemobileapp",
"addon.messages.type_strangers": "local_moodlemobileapp",
"addon.messages.unabletomessage": "message",
"addon.messages.unblockuser": "message",
"addon.messages.unblockuserconfirm": "message",
"addon.messages.userwouldliketocontactyou": "message",
"addon.messages.warningconversationmessagenotsent": "local_moodlemobileapp",
"addon.messages.warningmessagenotsent": "local_moodlemobileapp",
"addon.messages.wouldliketocontactyou": "message",
"addon.messages.you": "message",
"addon.messages.youhaveblockeduser": "message",
"addon.mod_assign.acceptsubmissionstatement": "local_moodlemobileapp",
"addon.mod_assign.addattempt": "assign",
"addon.mod_assign.addnewattempt": "assign",
@ -1540,6 +1562,7 @@
"core.quotausage": "moodle",
"core.redirectingtosite": "local_moodlemobileapp",
"core.refresh": "moodle",
"core.remove": "moodle",
"core.required": "moodle",
"core.requireduserdatamissing": "local_moodlemobileapp",
"core.resources": "moodle",

View File

@ -20,11 +20,15 @@ import { CoreComponentsModule } from '@components/components.module';
import { CoreDirectivesModule } from '@directives/directives.module';
import { CorePipesModule } from '@pipes/pipes.module';
import { AddonMessagesDiscussionsComponent } from '../components/discussions/discussions';
import { AddonMessagesConfirmedContactsComponent } from '../components/confirmed-contacts/confirmed-contacts';
import { AddonMessagesContactRequestsComponent } from '../components/contact-requests/contact-requests';
import { AddonMessagesContactsComponent } from '../components/contacts/contacts';
@NgModule({
declarations: [
AddonMessagesDiscussionsComponent,
AddonMessagesConfirmedContactsComponent,
AddonMessagesContactRequestsComponent,
AddonMessagesContactsComponent
],
imports: [
@ -39,6 +43,8 @@ import { AddonMessagesContactsComponent } from '../components/contacts/contacts'
],
exports: [
AddonMessagesDiscussionsComponent,
AddonMessagesConfirmedContactsComponent,
AddonMessagesContactRequestsComponent,
AddonMessagesContactsComponent
]
})

View File

@ -0,0 +1,16 @@
<ion-content>
<ion-refresher [enabled]="loaded" (ionRefresh)="refreshData($event)">
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
</ion-refresher>
<core-loading [hideUntil]="loaded" class="core-loading-center">
<ion-list no-margin>
<a ion-item text-wrap *ngFor="let contact of contacts" [title]="contact.fullname" (click)="selectUser(contact.id)" [class.core-split-item-selected]="contact.id == selectedUserId" detail-none>
<ion-avatar item-start core-user-avatar [user]="contact" [checkOnline]="true" [linkProfile]="false"></ion-avatar>
<h2><core-format-text [text]="contact.fullname"></core-format-text></h2>
<core-icon *ngIf="contact.isblocked" name="fa-ban" item-end></core-icon>
</a>
</ion-list>
<core-empty-box *ngIf="!contacts.length" icon="person" [message]="'addon.messages.nocontactsgetstarted' | translate"></core-empty-box>
<core-infinite-loading [enabled]="canLoadMore" (action)="loadMore($event)" [error]="loadMoreError" position="bottom"></core-infinite-loading>
</core-loading>
</ion-content>

View File

@ -0,0 +1,141 @@
// (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, EventEmitter, OnDestroy, OnInit, Output, ViewChild } from '@angular/core';
import { Content } from 'ionic-angular';
import { CoreEventsProvider } from '@providers/events';
import { CoreSitesProvider } from '@providers/sites';
import { AddonMessagesProvider } from '../../providers/messages';
import { CoreDomUtilsProvider } from '@providers/utils/dom';
/**
* Component that displays the list of confirmed contacts.
*/
@Component({
selector: 'addon-messages-confirmed-contacts',
templateUrl: 'addon-messages-confirmed-contacts.html',
})
export class AddonMessagesConfirmedContactsComponent implements OnInit, OnDestroy {
@Output() onUserSelected = new EventEmitter<{userId: number, onInit?: boolean}>();
@ViewChild(Content) content: Content;
loaded = false;
canLoadMore = false;
loadMoreError = false;
contacts = [];
selectedUserId: number;
protected memberInfoObserver;
constructor(private domUtils: CoreDomUtilsProvider, eventsProvider: CoreEventsProvider, sitesProvider: CoreSitesProvider,
private messagesProvider: AddonMessagesProvider) {
this.onUserSelected = new EventEmitter();
// Update block status of a user.
this.memberInfoObserver = eventsProvider.on(AddonMessagesProvider.MEMBER_INFO_CHANGED_EVENT, (data) => {
if (data.userBlocked || data.userUnblocked) {
const user = this.contacts.find((user) => user.id == data.userId);
if (user) {
user.isblocked = data.userBlocked;
}
} else if (data.contactRemoved) {
const index = this.contacts.findIndex((contact) => contact.id == data.userId);
if (index >= 0) {
this.contacts.splice(index, 1);
}
}
}, sitesProvider.getCurrentSiteId());
}
/**
* Component loaded.
*/
ngOnInit(): void {
this.fetchData().then(() => {
if (this.contacts.length) {
this.selectUser(this.contacts[0].id, true);
}
}).finally(() => {
this.loaded = true;
});
// Workaround for infinite scrolling.
this.content.resize();
}
/**
* Fetch contacts.
*
* @param {boolean} [refresh=false] True if we are refreshing contacts, false if we are loading more.
* @return {Promise<any>} Promise resolved when done.
*/
fetchData(refresh: boolean = false): Promise<any> {
this.loadMoreError = false;
const limitFrom = refresh ? 0 : this.contacts.length;
return this.messagesProvider.getUserContacts(limitFrom).then((result) => {
this.contacts = refresh ? result.contacts : this.contacts.concat(result.contacts);
this.canLoadMore = result.canLoadMore;
}).catch((error) => {
this.loadMoreError = true;
this.domUtils.showErrorModalDefault(error, 'addon.messages.errorwhileretrievingcontacts', true);
});
}
/**
* Refresh contacts.
*
* @param {any} [refresher] Refresher.
* @return {Promise<any>} Promise resolved when done.
*/
refreshData(refresher?: any): Promise<any> {
return this.messagesProvider.invalidateUserContacts().then(() => {
return this.fetchData(true);
}).finally(() => {
refresher && refresher.complete();
});
}
/**
* Load more contacts.
*
* @param {any} [infiniteComplete] Infinite scroll complete function. Only used from core-infinite-loading.
* @return {Promise<any>} Resolved when done.
*/
loadMore(infiniteComplete?: any): Promise<any> {
return this.fetchData().finally(() => {
infiniteComplete && infiniteComplete();
});
}
/**
* Notify that a contact has been selected.
*
* @param {number} userId User id.
* @param {boolean} [onInit=false] Whether the contact is selected on initial load.
*/
selectUser(userId: number, onInit: boolean = false): void {
this.selectedUserId = userId;
this.onUserSelected.emit({userId, onInit});
}
/**
* Component destroyed.
*/
ngOnDestroy(): void {
this.memberInfoObserver && this.memberInfoObserver.off();
}
}

View File

@ -0,0 +1,16 @@
<ion-content>
<ion-refresher [enabled]="loaded" (ionRefresh)="refreshData($event)">
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
</ion-refresher>
<core-loading [hideUntil]="loaded" class="core-loading-center">
<ion-list no-margin>
<a ion-item text-wrap *ngFor="let request of requests" [title]="request.fullname" (click)="selectUser(request.id)" [class.core-split-item-selected]="request.id == selectedUserId" detail-none>
<ion-avatar item-start core-user-avatar [user]="request" [checkOnline]="true" [linkProfile]="false"></ion-avatar>
<h2><core-format-text [text]="request.fullname"></core-format-text></h2>
<p *ngIf="!request.iscontact && !request.confirmedOrDeclined">{{ 'addon.messages.wouldliketocontactyou' | translate }}</p>
</a>
</ion-list>
<core-empty-box *ngIf="!requests.length" icon="person" [message]="'addon.messages.nocontactrequests' | translate"></core-empty-box>
<core-infinite-loading [enabled]="canLoadMore" (action)="loadMore($event)" [error]="loadMoreError" position="bottom"></core-infinite-loading>
</core-loading>
</ion-content>

View File

@ -0,0 +1,137 @@
// (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, EventEmitter, OnDestroy, OnInit, Output, ViewChild } from '@angular/core';
import { Content } from 'ionic-angular';
import { CoreEventsProvider } from '@providers/events';
import { CoreSitesProvider } from '@providers/sites';
import { AddonMessagesProvider } from '../../providers/messages';
import { CoreDomUtilsProvider } from '@providers/utils/dom';
/**
* Component that displays the list of contact requests.
*/
@Component({
selector: 'addon-messages-contact-requests',
templateUrl: 'addon-messages-contact-requests.html',
})
export class AddonMessagesContactRequestsComponent implements OnInit, OnDestroy {
@Output() onUserSelected = new EventEmitter<{userId: number, onInit?: boolean}>();
@ViewChild(Content) content: Content;
loaded = false;
canLoadMore = false;
loadMoreError = false;
requests = [];
selectedUserId: number;
protected memberInfoObserver;
constructor(private domUtils: CoreDomUtilsProvider, eventsProvider: CoreEventsProvider, sitesProvider: CoreSitesProvider,
private messagesProvider: AddonMessagesProvider) {
// Hide the "Would like to contact you" message when a contact request is confirmed.
this.memberInfoObserver = eventsProvider.on(AddonMessagesProvider.MEMBER_INFO_CHANGED_EVENT, (data) => {
if (data.contactRequestConfirmed || data.contactRequestDeclined) {
const index = this.requests.findIndex((request) => request.id == data.userId);
if (index >= 0) {
this.requests.splice(index, 1);
}
}
}, sitesProvider.getCurrentSiteId());
}
/**
* Component loaded.
*/
ngOnInit(): void {
this.fetchData().then(() => {
if (this.requests.length) {
this.selectUser(this.requests[0].id, true);
}
}).finally(() => {
this.loaded = true;
});
// Workaround for infinite scrolling.
this.content.resize();
}
/**
* Fetch contact requests.
*
* @param {boolean} [refresh=false] True if we are refreshing contact requests, false if we are loading more.
* @return {Promise<any>} Promise resolved when done.
*/
fetchData(refresh: boolean = false): Promise<any> {
this.loadMoreError = false;
const limitFrom = refresh ? 0 : this.requests.length;
return this.messagesProvider.getContactRequests(limitFrom).then((result) => {
this.requests = refresh ? result.requests : this.requests.concat(result.requests);
this.canLoadMore = result.canLoadMore;
}).catch((error) => {
this.loadMoreError = true;
this.domUtils.showErrorModalDefault(error, 'addon.messages.errorwhileretrievingcontacts', true);
});
}
/**
* Refresh contact requests.
*
* @param {any} [refresher] Refresher.
* @return {Promise<any>} Promise resolved when done.
*/
refreshData(refresher?: any): Promise<any> {
// Refresh the number of contacts requests to update badges.
this.messagesProvider.refreshContactRequestsCount();
return this.messagesProvider.invalidateContactRequestsCache().then(() => {
return this.fetchData(true);
}).finally(() => {
refresher && refresher.complete();
});
}
/**
* Load more contact requests.
*
* @param {any} [infiniteComplete] Infinite scroll complete function. Only used from core-infinite-loading.
* @return {Promise<any>} Resolved when done.
*/
loadMore(infiniteComplete?: any): Promise<any> {
return this.fetchData().finally(() => {
infiniteComplete && infiniteComplete();
});
}
/**
* Notify that a contact has been selected.
*
* @param {number} userId User id.
* @param {boolean} [onInit=false] Whether the contact is selected on initial load.
*/
selectUser(userId: number, onInit: boolean = false): void {
this.selectedUserId = userId;
this.onUserSelected.emit({userId, onInit});
}
/**
* Component destroyed.
*/
ngOnDestroy(): void {
this.memberInfoObserver && this.memberInfoObserver.off();
}
}

View File

@ -12,7 +12,7 @@
<h2>{{ 'core.searchresults' | translate }}</h2>
<ion-note item-end>{{ search.results.length }}</ion-note>
</ion-item-divider>
<a 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>
<a 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" class="addon-message-discussion">
<ion-avatar core-user-avatar [user]="result" item-start></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>
@ -20,15 +20,15 @@
</ion-list>
<ion-list *ngIf="!search.showResults" no-margin>
<a 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>
<a 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" class="addon-message-discussion">
<ion-avatar core-user-avatar [user]="discussion" item-start></ion-avatar>
<h2>
<core-format-text [text]="discussion.fullname"></core-format-text>
<ion-note *ngIf="discussion.message.timecreated > 0 || discussion.unread">
<span *ngIf="discussion.unread" class="core-primary-circle"></span>
<span *ngIf="discussion.message.timecreated > 0">{{discussion.message.timecreated / 1000 | coreDateDayOrTime}}</span>
</ion-note>
</h2>
<ion-note *ngIf="discussion.message.timecreated > 0 || discussion.unread">
<span *ngIf="discussion.unread" class="core-primary-circle"></span>
<span *ngIf="discussion.message.timecreated > 0">{{discussion.message.timecreated / 1000 | coreDateDayOrTime}}</span>
</ion-note>
<p><core-format-text clean="true" singleLine="true" [text]="discussion.message.message"></core-format-text></p>
</a>
</ion-list>

View File

@ -1,13 +1,15 @@
ion-app.app-root addon-messages-discussions {
ion-app.app-root .addon-message-discussion {
h2 {
display: flex;
justify-content: space-between;
.note {
margin: 0;
align-self: flex-end;
display: inline-flex;
font-size: initial;
margin-top: 6px;
core-format-text {
font-weight: bold;
}
}
.note {
position: absolute;
@include position(0, 0, null, null);
margin: 4px 8px;
font-size: 1.3rem;
}
}

View File

@ -209,7 +209,7 @@ export class AddonMessagesDiscussionsComponent implements OnDestroy {
return this.messagesProvider.searchMessages(query).then((searchResults) => {
this.search.showResults = true;
this.search.results = searchResults;
this.search.results = searchResults.messages;
}).catch((error) => {
this.domUtils.showErrorModalDefault(error, 'addon.messages.errorwhileretrievingmessages', true);
}).finally(() => {

View File

@ -1,6 +1,9 @@
{
"acceptandaddcontact": "Accept and add to contacts",
"addcontact": "Add contact",
"addcontactconfirm": "Are you sure you want to add {{$a}} to your contacts?",
"addtofavourites": "Star",
"addtoyourcontacts": "Add to contacts",
"blocknoncontacts": "Prevent non-contacts from messaging me",
"blockuser": "Block user",
"blockuserconfirm": "Are you sure you want to block {{$a}}?",
@ -11,7 +14,9 @@
"contactblocked": "Contact blocked",
"contactlistempty": "The contact list is empty",
"contactname": "Contact name",
"contactrequestsent": "Contact request sent",
"contacts": "Contacts",
"decline": "Decline",
"deleteallconfirm": "Are you sure you would like to delete this entire conversation? This will not delete it for other conversation participants.",
"deleteconversation": "Delete conversation",
"deletemessage": "Delete message",
@ -20,32 +25,50 @@
"errorwhileretrievingcontacts": "Error while retrieving contacts from the server.",
"errorwhileretrievingdiscussions": "Error while retrieving discussions from the server.",
"errorwhileretrievingmessages": "Error while retrieving messages from the server.",
"errorwhileretrievingusers": "Error while retrieving users from the server.",
"groupinfo": "Group info",
"groupmessages": "Group messages",
"info": "Info",
"isnotinyourcontacts": "{{$a}} is not in your contacts",
"messagenotsent": "The message was not sent. Please try again later.",
"message": "Message",
"messagepreferences": "Message preferences",
"messages": "Messages",
"newmessage": "New message",
"newmessages": "New messages",
"nocontactrequests": "No contact requests",
"noncontacts": "Non-contacts",
"nocontactsgetstarted": "Try searching for someone to add them as a contact",
"nofavourites": "No favourites",
"nogroupmessages": "No group messages",
"nomessages": "No messages",
"nousersfound": "No users found",
"numparticipants": "{{$a}} participants",
"removecontact": "Remove contact",
"removecontactconfirm": "Contact will be removed from your contacts list.",
"removecontactconfirm": "Are you sure you want to remove {{$a}} from your contacts?",
"removefromyourcontacts": "Remove from contacts",
"removefromfavourites": "Unstar",
"requests": "Requests",
"requirecontacttomessage": "You need to request {{$a}} to add you as a contact to be able to message them.",
"searchcombined": "Search people and messages",
"searchnocontactsfound": "No contacts found",
"searchnomessagesfound": "No messages found",
"searchnononcontactsfound": "No non contacts found",
"sendcontactrequest": "Send contact request",
"showdeletemessages": "Show delete messages",
"type_blocked": "Blocked",
"type_offline": "Offline",
"type_online": "Online",
"type_search": "Search results",
"type_strangers": "Others",
"unabletomessage": "You are unable to message this user",
"unblockuser": "Unblock user",
"unblockuserconfirm": "Are you sure you want to unblock {{$a}}?",
"userwouldliketocontactyou": "{{$a}} would like to contact you",
"warningconversationmessagenotsent": "Couldn't send message(s) to conversation {{conversation}}. {{error}}",
"warningmessagenotsent": "Couldn't send message(s) to user {{user}}. {{error}}",
"you": "You:"
"wouldliketocontactyou": "Would like to contact you",
"you": "You:",
"youhaveblockeduser": "You have blocked this user in the past",
"yourcontactrequestpending": "Your contact request is pending with {{$a}}"
}

View File

@ -25,6 +25,7 @@ import { CoreCronDelegate } from '@providers/cron';
import { AddonMessagesSendMessageUserHandler } from './providers/user-send-message-handler';
import { AddonMessagesAddContactUserHandler } from './providers/user-add-contact-handler';
import { AddonMessagesBlockContactUserHandler } from './providers/user-block-contact-handler';
import { AddonMessagesContactRequestLinkHandler } from './providers/contact-request-link-handler';
import { AddonMessagesDiscussionLinkHandler } from './providers/discussion-link-handler';
import { AddonMessagesIndexLinkHandler } from './providers/index-link-handler';
import { AddonMessagesSyncCronHandler } from './providers/sync-cron-handler';
@ -58,6 +59,7 @@ export const ADDON_MESSAGES_PROVIDERS: any[] = [
AddonMessagesSendMessageUserHandler,
AddonMessagesAddContactUserHandler,
AddonMessagesBlockContactUserHandler,
AddonMessagesContactRequestLinkHandler,
AddonMessagesDiscussionLinkHandler,
AddonMessagesIndexLinkHandler,
AddonMessagesSyncCronHandler,
@ -74,11 +76,13 @@ export class AddonMessagesModule {
sitesProvider: CoreSitesProvider, linkHelper: CoreContentLinksHelperProvider, updateManager: CoreUpdateManagerProvider,
settingsHandler: AddonMessagesSettingsHandler, settingsDelegate: CoreSettingsDelegate,
pushNotificationsDelegate: AddonPushNotificationsDelegate, utils: CoreUtilsProvider,
addContactHandler: AddonMessagesAddContactUserHandler, blockContactHandler: AddonMessagesBlockContactUserHandler) {
addContactHandler: AddonMessagesAddContactUserHandler, blockContactHandler: AddonMessagesBlockContactUserHandler,
contactRequestLinkHandler: AddonMessagesContactRequestLinkHandler) {
// Register handlers.
mainMenuDelegate.registerHandler(mainmenuHandler);
contentLinksDelegate.registerHandler(indexLinkHandler);
contentLinksDelegate.registerHandler(discussionLinkHandler);
contentLinksDelegate.registerHandler(contactRequestLinkHandler);
userDelegate.registerHandler(sendMessageHandler);
userDelegate.registerHandler(addContactHandler);
userDelegate.registerHandler(blockContactHandler);

View File

@ -0,0 +1,28 @@
<ion-header>
<ion-navbar core-back-button>
<ion-title>{{ 'addon.messages.contacts' | translate }}</ion-title>
<ion-buttons end>
<button ion-button icon-only (click)="gotoSearch()" [attr.aria-label]="'addon.messages.search' | translate">
<ion-icon name="search"></ion-icon>
</button>
<!-- Add an empty context menu so discussion page can add items in split view, otherwise the menu disappears in some cases. -->
<core-context-menu></core-context-menu>
</ion-buttons>
</ion-navbar>
</ion-header>
<core-split-view>
<ion-content>
<core-tabs>
<core-tab [title]="'addon.messages.contacts' | translate" (ionSelect)="selectUser('contacts')">
<ng-template>
<addon-messages-confirmed-contacts (onUserSelected)="selectUser('contacts', $event.userId, $event.onInit)"></addon-messages-confirmed-contacts>
</ng-template>
</core-tab>
<core-tab [title]="'addon.messages.requests' | translate" [badge]="contactRequestsCount" (ionSelect)="selectUser('requests')">
<ng-template>
<addon-messages-contact-requests (onUserSelected)="selectUser('requests', $event.userId, $event.onInit)"></addon-messages-contact-requests>
</ng-template>
</core-tab>
</core-tabs>
</ion-content>
</core-split-view>

View File

@ -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 { AddonMessagesContactsPage } from './contacts';
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: [
AddonMessagesContactsPage,
],
imports: [
CoreComponentsModule,
CoreDirectivesModule,
CorePipesModule,
AddonMessagesComponentsModule,
IonicPageModule.forChild(AddonMessagesContactsPage),
TranslateModule.forChild()
],
})
export class AddonMessagesContactsPageModule {}

View File

@ -0,0 +1,117 @@
// (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, NavController } 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';
import { CoreTabsComponent } from '@components/tabs/tabs';
/**
* Page that displays contacts and contact requests.
*/
@IonicPage({ segment: 'addon-messages-contacts' })
@Component({
selector: 'page-addon-messages-contacts',
templateUrl: 'contacts.html',
})
export class AddonMessagesContactsPage implements OnDestroy {
@ViewChild(CoreSplitViewComponent) splitviewCtrl: CoreSplitViewComponent;
@ViewChild(CoreTabsComponent) tabsComponent: CoreTabsComponent;
contactRequestsCount = 0;
protected loadSplitViewObserver: any;
protected siteId: string;
protected contactRequestsCountObserver: any;
protected conversationUserId: number; // User id of the conversation opened in the split view.
protected selectedUserId = {
contacts: null, // User id of the selected user in the confirmed contacts tab.
requests: null, // User id of the selected user in the contact requests tab.
};
constructor(eventsProvider: CoreEventsProvider, sitesProvider: CoreSitesProvider,
private navCtrl: NavController, private messagesProvider: AddonMessagesProvider) {
this.siteId = sitesProvider.getCurrentSiteId();
// Update the contact requests badge.
this.contactRequestsCountObserver = eventsProvider.on(AddonMessagesProvider.CONTACT_REQUESTS_COUNT_EVENT, (data) => {
this.contactRequestsCount = data.count;
}, this.siteId);
}
/**
* Page being initialized.
*/
ngOnInit(): void {
this.messagesProvider.getContactRequestsCount(this.siteId); // Badge already updated by the observer.
}
/**
* Navigate to the search page.
*/
gotoSearch(): void {
this.navCtrl.push('AddonMessagesSearchPage');
}
/**
* User entered the page.
*/
ionViewDidEnter(): void {
this.tabsComponent && this.tabsComponent.ionViewDidEnter();
}
/**
* User left the page.
*/
ionViewDidLeave(): void {
this.tabsComponent && this.tabsComponent.ionViewDidLeave();
}
/**
* Set the selected user and open the conversation in the split view if needed.
*
* @param {string} tab Active tab: "contacts" or "requests".
* @param {number} [userId] Id of the selected user, undefined to use the last selected user in the tab.
* @param {boolean} [onInit=false] Whether the contact was selected on initial load.
*/
selectUser(tab: string, userId?: number, onInit: boolean = false): void {
userId = userId || this.selectedUserId[tab];
if (!userId || userId == this.conversationUserId) {
// No user conversation to open or it is already opened.
return;
}
if (onInit && !this.splitviewCtrl.isOn()) {
// Do not open a conversation by default when split view is not visible.
return;
}
this.conversationUserId = userId;
this.selectedUserId[tab] = userId;
this.splitviewCtrl.push('AddonMessagesDiscussionPage', { userId });
}
/**
* Page destroyed.
*/
ngOnDestroy(): void {
this.contactRequestsCountObserver && this.contactRequestsCountObserver.off();
}
}

View File

@ -9,11 +9,15 @@
</ion-navbar>
<core-navbar-buttons end>
<core-context-menu>
<core-context-menu-item [hidden]="!showInfo || isGroup" [priority]="1000" [content]="'addon.messages.info' | translate" (action)="viewInfo()" [iconAction]="'information-circle'"></core-context-menu-item>
<core-context-menu-item [hidden]="!showInfo || !isGroup" [priority]="1000" [content]="'addon.messages.groupinfo' | translate" (action)="viewInfo()" [iconAction]="'information-circle'"></core-context-menu-item>
<core-context-menu-item [hidden]="!groupMessagingEnabled || !conversation" [priority]="800" [content]="(conversation && conversation.isfavourite ? 'addon.messages.removefromfavourites' : 'addon.messages.addtofavourites') | translate" (action)="changeFavourite($event)" [iconAction]="favouriteIcon" [closeOnClick]="false"></core-context-menu-item>
<core-context-menu-item [hidden]="!canDelete" [priority]="400" [content]="'addon.messages.showdeletemessages' | translate" (action)="toggleDelete()" [iconAction]="'trash'"></core-context-menu-item>
<core-context-menu-item [hidden]="!groupMessagingEnabled || !conversationId || isGroup" [priority]="200" [content]="'addon.messages.deleteconversation' | translate" (action)="deleteConversation($event)" [iconAction]="deleteIcon" [closeOnClick]="false"></core-context-menu-item>
<core-context-menu-item [hidden]="!showInfo || isGroup" [priority]="1000" [content]="'addon.messages.info' | translate" (action)="viewInfo()"></core-context-menu-item>
<core-context-menu-item [hidden]="!showInfo || !isGroup" [priority]="1000" [content]="'addon.messages.groupinfo' | translate" (action)="viewInfo()"></core-context-menu-item>
<core-context-menu-item [hidden]="!groupMessagingEnabled || !conversation" [priority]="800" [content]="(conversation && conversation.isfavourite ? 'addon.messages.removefromfavourites' : 'addon.messages.addtofavourites') | translate" (action)="changeFavourite($event)" [closeOnClick]="false"></core-context-menu-item>
<core-context-menu-item [hidden]="!otherMember || otherMember.isblocked" [priority]="700" [content]="'addon.messages.blockuser' | translate" (action)="blockUser()"></core-context-menu-item>
<core-context-menu-item [hidden]="!otherMember || !otherMember.isblocked" [priority]="700" [content]="'addon.messages.unblockuser' | translate" (action)="unblockUser()"></core-context-menu-item>
<core-context-menu-item [hidden]="!canDelete" [priority]="400" [content]="'addon.messages.showdeletemessages' | translate" (action)="toggleDelete()"></core-context-menu-item>
<core-context-menu-item [hidden]="!groupMessagingEnabled || !conversationId || isGroup" [priority]="200" [content]="'addon.messages.deleteconversation' | translate" (action)="deleteConversation($event)" [closeOnClick]="false"></core-context-menu-item>
<core-context-menu-item [hidden]="!otherMember || otherMember.iscontact || requestContactSent || requestContactReceived" [priority]="100" [content]="'addon.messages.addtoyourcontacts' | translate" (action)="createContactRequest()"></core-context-menu-item>
<core-context-menu-item [hidden]="!otherMember || !otherMember.iscontact" [priority]="100" [content]="'addon.messages.removefromyourcontacts' | translate" (action)="removeContact()"></core-context-menu-item>
</core-context-menu>
</core-navbar-buttons>
</ion-header>
@ -21,32 +25,32 @@
<core-loading [hideUntil]="loaded">
<!-- Load previous messages. -->
<core-infinite-loading [enabled]="canLoadMore" (action)="loadPrevious($event)" position="top" [error]="loadMoreError"></core-infinite-loading>
<ion-list class="addon-messages-discussion-container safe-area-page" [attr.aria-live]="polite">
<ion-list class="addon-messages-discussion-container safe-area-page" [class.addon-messages-discussion-group]="isGroup" [attr.aria-live]="'polite'">
<ng-container *ngFor="let message of messages; index as index; last as last">
<ion-chip *ngIf="message.showDate" class="addon-messages-date" color="light">
<ion-label>{{ message.timecreated | coreFormatDate: "LL" }}</ion-label>
</ion-chip>
<h6 text-center *ngIf="message.showDate" class="addon-messages-date">
{{ message.timecreated | coreFormatDate: "LL" }}
</h6>
<ion-chip class="addon-messages-unreadfrom" *ngIf="unreadMessageFrom && message.id == unreadMessageFrom" color="light">
<ion-label>{{ 'addon.messages.newmessages' | translate:{$a: title} }}</ion-label>
<ion-icon name="arrow-round-down"></ion-icon>
</ion-chip>
<ion-item text-wrap (longPress)="copyMessage(message)" class="addon-message" [class.addon-message-mine]="message.useridfrom == currentUserId" [@coreSlideInOut]="message.useridfrom == currentUserId ? '' : 'fromLeft'">
<ion-item text-wrap (longPress)="copyMessage(message)" class="addon-message" [class.addon-message-mine]="message.useridfrom == currentUserId" [class.addon-message-not-mine]="message.useridfrom != currentUserId" [class.addon-message-no-user]="!message.showUserData" [@coreSlideInOut]="message.useridfrom == currentUserId ? '' : 'fromLeft'">
<!-- User data. -->
<ion-avatar item-start *ngIf="message.showUserData">
<img [src]="members[message.useridfrom].profileimageurl" [alt]="'core.pictureof' | translate:{$a: members[message.useridfrom].fullname}" core-external-content onError="this.src='assets/img/user-avatar.png'">
</ion-avatar>
<h2 *ngIf="message.showUserData">{{ members[message.useridfrom].fullname }}</h2>
<h2 class="addon-message-user" >
<ion-avatar item-start core-user-avatar [user]="members[message.useridfrom]" [linkProfile]="false" *ngIf="message.showUserData"></ion-avatar>
<div *ngIf="message.showUserData">{{ members[message.useridfrom].fullname }}</div>
<ion-note *ngIf="!message.pending">{{ message.timecreated | coreFormatDate: "dftimedate" }}</ion-note>
<ion-note *ngIf="message.pending"><ion-icon name="time"></ion-icon></ion-note>
</h2>
<!-- Some messages have <p> and some others don't. Add a <p> so they all have same styles. -->
<p class="addon-message-text">
<core-format-text (afterRender)="last && scrollToBottom()" [text]="message.text"></core-format-text>
</p>
<ion-note *ngIf="!message.pending">
{{ message.timecreated | coreFormatDate: "dftimedate" }}
</ion-note>
<ion-note *ngIf="message.pending"><ion-icon name="time"></ion-icon></ion-note>
<button ion-button icon-only clear="true" *ngIf="!message.sending && showDelete" (click)="deleteMessage(message, index)" class="addon-messages-delete-button" [@coreSlideInOut]="'fromRight'" [attr.aria-label]=" 'addon.messages.deletemessage' | translate">
<ion-icon name="trash" color="danger"></ion-icon>
@ -59,7 +63,25 @@
</ion-content>
<ion-footer color="light" class="footer-adjustable" *ngIf="loaded && (!conversationId || conversation)">
<ion-toolbar color="light" position="bottom">
<!-- @todo: Check if the user can send messages. -->
<core-send-message-form (onSubmit)="sendMessage($event)" [showKeyboard]="showKeyboard" [placeholder]="'addon.messages.newmessage' | translate" (onResize)="resizeContent()"></core-send-message-form>
<p *ngIf="footerType == 'unable'" text-center margin-horizontal>{{ 'addon.messages.unabletomessage' | translate }}</p>
<div *ngIf="footerType == 'blocked'" padding-horizontal>
<p text-center>{{ 'addon.messages.youhaveblockeduser' | translate }}</p>
<button ion-button block text-wrap margin-bottom (click)="unblockUser()">{{ 'addon.messages.unblockuser' | translate }}</button>
</div>
<div *ngIf="footerType == 'requiresContact'" padding-horizontal>
<p text-center><strong>{{ 'addon.messages.isnotinyourcontacts' | translate: {$a: otherMember.fullname} }}</strong></p>
<p text-center>{{ 'addon.messages.requirecontacttomessage' | translate: {$a: otherMember.fullname} }}</p>
<button ion-button block text-wrap margin-bottom (click)="createContactRequest()">{{ 'addon.messages.sendcontactrequest' | translate }}</button>
</div>
<div *ngIf="footerType == 'requestReceived'" padding-horizontal>
<p text-center>{{ 'addon.messages.userwouldliketocontactyou' | translate: {$a: otherMember.fullname} }}</p>
<button ion-button block text-wrap margin-bottom (click)="confirmContactRequest()">{{ 'addon.messages.acceptandaddcontact' | translate }}</button>
<button ion-button block text-wrap margin-bottom color="light" (click)="declineContactRequest()">{{ 'addon.messages.decline' | translate }}</button>
</div>
<div *ngIf="footerType == 'requestSent' || (footerType == 'message' && requestContactSent)" padding-horizontal>
<p text-center><strong>{{ 'addon.messages.contactrequestsent' | translate }}</strong></p>
<p text-center>{{ 'addon.messages.yourcontactrequestpending' | translate: {$a: otherMember.fullname} }}</p>
</div>
<core-send-message-form *ngIf="footerType == 'message'" (onSubmit)="sendMessage($event)" [showKeyboard]="showKeyboard" [placeholder]="'addon.messages.newmessage' | translate" (onResize)="resizeContent()"></core-send-message-form>
</ion-toolbar>
</ion-footer>

View File

@ -1,39 +1,93 @@
// Messages.
$item-message-bg: $gray-lighter !default;
$item-message-bg: $white !default;
$item-message-note-text: $gray-dark !default;
$item-message-note-font-size: 75% !default;
$item-message-mine-bg: $blue-light !default;
$item-message-mine-bg: $gray-light !default;
ion-app.app-root page-addon-messages-discussion {
ion-content {
background-color: $gray-lighter !important;
}
.addon-messages-discussion-container {
display: flex;
flex-direction: column;
padding-bottom: 15px;
}
.addon-messages-date,
.addon-messages-unreadfrom {
margin-top: 10px;
.addon-messages-date {
font-weight: normal;
font-size: 1.4rem;
}
.addon-messages-unreadfrom {
color: $blue;
color: $core-color;
background-color: transparent;
margin-top: 6px;
ion-icon {
color: $core-color;
background-color: transparent;
}
}
// Message item.
.addon-message {
max-width: 80%;
border: 0;
border-radius: 16px;
padding: 10px;
margin: 4px;
border-radius: 4px;
padding: 8px;
@include margin(8px, 8px, 0, 8px);
background-color: $item-message-bg;
align-self: flex-start;
width: auto;
width: 90%;
max-width: 90%;
min-height: 0;
position: relative;
@include core-transition(width);
core-format-text > p:only-child {
display: inline;
}
.addon-message-user {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
margin-bottom: .5rem!important;
margin-top: 0;
ion-avatar {
display: block;
min-width: 30px;
min-height: 30px;
margin: 0;
img {
width: 30px;
height: 30px;
}
}
div {
font-weight: 500;
flex-grow: 1;
@include padding-horizontal(.5rem);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.note {
@include text-align('end');
color: $item-message-note-text;
font-size: $item-message-note-font-size;
}
}
&.addon-message-no-user .addon-message-user .note {
width: 100%;
}
&.activated {
background-color: darken($item-message-bg, 10%);
}
@ -53,12 +107,6 @@ ion-app.app-root page-addon-messages-discussion {
display: inline-flex;
}
.note {
align-self: flex-end;
color: $item-message-note-text;
font-size: $item-message-note-font-size;
@include margin(null, null, null, 10px);
}
.addon-messages-delete-button {
min-height: initial;
line-height: initial;
@ -76,11 +124,22 @@ ion-app.app-root page-addon-messages-discussion {
}
}
.addon-messages-discussion-group .addon-message + .addon-message-no-user,
.addon-message.addon-message-mine + .addon-message-no-user.addon-message-mine,
.addon-message.addon-message-not-mine + .addon-message-no-user.addon-message-not-mine {
h2 {
margin-bottom: 0;
}
margin-top: -4px;
padding-top: 0;
border-top-right-radius: 0;
border-top-left-radius: 0;
}
// Defines when an item-message is the user's.
.addon-message-mine {
background-color: $item-message-mine-bg;
align-self: flex-end;
max-width: 80%;
&.activated {
background-color: darken($item-message-mine-bg, 10%);
@ -97,12 +156,6 @@ ion-app.app-root page-addon-messages-discussion {
}
}
.addon-message .item-content,
.addon-message-mine .item-content {
background-color: transparent;
padding: 0;
}
.toolbar-title {
img {
@include margin-horizontal(null, 6px);

View File

@ -57,6 +57,8 @@ export class AddonMessagesDiscussionPage implements OnDestroy {
protected keyboardObserver: any;
protected scrollBottom = true;
protected viewDestroyed = false;
protected memberInfoObserver: any;
protected showLoadingModal = false; // Whether to show a loading modal while fetching data.
conversationId: number; // Conversation ID. Undefined if it's a new individual conversation.
conversation: any; // The conversation object (if it exists).
@ -77,6 +79,10 @@ export class AddonMessagesDiscussionPage implements OnDestroy {
members: any = {}; // Members that wrote a message, indexed by ID.
favouriteIcon = 'fa-star';
deleteIcon = 'trash';
otherMember: any; // Other member information (individual conversations only).
footerType: 'message' | 'blocked' | 'requiresContact' | 'requestSent' | 'requestReceived' | 'unable';
requestContactSent = false;
requestContactReceived = false;
constructor(private eventsProvider: CoreEventsProvider, sitesProvider: CoreSitesProvider, navParams: NavParams,
private userProvider: CoreUserProvider, private navCtrl: NavController, private messagesSync: AddonMessagesSyncProvider,
@ -108,6 +114,13 @@ export class AddonMessagesDiscussionPage implements OnDestroy {
}
}
}, this.siteId);
// Refresh data if info of a mamber of the conversation have changed.
this.memberInfoObserver = eventsProvider.on(AddonMessagesProvider.MEMBER_INFO_CHANGED_EVENT, (data) => {
if (data.userId && (this.members[data.userId] || this.otherMember && data.userId == this.otherMember.id)) {
this.fetchData();
}
}, this.siteId);
}
/**
@ -160,6 +173,25 @@ export class AddonMessagesDiscussionPage implements OnDestroy {
const backViewPage = this.navCtrl.getPrevious() && this.navCtrl.getPrevious().component.name;
this.showInfo = !backViewPage || backViewPage !== 'CoreUserProfilePage';
// Recalculate footer position when keyboard is shown or hidden.
this.keyboardObserver = this.eventsProvider.on(CoreEventsProvider.KEYBOARD_CHANGE, (kbHeight) => {
this.content.resize();
});
this.fetchData();
}
/**
* Convenience function to fetch the conversation data.
*
* @return {Promise<any>} Resolved when done.
*/
protected fetchData(): Promise<any> {
let loader;
if (this.showLoadingModal) {
loader = this.domUtils.showModalLoading();
}
if (!this.groupMessagingEnabled && this.userId) {
// Get the user profile to retrieve the user fullname and image.
this.userProvider.getProfile(this.userId, undefined, true).then((user) => {
@ -171,7 +203,7 @@ export class AddonMessagesDiscussionPage implements OnDestroy {
}
// Synchronize messages if needed.
this.messagesSync.syncDiscussion(this.conversationId, this.userId).catch(() => {
return this.messagesSync.syncDiscussion(this.conversationId, this.userId).catch(() => {
// Ignore errors.
}).then((warnings) => {
if (warnings && warnings[0]) {
@ -185,8 +217,22 @@ export class AddonMessagesDiscussionPage implements OnDestroy {
// Fetch the messages for the first time.
return this.fetchMessages();
}
}).then(() => {
let promise;
if (this.userId) {
promise = this.messagesProvider.getMemberInfo(this.userId);
} else {
// Group conversation.
promise = Promise.resolve(null);
}
return promise.then((member) => {
this.otherMember = member;
});
});
} else {
this.otherMember = null;
// Fetch the messages for the first time.
return this.fetchMessages().then(() => {
if (!this.title && this.messages.length) {
@ -207,11 +253,9 @@ export class AddonMessagesDiscussionPage implements OnDestroy {
this.resizeContent();
this.loaded = true;
this.setPolling(); // Make sure we're polling messages.
});
// Recalculate footer position when keyboard is shown or hidden.
this.keyboardObserver = this.eventsProvider.on(CoreEventsProvider.KEYBOARD_CHANGE, (kbHeight) => {
this.content.resize();
this.setContactRequestInfo();
this.setFooterType();
loader && loader.dismiss();
});
}
@ -985,6 +1029,71 @@ export class AddonMessagesDiscussionPage implements OnDestroy {
});
}
/**
* Calculate whether there are pending contact requests.
*/
protected setContactRequestInfo(): void {
this.requestContactSent = false;
this.requestContactReceived = false;
if (this.otherMember && !this.otherMember.iscontact) {
this.requestContactSent = this.otherMember.contactrequests.some((request) => {
return request.userid == this.currentUserId && request.requesteduserid == this.otherMember.id;
});
this.requestContactReceived = this.otherMember.contactrequests.some((request) => {
return request.userid == this.otherMember.id && request.requesteduserid == this.currentUserId;
});
}
}
/**
* Calculate what to display in the footer.
*/
protected setFooterType(): void {
if (!this.otherMember) {
// Group conversation or group messaging not available.
this.footerType = 'message';
} else if (this.otherMember.isblocked) {
this.footerType = 'blocked';
} else if (this.requestContactReceived) {
this.footerType = 'requestReceived';
} else if (this.otherMember.canmessage) {
this.footerType = 'message';
} else if (this.requestContactSent) {
this.footerType = 'requestSent';
} else if (this.otherMember.requirescontact) {
this.footerType = 'requiresContact';
} else {
this.footerType = 'unable';
}
}
/**
* Displays a confirmation modal to block the user of the individual conversation.
*
* @return {Promise<any>} Promise resolved when user is blocked or dialog is cancelled.
*/
blockUser(): Promise<any> {
if (!this.otherMember) {
// Should never happen.
return Promise.reject(null);
}
const template = this.translate.instant('addon.messages.blockuserconfirm', {$a: this.otherMember.fullname});
const okText = this.translate.instant('addon.messages.blockuser');
return this.domUtils.showConfirm(template, undefined, okText).then(() => {
const modal = this.domUtils.showModalLoading('core.sending', true);
this.showLoadingModal = true;
return this.messagesProvider.blockContact(this.otherMember.id).finally(() => {
modal.dismiss();
this.showLoadingModal = false;
});
}).catch((error) => {
this.domUtils.showErrorModalDefault(error, 'core.error', true);
});
}
/**
* Delete the conversation.
*
@ -1012,6 +1121,131 @@ export class AddonMessagesDiscussionPage implements OnDestroy {
});
}
/**
* Displays a confirmation modal to unblock the user of the individual conversation.
*
* @return {Promise<any>} Promise resolved when user is unblocked or dialog is cancelled.
*/
unblockUser(): Promise<any> {
if (!this.otherMember) {
// Should never happen.
return Promise.reject(null);
}
const template = this.translate.instant('addon.messages.unblockuserconfirm', {$a: this.otherMember.fullname});
const okText = this.translate.instant('addon.messages.unblockuser');
return this.domUtils.showConfirm(template, undefined, okText).then(() => {
const modal = this.domUtils.showModalLoading('core.sending', true);
this.showLoadingModal = true;
return this.messagesProvider.unblockContact(this.otherMember.id).finally(() => {
modal.dismiss();
this.showLoadingModal = false;
});
}).catch((error) => {
this.domUtils.showErrorModalDefault(error, 'core.error', true);
});
}
/**
* Displays a confirmation modal to send a contact request to the other user of the individual conversation.
*
* @return {Promise<any>} Promise resolved when the request is sent or the dialog is cancelled.
*/
createContactRequest(): Promise<any> {
if (!this.otherMember) {
// Should never happen.
return Promise.reject(null);
}
const template = this.translate.instant('addon.messages.addcontactconfirm', { $a: this.otherMember.fullname });
const okText = this.translate.instant('core.add');
return this.domUtils.showConfirm(template, undefined, okText).then(() => {
const modal = this.domUtils.showModalLoading('core.sending', true);
this.showLoadingModal = true;
return this.messagesProvider.createContactRequest(this.otherMember.id).finally(() => {
modal.dismiss();
this.showLoadingModal = false;
});
}).catch((error) => {
this.domUtils.showErrorModalDefault(error, 'core.error', true);
});
}
/**
* Confirms the contact request of the other user of the individual conversation.
*
* @return {Promise<any>} Promise resolved when the request is confirmed.
*/
confirmContactRequest(): Promise<any> {
if (!this.otherMember) {
// Should never happen.
return Promise.reject(null);
}
const modal = this.domUtils.showModalLoading('core.sending', true);
this.showLoadingModal = true;
return this.messagesProvider.confirmContactRequest(this.otherMember.id).finally(() => {
modal.dismiss();
this.showLoadingModal = false;
}).catch((error) => {
this.domUtils.showErrorModalDefault(error, 'core.error', true);
});
}
/**
* Declines the contact request of the other user of the individual conversation.
*
* @return {Promise<any>} Promise resolved when the request is confirmed.
*/
declineContactRequest(): Promise<any> {
if (!this.otherMember) {
// Should never happen.
return Promise.reject(null);
}
const modal = this.domUtils.showModalLoading('core.sending', true);
this.showLoadingModal = true;
return this.messagesProvider.declineContactRequest(this.otherMember.id).finally(() => {
modal.dismiss();
this.showLoadingModal = false;
}).catch((error) => {
this.domUtils.showErrorModalDefault(error, 'core.error', true);
});
}
/**
* Displays a confirmation modal to remove the other user of the conversation from contacts.
*
* @return {Promise<any>} Promise resolved when the request is sent or the dialog is cancelled.
*/
removeContact(): Promise<any> {
if (!this.otherMember) {
// Should never happen.
return Promise.reject(null);
}
const template = this.translate.instant('addon.messages.removecontactconfirm', { $a: this.otherMember.fullname });
const okText = this.translate.instant('core.remove');
return this.domUtils.showConfirm(template, undefined, okText).then(() => {
const modal = this.domUtils.showModalLoading('core.sending', true);
this.showLoadingModal = true;
return this.messagesProvider.removeContact(this.otherMember.id).finally(() => {
modal.dismiss();
this.showLoadingModal = false;
});
}).catch((error) => {
this.domUtils.showErrorModalDefault(error, 'core.error', true);
});
}
/**
* Page destroyed.
*/
@ -1020,6 +1254,7 @@ export class AddonMessagesDiscussionPage implements OnDestroy {
this.unsetPolling();
this.syncObserver && this.syncObserver.off();
this.keyboardObserver && this.keyboardObserver.off();
this.memberInfoObserver && this.memberInfoObserver.off();
this.viewDestroyed = true;
}
}

View File

@ -2,8 +2,8 @@
<ion-navbar core-back-button>
<ion-title>{{ 'addon.messages.messages' | translate }}</ion-title>
<ion-buttons end>
<button ion-button icon-only (click)="gotoContacts($event)" [attr.aria-label]="'addon.messages.contacts' | translate">
<ion-icon name="person"></ion-icon> <!-- @todo: Display number of pending requests. -->
<button ion-button icon-only (click)="gotoSearch()" [attr.aria-label]="'addon.messages.search' | translate">
<ion-icon name="search"></ion-icon>
</button>
<button ion-button icon-only (click)="gotoSettings($event)" [attr.aria-label]="'addon.messages.messagepreferences' | translate">
<ion-icon name="cog"></ion-icon>
@ -19,34 +19,13 @@
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
</ion-refresher>
<core-search-box *ngIf="search.enabled" (onSubmit)="searchMessage($event)" (onClear)="clearSearch($event)" [placeholder]=" 'addon.messages.message' | translate" autocorrect="off" spellcheck="false" lengthCheck="2" [disabled]="!loaded"></core-search-box>
<core-loading [hideUntil]="loaded" [message]="loadingMessage">
<!-- Search results. -->
<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>
</ion-item-divider>
<a ion-item text-wrap *ngFor="let result of search.results" [title]="result.fullname" (click)="gotoConversation(result.conversationid, result.userid, result.messageid)" [class.core-split-item-selected]="(result.conversationid && result.conversationid == selectedConversationId) || (result.userid && result.userid == selectedUserId)" detail-none>
<ion-avatar core-user-avatar [user]="result" [linkProfile]="false" item-start></ion-avatar>
<h2>
<p>
<core-format-text [text]="result.fullname"></core-format-text>
<core-icon name="fa-ban" *ngIf="result.isblocked" [attr.aria-label]="'addon.messages.contactblocked' | translate"></core-icon>
</p>
<ion-note *ngIf="result.lastmessagedate > 0">
{{result.lastmessagedate | coreDateDayOrTime}}
</ion-note>
</h2>
<p><core-format-text clean="true" singleLine="true" [text]="result.lastmessage" class="addon-message-last-message"></core-format-text></p>
</a>
</ion-list>
<!-- Conversations. -->
<ion-list *ngIf="!search.showResults">
<a ion-item text-wrap (click)="gotoContacts($event)" [attr.aria-label]="'addon.messages.contacts' | translate" class="addon-message-discussion">
<ion-icon name="person" item-start></ion-icon>
<h2>{{ 'addon.messages.contacts' | translate }}</h2>
<ion-badge *ngIf="contactRequestsCount > 0" item-end>{{contactRequestsCount}}</ion-badge>
</a>
<ion-list>
<!-- Favourite conversations. -->
<ion-item-divider color="light" text-wrap *ngIf="favourites.conversations" (click)="toggle(favourites)" class="core-expandable">
<core-icon *ngIf="!favourites.expanded" name="fa-caret-right" item-start></core-icon>
@ -94,16 +73,13 @@
</ion-item>
</div>
</ion-list>
<!-- Search didn't get any result. -->
<core-empty-box *ngIf="(!search.results || search.results.length <= 0) && search.showResults" icon="search" [message]="'core.noresults' | translate"></core-empty-box>
</core-loading>
</ion-content>
</core-split-view>
<!-- Template to render a list of conversations. -->
<ng-template #conversationsTemplate let-conversations="conversations">
<a ion-item text-wrap *ngFor="let conversation of conversations" [title]="conversation.name" detail-none (click)="gotoConversation(conversation.id, conversation.userid)" [class.core-split-item-selected]="(conversation.id && conversation.id == selectedConversationId) || (conversation.userid && conversation.userid == selectedUserId)" id="addon-message-conversation-{{ conversation.id ? conversation.id : 'user-' + conversation.userid }}">
<a ion-item text-wrap *ngFor="let conversation of conversations" [title]="conversation.name" (click)="gotoConversation(conversation.id, conversation.userid)" [class.core-split-item-selected]="(conversation.id && conversation.id == selectedConversationId) || (conversation.userid && conversation.userid == selectedUserId)" class="addon-message-discussion" id="addon-message-conversation-{{ conversation.id ? conversation.id : 'user-' + conversation.userid }}">
<!-- Group conversation image. -->
<ion-avatar item-start *ngIf="conversation.type != typeIndividual && conversation.imageurl">
<img [src]="conversation.imageurl" [alt]="conversation.name" core-external-content>
@ -113,18 +89,18 @@
<ion-avatar *ngIf="conversation.type == typeIndividual" core-user-avatar [user]="conversation.otherUser" [linkProfile]="false" [checkOnline]="conversation.showonlinestatus" item-start></ion-avatar>
<h2>
<p>
<core-format-text [text]="conversation.name"></core-format-text>
<core-icon name="fa-ban" *ngIf="conversation.isblocked" [attr.aria-label]="'addon.messages.contactblocked' | translate"></core-icon>
</p>
<ion-note *ngIf="conversation.lastmessagedate > 0 || conversation.unreadcount">
<ion-badge *ngIf="conversation.unreadcount > 0">{{ conversation.unreadcount }}</ion-badge>
<span *ngIf="conversation.lastmessagedate > 0">{{conversation.lastmessagedate | coreDateDayOrTime}}</span>
</ion-note>
<core-format-text [text]="conversation.name"></core-format-text>
<core-icon name="fa-ban" *ngIf="conversation.isblocked" [attr.aria-label]="'addon.messages.contactblocked' | translate"></core-icon>
</h2>
<p><core-format-text *ngIf="conversation.subname" [text]="conversation.subname"></core-format-text></p>
<p>
<span *ngIf="conversation.sentfromcurrentuser">{{ 'addon.messages.you' | translate }}</span> <core-format-text clean="true" singleLine="true" [text]="conversation.lastmessage" class="addon-message-last-message"></core-format-text>
<ion-note *ngIf="conversation.lastmessagedate > 0 || conversation.unreadcount">
<ion-badge *ngIf="conversation.unreadcount > 0">{{ conversation.unreadcount }}</ion-badge>
<span *ngIf="conversation.lastmessagedate > 0">{{conversation.lastmessagedate | coreDateDayOrTime}}</span>
</ion-note>
<p *ngIf="conversation.subname"><core-format-text [text]="conversation.subname"></core-format-text></p>
<p class="addon-message-last-message">
<span *ngIf="conversation.sentfromcurrentuser" class="addon-message-last-message-user">{{ 'addon.messages.you' | translate }}</span>
<core-format-text *ngIf="conversation.type != typeIndividual && conversation.members[0]" [text]="conversation.members[0].fullname + ':'" class="addon-message-last-message-user"></core-format-text>
<core-format-text clean="true" singleLine="true" [text]="conversation.lastmessage" class="addon-message-last-message-text"></core-format-text>
</p>
</a>
</ng-template>

View File

@ -1,17 +1,21 @@
ion-app.app-root page-addon-messages-group-conversations {
h2 {
.addon-message-last-message {
display: flex;
flex-direction: row;
justify-content: space-between;
.note {
margin: 0;
align-self: flex-end;
display: inline-flex;
font-size: initial;
}
align-items: center;
}
core-format-text.addon-message-last-message {
display: inline;
.addon-message-last-message-user {
white-space: nowrap;
color: $black;
@include margin(null, 2px, null, null)
}
.addon-message-last-message-text {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
flex-shrink: 1;
}
}

View File

@ -13,7 +13,7 @@
// limitations under the License.
import { Component, OnInit, OnDestroy, ViewChild } from '@angular/core';
import { IonicPage, Platform, NavParams, Content } from 'ionic-angular';
import { IonicPage, Platform, NavController, NavParams, Content } from 'ionic-angular';
import { TranslateService } from '@ngx-translate/core';
import { CoreEventsProvider } from '@providers/events';
import { CoreSitesProvider } from '@providers/sites';
@ -21,7 +21,6 @@ import { AddonMessagesProvider } from '../../providers/messages';
import { AddonMessagesOfflineProvider } from '../../providers/messages-offline';
import { CoreDomUtilsProvider } from '@providers/utils/dom';
import { CoreUtilsProvider } from '@providers/utils/utils';
import { CoreAppProvider } from '@providers/app';
import { AddonPushNotificationsDelegate } from '@addon/pushnotifications/providers/delegate';
import { CoreSplitViewComponent } from '@components/split-view/split-view';
import { CoreUserProvider } from '@core/user/providers/user';
@ -42,13 +41,7 @@ export class AddonMessagesGroupConversationsPage implements OnInit, OnDestroy {
loadingMessage: string;
selectedConversationId: number;
selectedUserId: number;
search = {
enabled: false,
showResults: false,
results: [],
loading: '',
text: ''
};
contactRequestsCount = 0;
favourites: any = {
type: null,
favourites: true
@ -74,14 +67,15 @@ export class AddonMessagesGroupConversationsPage implements OnInit, OnDestroy {
protected cronObserver: any;
protected openConversationObserver: any;
protected updateConversationListObserver: any;
protected contactRequestsCountObserver: any;
protected memberInfoObserver: any;
constructor(private eventsProvider: CoreEventsProvider, sitesProvider: CoreSitesProvider, translate: TranslateService,
private messagesProvider: AddonMessagesProvider, private domUtils: CoreDomUtilsProvider, navParams: NavParams,
private appProvider: CoreAppProvider, platform: Platform, utils: CoreUtilsProvider,
private navCtrl: NavController, platform: Platform, utils: CoreUtilsProvider,
pushNotificationsDelegate: AddonPushNotificationsDelegate, private messagesOffline: AddonMessagesOfflineProvider,
private userProvider: CoreUserProvider) {
this.search.loading = translate.instant('core.searching');
this.loadingString = translate.instant('core.loading');
this.siteId = sitesProvider.getCurrentSiteId();
this.currentUserId = sitesProvider.getCurrentSiteUserId();
@ -166,6 +160,32 @@ export class AddonMessagesGroupConversationsPage implements OnInit, OnDestroy {
this.refreshData();
}
});
// Update the contact requests badge.
this.contactRequestsCountObserver = eventsProvider.on(AddonMessagesProvider.CONTACT_REQUESTS_COUNT_EVENT, (data) => {
this.contactRequestsCount = data.count;
}, this.siteId);
// Update block status of a user.
this.memberInfoObserver = eventsProvider.on(AddonMessagesProvider.MEMBER_INFO_CHANGED_EVENT, (data) => {
if (!data.userBlocked && !data.userUnblocked) {
// The block status has not changed, ignore.
return;
}
const updateConversations = (conversations: any[]): void => {
if (!conversations || conversations.length <= 0) {
return;
}
const conversation = conversations.find((conv) => conv.userid == data.userId);
if (conversation) {
conversation.isblocked = data.userBlocked;
}
};
updateConversations(this.individual.conversations);
updateConversations(this.favourites.conversations);
}, this.siteId);
}
/**
@ -195,6 +215,8 @@ export class AddonMessagesGroupConversationsPage implements OnInit, OnDestroy {
}
}
});
this.messagesProvider.getContactRequestsCount(this.siteId); // Badge is updated by the observer.
}
/**
@ -204,7 +226,6 @@ export class AddonMessagesGroupConversationsPage implements OnInit, OnDestroy {
*/
protected fetchData(): Promise<any> {
this.loadingMessage = this.loadingString;
this.search.enabled = this.messagesProvider.isSearchMessagesEnabled();
// Load the first conversations of each type.
const promises = [];
@ -481,6 +502,7 @@ export class AddonMessagesGroupConversationsPage implements OnInit, OnDestroy {
if (refresher) {
// Actions to take if refresh comes from the user.
this.eventsProvider.trigger(AddonMessagesProvider.READ_CHANGED_EVENT, undefined, this.siteId);
this.messagesProvider.refreshContactRequestsCount(this.siteId);
refresher.complete();
}
});
@ -515,36 +537,10 @@ export class AddonMessagesGroupConversationsPage implements OnInit, OnDestroy {
}
/**
* Clear search and show conversations again.
* Navigate to the search page.
*/
clearSearch(): void {
this.loaded = false;
this.search.showResults = false;
this.search.text = ''; // Reset searched string.
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, 'addon.messages.errorwhileretrievingmessages', true);
}).finally(() => {
this.loaded = true;
});
gotoSearch(): void {
this.navCtrl.push('AddonMessagesSearchPage');
}
/**
@ -558,5 +554,7 @@ export class AddonMessagesGroupConversationsPage implements OnInit, OnDestroy {
this.cronObserver && this.cronObserver.off();
this.openConversationObserver && this.openConversationObserver.off();
this.updateConversationListObserver && this.updateConversationListObserver.off();
this.contactRequestsCountObserver && this.contactRequestsCountObserver.off();
this.memberInfoObserver && this.memberInfoObserver.off();
}
}

View File

@ -0,0 +1,56 @@
<ion-header>
<ion-navbar core-back-button>
<ion-title>{{ 'addon.messages.searchcombined' | translate }}</ion-title>
<ion-buttons end>
<!-- Add an empty context menu so discussion page can add items in split view, otherwise the menu disappears in some cases. -->
<core-context-menu></core-context-menu>
</ion-buttons>
</ion-navbar>
</ion-header>
<core-split-view>
<ion-content>
<core-search-box (onSubmit)="search($event)" (onClear)="clearSearch($event)" [disabled]="disableSearch" autocorrect="off" [spellcheck]="false" [autoFocus]="true" [lengthCheck]="1"></core-search-box>
<core-loading [hideUntil]="!displaySearching" [message]="'core.searching' | translate">
<ion-list *ngIf="displayResults">
<ng-container *ngTemplateOutlet="resultsTemplate; context: {item: contacts}"></ng-container>
<ng-container *ngTemplateOutlet="resultsTemplate; context: {item: nonContacts}"></ng-container>
<ng-container *ngTemplateOutlet="resultsTemplate; context: {item: messages}"></ng-container>
<!-- The infinite loading cannot be inside the ng-template, it fails because it doesn't find ion-content. -->
<core-infinite-loading [enabled]="messages.canLoadMore" (action)="search(query, 'messages', $event)" [error]="messages.loadMoreError"></core-infinite-loading>
</ion-list>
</core-loading>
</ion-content>
</core-split-view>
<!-- Template to render a list of results -->
<ng-template #resultsTemplate let-item="item">
<ion-item-divider color="light" text-wrap>{{ item.titleString | translate }}</ion-item-divider>
<ion-item text-wrap *ngIf="item.results.length == 0">
{{ item.emptyString | translate }}
</ion-item>
<!-- List of results -->
<a ion-item text-wrap *ngFor="let result of item.results" [title]="result.fullname" (click)="openDiscussion(result.id)" [class.core-split-item-selected]="result.id == selectedUserId" class="addon-message-discussion">
<ion-avatar item-start core-user-avatar [user]="result" [checkOnline]="true" [linkProfile]="false"></ion-avatar>
<h2>
<core-format-text [text]="result.fullname"></core-format-text>
<core-icon name="fa-ban" *ngIf="result.isblocked" [attr.aria-label]="'addon.messages.contactblocked' | translate"></core-icon>
</h2>
<ion-note *ngIf="result.lastmessagedate > 0">
{{result.lastmessagedate | coreDateDayOrTime}}
</ion-note>
<core-format-text *ngIf="result.lastmessage" clean="true" singleLine="true" [text]="result.lastmessage"></core-format-text>
</a>
<!-- Load more button for contacts and non-contacts -->
<ng-container *ngIf="item.type != 'messages'">
<div padding-horizontal *ngIf="item.canLoadMore && !item.loadingMore">
<button ion-button block color="light" (click)="search(query, item.type)">
{{ 'core.loadmore' | translate }}
</button>
</div>
<div *ngIf="item.loadingMore" padding text-center>
<ion-spinner></ion-spinner>
</div>
</ng-container>
</ng-template>

View File

@ -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 { AddonMessagesSearchPage } from './search';
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: [
AddonMessagesSearchPage,
],
imports: [
CoreComponentsModule,
CoreDirectivesModule,
CorePipesModule,
AddonMessagesComponentsModule,
IonicPageModule.forChild(AddonMessagesSearchPage),
TranslateModule.forChild()
],
})
export class AddonMessagesSearchPageModule {}

View File

@ -0,0 +1,244 @@
// (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';
import { CoreDomUtilsProvider } from '@providers/utils/dom';
import { CoreAppProvider } from '@providers/app';
/**
* Page for searching users.
*/
@IonicPage({ segment: 'addon-messages-search' })
@Component({
selector: 'page-addon-messages-search',
templateUrl: 'search.html',
})
export class AddonMessagesSearchPage implements OnDestroy {
disableSearch = false;
displaySearching = false;
displayResults = false;
query = '';
contacts = {
type: 'contacts',
titleString: 'addon.messages.contacts',
emptyString: 'addon.messages.searchnocontactsfound',
results: [],
canLoadMore: false,
loadingMore: false
};
nonContacts = {
type: 'noncontacts',
titleString: 'addon.messages.noncontacts',
emptyString: 'addon.messages.searchnononcontactsfound',
results: [],
canLoadMore: false,
loadingMore: false
};
messages = {
type: 'messages',
titleString: 'addon.messages.messages',
emptyString: 'addon.messages.searchnomessagesfound',
results: [],
canLoadMore: false,
loadingMore: false,
loadMoreError: false
};
selectedUserId = null;
protected memberInfoObserver;
@ViewChild(CoreSplitViewComponent) splitviewCtrl: CoreSplitViewComponent;
constructor(private appProvider: CoreAppProvider, private domUtils: CoreDomUtilsProvider, eventsProvider: CoreEventsProvider,
sitesProvider: CoreSitesProvider, private messagesProvider: AddonMessagesProvider) {
// Update block status of a user.
this.memberInfoObserver = eventsProvider.on(AddonMessagesProvider.MEMBER_INFO_CHANGED_EVENT, (data) => {
if (!data.userBlocked && !data.userUnblocked) {
// The block status has not changed, ignore.
return;
}
const contact = this.contacts.results.find((user) => user.id == data.userId);
if (contact) {
contact.isblocked = data.userBlocked;
} else {
const nonContact = this.nonContacts.results.find((user) => user.id == data.userId);
if (nonContact) {
nonContact.isblocked = data.userBlocked;
}
}
this.messages.results.forEach((message: any): void => {
if (message.userid == data.userId) {
message.isblocked = data.userBlocked;
}
});
}, sitesProvider.getCurrentSiteId());
}
/**
* Clear search.
*/
clearSearch(): void {
this.query = '';
this.displayResults = false;
this.splitviewCtrl.emptyDetails();
}
/**
* Start a new search or load more results.
*
* @param {string} query Text to search for.
* @param {strings} loadMore Load more contacts, noncontacts or messages. If undefined, start a new search.
* @param {any} [infiniteComplete] Infinite scroll complete function. Only used from core-infinite-loading.
* @return {Promise<any>} Resolved when done.
*/
search(query: string, loadMore?: 'contacts' | 'noncontacts' | 'messages', infiniteComplete?: any): Promise<any> {
this.appProvider.closeKeyboard();
this.query = query;
this.disableSearch = true;
this.displaySearching = !loadMore;
const promises = [];
let newContacts = [];
let newNonContacts = [];
let newMessages = [];
let canLoadMoreContacts = false;
let canLoadMoreNonContacts = false;
let canLoadMoreMessages = false;
if (!loadMore || loadMore == 'contacts' || loadMore == 'noncontacts') {
const limitNum = loadMore ? AddonMessagesProvider.LIMIT_SEARCH : AddonMessagesProvider.LIMIT_INITIAL_USER_SEARCH;
let limitFrom = 0;
if (loadMore == 'contacts') {
limitFrom = this.contacts.results.length;
this.contacts.loadingMore = true;
} else if (loadMore == 'noncontacts') {
limitFrom = this.nonContacts.results.length;
this.nonContacts.loadingMore = true;
}
promises.push(
this.messagesProvider.searchUsers(query, limitFrom, limitNum).then((result) => {
if (!loadMore || loadMore == 'contacts') {
newContacts = result.contacts;
canLoadMoreContacts = result.canLoadMoreContacts;
}
if (!loadMore || loadMore == 'noncontacts') {
newNonContacts = result.nonContacts;
canLoadMoreNonContacts = result.canLoadMoreNonContacts;
}
})
);
}
if (!loadMore || loadMore == 'messages') {
let limitFrom = 0;
if (loadMore == 'messages') {
limitFrom = this.messages.results.length;
this.messages.loadingMore = true;
}
promises.push(
this.messagesProvider.searchMessages(query, undefined, limitFrom).then((result) => {
newMessages = result.messages;
canLoadMoreMessages = result.canLoadMore;
})
);
}
return Promise.all(promises).then(() => {
if (!loadMore) {
this.contacts.results = [];
this.nonContacts.results = [];
this.messages.results = [];
}
this.displayResults = true;
if (!loadMore || loadMore == 'contacts') {
this.contacts.results.push(...newContacts);
this.contacts.canLoadMore = canLoadMoreContacts;
}
if (!loadMore || loadMore == 'noncontacts') {
this.nonContacts.results.push(...newNonContacts);
this.nonContacts.canLoadMore = canLoadMoreNonContacts;
}
if (!loadMore || loadMore == 'messages') {
this.messages.results.push(...newMessages);
this.messages.canLoadMore = canLoadMoreMessages;
this.messages.loadMoreError = false;
}
if (!loadMore) {
if (this.contacts.results.length > 0) {
this.openDiscussion(this.contacts.results[0].id, true);
} else if (this.nonContacts.results.length > 0) {
this.openDiscussion(this.nonContacts.results[0].id, true);
} else if (this.messages.results.length > 0) {
this.openDiscussion(this.messages.results[0].userid, true);
}
}
}).catch((error) => {
this.domUtils.showErrorModalDefault(error, 'addon.messages.errorwhileretrievingusers', true);
if (loadMore == 'messages') {
this.messages.loadMoreError = true;
}
}).finally(() => {
this.disableSearch = false;
this.displaySearching = false;
if (loadMore == 'contacts') {
this.contacts.loadingMore = false;
} else if (loadMore == 'noncontacts') {
this.nonContacts.loadingMore = false;
} else if (loadMore == 'messages') {
this.messages.loadingMore = false;
}
infiniteComplete && infiniteComplete();
});
}
/**
* Open a discussion in the split view.
*
* @param {number} userId User id.
* @param {boolean} [onInit=false] Whether the tser was selected on initial load.
*/
openDiscussion(userId: number, onInit: boolean = false): void {
if (!onInit || this.splitviewCtrl.isOn()) {
this.selectedUserId = userId;
this.splitviewCtrl.push('AddonMessagesDiscussionPage', { userId });
}
}
/**
* Component destroyed.
*/
ngOnDestroy(): void {
this.memberInfoObserver && this.memberInfoObserver.off();
}
}

View File

@ -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 { Injectable } from '@angular/core';
import { CoreContentLinksHandlerBase } from '@core/contentlinks/classes/base-handler';
import { CoreContentLinksAction } from '@core/contentlinks/providers/delegate';
import { CoreContentLinksHelperProvider } from '@core/contentlinks/providers/helper';
import { AddonMessagesProvider } from './messages';
/**
* Content links handler for a contact requests.
*/
@Injectable()
export class AddonMessagesContactRequestLinkHandler extends CoreContentLinksHandlerBase {
name = 'AddonMessagesContactRequestLinkHandler';
pattern = /\/message\/pendingcontactrequests\.php/;
constructor(private linkHelper: CoreContentLinksHelperProvider, private messagesProvider: AddonMessagesProvider) {
super();
}
/**
* Get the list of actions for a link (url).
*
* @param {string[]} siteIds List of sites the URL belongs to.
* @param {string} url The URL to treat.
* @param {any} params The params of the URL. E.g. 'mysite.com?id=1' -> {id: 1}
* @param {number} [courseId] Course ID related to the URL. Optional but recommended.
* @return {CoreContentLinksAction[]|Promise<CoreContentLinksAction[]>} List of (or promise resolved with list of) actions.
*/
getActions(siteIds: string[], url: string, params: any, courseId?: number):
CoreContentLinksAction[] | Promise<CoreContentLinksAction[]> {
return [{
action: (siteId, navCtrl?): void => {
// Always use redirect to make it the new history root (to avoid "loops" in history).
this.linkHelper.goInSite(navCtrl, 'AddonMessagesContactsPage', {}, siteId);
}
}];
}
/**
* Check if the handler is enabled for a certain site (site + user) and a URL.
* If not defined, defaults to true.
*
* @param {string} siteId The site ID.
* @param {string} url The URL to treat.
* @param {any} params The params of the URL. E.g. 'mysite.com?id=1' -> {id: 1}
* @param {number} [courseId] Course ID related to the URL. Optional but recommended.
* @return {boolean|Promise<boolean>} Whether the handler is enabled for the URL and site.
*/
isEnabled(siteId: string, url: string, params: any, courseId?: number): boolean | Promise<boolean> {
return this.messagesProvider.isPluginEnabled(siteId).then((enabled) => {
if (!enabled) {
return false;
}
return this.messagesProvider.isGroupMessagingEnabled();
});
}
}

View File

@ -43,6 +43,8 @@ export class AddonMessagesMainMenuHandler implements CoreMainMenuHandler, CoreCr
loading: true
};
protected updating = false;
constructor(private messagesProvider: AddonMessagesProvider, private sitesProvider: CoreSitesProvider,
private eventsProvider: CoreEventsProvider, private appProvider: CoreAppProvider,
private localNotificationsProvider: CoreLocalNotificationsProvider, private textUtils: CoreTextUtilsProvider,
@ -57,10 +59,15 @@ export class AddonMessagesMainMenuHandler implements CoreMainMenuHandler, CoreCr
this.updateBadge(data.siteId);
});
eventsProvider.on(AddonMessagesProvider.CONTACT_REQUESTS_COUNT_EVENT, (data) => {
this.updateBadge(data.siteId, data.count);
});
// Reset info on logout.
eventsProvider.on(CoreEventsProvider.LOGOUT, (data) => {
this.handler.badge = '';
this.handler.loading = true;
this.updating = false;
});
// If a message push notification is received, refresh the count.
@ -103,23 +110,55 @@ export class AddonMessagesMainMenuHandler implements CoreMainMenuHandler, CoreCr
/**
* Triggers an update for the badge number and loading status. Mandatory if showBadge is enabled.
*
* @param {string} siteId Site ID or current Site if undefined.
* @param {string} [siteId] Site ID or current Site if undefined.
* @param {number} [contactRequestsCount] Number of contact requests, if known.
*/
updateBadge(siteId?: string): void {
updateBadge(siteId?: string, contactRequestsCount?: number): void {
siteId = siteId || this.sitesProvider.getCurrentSiteId();
if (!siteId) {
return;
}
this.messagesProvider.getUnreadConversationsCount(undefined, siteId).then((unread) => {
// Leave badge enter if there is a 0+ or a 0.
this.handler.badge = parseInt(unread, 10) > 0 ? unread : '';
// Update badge.
this.pushNotificationsProvider.updateAddonCounter('AddonMessages', unread, siteId);
if (this.updating) {
// An update is already in prgoress.
return;
}
this.updating = true;
const promises = [];
let unreadCount = 0;
let unreadPlus = false;
promises.push(this.messagesProvider.getUnreadConversationsCount(undefined, siteId).then((unread) => {
unreadCount = parseInt(unread, 10);
unreadPlus = (typeof unread === 'string' && unread.slice(-1) === '+');
}).catch(() => {
this.handler.badge = '';
// Ignore error.
}));
// Get the number of contact requests in 3.6+ sites if needed.
if (contactRequestsCount == null && this.messagesProvider.isGroupMessagingEnabled()) {
promises.push(this.messagesProvider.getContactRequestsCount(siteId).then((count) => {
contactRequestsCount = count;
}).catch(() => {
// Ignore errors
}));
}
Promise.all(promises).then(() => {
const totalCount = unreadCount + (contactRequestsCount || 0);
if (totalCount > 0) {
this.handler.badge = totalCount + (unreadPlus ? '+' : '');
} else {
this.handler.badge = '';
}
// Update badge.
this.pushNotificationsProvider.updateAddonCounter('AddonMessages', totalCount, siteId);
}).finally(() => {
this.handler.loading = false;
this.updating = false;
});
}

View File

@ -21,6 +21,8 @@ import { AddonMessagesOfflineProvider } from './messages-offline';
import { CoreUtilsProvider } from '@providers/utils/utils';
import { CoreTimeUtilsProvider } from '@providers/utils/time';
import { CoreEmulatorHelperProvider } from '@core/emulator/providers/helper';
import { CoreEventsProvider } from '@providers/events';
import { CoreSite } from '@classes/site';
/**
* Service to handle messages.
@ -28,7 +30,6 @@ import { CoreEmulatorHelperProvider } from '@core/emulator/providers/helper';
@Injectable()
export class AddonMessagesProvider {
protected ROOT_CACHE_KEY = 'mmaMessages:';
protected LIMIT_SEARCH_MESSAGES = 50;
protected LIMIT_MESSAGES = AddonMessagesProvider.LIMIT_MESSAGES;
static NEW_MESSAGE_EVENT = 'addon_messages_new_message_event';
static READ_CHANGED_EVENT = 'addon_messages_read_changed_event';
@ -36,6 +37,8 @@ export class AddonMessagesProvider {
static OPEN_CONVERSATION_EVENT = 'addon_messages_open_conversation_event'; // Notify that a conversation should be opened.
static SPLIT_VIEW_LOAD_EVENT = 'addon_messages_split_view_load_event';
static UPDATE_CONVERSATION_LIST_EVENT = 'addon_messages_update_conversation_list_event';
static MEMBER_INFO_CHANGED_EVENT = 'addon_messages_member_changed_event';
static CONTACT_REQUESTS_COUNT_EVENT = 'addon_messages_contact_requests_count_event';
static POLL_INTERVAL = 10000;
static PUSH_SIMULATION_COMPONENT = 'AddonMessagesPushSimulation';
@ -44,7 +47,10 @@ export class AddonMessagesProvider {
static MESSAGE_PRIVACY_SITE = 2; // Privacy setting for being messaged by anyone on the site.
static MESSAGE_CONVERSATION_TYPE_INDIVIDUAL = 1; // An individual conversation.
static MESSAGE_CONVERSATION_TYPE_GROUP = 2; // A group conversation.
static LIMIT_CONTACTS = 50;
static LIMIT_MESSAGES = 50;
static LIMIT_INITIAL_USER_SEARCH = 3;
static LIMIT_SEARCH = 50;
static NOTIFICATION_PREFERENCES_KEY = 'message_provider_moodle_instantmessage';
@ -53,7 +59,7 @@ export class AddonMessagesProvider {
constructor(logger: CoreLoggerProvider, private sitesProvider: CoreSitesProvider, private appProvider: CoreAppProvider,
private userProvider: CoreUserProvider, private messagesOffline: AddonMessagesOfflineProvider,
private utils: CoreUtilsProvider, private timeUtils: CoreTimeUtilsProvider,
private emulatorHelper: CoreEmulatorHelperProvider) {
private emulatorHelper: CoreEmulatorHelperProvider, private eventsProvider: CoreEventsProvider) {
this.logger = logger.getInstance('AddonMessagesProvider');
}
@ -63,6 +69,7 @@ export class AddonMessagesProvider {
* @param {number} userId User ID of the person to add.
* @param {string} [siteId] Site ID. If not defined, use current site.
* @return {Promise<any>} Resolved when done.
* @deprecated since Moodle 3.6
*/
addContact(userId: number, siteId?: string): Promise<any> {
return this.sitesProvider.getSite(siteId).then((site) => {
@ -101,7 +108,90 @@ export class AddonMessagesProvider {
}
return promise.then(() => {
return this.invalidateAllContactsCache(site.getUserId(), site.getId());
return this.invalidateAllMemberInfo(userId, site).finally(() => {
const data = { userId, userBlocked: true };
this.eventsProvider.trigger(AddonMessagesProvider.MEMBER_INFO_CHANGED_EVENT, data, site.id);
});
});
});
}
/**
* Confirm a contact request from another user.
*
* @param {number} userId ID of the user who made the contact request.
* @param {string} [siteId] Site ID. If not defined, use current site.
* @return {Promise<any>} Resolved when done.
* @since 3.6
*/
confirmContactRequest(userId: number, siteId?: string): Promise<any> {
return this.sitesProvider.getSite(siteId).then((site) => {
const params = {
userid: userId,
requesteduserid: site.getUserId(),
};
return site.write('core_message_confirm_contact_request', params).then(() => {
return this.utils.allPromises([
this.invalidateAllMemberInfo(userId, site),
this.invalidateContactsCache(site.id),
this.invalidateUserContacts(site.id),
this.refreshContactRequestsCount(site.id),
]).finally(() => {
const data = { userId, contactRequestConfirmed: true };
this.eventsProvider.trigger(AddonMessagesProvider.MEMBER_INFO_CHANGED_EVENT, data, site.id);
});
});
});
}
/**
* Send a contact request to another user.
*
* @param {number} userId ID of the receiver of the contact request.
* @param {string} [siteId] Site ID. If not defined, use current site.
* @return {Promise<any>} Resolved when done.
* @since 3.6
*/
createContactRequest(userId: number, siteId?: string): Promise<any> {
return this.sitesProvider.getSite(siteId).then((site) => {
const params = {
userid: site.getUserId(),
requesteduserid: userId,
};
return site.write('core_message_create_contact_request', params).then(() => {
return this.invalidateAllMemberInfo(userId, site).finally(() => {
const data = { userId, contactRequestCreated: true };
this.eventsProvider.trigger(AddonMessagesProvider.MEMBER_INFO_CHANGED_EVENT, data, site.id);
});
});
});
}
/**
* Decline a contact request from another user.
*
* @param {number} userId ID of the user who made the contact request.
* @param {string} [siteId] Site ID. If not defined, use current site.
* @return {Promise<any>} Resolved when done.
* @since 3.6
*/
declineContactRequest(userId: number, siteId?: string): Promise<any> {
return this.sitesProvider.getSite(siteId).then((site) => {
const params = {
userid: userId,
requesteduserid: site.getUserId(),
};
return site.write('core_message_decline_contact_request', params).then(() => {
return this.utils.allPromises([
this.invalidateAllMemberInfo(userId, site),
this.refreshContactRequestsCount(site.id),
]).finally(() => {
const data = { userId, contactRequestDeclined: true };
this.eventsProvider.trigger(AddonMessagesProvider.MEMBER_INFO_CHANGED_EVENT, data, site.id);
});
});
});
}
@ -247,6 +337,33 @@ export class AddonMessagesProvider {
return this.ROOT_CACHE_KEY + 'contacts';
}
/**
* Get the cache key for comfirmed contacts.
*
* @return {string} Cache key.
*/
protected getCacheKeyForUserContacts(): string {
return this.ROOT_CACHE_KEY + 'userContacts';
}
/**
* Get the cache key for contact requests.
*
* @return {string} Cache key.
*/
protected getCacheKeyForContactRequests(): string {
return this.ROOT_CACHE_KEY + 'contactRequests';
}
/**
* Get the cache key for contact requests count.
*
* @return {string} Cache key.
*/
protected getCacheKeyForContactRequestsCount(): string {
return this.ROOT_CACHE_KEY + 'contactRequestsCount';
}
/**
* Get the cache key for a discussion.
*
@ -332,6 +449,17 @@ export class AddonMessagesProvider {
return this.getCommonCacheKeyForUserConversations(userId) + ':' + type + ':' + favourites;
}
/**
* Get cache key for member info.
*
* @param {number} userId User ID.
* @param {number} otherUserId The other user ID.
* @return {string} Cache key.
*/
protected getCacheKeyForMemberInfo(userId: number, otherUserId: number): string {
return this.ROOT_CACHE_KEY + 'memberInfo:' + userId + ':' + otherUserId;
}
/**
* Get common cache key for get user conversations.
*
@ -356,6 +484,7 @@ export class AddonMessagesProvider {
*
* @param {string} [siteId] Site ID. If not defined, use current site.
* @return {Promise<any>} Resolved with the WS data.
* @deprecated since Moodle 3.6
*/
getAllContacts(siteId?: string): Promise<any> {
siteId = siteId || this.sitesProvider.getCurrentSiteId();
@ -377,7 +506,7 @@ export class AddonMessagesProvider {
}
/**
* Get all the blocked contacts of the current user.
* Get all the users blocked by the current user.
*
* @param {string} [siteId] Site ID. If not defined, use current site.
* @return {Promise<any>} Resolved with the WS data.
@ -403,6 +532,7 @@ export class AddonMessagesProvider {
*
* @param {string} [siteId] Site ID. If not defined, use current site.
* @return {Promise<any>} Resolved with the WS data.
* @deprecated since Moodle 3.6
*/
getContacts(siteId?: string): Promise<any> {
return this.sitesProvider.getSite(siteId).then((site) => {
@ -430,6 +560,114 @@ export class AddonMessagesProvider {
});
}
/**
* Get the list of user contacts.
*
* @param {number} [limitFrom=0] Position of the first contact to fetch.
* @param {number} [limitNum] Number of contacts to fetch. Default is AddonMessagesProvider.LIMIT_CONTACTS.
* @param {string} [siteId] Site ID. If not defined, use current site.
* @return {Promise<{contacts: any[], canLoadMore: boolean}>} Resolved with the list of user contacts.
* @since 3.6
*/
getUserContacts(limitFrom: number = 0, limitNum: number = AddonMessagesProvider.LIMIT_CONTACTS , siteId?: string):
Promise<{contacts: any[], canLoadMore: boolean}> {
return this.sitesProvider.getSite(siteId).then((site) => {
const params = {
userid: site.getUserId(),
limitfrom: limitFrom,
limitnum: limitNum <= 0 ? 0 : limitNum + 1
};
const preSets = {
cacheKey: this.getCacheKeyForUserContacts()
};
return site.read('core_message_get_user_contacts', params, preSets).then((contacts) => {
if (!contacts || !contacts.length) {
return { contacts: [], canLoadMore: false };
}
this.userProvider.storeUsers(contacts, site.id);
if (limitNum <= 0) {
return { contacts, canLoadMore: false };
}
return {
contacts: contacts.slice(0, limitNum),
canLoadMore: contacts.length > limitNum
};
});
});
}
/**
* Get the contact request sent to the current user.
*
* @param {number} [limitFrom=0] Position of the first contact request to fetch.
* @param {number} [limitNum] Number of contact requests to fetch. Default is AddonMessagesProvider.LIMIT_CONTACTS.
* @param {string} [siteId] Site ID. If not defined, use current site.
* @return {Promise<{requests: any[], canLoadMore: boolean}>} Resolved with the list of contact requests.
* @since 3.6
*/
getContactRequests(limitFrom: number = 0, limitNum: number = AddonMessagesProvider.LIMIT_CONTACTS, siteId?: string):
Promise<{requests: any[], canLoadMore: boolean}> {
return this.sitesProvider.getSite(siteId).then((site) => {
const data = {
userid: site.getUserId(),
limitfrom: limitFrom,
limitnum: limitNum <= 0 ? 0 : limitNum + 1
};
const preSets = {
cacheKey: this.getCacheKeyForContactRequests()
};
return site.read('core_message_get_contact_requests', data, preSets).then((requests) => {
if (!requests || !requests.length) {
return { requests: [], canLoadMore: false };
}
this.userProvider.storeUsers(requests, site.id);
if (limitNum <= 0) {
return { requests, canLoadMore: false };
}
return {
requests: requests.slice(0, limitNum),
canLoadMore: requests.length > limitNum
};
});
});
}
/**
* Get the number of contact requests sent to the current user.
*
* @param {string} [siteId] Site ID. If not defined, use current site.
* @return {Promise<number>} Resolved with the number of contact requests.
* @since 3.6
*/
getContactRequestsCount(siteId?: string): Promise<number> {
return this.sitesProvider.getSite(siteId).then((site) => {
const data = {
userid: site.getUserId(),
};
const preSets = {
cacheKey: this.getCacheKeyForContactRequestsCount(),
typeExpected: 'number'
};
return site.read('core_message_get_received_contact_requests_count', data, preSets).then((count) => {
// Notify the new count so all badges are updated.
this.eventsProvider.trigger(AddonMessagesProvider.CONTACT_REQUESTS_COUNT_EVENT, { count }, site.id);
return count;
});
});
}
/**
* Get a conversation by the conversation ID.
*
@ -491,18 +729,20 @@ export class AddonMessagesProvider {
* @param {boolean} [newestFirst=true] Whether to order messages by newest first.
* @param {string} [siteId] Site ID. If not defined, use current site.
* @param {number} [userId] User ID. If not defined, current user in the site.
* @param {boolean} [preferCache] True if shouldn't call WS if data is cached, false otherwise.
* @return {Promise<any>} Promise resolved with the response.
* @since 3.6
*/
getConversationBetweenUsers(otherUserId: number, includeContactRequests?: boolean, includePrivacyInfo?: boolean,
messageOffset: number = 0, messageLimit: number = 1, memberOffset: number = 0, memberLimit: number = 2,
newestFirst: boolean = true, siteId?: string, userId?: number): Promise<any> {
newestFirst: boolean = true, siteId?: string, userId?: number, preferCache?: boolean): Promise<any> {
return this.sitesProvider.getSite(siteId).then((site) => {
userId = userId || site.getUserId();
const preSets = {
cacheKey: this.getCacheKeyForConversationBetweenUsers(userId, otherUserId)
cacheKey: this.getCacheKeyForConversationBetweenUsers(userId, otherUserId),
omitExpires: !!preferCache,
},
params: any = {
userid: userId,
@ -900,6 +1140,40 @@ export class AddonMessagesProvider {
});
}
/**
* Get conversation member info by user id, works even if no conversation betwen the users exists.
*
* @param {number} otherUserId The other user ID.
* @param {string} [siteId] Site ID. If not defined, use current site.
* @param {number} [userId] User ID. If not defined, current user in the site.
* @return {Promise<any>} Promise resolved with the member info.
* @since 3.6
*/
getMemberInfo(otherUserId: number, siteId?: string, userId?: number): Promise<any> {
return this.sitesProvider.getSite(siteId).then((site) => {
userId = userId || site.getUserId();
const preSets = {
cacheKey: this.getCacheKeyForMemberInfo(userId, otherUserId)
},
params: any = {
referenceuserid: userId,
userids: [otherUserId],
includecontactrequests: 1,
includeprivacyinfo: 1,
};
return site.read('core_message_get_member_info', params, preSets).then((members) => {
if (!members || members.length < 1) {
// Should never happen.
return Promise.reject(null);
}
return members[0];
});
});
}
/**
* Get the cache key for the get message preferences call.
*
@ -1146,6 +1420,42 @@ export class AddonMessagesProvider {
});
}
/**
* Invalidate user contacts cache.
*
* @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<any>} Resolved when done.
*/
invalidateUserContacts(siteId?: string): Promise<any> {
return this.sitesProvider.getSite(siteId).then((site) => {
return site.invalidateWsCacheForKey(this.getCacheKeyForUserContacts());
});
}
/**
* Invalidate contact requests cache.
*
* @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<any>} Resolved when done.
*/
invalidateContactRequestsCache(siteId?: string): Promise<any> {
return this.sitesProvider.getSite(siteId).then((site) => {
return site.invalidateWsCacheForKey(this.getCacheKeyForContactRequests());
});
}
/**
* Invalidate contact requests count cache.
*
* @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<any>} Resolved when done.
*/
invalidateContactRequestsCountCache(siteId?: string): Promise<any> {
return this.sitesProvider.getSite(siteId).then((site) => {
return site.invalidateWsCacheForKey(this.getCacheKeyForContactRequestsCount());
});
}
/**
* Invalidate conversation.
*
@ -1256,6 +1566,22 @@ export class AddonMessagesProvider {
});
}
/**
* Invalidate member info cache.
*
* @param {number} otherUserId The other user ID.
* @param {string} [siteId] Site ID. If not defined, current site.
* @param {number} [userId] User ID. If not defined, current user in the site.
* @return {Promise<any>} Resolved when done.
*/
invalidateMemberInfo(otherUserId: number, siteId?: string, userId?: number): Promise<any> {
return this.sitesProvider.getSite(siteId).then((site) => {
userId = userId || site.getUserId();
return site.invalidateWsCacheForKey(this.getCacheKeyForMemberInfo(userId, otherUserId));
});
}
/**
* Invalidate get message preferences.
*
@ -1268,6 +1594,31 @@ export class AddonMessagesProvider {
});
}
/**
* Invalidate all cache entries with member info.
*
* @param {number} userId Id of the user to invalidate.
* @param {CoreSite} site Site object.
* @return {Promie<any>} Promise resolved when done.
*/
protected invalidateAllMemberInfo(userId: number, site: CoreSite): Promise<any> {
return this.utils.allPromises([
this.invalidateMemberInfo(userId, site.id),
this.invalidateUserContacts(site.id),
this.invalidateContactRequestsCache(site.id),
this.invalidateConversations(site.id),
this.getConversationBetweenUsers(userId, undefined, undefined, undefined, undefined, undefined, undefined, undefined,
site.id, undefined, true).then((conversation) => {
return this.utils.allPromises([
this.invalidateConversation(conversation.id),
this.invalidateConversationMembers(conversation.id, site.id),
]);
}).catch(() => {
// The conversation does not exist or we can't fetch it now, ignore it.
})
]);
}
/**
* Checks if the a user is blocked by the current user.
*
@ -1276,6 +1627,12 @@ export class AddonMessagesProvider {
* @return {Promise<boolean>} Resolved with boolean, rejected when we do not know.
*/
isBlocked(userId: number, siteId?: string): Promise<boolean> {
if (this.isGroupMessagingEnabled()) {
return this.getMemberInfo(userId, siteId).then((member) => {
return member.isblocked;
});
}
return this.getBlockedContacts(siteId).then((blockedContacts) => {
if (!blockedContacts.users || blockedContacts.users.length < 1) {
return false;
@ -1295,6 +1652,12 @@ export class AddonMessagesProvider {
* @return {Promise<boolean>} Resolved with boolean, rejected when we do not know.
*/
isContact(userId: number, siteId?: string): Promise<boolean> {
if (this.isGroupMessagingEnabled()) {
return this.getMemberInfo(userId, siteId).then((member) => {
return member.iscontact;
});
}
return this.getContacts(siteId).then((contacts) => {
return ['online', 'offline'].some((type) => {
if (contacts[type] && contacts[type].length > 0) {
@ -1438,6 +1801,21 @@ export class AddonMessagesProvider {
return this.sitesProvider.getCurrentSite().write('core_message_mark_all_messages_as_read', params, preSets);
}
/**
* Refresh the number of contact requests sent to the current user.
*
* @param {string} [siteId] Site ID. If not defined, use current site.
* @return {Promise<number>} Resolved with the number of contact requests.
* @since 3.6
*/
refreshContactRequestsCount(siteId?: string): Promise<number> {
siteId = siteId || this.sitesProvider.getCurrentSiteId();
return this.invalidateContactRequestsCountCache(siteId).then(() => {
return this.getContactRequestsCount(siteId);
});
}
/**
* Remove a contact.
*
@ -1455,7 +1833,17 @@ export class AddonMessagesProvider {
};
return site.write('core_message_delete_contacts', params, preSets).then(() => {
return this.invalidateContactsCache(site.getId());
if (this.isGroupMessagingEnabled()) {
return this.utils.allPromises([
this.invalidateUserContacts(site.id),
this.invalidateAllMemberInfo(userId, site),
]).then(() => {
const data = { userId, contactRemoved: true };
this.eventsProvider.trigger(AddonMessagesProvider.MEMBER_INFO_CHANGED_EVENT, data, site.id);
});
} else {
return this.invalidateContactsCache(site.id);
}
});
});
}
@ -1496,28 +1884,91 @@ export class AddonMessagesProvider {
/**
* 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_SEARCH_MESSAGES.
* @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<any>} Promise resolved with the results.
* @param {string} query The query string.
* @param {number} [userId] The user ID. If not defined, current user.
* @param {number} [limitFrom=0] Position of the first result to get. Defaults to 0.
* @param {number} [limitNum] Number of results to get. Defaults to AddonMessagesProvider.LIMIT_SEARCH.
* @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_SEARCH_MESSAGES, siteId?: string):
Promise<any> {
searchMessages(query: string, userId?: number, limitFrom: number = 0, limitNum: number = AddonMessagesProvider.LIMIT_SEARCH,
siteId?: string): Promise<{messages: any[], canLoadMore: boolean}> {
return this.sitesProvider.getSite(siteId).then((site) => {
const param = {
const params = {
userid: userId || site.getUserId(),
search: query,
limitfrom: from,
limitnum: limit
limitfrom: limitFrom,
limitnum: limitNum <= 0 ? 0 : limitNum + 1
},
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;
return site.read('core_message_data_for_messagearea_search_messages', params, preSets).then((result) => {
if (!result.contacts || !result.contacts.length) {
return { messages: [], canLoadMore: false };
}
result.contacts.forEach((result) => {
result.id = result.userid;
});
this.userProvider.storeUsers(result.contacts, site.id);
if (limitNum <= 0) {
return { messages: result.contacts, canLoadMore: false };
}
return {
messages: result.contacts.slice(0, limitNum),
canLoadMore: result.contacts.length > limitNum
};
});
});
}
/**
* Search for users.
*
* @param {string} query Text to search for.
* @param {number} [limitFrom=0] Position of the first found user to fetch.
* @param {number} [limitNum] Number of found users to fetch. Defaults to AddonMessagesProvider.LIMIT_SEARCH.
* @param {string} [siteId] Site ID. If not defined, use current site.
* @return {Promise<any>} Resolved with two lists of found users: contacts and non-contacts.
* @since 3.6
*/
searchUsers(query: string, limitFrom: number = 0, limitNum: number = AddonMessagesProvider.LIMIT_SEARCH, siteId?: string):
Promise<{contacts: any[], nonContacts: any[], canLoadMoreContacts: boolean, canLoadMoreNonContacts: boolean}> {
return this.sitesProvider.getSite(siteId).then((site) => {
const data = {
userid: site.getUserId(),
search: query,
limitfrom: limitFrom,
limitnum: limitNum <= 0 ? 0 : limitNum + 1
},
preSets = {
getFromCache: false // Always try to get updated data. If it fails, it will get it from cache.
};
return site.read('core_message_message_search_users', data, preSets).then((result) => {
const contacts = result.contacts || [];
const nonContacts = result.noncontacts || [];
this.userProvider.storeUsers(contacts, site.id);
this.userProvider.storeUsers(nonContacts, site.id);
if (limitNum <= 0) {
return { contacts, nonContacts, canLoadMoreContacts: false, canLoadMoreNonContacts: false };
}
return {
contacts: contacts.slice(0, limitNum),
nonContacts: nonContacts.slice(0, limitNum),
canLoadMoreContacts: contacts.length > limitNum,
canLoadMoreNonContacts: nonContacts.length > limitNum
};
});
});
}
@ -1918,7 +2369,10 @@ export class AddonMessagesProvider {
}
return promise.then(() => {
return this.invalidateAllContactsCache(site.getUserId(), site.getId());
return this.invalidateAllMemberInfo(userId, site).finally(() => {
const data = { userId, userUnblocked: true };
this.eventsProvider.trigger(AddonMessagesProvider.MEMBER_INFO_CHANGED_EVENT, data, site.id);
});
});
});
}

View File

@ -95,14 +95,14 @@ export class AddonMessagesAddContactUserHandler implements CoreUserProfileHandle
this.messagesProvider.isContact(user.id).then((isContact) => {
if (isContact) {
const template = this.translate.instant('addon.messages.removecontactconfirm'),
title = this.translate.instant('addon.messages.removecontact');
const message = this.translate.instant('addon.messages.removecontactconfirm', {$a: user.fullname});
const okText = this.translate.instant('core.remove');
return this.domUtils.showConfirm(template, title, title).then(() => {
return this.domUtils.showConfirm(message, undefined, okText).then(() => {
return this.messagesProvider.removeContact(user.id);
});
} else {
return this.messagesProvider.addContact(user.id);
return this.addContact(user);
}
}).catch((error) => {
this.domUtils.showErrorModalDefault(error, 'core.error', true);
@ -125,10 +125,12 @@ export class AddonMessagesAddContactUserHandler implements CoreUserProfileHandle
protected checkButton(userId: number): Promise<void> {
this.updateButton(userId, {spinner: true});
const groupMessagingEnabled = this.messagesProvider.isGroupMessagingEnabled();
return this.messagesProvider.isContact(userId).then((isContact) => {
if (isContact) {
this.updateButton(userId, {
title: 'addon.messages.removecontact',
title: groupMessagingEnabled ? 'addon.messages.removefromyourcontacts' : 'addon.messages.removecontact',
class: 'addon-messages-removecontact-handler',
icon: 'remove',
hidden: false,
@ -136,7 +138,7 @@ export class AddonMessagesAddContactUserHandler implements CoreUserProfileHandle
});
} else {
this.updateButton(userId, {
title: 'addon.messages.addcontact',
title: groupMessagingEnabled ? 'addon.messages.addtoyourcontacts' : 'addon.messages.addcontact',
class: 'addon-messages-addcontact-handler',
icon: 'add',
hidden: false,
@ -160,6 +162,42 @@ export class AddonMessagesAddContactUserHandler implements CoreUserProfileHandle
this.eventsProvider.trigger(CoreUserDelegate.UPDATE_HANDLER_EVENT, { handler: this.name, data: data, userId: userId });
}
/**
* Add a contact or send a contact request if group messaging is enabled.
*
* @param {any} user User to add as contact.
* @return {Promise<any>} Promise resolved when done.
*/
protected addContact(user: any): Promise<any> {
if (!this.messagesProvider.isGroupMessagingEnabled()) {
return this.messagesProvider.addContact(user.id);
}
return this.messagesProvider.getMemberInfo(user.id).then((member) => {
const currentUserId = this.sitesProvider.getCurrentSiteUserId();
const requestSent = member.contactrequests.some((request) => {
return request.userid == currentUserId && request.requesteduserid == user.id;
});
if (requestSent) {
const message = this.translate.instant('addon.messages.yourcontactrequestpending', {$a: user.fullname});
return this.domUtils.showAlert(null, message);
}
const message = this.translate.instant('addon.messages.addcontactconfirm', {$a: user.fullname});
const okText = this.translate.instant('core.add');
return this.domUtils.showConfirm(message, undefined, okText).then(() => {
return this.messagesProvider.createContactRequest(user.id);
}).then(() => {
const message = this.translate.instant('addon.messages.contactrequestsent');
return this.domUtils.showAlert(null, message);
});
});
}
/**
* Destroyed method.
*/

View File

@ -48,7 +48,7 @@ export class AddonNotificationsProvider {
protected formatNotificationsData(notifications: any[]): void {
notifications.forEach((notification) => {
// Set message to show.
if (notification.contexturl && notification.contexturl.indexOf('/mod/forum/')) {
if (notification.contexturl && notification.contexturl.indexOf('/mod/forum/') >= 0) {
notification.mobiletext = notification.smallmessage;
} else {
notification.mobiletext = notification.fullmessage;

View File

@ -147,8 +147,11 @@
"addon.files.privatefiles": "Private files",
"addon.files.sitefiles": "Site files",
"addon.messageoutput_airnotifier.processorsettingsdesc": "Configure devices",
"addon.messages.acceptandaddcontact": "Accept and add to contacts",
"addon.messages.addcontact": "Add contact",
"addon.messages.addcontactconfirm": "Are you sure you want to add {{$a}} to your contacts?",
"addon.messages.addtofavourites": "Star",
"addon.messages.addtoyourcontacts": "Add to contacts",
"addon.messages.blocknoncontacts": "Prevent non-contacts from messaging me",
"addon.messages.blockuser": "Block user",
"addon.messages.blockuserconfirm": "Are you sure you want to block {{$a}}?",
@ -159,7 +162,9 @@
"addon.messages.contactblocked": "Contact blocked",
"addon.messages.contactlistempty": "The contact list is empty",
"addon.messages.contactname": "Contact name",
"addon.messages.contactrequestsent": "Contact request sent",
"addon.messages.contacts": "Contacts",
"addon.messages.decline": "Decline",
"addon.messages.deleteallconfirm": "Are you sure you would like to delete this entire conversation? This will not delete it for other conversation participants.",
"addon.messages.deleteconversation": "Delete conversation",
"addon.messages.deletemessage": "Delete message",
@ -168,34 +173,52 @@
"addon.messages.errorwhileretrievingcontacts": "Error while retrieving contacts from the server.",
"addon.messages.errorwhileretrievingdiscussions": "Error while retrieving discussions from the server.",
"addon.messages.errorwhileretrievingmessages": "Error while retrieving messages from the server.",
"addon.messages.errorwhileretrievingusers": "Error while retrieving users from the server.",
"addon.messages.groupinfo": "Group info",
"addon.messages.groupmessages": "Group messages",
"addon.messages.info": "Info",
"addon.messages.isnotinyourcontacts": "{{$a}} is not in your contacts",
"addon.messages.message": "Message",
"addon.messages.messagenotsent": "The message was not sent. Please try again later.",
"addon.messages.messagepreferences": "Message preferences",
"addon.messages.messages": "Messages",
"addon.messages.newmessage": "New message",
"addon.messages.newmessages": "New messages",
"addon.messages.nocontactrequests": "No contact requests",
"addon.messages.nocontactsgetstarted": "Try searching for someone to add them as a contact",
"addon.messages.nofavourites": "No favourites",
"addon.messages.nogroupmessages": "No group messages",
"addon.messages.nomessages": "No messages",
"addon.messages.noncontacts": "Non-contacts",
"addon.messages.nousersfound": "No users found",
"addon.messages.numparticipants": "{{$a}} participants",
"addon.messages.removecontact": "Remove contact",
"addon.messages.removecontactconfirm": "Contact will be removed from your contacts list.",
"addon.messages.removecontactconfirm": "Are you sure you want to remove {{$a}} from your contacts?",
"addon.messages.removefromfavourites": "Unstar",
"addon.messages.removefromyourcontacts": "Remove from contacts",
"addon.messages.requests": "Requests",
"addon.messages.requirecontacttomessage": "You need to request {{$a}} to add you as a contact to be able to message them.",
"addon.messages.searchcombined": "Search people and messages",
"addon.messages.searchnocontactsfound": "No contacts found",
"addon.messages.searchnomessagesfound": "No messages found",
"addon.messages.searchnononcontactsfound": "No non contacts found",
"addon.messages.sendcontactrequest": "Send contact request",
"addon.messages.showdeletemessages": "Show delete messages",
"addon.messages.type_blocked": "Blocked",
"addon.messages.type_offline": "Offline",
"addon.messages.type_online": "Online",
"addon.messages.type_search": "Search results",
"addon.messages.type_strangers": "Others",
"addon.messages.unabletomessage": "You are unable to message this user",
"addon.messages.unblockuser": "Unblock user",
"addon.messages.unblockuserconfirm": "Are you sure you want to unblock {{$a}}?",
"addon.messages.userwouldliketocontactyou": "{{$a}} would like to contact you",
"addon.messages.warningconversationmessagenotsent": "Couldn't send message(s) to conversation {{conversation}}. {{error}}",
"addon.messages.warningmessagenotsent": "Couldn't send message(s) to user {{user}}. {{error}}",
"addon.messages.wouldliketocontactyou": "Would like to contact you",
"addon.messages.you": "You:",
"addon.messages.youhaveblockeduser": "You have blocked this user in the past",
"addon.messages.yourcontactrequestpending": "Your contact request is pending with {{$a}}",
"addon.mod_assign.acceptsubmissionstatement": "Please accept the submission statement.",
"addon.mod_assign.addattempt": "Allow another attempt",
"addon.mod_assign.addnewattempt": "Add a new attempt",
@ -1540,6 +1563,7 @@
"core.quotausage": "You have currently used {{$a.used}} of your {{$a.total}} limit.",
"core.redirectingtosite": "You will be redirected to the site.",
"core.refresh": "Refresh",
"core.remove": "Remove",
"core.required": "Required",
"core.requireduserdatamissing": "This user lacks some required profile data. Please enter the data in your site and try again.<br>{{$a}}",
"core.resources": "Resources",

View File

@ -54,11 +54,7 @@ export class CoreContextMenuPopoverComponent {
event.preventDefault();
event.stopPropagation();
if (!item.iconAction) {
this.logger.warn('Items with action must have an icon action to work', item);
return false;
} else if (item.iconAction == 'spinner') {
if (item.iconAction == 'spinner') {
return false;
}

View File

@ -32,7 +32,7 @@ export class CoreContextMenuComponent implements OnInit, OnDestroy {
@Input() icon?: string; // Icon to be shown on the navigation bar. Default: Kebab menu icon.
@Input() title?: string; // Aria label and text to be shown on the top of the popover.
hideMenu: boolean;
hideMenu = true; // It will be unhidden when items are added.
ariaLabel: string;
protected items: CoreContextMenuItemComponent[] = [];
protected itemsMovedToParent: CoreContextMenuItemComponent[] = [];

View File

@ -188,6 +188,7 @@
"quotausage": "You have currently used {{$a.used}} of your {{$a.total}} limit.",
"redirectingtosite": "You will be redirected to the site.",
"refresh": "Refresh",
"remove": "Remove",
"required": "Required",
"requireduserdatamissing": "This user lacks some required profile data. Please enter the data in your site and try again.<br>{{$a}}",
"resources": "Resources",

View File

@ -184,9 +184,4 @@ ion-app.app-root {
width: min-content;
display: inline;
}
// Message item.
.item-message core-format-text > p:only-child {
display: inline;
}
}