MOBILE-3631 messages: Search page
parent
e2ac9c6e75
commit
99448a3054
|
@ -29,6 +29,11 @@ function buildRoutes(injector: Injector): Routes {
|
|||
loadChildren: () => import('./pages/group-conversations/group-conversations.module')
|
||||
.then(m => m.AddonMessagesGroupConversationsPageModule),
|
||||
},
|
||||
{
|
||||
path: 'search',
|
||||
loadChildren: () => import('./pages/search/search.module')
|
||||
.then(m => m.AddonMessagesSearchPageModule),
|
||||
},
|
||||
...buildTabMainRoutes(injector, {
|
||||
redirectTo: 'index',
|
||||
pathMatch: 'full',
|
||||
|
|
|
@ -0,0 +1,75 @@
|
|||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-buttons slot="start">
|
||||
<ion-back-button [attr.aria-label]="'core.back' | translate"></ion-back-button>
|
||||
</ion-buttons>
|
||||
<ion-title>{{ 'addon.messages.searchcombined' | translate }}</ion-title>
|
||||
<ion-buttons slot="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-toolbar>
|
||||
</ion-header>
|
||||
<!-- @todo <core-split-view>-->
|
||||
<ion-content>
|
||||
<core-search-box (onSubmit)="search($event)" (onClear)="clearSearch($event)" [disabled]="disableSearch" autocorrect="off"
|
||||
[spellcheck]="false" [autoFocus]="true" [lengthCheck]="1" searchArea="AddonMessagesSearch"></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-empty-box
|
||||
*ngIf="displayResults && !contacts.results.length && !nonContacts.results.length && !messages.results.length"
|
||||
icon="fas-search" [message]="'core.noresults' | translate">
|
||||
</core-empty-box>
|
||||
</core-loading>
|
||||
</ion-content>
|
||||
|
||||
<!-- Template to render a list of results -->
|
||||
<ng-template #resultsTemplate let-item="item">
|
||||
<ng-container *ngIf="item.results.length > 0">
|
||||
<ion-item-divider class="ion-text-wrap">
|
||||
<ion-label>{{ item.titleString | translate }}</ion-label>
|
||||
</ion-item-divider>
|
||||
|
||||
<!-- List of results -->
|
||||
<ion-item class="ion-text-wrap" *ngFor="let result of item.results" [title]="result.fullname"
|
||||
(click)="openConversation(result)" [class.core-split-item-selected]="result == selectedResult"
|
||||
class="addon-message-discussion">
|
||||
<core-user-avatar slot="start" [user]="result" [checkOnline]="true" [linkProfile]="false"></core-user-avatar>
|
||||
<ion-label>
|
||||
<h2>
|
||||
<core-format-text [text]="result.fullname" [highlight]="result.highlightName" [filter]="false"></core-format-text>
|
||||
<ion-icon name="fa-ban" *ngIf="result.isblocked" [title]="'addon.messages.contactblocked' | translate">
|
||||
</ion-icon>
|
||||
</h2>
|
||||
<ion-note *ngIf="result.lastmessagedate > 0">
|
||||
{{result.lastmessagedate | coreDateDayOrTime}}
|
||||
</ion-note>
|
||||
<p class="addon-message-last-message">
|
||||
<span *ngIf="result.sentfromcurrentuser" class="addon-message-last-message-user">{{ 'addon.messages.you' | translate }}</span>
|
||||
<core-format-text clean="true" singleLine="true" [text]="result.lastmessage" [highlight]="result.highlightMessage" contextLevel="system" [contextInstanceId]="0" class="addon-message-last-message-text"></core-format-text>
|
||||
</p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
|
||||
<!-- Load more button for contacts and non-contacts -->
|
||||
<ng-container *ngIf="item.type != 'messages'">
|
||||
<div class="ion-padding-horizontal" *ngIf="item.canLoadMore && !item.loadingMore">
|
||||
<ion-button expand="block" color="light" (click)="search(query, item.type)">
|
||||
{{ 'core.loadmore' | translate }}
|
||||
</ion-button>
|
||||
</div>
|
||||
<div *ngIf="item.loadingMore" class="ion-padding ion-text-center">
|
||||
<ion-spinner></ion-spinner>
|
||||
</div>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
</ng-template>
|
|
@ -0,0 +1,47 @@
|
|||
// (C) Copyright 2015 Moodle Pty Ltd.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { NgModule } from '@angular/core';
|
||||
import { IonicModule } from '@ionic/angular';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { RouterModule, Routes } from '@angular/router';
|
||||
import { CommonModule } from '@angular/common';
|
||||
|
||||
import { CoreSharedModule } from '@/core/shared.module';
|
||||
import { CoreSearchComponentsModule } from '@features/search/components/components.module';
|
||||
|
||||
import { AddonMessagesSearchPage } from './search.page';
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: AddonMessagesSearchPage,
|
||||
},
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
RouterModule.forChild(routes),
|
||||
CommonModule,
|
||||
IonicModule,
|
||||
TranslateModule.forChild(),
|
||||
CoreSharedModule,
|
||||
CoreSearchComponentsModule,
|
||||
],
|
||||
declarations: [
|
||||
AddonMessagesSearchPage,
|
||||
],
|
||||
exports: [RouterModule],
|
||||
})
|
||||
export class AddonMessagesSearchPageModule {}
|
|
@ -0,0 +1,311 @@
|
|||
// (C) Copyright 2015 Moodle Pty Ltd.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { Component, OnDestroy } from '@angular/core';
|
||||
import { CoreEventObserver, CoreEvents } from '@singletons/events';
|
||||
import { CoreSites } from '@services/sites';
|
||||
import {
|
||||
AddonMessagesProvider,
|
||||
AddonMessagesConversationMember,
|
||||
AddonMessagesMessageAreaContact,
|
||||
AddonMessagesMemberInfoChangedEventData,
|
||||
AddonMessages,
|
||||
} from '../../services/messages';
|
||||
import { CoreDomUtils } from '@services/utils/dom';
|
||||
import { CoreApp } from '@services/app';
|
||||
import { CoreNavigator } from '@services/navigator';
|
||||
import { Params } from '@angular/router';
|
||||
// @todo import { CoreSplitViewComponent } from '@components/split-view/split-view';
|
||||
|
||||
/**
|
||||
* Page for searching users.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'page-addon-messages-search',
|
||||
templateUrl: 'search.html',
|
||||
})
|
||||
export class AddonMessagesSearchPage implements OnDestroy {
|
||||
|
||||
disableSearch = false;
|
||||
displaySearching = false;
|
||||
displayResults = false;
|
||||
query = '';
|
||||
contacts: AddonMessagesSearchResults = {
|
||||
type: 'contacts',
|
||||
titleString: 'addon.messages.contacts',
|
||||
results: [],
|
||||
canLoadMore: false,
|
||||
loadingMore: false,
|
||||
};
|
||||
|
||||
nonContacts: AddonMessagesSearchResults = {
|
||||
type: 'noncontacts',
|
||||
titleString: 'addon.messages.noncontacts',
|
||||
results: [],
|
||||
canLoadMore: false,
|
||||
loadingMore: false,
|
||||
};
|
||||
|
||||
messages: AddonMessagesSearchMessageResults = {
|
||||
type: 'messages',
|
||||
titleString: 'addon.messages.messages',
|
||||
results: [],
|
||||
canLoadMore: false,
|
||||
loadingMore: false,
|
||||
loadMoreError: false,
|
||||
};
|
||||
|
||||
selectedResult?: AddonMessagesConversationMember | AddonMessagesMessageAreaContact;
|
||||
|
||||
protected memberInfoObserver: CoreEventObserver;
|
||||
|
||||
// @todo @ViewChild(CoreSplitViewComponent) splitviewCtrl: CoreSplitViewComponent;
|
||||
|
||||
constructor() {
|
||||
// Update block status of a user.
|
||||
this.memberInfoObserver = CoreEvents.on<AddonMessagesMemberInfoChangedEventData>(
|
||||
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: AddonMessagesMessageAreaContact): void => {
|
||||
if (message.userid == data.userId) {
|
||||
message.isblocked = !!data.userBlocked;
|
||||
}
|
||||
});
|
||||
},
|
||||
CoreSites.instance.getCurrentSiteId(),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear search.
|
||||
*/
|
||||
clearSearch(): void {
|
||||
this.query = '';
|
||||
this.displayResults = false;
|
||||
// @todo this.splitviewCtrl.emptyDetails();
|
||||
}
|
||||
|
||||
/**
|
||||
* Start a new search or load more results.
|
||||
*
|
||||
* @param query Text to search for.
|
||||
* @param loadMore Load more contacts, noncontacts or messages. If undefined, start a new search.
|
||||
* @param infiniteComplete Infinite scroll complete function. Only used from core-infinite-loading.
|
||||
* @return Resolved when done.
|
||||
*/
|
||||
async search(query: string, loadMore?: 'contacts' | 'noncontacts' | 'messages', infiniteComplete?: () => void): Promise<void> {
|
||||
CoreApp.instance.closeKeyboard();
|
||||
|
||||
this.query = query;
|
||||
this.disableSearch = true;
|
||||
this.displaySearching = !loadMore;
|
||||
|
||||
const promises: Promise<void>[] = [];
|
||||
let newContacts: AddonMessagesConversationMember[] = [];
|
||||
let newNonContacts: AddonMessagesConversationMember[] = [];
|
||||
let newMessages: AddonMessagesMessageAreaContact[] = [];
|
||||
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(
|
||||
AddonMessages.instance.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;
|
||||
}
|
||||
|
||||
return;
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
if (!loadMore || loadMore == 'messages') {
|
||||
let limitFrom = 0;
|
||||
if (loadMore == 'messages') {
|
||||
limitFrom = this.messages.results.length;
|
||||
this.messages.loadingMore = true;
|
||||
}
|
||||
|
||||
promises.push(
|
||||
AddonMessages.instance.searchMessages(query, undefined, limitFrom).then((result) => {
|
||||
newMessages = result.messages;
|
||||
canLoadMoreMessages = result.canLoadMore;
|
||||
|
||||
return;
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
await Promise.all(promises);
|
||||
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;
|
||||
this.setHighlight(newContacts, true);
|
||||
}
|
||||
|
||||
if (!loadMore || loadMore == 'noncontacts') {
|
||||
this.nonContacts.results.push(...newNonContacts);
|
||||
this.nonContacts.canLoadMore = canLoadMoreNonContacts;
|
||||
this.setHighlight(newNonContacts, true);
|
||||
}
|
||||
|
||||
if (!loadMore || loadMore == 'messages') {
|
||||
this.messages.results.push(...newMessages);
|
||||
this.messages.canLoadMore = canLoadMoreMessages;
|
||||
this.messages.loadMoreError = false;
|
||||
this.setHighlight(newMessages, false);
|
||||
}
|
||||
|
||||
if (!loadMore) {
|
||||
if (this.contacts.results.length > 0) {
|
||||
this.openConversation(this.contacts.results[0], true);
|
||||
} else if (this.nonContacts.results.length > 0) {
|
||||
this.openConversation(this.nonContacts.results[0], true);
|
||||
} else if (this.messages.results.length > 0) {
|
||||
this.openConversation(this.messages.results[0], true);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
CoreDomUtils.instance.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 conversation in the split view.
|
||||
*
|
||||
* @param result User or message.
|
||||
* @param onInit Whether the tser was selected on initial load.
|
||||
*/
|
||||
openConversation(result: AddonMessagesConversationMember | AddonMessagesMessageAreaContact, onInit: boolean = false): void {
|
||||
if (!onInit /* @todo || this.splitviewCtrl.isOn()*/) {
|
||||
this.selectedResult = result;
|
||||
|
||||
const params: Params = {};
|
||||
if ('conversationid' in result) {
|
||||
params.conversationId = result.conversationid;
|
||||
} else {
|
||||
params.userId = result.id;
|
||||
}
|
||||
|
||||
// @todo this.splitviewCtrl.push('AddonMessagesDiscussionPage', params);
|
||||
CoreNavigator.instance.navigateToSitePath('discussion', { params });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the highlight values for each entry.
|
||||
*
|
||||
* @param results Results to highlight.
|
||||
* @param isUser Whether the results are from a user search or from a message search.
|
||||
*/
|
||||
setHighlight(
|
||||
results: (AddonMessagesConversationMemberWithHighlight | AddonMessagesMessageAreaContactWithHighlight)[],
|
||||
isUser = false,
|
||||
): void {
|
||||
results.forEach((result) => {
|
||||
result.highlightName = isUser ? this.query : undefined;
|
||||
result.highlightMessage = !isUser ? this.query : undefined;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Component destroyed.
|
||||
*/
|
||||
ngOnDestroy(): void {
|
||||
this.memberInfoObserver?.off();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
type AddonMessagesSearchResults = {
|
||||
type: string;
|
||||
titleString: string;
|
||||
results: AddonMessagesConversationMemberWithHighlight[];
|
||||
canLoadMore: boolean;
|
||||
loadingMore: boolean;
|
||||
};
|
||||
|
||||
type AddonMessagesSearchMessageResults = {
|
||||
type: string;
|
||||
titleString: string;
|
||||
results: AddonMessagesMessageAreaContactWithHighlight[];
|
||||
canLoadMore: boolean;
|
||||
loadingMore: boolean;
|
||||
loadMoreError: boolean;
|
||||
};
|
||||
|
||||
type AddonMessagesSearchResultHighlight = {
|
||||
highlightName?: string;
|
||||
highlightMessage?: string;
|
||||
};
|
||||
|
||||
type AddonMessagesConversationMemberWithHighlight = AddonMessagesConversationMember & AddonMessagesSearchResultHighlight;
|
||||
type AddonMessagesMessageAreaContactWithHighlight = AddonMessagesMessageAreaContact & AddonMessagesSearchResultHighlight;
|
|
@ -406,6 +406,12 @@ ion-button.core-button-select {
|
|||
flex-direction: row;
|
||||
}
|
||||
|
||||
// Text formats.
|
||||
// Highlight text.
|
||||
.matchtext {
|
||||
background-color: var(--text-hightlight-background-color);
|
||||
}
|
||||
|
||||
// Text for accessibility, hidden from the view.
|
||||
.accesshide {
|
||||
position: absolute;
|
||||
|
|
|
@ -93,6 +93,8 @@
|
|||
--ion-text-color-rgb: 58,58,58;
|
||||
--ion-card-color: var(--ion-text-color);
|
||||
|
||||
--text-hightlight-background-color: var(--custom-text-hightlight-background-color, #99c1ed);
|
||||
|
||||
ion-content {
|
||||
--background: var(--gray-light);
|
||||
--contrast-background: var(--white);
|
||||
|
|
Loading…
Reference in New Issue