Merge remote-tracking branch 'origin/demo' into starter

# Conflicts:
#	src/app/app.routing.ts
#	src/app/mock-api/common/navigation/data.ts
#	src/app/modules/admin/apps/academy/academy.service.ts
#	src/app/modules/admin/apps/academy/details/details.component.html
#	src/app/modules/admin/apps/academy/list/list.component.html
#	src/app/modules/admin/apps/mailbox/list/list.component.ts
#	src/app/modules/admin/docs/changelog/changelog.ts
#	src/app/modules/admin/pages/pricing/modern/modern.component.html
#	src/app/modules/admin/pages/pricing/simple/simple.component.html
#	src/app/modules/admin/pages/pricing/single/single.component.html
#	src/app/modules/admin/pages/pricing/table/table.component.html
This commit is contained in:
sercan 2021-04-30 19:40:07 +03:00
commit 40894e0aa3
53 changed files with 5764 additions and 1867 deletions

2
.nvmrc
View File

@ -1 +1 @@
14
12

2599
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
{
"name": "@fuse/demo",
"version": "12.1.0",
"version": "12.2.0",
"license": "https://themeforest.net/licenses/standard",
"private": true,
"scripts": {
@ -12,17 +12,17 @@
"e2e": "ng e2e"
},
"dependencies": {
"@angular/animations": "11.2.11",
"@angular/cdk": "11.2.10",
"@angular/common": "11.2.11",
"@angular/compiler": "11.2.11",
"@angular/core": "11.2.11",
"@angular/forms": "11.2.11",
"@angular/material": "11.2.10",
"@angular/material-moment-adapter": "11.2.10",
"@angular/platform-browser": "11.2.11",
"@angular/platform-browser-dynamic": "11.2.11",
"@angular/router": "11.2.11",
"@angular/animations": "11.2.12",
"@angular/cdk": "11.2.11",
"@angular/common": "11.2.12",
"@angular/compiler": "11.2.12",
"@angular/core": "11.2.12",
"@angular/forms": "11.2.12",
"@angular/material": "11.2.11",
"@angular/material-moment-adapter": "11.2.11",
"@angular/platform-browser": "11.2.12",
"@angular/platform-browser-dynamic": "11.2.12",
"@angular/router": "11.2.12",
"@fullcalendar/angular": "4.4.5-beta",
"@fullcalendar/core": "4.4.2",
"@fullcalendar/daygrid": "4.4.2",
@ -38,7 +38,7 @@
"moment": "2.29.1",
"ng-apexcharts": "1.5.9",
"ngx-markdown": "11.1.3",
"ngx-quill": "13.2.0",
"ngx-quill": "13.3.1",
"perfect-scrollbar": "1.5.0",
"quill": "1.3.7",
"rrule": "2.6.8",
@ -48,10 +48,10 @@
"zone.js": "0.11.4"
},
"devDependencies": {
"@angular-devkit/build-angular": "0.1102.10",
"@angular/cli": "11.2.10",
"@angular/compiler-cli": "11.2.11",
"@angular/language-service": "11.2.11",
"@angular-devkit/build-angular": "0.1102.11",
"@angular/cli": "11.2.11",
"@angular/compiler-cli": "11.2.12",
"@angular/language-service": "11.2.12",
"@tailwindcss/aspect-ratio": "0.2.0",
"@tailwindcss/line-clamp": "0.2.0",
"@tailwindcss/typography": "0.4.0",
@ -73,7 +73,7 @@
"karma-jasmine": "4.0.1",
"karma-jasmine-html-reporter": "1.5.4",
"lodash": "4.17.21",
"postcss": "8.2.12",
"postcss": "8.2.13",
"protractor": "7.0.0",
"tailwindcss": "2.1.2",
"ts-node": "8.3.0",

View File

@ -0,0 +1,7 @@
<!-- Button -->
<button
mat-icon-button
[matTooltip]="'Toggle Fullscreen'"
(click)="toggleFullscreen()">
<mat-icon [svgIcon]="'heroicons_outline:arrows-expand'"></mat-icon>
</button>

View File

@ -0,0 +1,164 @@
import { ChangeDetectionStrategy, Component, Inject, OnInit, ViewEncapsulation } from '@angular/core';
import { DOCUMENT } from '@angular/common';
import { FSDocument, FSDocumentElement } from '@fuse/components/fullscreen/fullscreen.types';
@Component({
selector : 'fuse-fullscreen',
templateUrl : './fullscreen.component.html',
encapsulation : ViewEncapsulation.None,
changeDetection: ChangeDetectionStrategy.OnPush,
exportAs : 'fuseFullscreen'
})
export class FuseFullscreenComponent implements OnInit
{
private _fsDoc: FSDocument;
private _fsDocEl: FSDocumentElement;
private _isFullscreen: boolean = false;
/**
* Constructor
*/
constructor(@Inject(DOCUMENT) private _document: Document)
{
this._fsDoc = _document as FSDocument;
}
// -----------------------------------------------------------------------------------------------------
// @ Lifecycle hooks
// -----------------------------------------------------------------------------------------------------
/**
* On init
*/
ngOnInit(): void
{
this._fsDocEl = document.documentElement as FSDocumentElement;
}
// -----------------------------------------------------------------------------------------------------
// @ Public methods
// -----------------------------------------------------------------------------------------------------
/**
* Toggle the fullscreen mode
*/
toggleFullscreen(): void
{
// Check if the fullscreen is open
this._isFullscreen = this._getBrowserFullscreenElement() !== null;
// Toggle the fullscreen
if ( this._isFullscreen )
{
this._closeFullscreen();
}
else
{
this._openFullscreen();
}
}
// -----------------------------------------------------------------------------------------------------
// @ Private methods
// -----------------------------------------------------------------------------------------------------
/**
* Get browser's fullscreen element
*
* @private
*/
private _getBrowserFullscreenElement(): Element
{
if ( typeof this._fsDoc.fullscreenElement !== 'undefined' )
{
return this._fsDoc.fullscreenElement;
}
if ( typeof this._fsDoc.mozFullScreenElement !== 'undefined' )
{
return this._fsDoc.mozFullScreenElement;
}
if ( typeof this._fsDoc.msFullscreenElement !== 'undefined' )
{
return this._fsDoc.msFullscreenElement;
}
if ( typeof this._fsDoc.webkitFullscreenElement !== 'undefined' )
{
return this._fsDoc.webkitFullscreenElement;
}
throw new Error('Fullscreen mode is not supported by this browser');
}
/**
* Open the fullscreen
*
* @private
*/
private _openFullscreen(): void
{
if ( this._fsDocEl.requestFullscreen )
{
this._fsDocEl.requestFullscreen();
return;
}
// Firefox
if ( this._fsDocEl.mozRequestFullScreen )
{
this._fsDocEl.mozRequestFullScreen();
return;
}
// Chrome, Safari and Opera
if ( this._fsDocEl.webkitRequestFullscreen )
{
this._fsDocEl.webkitRequestFullscreen();
return;
}
// IE/Edge
if ( this._fsDocEl.msRequestFullscreen )
{
this._fsDocEl.msRequestFullscreen();
return;
}
}
/**
* Close the fullscreen
*
* @private
*/
private _closeFullscreen(): void
{
if ( this._fsDoc.exitFullscreen )
{
this._fsDoc.exitFullscreen();
return;
}
// Firefox
if ( this._fsDoc.mozCancelFullScreen )
{
this._fsDoc.mozCancelFullScreen();
return;
}
// Chrome, Safari and Opera
if ( this._fsDoc.webkitExitFullscreen )
{
this._fsDoc.webkitExitFullscreen();
return;
}
// IE/Edge
else if ( this._fsDoc.msExitFullscreen )
{
this._fsDoc.msExitFullscreen();
return;
}
}
}

View File

@ -0,0 +1,22 @@
import { NgModule } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { MatIconModule } from '@angular/material/icon';
import { MatTooltipModule } from '@angular/material/tooltip';
import { FuseFullscreenComponent } from '@fuse/components/fullscreen/fullscreen.component';
@NgModule({
declarations: [
FuseFullscreenComponent
],
imports : [
MatButtonModule,
MatIconModule,
MatTooltipModule
],
exports : [
FuseFullscreenComponent
]
})
export class FuseFullscreenModule
{
}

View File

@ -0,0 +1,16 @@
export interface FSDocument extends HTMLDocument
{
mozFullScreenElement?: Element;
mozCancelFullScreen?: () => void;
msFullscreenElement?: Element;
msExitFullscreen?: () => void;
webkitFullscreenElement?: Element;
webkitExitFullscreen?: () => void;
}
export interface FSDocumentElement extends HTMLElement
{
mozRequestFullScreen?: () => void;
msRequestFullscreen?: () => void;
webkitRequestFullscreen?: () => void;
}

View File

@ -0,0 +1 @@
export * from '@fuse/components/fullscreen/public-api';

View File

@ -0,0 +1,3 @@
export * from '@fuse/components/fullscreen/fullscreen.component';
export * from '@fuse/components/fullscreen/fullscreen.module';
export * from '@fuse/components/fullscreen/fullscreen.types';

View File

@ -72,13 +72,17 @@ function generateThemesObject(themes)
return _.map(_.cloneDeep(themes), (value, key) =>
{
const theme = normalizeTheme(value);
const primary = (theme && theme.primary && theme.primary.DEFAULT) ? theme.primary.DEFAULT : normalizedDefaultTheme.primary.DEFAULT;
const accent = (theme && theme.accent && theme.accent.DEFAULT) ? theme.accent.DEFAULT : normalizedDefaultTheme.accent.DEFAULT;
const warn = (theme && theme.warn && theme.warn.DEFAULT) ? theme.warn.DEFAULT : normalizedDefaultTheme.warn.DEFAULT;
return _.fromPairs([
[
key,
{
primary: theme?.primary?.DEFAULT ?? normalizedDefaultTheme.primary.DEFAULT,
accent : theme?.accent?.DEFAULT ?? normalizedDefaultTheme.accent.DEFAULT,
warn : theme?.warn?.DEFAULT ?? normalizedDefaultTheme.warn.DEFAULT
primary,
accent,
warn
}
]
]);

View File

@ -1,4 +1,4 @@
import { Version } from '@fuse/version/version';
const __FUSE_VERSION__ = '12.1.0';
const __FUSE_VERSION__ = '12.2.0';
export const FUSE_VERSION = new Version(__FUSE_VERSION__).full;

View File

@ -63,6 +63,7 @@
</ng-container>
<!-- Components -->
<div class="flex items-center pl-2 ml-auto space-x-2">
<fuse-fullscreen></fuse-fullscreen>
<search [appearance]="'bar'"></search>
<shortcuts [shortcuts]="data.shortcuts"></shortcuts>
<messages [messages]="data.messages"></messages>

View File

@ -5,6 +5,7 @@ import { MatButtonModule } from '@angular/material/button';
import { MatDividerModule } from '@angular/material/divider';
import { MatIconModule } from '@angular/material/icon';
import { MatMenuModule } from '@angular/material/menu';
import { FuseFullscreenModule } from '@fuse/components/fullscreen';
import { FuseNavigationModule } from '@fuse/components/navigation';
import { MessagesModule } from 'app/layout/common/messages/messages.module';
import { NotificationsModule } from 'app/layout/common/notifications/notifications.module';
@ -25,6 +26,7 @@ import { CenteredLayoutComponent } from 'app/layout/layouts/horizontal/centered/
MatDividerModule,
MatIconModule,
MatMenuModule,
FuseFullscreenModule,
FuseNavigationModule,
MessagesModule,
NotificationsModule,

View File

@ -46,6 +46,7 @@
</ng-container>
<!-- Components -->
<div class="flex items-center pl-2 ml-auto space-x-2">
<fuse-fullscreen></fuse-fullscreen>
<search [appearance]="'bar'"></search>
<shortcuts [shortcuts]="data.shortcuts"></shortcuts>
<messages [messages]="data.messages"></messages>

View File

@ -5,6 +5,7 @@ import { MatButtonModule } from '@angular/material/button';
import { MatDividerModule } from '@angular/material/divider';
import { MatIconModule } from '@angular/material/icon';
import { MatMenuModule } from '@angular/material/menu';
import { FuseFullscreenModule } from '@fuse/components/fullscreen';
import { FuseNavigationModule } from '@fuse/components/navigation';
import { MessagesModule } from 'app/layout/common/messages/messages.module';
import { NotificationsModule } from 'app/layout/common/notifications/notifications.module';
@ -25,6 +26,7 @@ import { EnterpriseLayoutComponent } from 'app/layout/layouts/horizontal/enterpr
MatDividerModule,
MatIconModule,
MatMenuModule,
FuseFullscreenModule,
FuseNavigationModule,
MessagesModule,
NotificationsModule,

View File

@ -52,6 +52,7 @@
</ng-container>
<!-- Components -->
<div class="flex items-center pl-2 ml-auto space-x-2">
<fuse-fullscreen></fuse-fullscreen>
<search [appearance]="'bar'"></search>
<shortcuts [shortcuts]="data.shortcuts"></shortcuts>
<messages [messages]="data.messages"></messages>

View File

@ -5,6 +5,7 @@ import { MatButtonModule } from '@angular/material/button';
import { MatDividerModule } from '@angular/material/divider';
import { MatIconModule } from '@angular/material/icon';
import { MatMenuModule } from '@angular/material/menu';
import { FuseFullscreenModule } from '@fuse/components/fullscreen';
import { FuseNavigationModule } from '@fuse/components/navigation';
import { MessagesModule } from 'app/layout/common/messages/messages.module';
import { NotificationsModule } from 'app/layout/common/notifications/notifications.module';
@ -25,6 +26,7 @@ import { MaterialLayoutComponent } from 'app/layout/layouts/horizontal/material/
MatDividerModule,
MatIconModule,
MatMenuModule,
FuseFullscreenModule,
FuseNavigationModule,
MessagesModule,
NotificationsModule,

View File

@ -55,6 +55,7 @@
</ng-container>
<!-- Components -->
<div class="flex items-center pl-2 ml-auto space-x-2">
<fuse-fullscreen></fuse-fullscreen>
<search [appearance]="'bar'"></search>
<shortcuts [shortcuts]="data.shortcuts"></shortcuts>
<messages [messages]="data.messages"></messages>

View File

@ -5,6 +5,7 @@ import { MatButtonModule } from '@angular/material/button';
import { MatDividerModule } from '@angular/material/divider';
import { MatIconModule } from '@angular/material/icon';
import { MatMenuModule } from '@angular/material/menu';
import { FuseFullscreenModule } from '@fuse/components/fullscreen';
import { FuseNavigationModule } from '@fuse/components/navigation';
import { MessagesModule } from 'app/layout/common/messages/messages.module';
import { NotificationsModule } from 'app/layout/common/notifications/notifications.module';
@ -25,6 +26,7 @@ import { ModernLayoutComponent } from 'app/layout/layouts/horizontal/modern/mode
MatDividerModule,
MatIconModule,
MatMenuModule,
FuseFullscreenModule,
FuseNavigationModule,
MessagesModule,
NotificationsModule,

View File

@ -36,6 +36,7 @@
</button>
<!-- Components -->
<div class="flex items-center pl-2 ml-auto space-x-2">
<fuse-fullscreen></fuse-fullscreen>
<search [appearance]="'bar'"></search>
<shortcuts [shortcuts]="data.shortcuts"></shortcuts>
<messages [messages]="data.messages"></messages>

View File

@ -5,6 +5,7 @@ import { MatButtonModule } from '@angular/material/button';
import { MatDividerModule } from '@angular/material/divider';
import { MatIconModule } from '@angular/material/icon';
import { MatMenuModule } from '@angular/material/menu';
import { FuseFullscreenModule } from '@fuse/components/fullscreen';
import { FuseNavigationModule } from '@fuse/components/navigation';
import { MessagesModule } from 'app/layout/common/messages/messages.module';
import { NotificationsModule } from 'app/layout/common/notifications/notifications.module';
@ -25,6 +26,7 @@ import { ClassicLayoutComponent } from 'app/layout/layouts/vertical/classic/clas
MatDividerModule,
MatIconModule,
MatMenuModule,
FuseFullscreenModule,
FuseNavigationModule,
MessagesModule,
NotificationsModule,

View File

@ -66,6 +66,7 @@
</button>
<!-- Components -->
<div class="flex items-center pl-2 ml-auto space-x-2">
<fuse-fullscreen></fuse-fullscreen>
<search [appearance]="'bar'"></search>
<shortcuts [shortcuts]="data.shortcuts"></shortcuts>
<messages [messages]="data.messages"></messages>

View File

@ -6,6 +6,7 @@ import { MatDividerModule } from '@angular/material/divider';
import { MatIconModule } from '@angular/material/icon';
import { MatMenuModule } from '@angular/material/menu';
import { FuseNavigationModule } from '@fuse/components/navigation';
import { FuseFullscreenModule } from '@fuse/components/fullscreen/fullscreen.module';
import { MessagesModule } from 'app/layout/common/messages/messages.module';
import { NotificationsModule } from 'app/layout/common/notifications/notifications.module';
import { SearchModule } from 'app/layout/common/search/search.module';
@ -25,6 +26,7 @@ import { ClassyLayoutComponent } from 'app/layout/layouts/vertical/classy/classy
MatDividerModule,
MatIconModule,
MatMenuModule,
FuseFullscreenModule,
FuseNavigationModule,
MessagesModule,
NotificationsModule,

View File

@ -31,6 +31,7 @@
</button>
<!-- Components -->
<div class="flex items-center pl-2 ml-auto space-x-2">
<fuse-fullscreen></fuse-fullscreen>
<search [appearance]="'bar'"></search>
<shortcuts [shortcuts]="data.shortcuts"></shortcuts>
<messages [messages]="data.messages"></messages>

View File

@ -5,6 +5,7 @@ import { MatButtonModule } from '@angular/material/button';
import { MatDividerModule } from '@angular/material/divider';
import { MatIconModule } from '@angular/material/icon';
import { MatMenuModule } from '@angular/material/menu';
import { FuseFullscreenModule } from '@fuse/components/fullscreen';
import { FuseNavigationModule } from '@fuse/components/navigation';
import { MessagesModule } from 'app/layout/common/messages/messages.module';
import { NotificationsModule } from 'app/layout/common/notifications/notifications.module';
@ -25,6 +26,7 @@ import { CompactLayoutComponent } from 'app/layout/layouts/vertical/compact/comp
MatDividerModule,
MatIconModule,
MatMenuModule,
FuseFullscreenModule,
FuseNavigationModule,
MessagesModule,
NotificationsModule,

View File

@ -40,6 +40,7 @@
</div>
<!-- Components -->
<div class="flex items-center pl-2 ml-auto space-x-2">
<fuse-fullscreen></fuse-fullscreen>
<search [appearance]="'bar'"></search>
<shortcuts [shortcuts]="data.shortcuts"></shortcuts>
<messages [messages]="data.messages"></messages>

View File

@ -5,6 +5,7 @@ import { MatButtonModule } from '@angular/material/button';
import { MatDividerModule } from '@angular/material/divider';
import { MatIconModule } from '@angular/material/icon';
import { MatMenuModule } from '@angular/material/menu';
import { FuseFullscreenModule } from '@fuse/components/fullscreen';
import { FuseNavigationModule } from '@fuse/components/navigation';
import { MessagesModule } from 'app/layout/common/messages/messages.module';
import { NotificationsModule } from 'app/layout/common/notifications/notifications.module';
@ -25,6 +26,7 @@ import { DenseLayoutComponent } from 'app/layout/layouts/vertical/dense/dense.co
MatDividerModule,
MatIconModule,
MatMenuModule,
FuseFullscreenModule,
FuseNavigationModule,
MessagesModule,
NotificationsModule,

View File

@ -45,6 +45,7 @@
</button>
<!-- Components -->
<div class="flex items-center pl-2 ml-auto space-x-2">
<fuse-fullscreen></fuse-fullscreen>
<search [appearance]="'bar'"></search>
<shortcuts [shortcuts]="data.shortcuts"></shortcuts>
<messages [messages]="data.messages"></messages>

View File

@ -5,6 +5,7 @@ import { MatButtonModule } from '@angular/material/button';
import { MatDividerModule } from '@angular/material/divider';
import { MatIconModule } from '@angular/material/icon';
import { MatMenuModule } from '@angular/material/menu';
import { FuseFullscreenModule } from '@fuse/components/fullscreen';
import { FuseNavigationModule } from '@fuse/components/navigation';
import { MessagesModule } from 'app/layout/common/messages/messages.module';
import { NotificationsModule } from 'app/layout/common/notifications/notifications.module';
@ -25,6 +26,7 @@ import { FuturisticLayoutComponent } from 'app/layout/layouts/vertical/futuristi
MatDividerModule,
MatIconModule,
MatMenuModule,
FuseFullscreenModule,
FuseNavigationModule,
MessagesModule,
NotificationsModule,

View File

@ -32,6 +32,7 @@
</button>
<!-- Components -->
<div class="flex items-center pl-2 ml-auto space-x-2">
<fuse-fullscreen></fuse-fullscreen>
<search [appearance]="'bar'"></search>
<shortcuts [shortcuts]="data.shortcuts"></shortcuts>
<messages [messages]="data.messages"></messages>

View File

@ -5,6 +5,7 @@ import { MatButtonModule } from '@angular/material/button';
import { MatDividerModule } from '@angular/material/divider';
import { MatIconModule } from '@angular/material/icon';
import { MatMenuModule } from '@angular/material/menu';
import { FuseFullscreenModule } from '@fuse/components/fullscreen';
import { FuseNavigationModule } from '@fuse/components/navigation';
import { MessagesModule } from 'app/layout/common/messages/messages.module';
import { NotificationsModule } from 'app/layout/common/notifications/notifications.module';
@ -25,6 +26,7 @@ import { ThinLayoutComponent } from 'app/layout/layouts/vertical/thin/thin.compo
MatDividerModule,
MatIconModule,
MatMenuModule,
FuseFullscreenModule,
FuseNavigationModule,
MessagesModule,
NotificationsModule,

View File

@ -75,7 +75,10 @@ export class AcademyMockApi
// Find the course and attach steps to it
const course = courses.find((item) => item.id === id);
course.steps = steps;
if ( course )
{
course.steps = steps;
}
return [
200,

View File

@ -0,0 +1,167 @@
import { Injectable } from '@angular/core';
import { assign, cloneDeep, omit } from 'lodash-es';
import { FuseMockApiService } from '@fuse/lib/mock-api';
import { chats as chatsData, contacts as contactsData, messages as messagesData, profile as profileData } from 'app/mock-api/apps/chat/data';
@Injectable({
providedIn: 'root'
})
export class ChatMockApi
{
private _chats: any[] = chatsData;
private _contacts: any[] = contactsData;
private _messages: any[] = messagesData;
private _profile: any = profileData;
/**
* Constructor
*/
constructor(private _fuseMockApiService: FuseMockApiService)
{
// Register Mock API handlers
this.registerHandlers();
// Modify the chats array to attach certain data to it
this._chats = this._chats.map((chat) => ({
...chat,
// Get the actual contact object from the id and attach it to the chat
contact: this._contacts.find((contact) => contact.id === chat.contactId),
// Since we use same set of messages on all chats, we assign them here.
messages: this._messages.map((message) => ({
...message,
chatId : chat.id,
contactId: message.contactId === 'me' ? this._profile.id : chat.contactId,
isMine : message.contactId === 'me'
}))
}));
}
// -----------------------------------------------------------------------------------------------------
// @ Public methods
// -----------------------------------------------------------------------------------------------------
/**
* Register Mock API handlers
*/
registerHandlers(): void
{
// -----------------------------------------------------------------------------------------------------
// @ Chats - GET
// -----------------------------------------------------------------------------------------------------
this._fuseMockApiService
.onGet('api/apps/chat/chats')
.reply(() => {
// Clone the chats
const chats = cloneDeep(this._chats);
// Return the response
return [200, chats];
});
// -----------------------------------------------------------------------------------------------------
// @ Chat - GET
// -----------------------------------------------------------------------------------------------------
this._fuseMockApiService
.onGet('api/apps/chat/chat')
.reply(({request}) => {
// Get the chat id
const id = request.params.get('id');
// Clone the chats
const chats = cloneDeep(this._chats);
// Find the chat we need
const chat = chats.find((item) => item.id === id);
// Return the response
return [200, chat];
});
// -----------------------------------------------------------------------------------------------------
// @ Chat - PATCH
// -----------------------------------------------------------------------------------------------------
this._fuseMockApiService
.onPatch('api/apps/chat/chat')
.reply(({request}) => {
// Get the id and chat
const id = request.body.id;
const chat = cloneDeep(request.body.chat);
// Prepare the updated chat
let updatedChat = null;
// Find the chat and update it
this._chats.forEach((item, index, chats) => {
if ( item.id === id )
{
// Update the chat
chats[index] = assign({}, chats[index], chat);
// Store the updated chat
updatedChat = chats[index];
}
});
// Return the response
return [200, updatedChat];
});
// -----------------------------------------------------------------------------------------------------
// @ Contacts - GET
// -----------------------------------------------------------------------------------------------------
this._fuseMockApiService
.onGet('api/apps/chat/contacts')
.reply(() => {
// Clone the contacts
let contacts = cloneDeep(this._contacts);
// Sort the contacts by the name field by default
contacts.sort((a, b) => a.name.localeCompare(b.name));
// Omit details and attachments from contacts
contacts = contacts.map((contact) => omit(contact, ['details', 'attachments']));
// Return the response
return [200, contacts];
});
// -----------------------------------------------------------------------------------------------------
// @ Contact Details - GET
// -----------------------------------------------------------------------------------------------------
this._fuseMockApiService
.onGet('api/apps/chat/contact')
.reply(({request}) => {
// Get the contact id
const id = request.params.get('id');
// Clone the contacts
const contacts = cloneDeep(this._contacts);
// Find the contact
const contact = contacts.find((item) => item.id === id);
// Return the response
return [200, contact];
});
// -----------------------------------------------------------------------------------------------------
// @ Profile - GET
// -----------------------------------------------------------------------------------------------------
this._fuseMockApiService
.onGet('api/apps/chat/profile')
.reply(() => {
// Clone the profile
const profile = cloneDeep(this._profile);
// Return the response
return [200, profile];
});
}
}

File diff suppressed because it is too large Load Diff

View File

@ -2,6 +2,7 @@ import { AcademyMockApi } from 'app/mock-api/apps/academy/api';
import { AnalyticsMockApi } from 'app/mock-api/dashboards/analytics/api';
import { AuthMockApi } from 'app/mock-api/common/auth/api';
import { CalendarMockApi } from 'app/mock-api/apps/calendar/api';
import { ChatMockApi } from 'app/mock-api/apps/chat/api';
import { ContactsMockApi } from 'app/mock-api/apps/contacts/api';
import { ECommerceInventoryMockApi } from 'app/mock-api/apps/ecommerce/inventory/api';
import { FileManagerMockApi } from 'app/mock-api/apps/file-manager/api';
@ -22,6 +23,7 @@ export const mockApiServices = [
AnalyticsMockApi,
AuthMockApi,
CalendarMockApi,
ChatMockApi,
ContactsMockApi,
ECommerceInventoryMockApi,
FileManagerMockApi,

View File

@ -0,0 +1,8 @@
<div class="absolute inset-0 flex flex-col min-w-0 overflow-hidden">
<!-- Main -->
<div class="flex flex-auto overflow-hidden">
<router-outlet></router-outlet>
</div>
</div>

View File

@ -0,0 +1,17 @@
import { ChangeDetectionStrategy, Component, ViewEncapsulation } from '@angular/core';
@Component({
selector : 'chat',
templateUrl : './chat.component.html',
encapsulation : ViewEncapsulation.None,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ChatComponent
{
/**
* Constructor
*/
constructor()
{
}
}

View File

@ -0,0 +1,44 @@
import { NgModule } from '@angular/core';
import { RouterModule } from '@angular/router';
import { MatButtonModule } from '@angular/material/button';
import { MatCheckboxModule } from '@angular/material/checkbox';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatIconModule } from '@angular/material/icon';
import { MatInputModule } from '@angular/material/input';
import { MatMenuModule } from '@angular/material/menu';
import { MatSidenavModule } from '@angular/material/sidenav';
import { FuseAutogrowModule } from '@fuse/directives/autogrow';
import { SharedModule } from 'app/shared/shared.module';
import { chatRoutes } from 'app/modules/admin/apps/chat/chat.routing';
import { ChatComponent } from 'app/modules/admin/apps/chat/chat.component';
import { ChatsComponent } from 'app/modules/admin/apps/chat/chats/chats.component';
import { ContactInfoComponent } from 'app/modules/admin/apps/chat/contact-info/contact-info.component';
import { ConversationComponent } from 'app/modules/admin/apps/chat/conversation/conversation.component';
import { NewChatComponent } from 'app/modules/admin/apps/chat/new-chat/new-chat.component';
import { ProfileComponent } from 'app/modules/admin/apps/chat/profile/profile.component';
@NgModule({
declarations: [
ChatComponent,
ChatsComponent,
ContactInfoComponent,
ConversationComponent,
NewChatComponent,
ProfileComponent
],
imports: [
RouterModule.forChild(chatRoutes),
MatButtonModule,
MatCheckboxModule,
MatFormFieldModule,
MatIconModule,
MatInputModule,
MatMenuModule,
MatSidenavModule,
FuseAutogrowModule,
SharedModule,
]
})
export class ChatModule
{
}

View File

@ -0,0 +1,147 @@
import { Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, Resolve, Router, RouterStateSnapshot } from '@angular/router';
import { Observable, throwError } from 'rxjs';
import { catchError } from 'rxjs/operators';
import { ChatService } from 'app/modules/admin/apps/chat/chat.service';
import { Chat, Contact, Profile } from 'app/modules/admin/apps/chat/chat.types';
@Injectable({
providedIn: 'root'
})
export class ChatChatsResolver implements Resolve<any>
{
/**
* Constructor
*/
constructor(
private _chatService: ChatService,
private _router: Router
)
{
}
// -----------------------------------------------------------------------------------------------------
// @ Public methods
// -----------------------------------------------------------------------------------------------------
/**
* Resolver
*
* @param route
* @param state
*/
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<Chat[]> | any
{
return this._chatService.getChats();
}
}
@Injectable({
providedIn: 'root'
})
export class ChatChatResolver implements Resolve<any>
{
/**
* Constructor
*/
constructor(
private _chatService: ChatService,
private _router: Router
)
{
}
// -----------------------------------------------------------------------------------------------------
// @ Public methods
// -----------------------------------------------------------------------------------------------------
/**
* Resolver
*
* @param route
* @param state
*/
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<Chat>
{
return this._chatService.getChatById(route.paramMap.get('id'))
.pipe(
// Error here means the requested chat is not available
catchError((error) => {
// Log the error
console.error(error);
// Get the parent url
const parentUrl = state.url.split('/').slice(0, -1).join('/');
// Navigate to there
this._router.navigateByUrl(parentUrl);
// Throw an error
return throwError(error);
})
);
}
}
@Injectable({
providedIn: 'root'
})
export class ChatContactsResolver implements Resolve<any>
{
/**
* Constructor
*/
constructor(
private _chatService: ChatService,
private _router: Router
)
{
}
// -----------------------------------------------------------------------------------------------------
// @ Public methods
// -----------------------------------------------------------------------------------------------------
/**
* Resolver
*
* @param route
* @param state
*/
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<Contact[]> | any
{
return this._chatService.getContacts();
}
}
@Injectable({
providedIn: 'root'
})
export class ChatProfileResolver implements Resolve<any>
{
/**
* Constructor
*/
constructor(
private _chatService: ChatService,
private _router: Router
)
{
}
// -----------------------------------------------------------------------------------------------------
// @ Public methods
// -----------------------------------------------------------------------------------------------------
/**
* Resolver
*
* @param route
* @param state
*/
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<Profile> | any
{
return this._chatService.getProfile();
}
}

View File

@ -0,0 +1,37 @@
import { Route } from '@angular/router';
import { ChatChatResolver, ChatChatsResolver, ChatContactsResolver, ChatProfileResolver } from 'app/modules/admin/apps/chat/chat.resolvers';
import { ChatComponent } from 'app/modules/admin/apps/chat/chat.component';
import { ChatsComponent } from 'app/modules/admin/apps/chat/chats/chats.component';
import { ConversationComponent } from 'app/modules/admin/apps/chat/conversation/conversation.component';
export const chatRoutes: Route[] = [
{
path : '',
component: ChatComponent,
resolve : {
chats : ChatChatsResolver,
contacts: ChatContactsResolver,
profile : ChatProfileResolver
},
children : [
{
path : '',
component: ChatsComponent,
children : [
{
path : '',
component: ConversationComponent,
children : [
{
path : ':id',
resolve: {
conversation: ChatChatResolver
}
}
]
}
]
}
]
}
];

View File

@ -0,0 +1,202 @@
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { BehaviorSubject, Observable, of, throwError } from 'rxjs';
import { filter, map, switchMap, take, tap } from 'rxjs/operators';
import { Chat, Contact, Profile } from 'app/modules/admin/apps/chat/chat.types';
@Injectable({
providedIn: 'root'
})
export class ChatService
{
private _chat: BehaviorSubject<Chat> = new BehaviorSubject(null);
private _chats: BehaviorSubject<Chat[]> = new BehaviorSubject(null);
private _contact: BehaviorSubject<Contact> = new BehaviorSubject(null);
private _contacts: BehaviorSubject<Contact[]> = new BehaviorSubject(null);
private _profile: BehaviorSubject<Profile> = new BehaviorSubject(null);
/**
* Constructor
*/
constructor(private _httpClient: HttpClient)
{
}
// -----------------------------------------------------------------------------------------------------
// @ Accessors
// -----------------------------------------------------------------------------------------------------
/**
* Getter for chat
*/
get chat$(): Observable<Chat>
{
return this._chat.asObservable();
}
/**
* Getter for chats
*/
get chats$(): Observable<Chat[]>
{
return this._chats.asObservable();
}
/**
* Getter for contact
*/
get contact$(): Observable<Contact>
{
return this._contact.asObservable();
}
/**
* Getter for contacts
*/
get contacts$(): Observable<Contact[]>
{
return this._contacts.asObservable();
}
/**
* Getter for profile
*/
get profile$(): Observable<Profile>
{
return this._profile.asObservable();
}
// -----------------------------------------------------------------------------------------------------
// @ Public methods
// -----------------------------------------------------------------------------------------------------
/**
* Get chats
*/
getChats(): Observable<any>
{
return this._httpClient.get<Chat[]>('api/apps/chat/chats').pipe(
tap((response: Chat[]) => {
this._chats.next(response);
})
);
}
/**
* Get contact
*
* @param id
*/
getContact(id: string): Observable<any>
{
return this._httpClient.get<Contact>('api/apps/chat/contacts', {params: {id}}).pipe(
tap((response: Contact) => {
this._contact.next(response);
})
);
}
/**
* Get contacts
*/
getContacts(): Observable<any>
{
return this._httpClient.get<Contact[]>('api/apps/chat/contacts').pipe(
tap((response: Contact[]) => {
this._contacts.next(response);
})
);
}
/**
* Get profile
*/
getProfile(): Observable<any>
{
return this._httpClient.get<Profile>('api/apps/chat/profile').pipe(
tap((response: Profile) => {
this._profile.next(response);
})
);
}
/**
* Get chat
*
* @param id
*/
getChatById(id: string): Observable<any>
{
return this._httpClient.get<Chat>('api/apps/chat/chat', {params: {id}}).pipe(
map((chat) => {
// Update the chat
this._chat.next(chat);
// Return the chat
return chat;
}),
switchMap((chat) => {
if ( !chat )
{
return throwError('Could not found chat with id of ' + id + '!');
}
return of(chat);
})
);
}
/**
* Update chat
*
* @param id
* @param chat
*/
updateChat(id: string, chat: Chat): Observable<Chat>
{
return this.chats$.pipe(
take(1),
switchMap(chats => this._httpClient.patch<Chat>('api/apps/chat/chat', {
id,
chat
}).pipe(
map((updatedChat) => {
// Find the index of the updated chat
const index = chats.findIndex(item => item.id === id);
// Update the chat
chats[index] = updatedChat;
// Update the chats
this._chats.next(chats);
// Return the updated contact
return updatedChat;
}),
switchMap(updatedChat => this.chat$.pipe(
take(1),
filter(item => item && item.id === id),
tap(() => {
// Update the chat if it's selected
this._chat.next(updatedChat);
// Return the updated chat
return updatedChat;
})
))
))
);
}
/**
* Reset the selected chat
*/
resetChat(): void
{
this._chat.next(null);
}
}

View File

@ -0,0 +1,55 @@
export interface Profile
{
id?: string;
name?: string;
email?: string;
avatar?: string;
about?: string;
}
export interface Contact
{
id?: string;
avatar?: string;
name?: string;
about?: string;
details?: {
emails?: {
email?: string;
label?: string;
}[];
phoneNumbers?: {
country?: string;
number?: string;
label?: string;
}[];
title?: string;
company?: string;
birthday?: string;
address?: string;
};
attachments?: {
media?: any[]
docs?: any[]
links?: any[]
};
}
export interface Chat
{
id?: string;
contactId?: string;
contact?: Contact;
unreadCount?: number;
muted?: boolean;
lastMessage?: string;
lastMessageAt?: string;
messages?: {
id?: string;
chatId?: string;
contactId?: string;
isMine?: boolean;
value?: string;
createdAt?: string;
}[];
}

View File

@ -0,0 +1,190 @@
<div class="relative flex flex-auto w-full bg-card dark:bg-transparent">
<mat-drawer-container
class="flex-auto h-full"
[hasBackdrop]="false">
<!-- Drawer -->
<mat-drawer
class="w-100 border-r shadow-none dark:bg-gray-900"
[autoFocus]="false"
[(opened)]="drawerOpened"
#drawer>
<!-- New chat -->
<ng-container *ngIf="drawerComponent === 'new-chat'">
<chat-new-chat [drawer]="drawer"></chat-new-chat>
</ng-container>
<!-- Profile -->
<ng-container *ngIf="drawerComponent === 'profile'">
<chat-profile [drawer]="drawer"></chat-profile>
</ng-container>
</mat-drawer>
<!-- Drawer content -->
<mat-drawer-content class="flex overflow-hidden">
<!-- Chats list -->
<ng-container *ngIf="chats && chats.length > 0; else noChats">
<div class="relative flex flex-auto flex-col w-full min-w-0 lg:min-w-100 lg:max-w-100 border-r bg-card dark:bg-transparent">
<!-- Header -->
<div class="flex flex-col flex-0 py-4 px-8 border-b bg-gray-50 dark:bg-transparent">
<div class="flex items-center">
<div
class="flex items-center mr-1 cursor-pointer"
(click)="openProfile()">
<div class="w-10 h-10">
<ng-container *ngIf="profile.avatar">
<img
class="object-cover w-full h-full rounded-full object-cover"
[src]="profile.avatar"
alt="Profile avatar"/>
</ng-container>
<ng-container *ngIf="!profile.avatar">
<div class="flex items-center justify-center w-full h-full rounded-full text-lg uppercase bg-gray-200 text-gray-600 dark:bg-gray-700 dark:text-gray-200">
{{profile.name.charAt(0)}}
</div>
</ng-container>
</div>
<div class="ml-4 font-medium truncate">{{profile.name}}</div>
</div>
<button
class="ml-auto"
mat-icon-button
(click)="openNewChat()">
<mat-icon [svgIcon]="'heroicons_outline:plus-circle'"></mat-icon>
</button>
<button
class="ml-1 -mr-4"
mat-icon-button
[matMenuTriggerFor]="chatsHeaderMenu">
<mat-icon [svgIcon]="'heroicons_outline:dots-vertical'"></mat-icon>
<mat-menu #chatsHeaderMenu>
<button mat-menu-item>
<mat-icon [svgIcon]="'heroicons_outline:user-group'"></mat-icon>
New group
</button>
<button mat-menu-item>
<mat-icon [svgIcon]="'heroicons_outline:chat-alt-2'"></mat-icon>
Create a room
</button>
<button
mat-menu-item
(click)="openProfile()">
<mat-icon [svgIcon]="'heroicons_outline:user-circle'"></mat-icon>
Profile
</button>
<button mat-menu-item>
<mat-icon [svgIcon]="'heroicons_outline:archive'"></mat-icon>
Archived
</button>
<button mat-menu-item>
<mat-icon [svgIcon]="'heroicons_outline:star'"></mat-icon>
Starred
</button>
<button mat-menu-item>
<mat-icon [svgIcon]="'heroicons_outline:cog'"></mat-icon>
Settings
</button>
</mat-menu>
</button>
</div>
<!-- Search -->
<div class="mt-4">
<mat-form-field
class="fuse-mat-no-subscript fuse-mat-rounded fuse-mat-dense w-full"
[floatLabel]="'always'">
<mat-icon
matPrefix
class="icon-size-5"
[svgIcon]="'heroicons_solid:search'"></mat-icon>
<input
matInput
[autocomplete]="'off'"
[placeholder]="'Search or start new chat'"
(input)="filterChats(searchField.value)"
#searchField>
</mat-form-field>
</div>
</div>
<!-- Chats -->
<div class="flex-auto overflow-y-auto">
<ng-container *ngIf="filteredChats.length > 0; else noChats">
<ng-container *ngFor="let chat of filteredChats; trackBy: trackByFn">
<div
class="z-20 flex items-center py-5 px-8 cursor-pointer border-b hover:bg-hover"
[ngClass]="{'bg-primary-50 dark:bg-hover': selectedChat && selectedChat.id === chat.id}"
[routerLink]="[chat.id]">
<div class="relative flex flex-0 items-center justify-center w-10 h-10">
<ng-container *ngIf="chat.unreadCount > 0">
<div
class="absolute bottom-0 right-0 flex-0 w-2 h-2 -ml-0.5 rounded-full ring-2 ring-bg-card dark:ring-gray-900 bg-primary dark:bg-primary-500 text-on-primary"
[class.ring-primary-50]="selectedChat && selectedChat.id === chat.id"></div>
</ng-container>
<ng-container *ngIf="chat.contact.avatar">
<img
class="w-full h-full rounded-full object-cover"
[src]="chat.contact.avatar"
alt="Contact avatar"/>
</ng-container>
<ng-container *ngIf="!chat.contact.avatar">
<div class="flex items-center justify-center w-full h-full rounded-full text-lg uppercase bg-gray-200 text-gray-600 dark:bg-gray-700 dark:text-gray-200">
{{chat.contact.name.charAt(0)}}
</div>
</ng-container>
</div>
<div class="min-w-0 ml-4">
<div class="font-medium leading-5 truncate">{{chat.contact.name}}</div>
<div
class="leading-5 truncate text-secondary"
[class.text-primary]="chat.unreadCount > 0"
[class.dark:text-primary-500]="chat.unreadCount > 0">
{{chat.lastMessage}}
</div>
</div>
<div class="flex flex-col items-end self-start ml-auto pl-2">
<div class="text-sm leading-5 text-secondary">{{chat.lastMessageAt}}</div>
<ng-container *ngIf="chat.muted">
<mat-icon
class="icon-size-5 text-hint"
[svgIcon]="'heroicons_solid:volume-off'"></mat-icon>
</ng-container>
</div>
</div>
</ng-container>
</ng-container>
</div>
</div>
</ng-container>
<!-- No chats template -->
<ng-template #noChats>
<div class="flex flex-auto flex-col items-center justify-center h-full">
<mat-icon
class="icon-size-24"
[svgIcon]="'iconsmind:speach_bubble'"></mat-icon>
<div class="mt-4 text-2xl font-semibold tracking-tight text-secondary">No chats</div>
</div>
</ng-template>
<!-- Conversation -->
<ng-container *ngIf="chats && chats.length > 0">
<div
class="flex-auto"
[ngClass]="{'z-20 absolute inset-0 lg:static lg:inset-auto flex': selectedChat && selectedChat.id,
'hidden lg:flex': !selectedChat || !selectedChat.id}">
<router-outlet></router-outlet>
</div>
</ng-container>
</mat-drawer-content>
</mat-drawer-container>
</div>

View File

@ -0,0 +1,138 @@
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnDestroy, OnInit, ViewEncapsulation } from '@angular/core';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
import { Chat, Profile } from 'app/modules/admin/apps/chat/chat.types';
import { ChatService } from 'app/modules/admin/apps/chat/chat.service';
@Component({
selector : 'chat-chats',
templateUrl : './chats.component.html',
encapsulation : ViewEncapsulation.None,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ChatsComponent implements OnInit, OnDestroy
{
chats: Chat[];
drawerComponent: 'profile' | 'new-chat';
drawerOpened: boolean = false;
filteredChats: Chat[];
profile: Profile;
selectedChat: Chat;
private _unsubscribeAll: Subject<any> = new Subject<any>();
/**
* Constructor
*/
constructor(
private _chatService: ChatService,
private _changeDetectorRef: ChangeDetectorRef
)
{
}
// -----------------------------------------------------------------------------------------------------
// @ Lifecycle hooks
// -----------------------------------------------------------------------------------------------------
/**
* On init
*/
ngOnInit(): void
{
// Chats
this._chatService.chats$
.pipe(takeUntil(this._unsubscribeAll))
.subscribe((chats: Chat[]) => {
this.chats = this.filteredChats = chats;
// Mark for check
this._changeDetectorRef.markForCheck();
});
// Profile
this._chatService.profile$
.pipe(takeUntil(this._unsubscribeAll))
.subscribe((profile: Profile) => {
this.profile = profile;
// Mark for check
this._changeDetectorRef.markForCheck();
});
// Selected chat
this._chatService.chat$
.pipe(takeUntil(this._unsubscribeAll))
.subscribe((chat: Chat) => {
this.selectedChat = chat;
// Mark for check
this._changeDetectorRef.markForCheck();
});
}
/**
* On destroy
*/
ngOnDestroy(): void
{
// Unsubscribe from all subscriptions
this._unsubscribeAll.next();
this._unsubscribeAll.complete();
}
// -----------------------------------------------------------------------------------------------------
// @ Public methods
// -----------------------------------------------------------------------------------------------------
/**
* Filter the chats
*
* @param query
*/
filterChats(query: string): void
{
// Reset the filter
if ( !query )
{
this.filteredChats = this.chats;
return;
}
this.filteredChats = this.chats.filter((chat) => chat.contact.name.toLowerCase().includes(query.toLowerCase()));
}
/**
* Open the new chat sidebar
*/
openNewChat(): void
{
this.drawerComponent = 'new-chat';
this.drawerOpened = true;
// Mark for check
this._changeDetectorRef.markForCheck();
}
/**
* Open the profile sidebar
*/
openProfile(): void
{
this.drawerComponent = 'profile';
this.drawerOpened = true;
// Mark for check
this._changeDetectorRef.markForCheck();
}
/**
* Track by function for ngFor loops
*
* @param index
* @param item
*/
trackByFn(index: number, item: any): any
{
return item.id || index;
}
}

View File

@ -0,0 +1,86 @@
<div class="flex flex-col flex-auto h-full bg-card dark:bg-default">
<!-- Header -->
<div class="flex flex-0 items-center h-18 px-4 border-b bg-gray-50 dark:bg-transparent">
<button
mat-icon-button
(click)="drawer.close()">
<mat-icon [svgIcon]="'heroicons_outline:x'"></mat-icon>
</button>
<div class="ml-2 text-lg font-medium">Contact info</div>
</div>
<div class="overflow-y-auto">
<!-- Contact avatar & info -->
<div class="flex flex-col items-center mt-8">
<div class="w-40 h-40 rounded-full">
<ng-container *ngIf="chat.contact.avatar">
<img
class="w-full h-full rounded-full object-cover"
[src]="chat.contact.avatar"
[alt]="'Contact avatar'">
</ng-container>
<ng-container *ngIf="!chat.contact.avatar">
<div class="flex items-center justify-center w-full h-full rounded-full text-8xl font-semibold uppercase bg-gray-200 text-gray-600 dark:bg-gray-700 dark:text-gray-200">
{{chat.contact.name.charAt(0)}}
</div>
</ng-container>
</div>
<div class="mt-4 text-lg font-medium">{{chat.contact.name}}</div>
<div class="mt-0.5 text-md text-secondary">{{chat.contact.about}}</div>
</div>
<div class="py-10 px-7">
<!-- Media -->
<div class="text-lg font-medium">Media</div>
<div class="grid grid-cols-4 gap-1 mt-4">
<ng-container *ngFor="let media of chat.contact.attachments.media">
<img
class="h-20 rounded object-cover"
[src]="media"/>
</ng-container>
</div>
<!-- Details -->
<div class="mt-10 space-y-4">
<div class="text-lg font-medium mb-3">Details</div>
<ng-container *ngIf="chat.contact.details.emails.length">
<div>
<div class="font-medium text-secondary">Email</div>
<div class="">{{chat.contact.details.emails[0].email}}</div>
</div>
</ng-container>
<ng-container *ngIf="chat.contact.details.phoneNumbers.length">
<div>
<div class="font-medium text-secondary">Phone number</div>
<div class="">{{chat.contact.details.phoneNumbers[0].number}}</div>
</div>
</ng-container>
<ng-container *ngIf="chat.contact.details.title">
<div>
<div class="font-medium text-secondary">Title</div>
<div class="">{{chat.contact.details.title}}</div>
</div>
</ng-container>
<ng-container *ngIf="chat.contact.details.company">
<div>
<div class="font-medium text-secondary">Company</div>
<div class="">{{chat.contact.details.company}}</div>
</div>
</ng-container>
<ng-container *ngIf="chat.contact.details.birthday">
<div>
<div class="font-medium text-secondary">Birthday</div>
<div class="">{{chat.contact.details.birthday}}</div>
</div>
</ng-container>
<ng-container *ngIf="chat.contact.details.address">
<div>
<div class="font-medium text-secondary">Address</div>
<div class="">{{chat.contact.details.address}}</div>
</div>
</ng-container>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,22 @@
import { ChangeDetectionStrategy, Component, Input, ViewEncapsulation } from '@angular/core';
import { MatDrawer } from '@angular/material/sidenav';
import { Chat, Contact } from 'app/modules/admin/apps/chat/chat.types';
@Component({
selector : 'chat-contact-info',
templateUrl : './contact-info.component.html',
encapsulation : ViewEncapsulation.None,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ContactInfoComponent
{
@Input() chat: Chat;
@Input() drawer: MatDrawer;
/**
* Constructor
*/
constructor()
{
}
}

View File

@ -0,0 +1,215 @@
<div class="flex flex-col flex-auto overflow-y-auto lg:overflow-hidden bg-card dark:bg-default">
<ng-container *ngIf="chat; else selectChatOrStartNew">
<mat-drawer-container
class="flex-auto h-full"
[hasBackdrop]="false">
<!-- Drawer -->
<mat-drawer
class="w-100 border-r shadow-none dark:bg-gray-900"
[autoFocus]="false"
[mode]="'side'"
[position]="'end'"
[(opened)]="drawerOpened"
#drawer>
<!-- Contact info -->
<chat-contact-info
[drawer]="drawer"
[chat]="chat"></chat-contact-info>
</mat-drawer>
<!-- Drawer content -->
<mat-drawer-content class="flex flex-col overflow-hidden">
<!-- Header -->
<div class="flex flex-0 items-center h-18 px-4 md:px-6 border-b bg-gray-50 dark:bg-transparent">
<!-- Back button -->
<a
class="lg:hidden md:-ml-2"
mat-icon-button
[routerLink]="['./']"
(click)="resetChat()">
<mat-icon [svgIcon]="'heroicons_outline:arrow-narrow-left'"></mat-icon>
</a>
<!-- Contact info -->
<div
class="flex items-center ml-2 lg:ml-0 mr-2 cursor-pointer"
(click)="openContactInfo()">
<div class="relative flex flex-0 items-center justify-center w-10 h-10">
<ng-container *ngIf="chat.contact.avatar">
<img
class="w-full h-full rounded-full object-cover"
[src]="chat.contact.avatar"
alt="Contact avatar"/>
</ng-container>
<ng-container *ngIf="!chat.contact.avatar">
<div class="flex items-center justify-center w-full h-full rounded-full text-lg uppercase bg-gray-200 text-gray-600 dark:bg-gray-700 dark:text-gray-200">
{{chat.contact.name.charAt(0)}}
</div>
</ng-container>
</div>
<div class="ml-4 text-lg font-medium leading-5 truncate">{{chat.contact.name}}</div>
</div>
<button
class="ml-auto"
mat-icon-button
[matMenuTriggerFor]="conversationHeaderMenu">
<mat-icon [svgIcon]="'heroicons_outline:dots-vertical'"></mat-icon>
<mat-menu #conversationHeaderMenu>
<button
mat-menu-item
(click)="openContactInfo()">
<mat-icon [svgIcon]="'heroicons_outline:user-circle'"></mat-icon>
Contact info
</button>
<button mat-menu-item>
<mat-icon [svgIcon]="'heroicons_outline:check-circle'"></mat-icon>
Select messages
</button>
<button
mat-menu-item
(click)="toggleMuteNotifications()">
<ng-container *ngIf="!chat.muted">
<mat-icon [svgIcon]="'heroicons_outline:volume-off'"></mat-icon>
Mute notifications
</ng-container>
<ng-container *ngIf="chat.muted">
<mat-icon [svgIcon]="'heroicons_outline:volume-up'"></mat-icon>
Unmute notifications
</ng-container>
</button>
<button mat-menu-item>
<mat-icon [svgIcon]="'heroicons_outline:backspace'"></mat-icon>
Clear messages
</button>
<button mat-menu-item>
<mat-icon [svgIcon]="'heroicons_outline:trash'"></mat-icon>
Delete chat
</button>
</mat-menu>
</button>
</div>
<!-- Conversation -->
<div class="flex overflow-y-auto flex-col-reverse">
<div class="flex flex-col flex-auto flex-shrink p-6 bg-card dark:bg-transparent">
<ng-container *ngFor="let message of chat.messages; let i = index; let first = first; let last = last; trackBy: trackByFn">
<!-- Start of the day -->
<ng-container *ngIf="first || (chat.messages[i - 1].createdAt | date:'d') !== (message.createdAt | date:'d')">
<div class="flex items-center justify-center my-3 -mx-6">
<div class="flex-auto border-b"></div>
<div class="flex-0 mx-4 text-sm font-medium leading-5 text-secondary">
{{message.createdAt | date: 'longDate'}}
</div>
<div class="flex-auto border-b"></div>
</div>
</ng-container>
<div
class="flex flex-col"
[ngClass]="{'items-end': message.isMine,
'items-start': !message.isMine,
'mt-0.5': i > 0 && chat.messages[i - 1].isMine === message.isMine,
'mt-3': i > 0 && chat.messages[i - 1].isMine !== message.isMine}">
<!-- Bubble -->
<div
class="relative max-w-3/4 px-3 py-2 rounded-lg"
[ngClass]="{'bg-blue-500 text-blue-50': message.isMine,
'bg-gray-500 text-gray-50': !message.isMine}">
<!-- Speech bubble tail -->
<ng-container *ngIf="last || chat.messages[i + 1].isMine !== message.isMine">
<div
class="absolute bottom-0 w-3 transform"
[ngClass]="{'text-blue-500 -right-1 -mr-px mb-px': message.isMine,
'text-gray-500 -left-1 -ml-px mb-px -scale-x-1': !message.isMine}">
<ng-container *ngTemplateOutlet="speechBubbleExtension"></ng-container>
</div>
</ng-container>
<!-- Message -->
<div
class="min-w-4 leading-5"
[innerHTML]="message.value">
</div>
</div>
<!-- Time -->
<ng-container
*ngIf="first
|| last
|| chat.messages[i + 1].isMine !== message.isMine
|| chat.messages[i + 1].createdAt !== message.createdAt">
<div
class="my-0.5 text-sm font-medium text-secondary"
[ngClass]="{'mr-3': message.isMine,
'ml-3': !message.isMine}">
{{message.createdAt | date:'HH:mm'}}
</div>
</ng-container>
</div>
</ng-container>
</div>
</div>
<!-- Message field -->
<div class="flex items-end p-4 border-t bg-gray-50 dark:bg-transparent">
<div class="flex items-center h-11 my-px">
<button mat-icon-button>
<mat-icon [svgIcon]="'heroicons_outline:emoji-happy'"></mat-icon>
</button>
<button
class="ml-0.5"
mat-icon-button>
<mat-icon [svgIcon]="'heroicons_outline:paper-clip'"></mat-icon>
</button>
</div>
<mat-form-field class="fuse-mat-dense fuse-mat-no-subscript fuse-mat-rounded fuse-mat-bold w-full ml-4">
<textarea
class="min-h-5 my-0 resize-none"
style="margin: 11px 0 !important; padding: 0 !important;"
[rows]="1"
matInput
#messageInput></textarea>
</mat-form-field>
<div class="flex items-center h-11 my-px ml-4">
<button
mat-icon-button>
<mat-icon
class="transform rotate-90"
[svgIcon]="'heroicons_outline:paper-airplane'"></mat-icon>
</button>
</div>
</div>
</mat-drawer-content>
</mat-drawer-container>
</ng-container>
<!-- Select chat or start new template -->
<ng-template #selectChatOrStartNew>
<div class="flex flex-col flex-auto items-center justify-center bg-gray-100 dark:bg-transparent">
<mat-icon
class="icon-size-24"
[svgIcon]="'iconsmind:speach_bubble'"></mat-icon>
<div class="mt-4 text-2xl font-semibold tracking-tight text-secondary">Select a conversation or start a new chat</div>
</div>
</ng-template>
<!-- Speech bubble tail SVG -->
<!-- @formatter:off -->
<ng-template #speechBubbleExtension>
<svg width="100%" height="100%" viewBox="0 0 66 66" xmlns="http://www.w3.org/2000/svg">
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<path d="M1.01522827,0.516204834 C-8.83532715,54.3062744 61.7609863,70.5215302 64.8009949,64.3061218 C68.8074951,54.8859711 30.1663208,52.9997559 37.5036011,0.516204834 L1.01522827,0.516204834 Z" fill="currentColor" fill-rule="nonzero"></path>
</g>
</svg>
</ng-template>
<!-- @formatter:on -->
</div>

View File

@ -0,0 +1,143 @@
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, HostListener, NgZone, OnDestroy, OnInit, ViewChild, ViewEncapsulation } from '@angular/core';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
import { Chat } from 'app/modules/admin/apps/chat/chat.types';
import { ChatService } from 'app/modules/admin/apps/chat/chat.service';
@Component({
selector : 'chat-conversation',
templateUrl : './conversation.component.html',
encapsulation : ViewEncapsulation.None,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ConversationComponent implements OnInit, OnDestroy
{
@ViewChild('messageInput') messageInput: ElementRef;
chat: Chat;
drawerOpened: boolean = false;
private _unsubscribeAll: Subject<any> = new Subject<any>();
/**
* Constructor
*/
constructor(
private _changeDetectorRef: ChangeDetectorRef,
private _chatService: ChatService,
private _ngZone: NgZone
)
{
}
// -----------------------------------------------------------------------------------------------------
// @ Lifecycle hooks
// -----------------------------------------------------------------------------------------------------
/**
* On init
*/
ngOnInit(): void
{
// Chat
this._chatService.chat$
.pipe(takeUntil(this._unsubscribeAll))
.subscribe((chat: Chat) => {
this.chat = chat;
// Mark for check
this._changeDetectorRef.markForCheck();
});
}
/**
* On destroy
*/
ngOnDestroy(): void
{
// Unsubscribe from all subscriptions
this._unsubscribeAll.next();
this._unsubscribeAll.complete();
}
// -----------------------------------------------------------------------------------------------------
// @ Public methods
// -----------------------------------------------------------------------------------------------------
/**
* Open the contact info
*/
openContactInfo(): void
{
// Open the drawer
this.drawerOpened = true;
// Mark for check
this._changeDetectorRef.markForCheck();
}
/**
* Reset the chat
*/
resetChat(): void
{
this._chatService.resetChat();
// Mark for check
this._changeDetectorRef.markForCheck();
}
/**
* Toggle mute notifications
*/
toggleMuteNotifications(): void
{
// Toggle the muted
this.chat.muted = !this.chat.muted;
// Update the chat on the server
this._chatService.updateChat(this.chat.id, this.chat).subscribe();
}
/**
* Track by function for ngFor loops
*
* @param index
* @param item
*/
trackByFn(index: number, item: any): any
{
return item.id || index;
}
// -----------------------------------------------------------------------------------------------------
// @ Private methods
// -----------------------------------------------------------------------------------------------------
/**
* Resize on 'input' and 'ngModelChange' events
*
* @private
*/
@HostListener('input')
@HostListener('ngModelChange')
private _resizeMessageInput(): void
{
// This doesn't need to trigger Angular's change detection by itself
this._ngZone.runOutsideAngular(() => {
setTimeout(() => {
// Set the height to 'auto' so we can correctly read the scrollHeight
this.messageInput.nativeElement.style.height = 'auto';
// Detect the changes so the height is applied
this._changeDetectorRef.detectChanges();
// Get the scrollHeight and subtract the vertical padding
this.messageInput.nativeElement.style.height = `${this.messageInput.nativeElement.scrollHeight}px`;
// Detect the changes one more time to apply the final height
this._changeDetectorRef.detectChanges();
});
});
}
}

View File

@ -0,0 +1,51 @@
<div class="flex flex-col flex-auto h-full overflow-hidden bg-card dark:bg-default">
<!-- Header -->
<div class="flex flex-0 items-center h-18 -mb-px px-6 bg-gray-50 dark:bg-transparent">
<button
mat-icon-button
(click)="drawer.close()">
<mat-icon [svgIcon]="'heroicons_outline:arrow-narrow-left'"></mat-icon>
</button>
<div class="ml-2 text-2xl font-semibold">New chat</div>
</div>
<div class="relative overflow-y-auto">
<ng-container *ngIf="contacts.length; else noContacts">
<ng-container *ngFor="let contact of contacts; let i = index; trackBy: trackByFn">
<!-- Group -->
<ng-container *ngIf="i === 0 || contact.name.charAt(0) !== contacts[i - 1].name.charAt(0)">
<div class="z-10 sticky top-0 -mt-px px-6 py-1 md:px-8 border-t border-b font-medium uppercase text-secondary bg-gray-100 dark:bg-gray-900">
{{contact.name.charAt(0)}}
</div>
</ng-container>
<!-- Contact -->
<div class="z-20 flex items-center px-6 py-4 md:px-8 cursor-pointer hover:bg-hover border-b">
<div class="flex flex-0 items-center justify-center w-10 h-10 rounded-full overflow-hidden">
<ng-container *ngIf="contact.avatar">
<img
class="object-cover w-full h-full"
[src]="contact.avatar"
alt="Contact avatar"/>
</ng-container>
<ng-container *ngIf="!contact.avatar">
<div class="flex items-center justify-center w-full h-full rounded-full text-lg uppercase bg-gray-200 text-gray-600 dark:bg-gray-700 dark:text-gray-200">
{{contact.name.charAt(0)}}
</div>
</ng-container>
</div>
<div class="min-w-0 ml-4">
<div class="font-medium leading-5 truncate">{{contact.name}}</div>
<div class="leading-5 truncate text-secondary">{{contact.about}}</div>
</div>
</div>
</ng-container>
</ng-container>
</div>
<!-- No contacts -->
<ng-template #noContacts>
<div class="p-8 sm:p-16 border-t text-4xl font-semibold tracking-tight text-center">There are no contacts!</div>
</ng-template>
</div>

View File

@ -0,0 +1,68 @@
import { ChangeDetectionStrategy, Component, Input, OnDestroy, OnInit, ViewEncapsulation } from '@angular/core';
import { MatDrawer } from '@angular/material/sidenav';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
import { Contact } from 'app/modules/admin/apps/chat/chat.types';
import { ChatService } from 'app/modules/admin/apps/chat/chat.service';
@Component({
selector : 'chat-new-chat',
templateUrl : './new-chat.component.html',
encapsulation: ViewEncapsulation.None,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class NewChatComponent implements OnInit, OnDestroy
{
@Input() drawer: MatDrawer;
contacts: Contact[] = [];
private _unsubscribeAll: Subject<any> = new Subject<any>();
/**
* Constructor
*/
constructor(private _chatService: ChatService)
{
}
// -----------------------------------------------------------------------------------------------------
// @ Lifecycle hooks
// -----------------------------------------------------------------------------------------------------
/**
* On init
*/
ngOnInit(): void
{
// Contacts
this._chatService.contacts$
.pipe(takeUntil(this._unsubscribeAll))
.subscribe((contacts: Contact[]) => {
this.contacts = contacts;
});
}
/**
* On destroy
*/
ngOnDestroy(): void
{
// Unsubscribe from all subscriptions
this._unsubscribeAll.next();
this._unsubscribeAll.complete();
}
// -----------------------------------------------------------------------------------------------------
// @ Public methods
// -----------------------------------------------------------------------------------------------------
/**
* Track by function for ngFor loops
*
* @param index
* @param item
*/
trackByFn(index: number, item: any): any
{
return item.id || index;
}
}

View File

@ -0,0 +1,80 @@
<div class="flex flex-col flex-auto overflow-y-auto bg-card dark:bg-default">
<!-- Header -->
<div class="flex flex-0 items-center h-18 px-6 border-b bg-gray-50 dark:bg-transparent">
<button
mat-icon-button
(click)="drawer.close()">
<mat-icon [svgIcon]="'heroicons_outline:arrow-narrow-left'"></mat-icon>
</button>
<div class="ml-2 text-2xl font-semibold">Profile</div>
</div>
<div class="px-6">
<!-- Profile photo -->
<div class="group relative flex flex-0 mt-8 mx-auto w-40 h-40 rounded-full">
<div class="hidden group-hover:flex absolute inset-0 flex-col items-center justify-center backdrop-filter backdrop-blur bg-opacity-80 rounded-full cursor-pointer bg-gray-800">
<mat-icon
class="text-white"
[svgIcon]="'heroicons_outline:camera'"></mat-icon>
<div class="mt-2 mx-6 font-medium text-center text-white">Change Profile Photo</div>
</div>
<ng-container *ngIf="profile.avatar">
<img
class="w-full h-full rounded-full object-cover"
[src]="profile.avatar"
[alt]="'Profile avatar'">
</ng-container>
<ng-container *ngIf="!profile.avatar">
<div class="flex items-center justify-center w-full h-full rounded-full text-8xl font-semibold uppercase bg-gray-200 text-gray-600 dark:bg-gray-700 dark:text-gray-200">
{{profile.name.charAt(0)}}
</div>
</ng-container>
</div>
<!-- Profile info -->
<div class="flex flex-col mt-8 mx-2">
<mat-form-field>
<mat-label>Name</mat-label>
<mat-icon
class="icon-size-5"
matPrefix
[svgIcon]="'heroicons_solid:user-circle'"></mat-icon>
<input
matInput
[ngModel]="profile.name">
</mat-form-field>
<mat-form-field>
<mat-label>Email</mat-label>
<mat-icon
class="icon-size-5"
matPrefix
[svgIcon]="'heroicons_solid:mail'"></mat-icon>
<input
matInput
[ngModel]="profile.email">
</mat-form-field>
<mat-form-field>
<mat-label>About</mat-label>
<mat-icon
class="icon-size-5"
matPrefix
[svgIcon]="'heroicons_solid:identification'"></mat-icon>
<input
matInput
[ngModel]="profile.about">
</mat-form-field>
<div class="flex items-center justify-end mt-4">
<button
(click)="drawer.close()"
mat-button>Cancel
</button>
<button
class="ml-2"
mat-flat-button
[color]="'primary'">Save
</button>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,53 @@
import { ChangeDetectionStrategy, Component, Input, OnDestroy, OnInit, ViewEncapsulation } from '@angular/core';
import { MatDrawer } from '@angular/material/sidenav';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
import { Profile } from 'app/modules/admin/apps/chat/chat.types';
import { ChatService } from 'app/modules/admin/apps/chat/chat.service';
@Component({
selector : 'chat-profile',
templateUrl : './profile.component.html',
encapsulation: ViewEncapsulation.None,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ProfileComponent implements OnInit, OnDestroy
{
@Input() drawer: MatDrawer;
profile: Profile;
private _unsubscribeAll: Subject<any> = new Subject<any>();
/**
* Constructor
*/
constructor(private _chatService: ChatService)
{
}
// -----------------------------------------------------------------------------------------------------
// @ Lifecycle hooks
// -----------------------------------------------------------------------------------------------------
/**
* On init
*/
ngOnInit(): void
{
// Profile
this._chatService.profile$
.pipe(takeUntil(this._unsubscribeAll))
.subscribe((profile: Profile) => {
this.profile = profile;
});
}
/**
* On destroy
*/
ngOnDestroy(): void
{
// Unsubscribe from all subscriptions
this._unsubscribeAll.next();
this._unsubscribeAll.complete();
}
}

View File

@ -78,7 +78,7 @@ const config = {
// development and production, we will decide whether to purge or not
// by looking at the process arguments. If there is a "build" argument
// with the "ng" command then we will enable the purge.
enabled: process?.argv?.find(arg => arg.includes('ng')) && process?.argv?.indexOf('build') !== -1,
enabled: process && process.argv && process.argv.find(arg => arg.includes('ng')) && process.argv.indexOf('build') !== -1,
content: ['./src/**/*.{html,scss,ts}'],
options: {
safelist: {