Filter pipe added,

spacing class helpers added(margin, padding),
Normalize css added,
Icon size class helpers added,
slideInLeft, slideInRight animation added,
Chat app almost completed.
This commit is contained in:
mustafahlvc 2017-07-23 10:32:13 +03:00
parent b9569a5ba8
commit 56e6b854c2
42 changed files with 2620 additions and 25 deletions

View File

@ -20,6 +20,7 @@ import { PerfectScrollbarConfigInterface, PerfectScrollbarModule } from 'ngx-per
import { HttpModule } from '@angular/http';
import { InMemoryWebApiModule } from 'angular-in-memory-web-api';
import { FuseFakeDbService } from './fuse-fake-db/fuse-fake-db.service';
import { INTERNAL_BROWSER_DYNAMIC_PLATFORM_PROVIDERS } from '@angular/platform-browser-dynamic/src/platform_providers';
const PERFECT_SCROLLBAR_CONFIG: PerfectScrollbarConfigInterface = {
suppressScrollX: true
@ -30,6 +31,10 @@ const appRoutes: Routes = [
path : 'apps/mail',
loadChildren: './main/apps/mail/mail.module#MailModule'
},
{
path : 'apps/chat',
loadChildren: './main/apps/chat/chat.module#ChatModule'
},
{
path : '**',
redirectTo: 'apps/dashboards/project'

View File

@ -1,4 +1,4 @@
import {trigger, state, transition, animate, style} from '@angular/animations';
import { trigger, state, transition, animate, style } from '@angular/animations';
export class Animations
{
@ -8,4 +8,16 @@ export class Animations
transition('1 => 0', animate('300ms ease-out')),
transition('0 => 1', animate('300ms ease-in'))
]);
public static slideInLeft = trigger('slideInLeft', [
state('void', style({transform: 'translateX(-100%)', display: 'none'})),
state('*', style({transform: 'translateX(0)', display: 'flex'})),
transition('void => *', animate('300ms')),
transition('* => void', animate('300ms'))
]);
public static slideInRight = trigger('slideInRight', [
state('void', style({transform: 'translateX(100%)', display: 'none'})),
state('*', style({transform: 'translateX(0)', display: 'flex'})),
transition('void => *', animate('300ms')),
transition('* => void', animate('300ms'))
]);
}

View File

@ -1,5 +1,5 @@
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { CommonModule } from '@angular/common';
import { MaterialModule } from './material.module';
@ -23,7 +23,8 @@ import { FusePipesModule } from '../pipes/pipes.module';
CommonModule,
FormsModule,
FusePipesModule,
PerfectScrollbarModule
PerfectScrollbarModule,
ReactiveFormsModule
],
exports : [
FlexLayoutModule,
@ -33,7 +34,8 @@ import { FusePipesModule } from '../pipes/pipes.module';
FuseMdSidenavHelperDirective,
FuseMdSidenavTogglerDirective,
FusePipesModule,
PerfectScrollbarModule
PerfectScrollbarModule,
ReactiveFormsModule
]
})

View File

@ -0,0 +1,152 @@
/**
* Created by vadimdez on 28/06/16.
*/
import { Pipe, Injectable } from '@angular/core';
@Pipe({
name: 'filterBy',
pure: false
})
@Injectable()
export class FilterPipe {
private filterByString(filter) {
if (filter) {
filter = filter.toLowerCase();
}
return value => {
return !filter || (value ? ('' + value).toLowerCase().indexOf(filter) !== -1 : false);
}
}
private filterByBoolean(filter) {
return value => {
return Boolean(value) === filter;
}
}
private filterByObject(filter) {
return value => {
for (let key in filter) {
if (key === '$or') {
if (!this.filterByOr(filter.$or)(this.getValue(value))) {
return false;
}
continue;
}
if (!value.hasOwnProperty(key) && !Object.getOwnPropertyDescriptor(Object.getPrototypeOf(value), key)) {
return false;
}
let val = this.getValue(value[key]);
const filterType = typeof filter[key];
let isMatching;
if (filterType === 'boolean') {
isMatching = this.filterByBoolean(filter[key])(val);
} else if (filterType === 'string') {
isMatching = this.filterByString(filter[key])(val);
} else if (filterType === 'object') {
isMatching = this.filterByObject(filter[key])(val);
} else {
isMatching = this.filterDefault(filter[key])(val);
}
if (!isMatching) {
return false;
}
}
return true;
}
}
/**
* Filter value by $or
*
* @param filter
* @returns {(value:any)=>boolean}
*/
private filterByOr(filter: any[]) {
return (value: any) => {
let hasMatch = false;
const length = filter.length;
const isArray = value instanceof Array;
const arrayComparison = (i) => {
return value.indexOf(filter[i]) !== -1;
};
const otherComparison = (i) => {
return value === filter[i];
};
const comparison = isArray ? arrayComparison : otherComparison;
for (let i = 0; i < length; i++) {
if (comparison(i)) {
hasMatch = true;
break;
}
}
return hasMatch;
};
}
/**
* Checks function's value if type is function otherwise same value
* @param value
* @returns {any}
*/
private getValue(value: any) {
return typeof value === 'function' ? value() : value;
}
/**
* Defatul filterDefault function
*
* @param filter
* @returns {(value:any)=>boolean}
*/
private filterDefault(filter) {
return value => {
return filter === undefined || filter == value;
}
}
private isNumber(value) {
return !isNaN(parseInt(value, 10)) && isFinite(value);
}
transform(array: any[], filter: any): any {
const type = typeof filter;
if (!array) {
return array;
}
if (type === 'boolean') {
return array.filter(this.filterByBoolean(filter));
}
if (type === 'string') {
if (this.isNumber(filter)) {
return array.filter(this.filterDefault(filter));
}
return array.filter(this.filterByString(filter));
}
if (type === 'object') {
return array.filter(this.filterByObject(filter));
}
if (type === 'function') {
return array.filter(filter);
}
return array.filter(this.filterDefault(filter));
}
}

View File

@ -4,20 +4,24 @@ import { KeysPipe } from './keys.pipe';
import { GetByIdPipe } from './getById.pipe';
import { HtmlToPlaintextPipe } from './htmlToPlaintext.pipe';
import { SearchPipe } from './search.pipe';
import { FilterPipe } from './filter.pipe';
@NgModule({
declarations: [
KeysPipe,
GetByIdPipe,
HtmlToPlaintextPipe,
SearchPipe
SearchPipe,
FilterPipe
],
imports : [],
exports : [
KeysPipe,
GetByIdPipe,
HtmlToPlaintextPipe,
SearchPipe
SearchPipe,
FilterPipe
]
})

View File

@ -20,12 +20,16 @@
@include angular-material-typography($custom-typography);
// Partials
@import "partials/normalize";
@import "partials/spacing";
@import "partials/global";
@import "partials/_material";
@import "partials/icons";
@import "partials/material";
@import "partials/angular-material-fix";
@import "partials/typography";
@import "partials/page-layouts";
@import "partials/navigation";
@import "partials/forms";
// Plugins
@import "partials/plugins/plugins";

View File

@ -3,4 +3,4 @@
.mat-button-ripple {
border-radius: 50%;
}
}
}

View File

@ -0,0 +1,15 @@
button,
input[type=email],
input[type=tel],
input[type=text],
input[type=password],
input[type=image],
input[type=submit],
input[type=button],
input[type=search],
textarea {
appearance: none;
-moz-appearance: none;
-webkit-appearance: none;
outline: none;
}

View File

@ -2,6 +2,10 @@
box-sizing: border-box;
}
html {
}
html, body {
margin: 0;
width: 100%;
@ -21,4 +25,4 @@ body {
> md-sidenav-container {
height: 100%;
}
}
}

View File

@ -0,0 +1,22 @@
i,
md-icon {
color: rgba(0, 0, 0, 0.54);
font-size: 24px;
width: 24px;
height: 24px;
min-width: 24px;
min-height: 24px;
line-height: 24px;
@for $size from 2 through 128 {
&.s-#{$size * 2} {
font-size: #{($size * 2) + 'px'} !important;
width: #{($size * 2) + 'px'} !important;
height: #{($size * 2) + 'px'} !important;
min-width: #{($size * 2) + 'px'} !important;
min-height: #{($size * 2) + 'px'} !important;
line-height: #{($size * 2) + 'px'} !important;
}
}
}

View File

@ -39,6 +39,54 @@
}
}
.avatar-wrapper {
position: relative;
.avatar {
margin-top: 0;
margin-bottom: 0;
}
md-icon.status {
position: absolute;
top: 28px;
left: 28px;
}
}
md-icon.status {
border-radius: 50%;
&.online {
color: #4CAF50;
&:before {
content: "check_circle";
}
}
&.do-not-disturb {
color: #F44336;
&:before {
content: "do_not_disturb_on";
}
}
&.away {
background-color: #FFC107;
color: #FFFFFF;
&:before {
content: "access_time";
}
}
&.offline {
color: #646464;
background-color: #FFFFFF;
&:before {
content: "not_interested";
}
}
}
/*----------------------------------------------------------------*/
/* Forms
/*----------------------------------------------------------------*/

View File

@ -0,0 +1,447 @@
/*! normalize.css v7.0.0 | MIT License | github.com/necolas/normalize.css */
/* Document
========================================================================== */
/**
* 1. Correct the line height in all browsers.
* 2. Prevent adjustments of font size after orientation changes in
* IE on Windows Phone and in iOS.
*/
html {
line-height: 1.15; /* 1 */
-ms-text-size-adjust: 100%; /* 2 */
-webkit-text-size-adjust: 100%; /* 2 */
}
/* Sections
========================================================================== */
/**
* Remove the margin in all browsers (opinionated).
*/
body {
margin: 0;
}
/**
* Add the correct display in IE 9-.
*/
article,
aside,
footer,
header,
nav,
section {
display: block;
}
/**
* Correct the font size and margin on `h1` elements within `section` and
* `article` contexts in Chrome, Firefox, and Safari.
*/
h1 {
font-size: 2em;
margin: 0.67em 0;
}
/* Grouping content
========================================================================== */
/**
* Add the correct display in IE 9-.
* 1. Add the correct display in IE.
*/
figcaption,
figure,
main { /* 1 */
display: block;
}
/**
* Add the correct margin in IE 8.
*/
figure {
margin: 1em 40px;
}
/**
* 1. Add the correct box sizing in Firefox.
* 2. Show the overflow in Edge and IE.
*/
hr {
box-sizing: content-box; /* 1 */
height: 0; /* 1 */
overflow: visible; /* 2 */
}
/**
* 1. Correct the inheritance and scaling of font size in all browsers.
* 2. Correct the odd `em` font sizing in all browsers.
*/
pre {
font-family: monospace, monospace; /* 1 */
font-size: 1em; /* 2 */
}
/* Text-level semantics
========================================================================== */
/**
* 1. Remove the gray background on active links in IE 10.
* 2. Remove gaps in links underline in iOS 8+ and Safari 8+.
*/
a {
background-color: transparent; /* 1 */
-webkit-text-decoration-skip: objects; /* 2 */
}
/**
* 1. Remove the bottom border in Chrome 57- and Firefox 39-.
* 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari.
*/
abbr[title] {
border-bottom: none; /* 1 */
text-decoration: underline; /* 2 */
text-decoration: underline dotted; /* 2 */
}
/**
* Prevent the duplicate application of `bolder` by the next rule in Safari 6.
*/
b,
strong {
font-weight: inherit;
}
/**
* Add the correct font weight in Chrome, Edge, and Safari.
*/
b,
strong {
font-weight: bolder;
}
/**
* 1. Correct the inheritance and scaling of font size in all browsers.
* 2. Correct the odd `em` font sizing in all browsers.
*/
code,
kbd,
samp {
font-family: monospace, monospace; /* 1 */
font-size: 1em; /* 2 */
}
/**
* Add the correct font style in Android 4.3-.
*/
dfn {
font-style: italic;
}
/**
* Add the correct background and color in IE 9-.
*/
mark {
background-color: #ff0;
color: #000;
}
/**
* Add the correct font size in all browsers.
*/
small {
font-size: 80%;
}
/**
* Prevent `sub` and `sup` elements from affecting the line height in
* all browsers.
*/
sub,
sup {
font-size: 75%;
line-height: 0;
position: relative;
vertical-align: baseline;
}
sub {
bottom: -0.25em;
}
sup {
top: -0.5em;
}
/* Embedded content
========================================================================== */
/**
* Add the correct display in IE 9-.
*/
audio,
video {
display: inline-block;
}
/**
* Add the correct display in iOS 4-7.
*/
audio:not([controls]) {
display: none;
height: 0;
}
/**
* Remove the border on images inside links in IE 10-.
*/
img {
border-style: none;
}
/**
* Hide the overflow in IE.
*/
svg:not(:root) {
overflow: hidden;
}
/* Forms
========================================================================== */
/**
* 1. Change the font styles in all browsers (opinionated).
* 2. Remove the margin in Firefox and Safari.
*/
button,
input,
optgroup,
select,
textarea {
font-family: sans-serif; /* 1 */
font-size: 100%; /* 1 */
line-height: 1.15; /* 1 */
margin: 0; /* 2 */
}
/**
* Show the overflow in IE.
* 1. Show the overflow in Edge.
*/
button,
input { /* 1 */
overflow: visible;
}
/**
* Remove the inheritance of text transform in Edge, Firefox, and IE.
* 1. Remove the inheritance of text transform in Firefox.
*/
button,
select { /* 1 */
text-transform: none;
}
/**
* 1. Prevent a WebKit bug where (2) destroys native `audio` and `video`
* controls in Android 4.
* 2. Correct the inability to style clickable types in iOS and Safari.
*/
button,
html [type="button"], /* 1 */
[type="reset"],
[type="submit"] {
-webkit-appearance: button; /* 2 */
}
/**
* Remove the inner border and padding in Firefox.
*/
button::-moz-focus-inner,
[type="button"]::-moz-focus-inner,
[type="reset"]::-moz-focus-inner,
[type="submit"]::-moz-focus-inner {
border-style: none;
padding: 0;
}
/**
* Restore the focus styles unset by the previous rule.
*/
button:-moz-focusring,
[type="button"]:-moz-focusring,
[type="reset"]:-moz-focusring,
[type="submit"]:-moz-focusring {
outline: 1px dotted ButtonText;
}
/**
* Correct the padding in Firefox.
*/
fieldset {
padding: 0.35em 0.75em 0.625em;
}
/**
* 1. Correct the text wrapping in Edge and IE.
* 2. Correct the color inheritance from `fieldset` elements in IE.
* 3. Remove the padding so developers are not caught out when they zero out
* `fieldset` elements in all browsers.
*/
legend {
box-sizing: border-box; /* 1 */
color: inherit; /* 2 */
display: table; /* 1 */
max-width: 100%; /* 1 */
padding: 0; /* 3 */
white-space: normal; /* 1 */
}
/**
* 1. Add the correct display in IE 9-.
* 2. Add the correct vertical alignment in Chrome, Firefox, and Opera.
*/
progress {
display: inline-block; /* 1 */
vertical-align: baseline; /* 2 */
}
/**
* Remove the default vertical scrollbar in IE.
*/
textarea {
overflow: auto;
}
/**
* 1. Add the correct box sizing in IE 10-.
* 2. Remove the padding in IE 10-.
*/
[type="checkbox"],
[type="radio"] {
box-sizing: border-box; /* 1 */
padding: 0; /* 2 */
}
/**
* Correct the cursor style of increment and decrement buttons in Chrome.
*/
[type="number"]::-webkit-inner-spin-button,
[type="number"]::-webkit-outer-spin-button {
height: auto;
}
/**
* 1. Correct the odd appearance in Chrome and Safari.
* 2. Correct the outline style in Safari.
*/
[type="search"] {
-webkit-appearance: textfield; /* 1 */
outline-offset: -2px; /* 2 */
}
/**
* Remove the inner padding and cancel buttons in Chrome and Safari on macOS.
*/
[type="search"]::-webkit-search-cancel-button,
[type="search"]::-webkit-search-decoration {
-webkit-appearance: none;
}
/**
* 1. Correct the inability to style clickable types in iOS and Safari.
* 2. Change font properties to `inherit` in Safari.
*/
::-webkit-file-upload-button {
-webkit-appearance: button; /* 1 */
font: inherit; /* 2 */
}
/* Interactive
========================================================================== */
/*
* Add the correct display in IE 9-.
* 1. Add the correct display in Edge, IE, and Firefox.
*/
details, /* 1 */
menu {
display: block;
}
/*
* Add the correct display in all browsers.
*/
summary {
display: list-item;
}
/* Scripting
========================================================================== */
/**
* Add the correct display in IE 9-.
*/
canvas {
display: inline-block;
}
/**
* Add the correct display in IE.
*/
template {
display: none;
}
/* Hidden
========================================================================== */
/**
* Add the correct display in IE 10-.
*/
[hidden] {
display: none;
}

View File

@ -0,0 +1,62 @@
// Margin and Padding
@each $prop, $abbrev in (margin: m, padding: p) {
@for $index from 0 through 64 {
$size: $index *4;
$length: #{$size}px;
.#{$abbrev}-#{$size} {
#{$prop}: $length !important;
}
.#{$abbrev}t-#{$size} {
#{$prop}-top: $length !important;
}
.#{$abbrev}r-#{$size} {
#{$prop}-right: $length !important;
}
.#{$abbrev}b-#{$size} {
#{$prop}-bottom: $length !important;
}
.#{$abbrev}l-#{$size} {
#{$prop}-left: $length !important;
}
.#{$abbrev}x-#{$size} {
#{$prop}-right: $length !important;
#{$prop}-left: $length !important;
}
.#{$abbrev}y-#{$size} {
#{$prop}-top: $length !important;
#{$prop}-bottom: $length !important;
}
}
}
// Some special margin utils
.m-auto {
margin: auto !important;
}
.mt-auto {
margin-top: auto !important;
}
.mr-auto {
margin-right: auto !important;
}
.mb-auto {
margin-bottom: auto !important;
}
.ml-auto {
margin-left: auto !important;
}
.mx-auto {
margin-right: auto !important;
margin-left: auto !important;
}
.my-auto {
margin-top: auto !important;
margin-bottom: auto !important;
}

View File

@ -0,0 +1,325 @@
export class ChatFakeDb
{
public static contacts = [
{
'id' : '5725a680b3249760ea21de52',
'name' : 'Alice Freeman',
'avatar': 'assets/images/avatars/alice.jpg',
'status': 'online',
'mood' : 'I never sign anything until I pretend to read it first..'
},
{
'id' : '5725a680606588342058356d',
'name' : 'Arnold',
'avatar': 'assets/images/avatars/Arnold.jpg',
'status': 'do-not-disturb',
'mood' : 'Looks like Andrew Jackson\'s been tossed to the back of the bus.'
},
{
'id' : '5725a68009e20d0a9e9acf2a',
'name' : 'Barrera',
'avatar': 'assets/images/avatars/Barrera.jpg',
'status': 'do-not-disturb',
'mood' : 'Love is going to bed early.Marriage is going to sleep early.',
'unread': null
},
{
'id' : '5725a6809fdd915739187ed5',
'name' : 'Blair',
'avatar': 'assets/images/avatars/Blair.jpg',
'status': 'offline',
'mood' : 'I would be unstoppable. If i could just get started.',
'unread': 3
},
{
'id' : '5725a68007920cf75051da64',
'name' : 'Boyle',
'avatar': 'assets/images/avatars/Boyle.jpg',
'status': 'offline',
'mood' : '\'GOOD MORNING COFFEE\'....Meet your maker!!!!'
},
{
'id' : '5725a68031fdbb1db2c1af47',
'name' : 'Christy',
'avatar': 'assets/images/avatars/Christy.jpg',
'status': 'offline',
'mood' : 'We always hold hands. If I let go, she shops.',
},
{
'id' : '5725a680bc670af746c435e2',
'name' : 'Copeland',
'avatar': 'assets/images/avatars/Copeland.jpg',
'status': 'online',
'mood' : 'I get enough exercise just pushing my luck.',
},
{
'id' : '5725a680e7eb988a58ddf303',
'name' : 'Estes',
'avatar': 'assets/images/avatars/Estes.jpg',
'status': 'away',
'mood' : 'What comes after the man bun hairstyle? The he-hive!',
},
{
'id' : '5725a680dcb077889f758961',
'name' : 'Harper',
'avatar': 'assets/images/avatars/Harper.jpg',
'status': 'offline',
'mood' : 'Always try to be modest and be proud of it!',
},
{
'id' : '5725a6806acf030f9341e925',
'name' : 'Helen',
'avatar': 'assets/images/avatars/Helen.jpg',
'status': 'away',
'mood' : 'Why are there stitch marks on zombies? Who\'s giving them medical attention?',
},
{
'id' : '5725a680ae1ae9a3c960d487',
'name' : 'Henderson',
'avatar': 'assets/images/avatars/Henderson.jpg',
'status': 'offline',
'mood' : 'I can\'t decide if people who wear pajamas in public have given up on life or are living it to the fullest.',
},
{
'id' : '5725a680b8d240c011dd224b',
'name' : 'Josefina',
'avatar': 'assets/images/avatars/Josefina.jpg',
'status': 'online',
'mood' : 'The fastest way to being happy is to make other people happy. You go first',
},
{
'id' : '5725a68034cb3968e1f79eac',
'name' : 'Katina',
'avatar': 'assets/images/avatars/Katina.jpg',
'status': 'away',
'mood' : 'If I was a rat,,, I wouldn\'t give anyone my ass.',
},
{
'id' : '5725a6801146cce777df2a08',
'name' : 'Lily',
'avatar': 'assets/images/avatars/Lily.jpg',
'status': 'do-not-disturb',
'mood' : 'A zip line but from the sofa to the fridge',
},
{
'id' : '5725a6808a178bfd034d6ecf',
'name' : 'Mai',
'avatar': 'assets/images/avatars/Mai.jpg',
'status': 'away',
'mood' : 'If a girl tells you she has a nipple ring, the only correct response is \'I don\'t believe you.\'',
},
{
'id' : '5725a680653c265f5c79b5a9',
'name' : 'Nancy',
'avatar': 'assets/images/avatars/Nancy.jpg',
'status': 'do-not-disturb',
'mood' : 'Prison counts as a gated community, right?',
},
{
'id' : '5725a680bbcec3cc32a8488a',
'name' : 'Nora',
'avatar': 'assets/images/avatars/Nora.jpg',
'status': 'do-not-disturb',
'mood' : 'I never date left handed women. Righty tighty, lefty loosey.',
},
{
'id' : '5725a6803d87f1b77e17b62b',
'name' : 'Odessa',
'avatar': 'assets/images/avatars/Odessa.jpg',
'status': 'away',
'mood' : 'A day without sunshine is like, night.',
},
{
'id' : '5725a680e87cb319bd9bd673',
'name' : 'Reyna',
'avatar': 'assets/images/avatars/Reyna.jpg',
'status': 'offline',
'mood' : 'I can\'t wait for summer in Canada...',
},
{
'id' : '5725a6802d10e277a0f35775',
'name' : 'Shauna',
'avatar': 'assets/images/avatars/Shauna.jpg',
'status': 'online',
'mood' : 'My take home pay doesnt ven take me home.',
'unread': null,
},
{
'id' : '5725a680aef1e5cf26dd3d1f',
'name' : 'Shepard',
'avatar': 'assets/images/avatars/Shepard.jpg',
'status': 'online',
'mood' : 'I don\'t speak Spanish, but I\'m pretty sure \'Dora\' means \'annoying\'',
},
{
'id' : '5725a680cd7efa56a45aea5d',
'name' : 'Tillman',
'avatar': 'assets/images/avatars/Tillman.jpg',
'status': 'do-not-disturb',
'mood' : '',
},
{
'id' : '5725a680fb65c91a82cb35e2',
'name' : 'Trevino',
'avatar': 'assets/images/avatars/Trevino.jpg',
'status': 'away',
'mood' : 'Apparently, a rat and a plastic tube does not count as a DIY abortion kit.',
},
{
'id' : '5725a68018c663044be49cbf',
'name' : 'Tyson',
'avatar': 'assets/images/avatars/Tyson.jpg',
'status': 'do-not-disturb',
'mood' : 'I\'m wondering why life keeps teaching me lessons I have no desire to learn...',
},
{
'id' : '5725a6809413bf8a0a5272b1',
'name' : 'Velazquez',
'avatar': 'assets/images/avatars/Velazquez.jpg',
'status': 'online',
'mood' : 'Modulation in all things.',
}
];
public static chats = [
{
'id' : '1725a680b3249760ea21de52',
'dialog': [
{
'who' : '5725a680b3249760ea21de52',
'message': 'Quickly come to the meeting room 1B, we have a big server issue',
'time' : '2017-03-22T08:54:28.299Z'
},
{
'who' : '5725a6802d10e277a0f35724',
'message': 'Im having breakfast right now, cant you wait for 10 minutes?',
'time' : '2017-03-22T08:55:28.299Z'
},
{
'who' : '5725a680b3249760ea21de52',
'message': 'We are losing money! Quick!',
'time' : '2017-03-22T09:00:28.299Z'
},
{
'who' : '5725a6802d10e277a0f35724',
'message': 'Its not my money, you know. I will eat my breakfast and then I will come to the meeting room.',
'time' : '2017-03-22T09:02:28.299Z'
},
{
'who' : '5725a680b3249760ea21de52',
'message': 'You are the worst!',
'time' : '2017-03-22T09:05:28.299Z'
},
{
'who' : '5725a680b3249760ea21de52',
'message': 'We are losing money! Quick!',
'time' : '2017-03-22T09:15:28.299Z'
},
{
'who' : '5725a6802d10e277a0f35724',
'message': 'Its not my money, you know. I will eat my breakfast and then I will come to the meeting room.',
'time' : '2017-03-22T09:20:28.299Z'
},
{
'who' : '5725a680b3249760ea21de52',
'message': 'You are the worst!',
'time' : '2017-03-22T09:22:28.299Z'
},
{
'who' : '5725a680b3249760ea21de52',
'message': 'We are losing money! Quick!',
'time' : '2017-03-22T09:25:28.299Z'
},
{
'who' : '5725a6802d10e277a0f35724',
'message': 'Its not my money, you know. I will eat my breakfast and then I will come to the meeting room.',
'time' : '2017-03-22T09:27:28.299Z'
},
{
'who' : '5725a680b3249760ea21de52',
'message': 'You are the worst!',
'time' : '2017-03-22T09:33:28.299Z'
},
{
'who' : '5725a680b3249760ea21de52',
'message': 'We are losing money! Quick!',
'time' : '2017-03-22T09:35:28.299Z'
},
{
'who' : '5725a6802d10e277a0f35724',
'message': 'Its not my money, you know. I will eat my breakfast and then I will come to the meeting room.',
'time' : '2017-03-22T09:45:28.299Z'
},
{
'who' : '5725a680b3249760ea21de52',
'message': 'You are the worst!',
'time' : '2017-03-22T10:00:28.299Z'
}
]
},
{
'id' : '2725a680b8d240c011dd2243',
'dialog': [
{
'who' : '5725a680b8d240c011dd224b',
'message': 'Quickly come to the meeting room 1B, we have a big server issue',
'time' : '2017-04-22T01:00:00.299Z'
},
{
'who' : '5725a6802d10e277a0f35724',
'message': 'Im having breakfast right now, cant you wait for 10 minutes?',
'time' : '2017-04-22T01:05:00.299Z'
},
{
'who' : '5725a680b8d240c011dd224b',
'message': 'We are losing money! Quick!',
'time' : '2017-04-22T01:10:00.299Z'
}
]
},
{
'id' : '3725a6809413bf8a0a5272b4',
'dialog': [
{
'who' : '5725a6809413bf8a0a5272b1',
'message': 'Quickly come to the meeting room 1B, we have a big server issue',
'time' : '2017-04-22T02:10:00.299Z'
}
]
}
];
public static user = [
{
'id' : '5725a6802d10e277a0f35724',
'name' : 'John Doe',
'avatar' : 'assets/images/avatars/profile.jpg',
'status' : 'online',
'mood' : 'it\'s a status....not your diary...',
'chatList': [
{
'id' : '1725a680b3249760ea21de52',
'contactId' : '5725a680b3249760ea21de52',
'name' : 'Alice Freeman',
'unread' : 4,
'lastMessageTime': '2017-06-12T02:10:18.931Z'
},
{
'id' : '2725a680b8d240c011dd2243',
'contactId' : '5725a680b8d240c011dd224b',
'name' : 'Josefina',
'unread' : null,
'lastMessageTime': '2017-02-18T10:30:18.931Z'
},
{
'id' : '3725a6809413bf8a0a5272b4',
'contactId' : '5725a6809413bf8a0a5272b1',
'name' : 'Velazquez',
'unread' : 2,
'lastMessageTime': '2017-03-18T12:30:18.931Z'
}
]
}
];
}

View File

@ -1,5 +1,6 @@
import {InMemoryDbService} from 'angular-in-memory-web-api';
import {MailFakeDb} from './mail';
import {ChatFakeDb} from './chat';
export class FuseFakeDbService implements InMemoryDbService
{
@ -7,8 +8,11 @@ export class FuseFakeDbService implements InMemoryDbService
{
return {
'mail-mails' : MailFakeDb.mails,
'mail-folders': MailFakeDb.folders,
'mail-labels' : MailFakeDb.labels
'mail-folders' : MailFakeDb.folders,
'mail-labels' : MailFakeDb.labels,
'chat-contacts': ChatFakeDb.contacts,
'chat-chats' : ChatFakeDb.chats,
'chat-user' : ChatFakeDb.user,
};
}

View File

@ -0,0 +1,17 @@
<div fxFlex fxLayout="column" fxLayoutAlign="center center">
<div class="big-circle mat-elevation-z1" fxLayout="column" fxLayoutAlign="center center">
<md-icon class="s-128">chat</md-icon>
</div>
<span class="app-title my-24">Chat App</span>
<span fxHide fxShow.gt-md class="app-message">Select contact to start the chat!..</span>
<button md-raised-button fxHide.gt-md class="" fuseMdSidenavToggler="chat-left-sidenav">
Select contact to start the chat!..
</button>
</div>

View File

@ -0,0 +1,30 @@
@import "src/app/core/scss/fuse";
:host {
display: flex;
flex: 1;
height: 100%;
background: linear-gradient(to bottom, rgba(255, 255, 255, 0.8) 0%, rgba(255, 255, 255, 0.6) 20%, rgba(255, 255, 255, 0.8));
.big-circle {
background: linear-gradient(to bottom, rgba(255, 255, 255, 0.8) 0%, rgba(255, 255, 255, 0.6) 20%, rgba(255, 255, 255, 0.8));
border-radius: 50%;
width: 300px;
height: 300px;
line-height: 300px;
text-align: center;
md-icon{
color: mat-color($accent);
}
}
.app-title {
font-weight: 500;
font-size: 32px;
}
.secondary-text {
font-size: 16px;
}
}

View File

@ -0,0 +1,19 @@
import {Component, OnInit} from '@angular/core';
@Component({
selector : 'fuse-chat-start',
templateUrl: './chat-start.component.html',
styleUrls : ['./chat-start.component.scss']
})
export class ChatStartComponent implements OnInit
{
constructor()
{
}
ngOnInit()
{
}
}

View File

@ -0,0 +1,115 @@
<!-- CHAT -->
<div class="chat" fxFlex fxLayout="column">
<!-- CHAT TOOLBAR -->
<md-toolbar class="chat-toolbar">
<div fxFlex fxLayout="row" fxLayoutAlign="space-between center">
<div fxLayout="row" fxLayoutAlign="start center">
<!-- RESPONSIVE CHATS BUTTON-->
<div md-button fxHide.gt-md class="responsive-chats-button mat-icon-button mr-16"
fuseMdSidenavToggler="chat-left-sidenav"
aria-label="chats button">
<md-icon class="s-36">chat</md-icon>
</div>
<!-- / RESPONSIVE CHATS BUTTON-->
<!-- CHAT CONTACT-->
<div class="chat-contact" fxLayout="row" fxLayoutAlign="start center"
fuseMdSidenavToggler="chat-right-sidenav" (click)="selectContact()">
<div class="avatar-wrapper">
<img [src]="contact.avatar"
class="avatar"
alt="{{contact.name}}"/>
<md-icon class="s-16 status"
[ngClass]="contact.status">
</md-icon>
</div>
<div class="chat-contact-name">
{{contact.name}}
</div>
</div>
<!-- / CHAT CONTACT-->
</div>
<div>
<button md-button class="mat-icon-button" [mdMenuTriggerFor]="contactMenu"
aria-label="more">
<md-icon>more_vert</md-icon>
</button>
<md-menu #contactMenu="mdMenu">
<button md-menu-item fuseMdSidenavToggler="chat-right-sidenav" (click)="selectContact()">
Contact Info
</button>
</md-menu>
</div>
</div>
</md-toolbar>
<!-- / CHAT TOOLBAR -->
<!-- CHAT CONTENT -->
<div id="chat-content" fxFlex perfect-scrollbar>
<!-- CHAT MESSAGES -->
<div class="chat-messages">
<!-- MESSAGE -->
<div fxLayout="row" *ngFor="let message of dialog" class="message-row"
[ngClass]="{'user' :message.who === user.id}">
<img *ngIf="message.who === contact.id"
src="{{contact.avatar}}"
class="avatar"
alt="{{contact.name}}"/>
<img *ngIf="message.who ===user.id" class="avatar" src="{{user.avatar}}">
<div class="bubble">
<div class="message">{{message.message}}</div>
<div class="time secondary-text">{{message.time | date:'medium'}}</div>
</div>
</div>
<!-- / MESSAGE -->
</div>
<!-- CHAT MESSAGES -->
</div>
<!-- / CHAT CONTENT -->
<!-- CHAT FOOTER -->
<div class="chat-footer" fxLayout="row" fxLayoutAlign="center center">
<!-- REPLY FORM -->
<form #replyForm="ngForm"
(ngSubmit)="reply($event)"
(keyup.enter)="reply($event)"
fxFlex class="reply-form"
fxLayout="row"
fxLayoutAlign="start center">
<md-input-container class="" fxFlex floatPlaceholder="never">
<textarea mdInput #replyInput placeholder="Type and hit enter to send message"
ngModel name="message"></textarea>
</md-input-container>
<button md-fab class="" type="submit" aria-label="Send message">
<md-icon>send</md-icon>
</button>
</form>
<!-- / REPLY FORM -->
</div>
<!-- / CHAT FOOTER-->
</div>
<!-- / CHAT -->

View File

@ -0,0 +1,135 @@
:host {
display: flex;
flex: 1;
height: 100%;
background: linear-gradient(to bottom, rgba(255, 255, 255, 0.8) 0%, rgba(255, 255, 255, 0.6) 20%, rgba(255, 255, 255, 0.8));
overflow: hidden;
.chat {
height: 100%;
.chat-toolbar {
min-height: 64px;
background-color: #F3F4F5;
color: rgba(0, 0, 0, 0.87);
border-bottom: 1px solid rgba(0, 0, 0, .08);
.responsive-chats-button {
padding: 0;
}
.chat-contact {
cursor: pointer;
.avatar {
margin-right: 16px;
}
.chat-contact-name {
}
}
}
#chat-content {
background: transparent;
.message-row {
padding: 16px;
.bubble {
position: relative;
padding: 6px 7px 8px 9px;
background-color: #FFF;
box-shadow: 0 1px .5px rgba(0, 0, 0, .13);
border-radius: 6px;
&:before {
background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAmCAMAAADp2asXAAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAADGUExURQAAAP////b29vn5+f///wAAAP///wAAAAAAAP///9ra2v////j4+PHx8fv7++Hh4fHx8f////////////////39/QAAAP////////z8/P////39/f39/fz8/P////////////z8/P////////////z8/P////////////v7+/Hx8f///9bW1vz8/K2trf////39/f39/WJiYgAAAExMTFtbWwAAAN3d3cjIyPr6+vX19QAAAO7u7vz8/NTU1Ofn5zMzM////zGPlXsAAABBdFJOUwAcm/kREh4CCDWL1SneR6TfAQffhMYK/A5nRrLWfRc5DW2ih5f+19Kn+9v4g/1LCJuXHwQUKgahcXS6DNnlDMMKKzPoTgAAAKBJREFUKM+V08USwmAQA+C/0NIWd3d3d8/7vxTMcIPkQK7f7CG7s8bQAOY/SCuwFYQU1P+eiCqIK2gpWCmoCrAgoKQgJ8CHgIqAMjg0MxxSQ3DogEMWFBZtUPAHYGB1CyDQWE6AH7BrfXzlAxGAQhECTGAmwN1Okz0Gb/LW4fEItIfrOfNELMh3tck7u+PhcT2zQ7l77/K8iY8yJwV3BeYFqpc/uSyPGdAAAAAASUVORK5CYII=);
content: '';
position: absolute;
left: -11px;
bottom: 3px;
width: 12px;
height: 19px;
background-position: 50% 50%;
background-repeat: no-repeat;
background-size: contain;
}
.message {
white-space: pre-wrap;
}
.time {
font-size: 11px;
margin-top: 8px;
text-align: right;
}
}
&.contact {
.avatar {
margin: 0 16px 0 0;
}
}
&.user {
align-items: flex-end;
.avatar {
order: 2;
margin: 0 0 0 16px;
}
.bubble {
margin-left: auto;
background-color: #E8F5E9;
border: 1px solid #DFEBE0;
order: 1;
&:before {
right: -11px;
left: auto;
background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAmCAMAAADp2asXAAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAD2UExURQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGRsXAAAANzwzNPmxNrtyau5oIWRedDkwNntyczgwdfpyJ+/n97wzsLWtNjsytvwzczfvtPmxau6nNjqxtrtyio1KtzwzNjryAAAANzwzgAAANzwzK7Aor/Us9Lnw8vevAAAAMzevtbpxrvMrX+IdwAAAEROOi45Lr3MrZGjf9LoxX+MctnqydLkwhgYGMzfv9vuyQAAANzwzNvuy9zxy7vMu7XGqNvtzKKykwAAANruzKq6nLnMriQkGMXXuL3PsNjsySgzKAAAANLkw83fvd3vy9z4xtzwzRpFmIEAAABQdFJOUwAXChEGBAMBAgwhDvJ7k0YqMc0Zmwj6apf2kjU0+dkw/swh/CP9j2Wr2gndvaYeBRoxQg6gUPt/FaHJGdTj9A9k7XQLeE6iFcN12xkSt9r4NKizowAAAMFJREFUKM+V0sdywlAMBVDbMX7PQCihQ+iQ0HsJvfem/P/PwBIzugu0PXNnNNJVyPmhsIPhhoB2COwIGuLdhAcl3AhCBoBoHUC6BCBbA0C/EkBFB5D/FjxQwQYg1RI8UKINgDoSAPUlAPqUAMgfAEBfXsEDBV0+Hogi4Zhg4THj9YwHoqEBYOrgYTI3GVgMNn8r+Qq94k9yZNosW/3Hy9VuTjWfHkOX6367bGZUU7de66ieHZrO1OGg8Z1WTgYAFLgD5S1PCkzo1B0AAAAASUVORK5CYII=);
}
}
}
}
}
.chat-footer {
min-height: 64px;
max-height: 96px;
background-color: #F3F4F5;
color: rgba(0, 0, 0, 0.87);
border-top: 1px solid rgba(0, 0, 0, .08);
padding: 8px 8px 8px 16px;
.reply-form {
md-input-container {
margin: 0;
padding-right: 16px;
textarea {
overflow: auto;
max-height: 80px;
transition: height 200ms ease;
&.grow {
height: 80px;
}
}
.md-errors-spacer {
display: none;
}
}
.md-button {
margin: 0;
}
}
}
}
}

View File

@ -0,0 +1,101 @@
import { AfterViewInit, Component, OnInit, ViewChild, ViewChildren } from '@angular/core';
import { ChatService } from '../chat.service';
import { NgForm } from '@angular/forms';
import { PerfectScrollbarDirective } from 'ngx-perfect-scrollbar';
@Component({
selector : 'fuse-chat-view',
templateUrl: './chat-view.component.html',
styleUrls : ['./chat-view.component.scss']
})
export class ChatViewComponent implements OnInit, AfterViewInit
{
user: any;
chat: any;
dialog: any;
contact: any;
replyInput: any;
selectedChat: any;
@ViewChild(PerfectScrollbarDirective) directiveScroll: PerfectScrollbarDirective;
@ViewChildren('replyInput') replyInputField;
@ViewChild('replyForm') replyForm: NgForm;
constructor(private chatService: ChatService)
{
}
ngOnInit()
{
this.user = this.chatService.user;
this.chatService.onChatSelected
.subscribe(chatData => {
if ( chatData )
{
this.selectedChat = chatData;
this.contact = chatData.contact;
this.dialog = chatData.dialog;
this.readyToReply();
}
});
}
ngAfterViewInit()
{
this.replyInput = this.replyInputField.first.nativeElement;
this.readyToReply();
}
selectContact()
{
this.chatService.selectContact(this.contact);
}
readyToReply()
{
setTimeout(() => {
this.replyForm.reset();
this.focusReplyInput();
this.scrollToBottom();
});
}
focusReplyInput()
{
setTimeout(() => {
this.replyInput.focus();
});
}
scrollToBottom(speed?: number)
{
speed = speed || 400;
if ( this.directiveScroll )
{
setTimeout(() => {
this.directiveScroll.scrollToBottom(0, speed);
});
}
}
reply(event)
{
// Message
const message = {
who : this.user.id,
message: this.replyForm.form.value.message,
time : new Date().toISOString()
};
// Add the message to the chat
this.dialog.push(message);
// Update the server
this.chatService.updateDialog(this.selectedChat.chatId, this.dialog).then(response => {
this.readyToReply();
});
}
}

View File

@ -1,3 +1,43 @@
<p>
chat works!
</p>
<div class="page-layout carded fullwidth">
<!-- TOP BACKGROUND -->
<div class="top-bg md-accent-bg"></div>
<!-- / TOP BACKGROUND -->
<!-- CENTER -->
<div class="center">
<!-- CONTENT CARD -->
<div class="content-card">
<md-sidenav-container>
<!-- LEFT SIDENAV -->
<md-sidenav class="sidenav mat-sidenav-opened" align="start" opened="true" mode="side"
fuseMdSidenavHelper="chat-left-sidenav" md-is-locked-open="gt-md">
<fuse-chat-left-sidenav></fuse-chat-left-sidenav>
</md-sidenav>
<!-- / LEFT SIDENAV -->
<!-- CONTENT -->
<fuse-chat-start *ngIf="!selectedChat"></fuse-chat-start>
<fuse-chat-view *ngIf="selectedChat"></fuse-chat-view>
<!-- / CONTENT -->
<!-- RIGHT SIDENAV -->
<md-sidenav class="sidenav mat-sidenav-opened" align="end" opened="false" mode="over"
fuseMdSidenavHelper="chat-right-sidenav">
<fuse-chat-right-sidenav></fuse-chat-right-sidenav>
</md-sidenav>
<!-- / RIGHT SIDENAV -->
</md-sidenav-container>
</div>
<!-- / CONTENT CARD -->
</div>
<!-- / CENTER -->
</div>

View File

@ -0,0 +1,31 @@
:host {
height: 100% !important;
.center {
padding: 32px 32px 0 32px;
max-width: 1400px;
height: 100%;
margin: 0 auto;
.content-card {
position: relative;
background: url('/assets/images/patterns/rain-grey.png') repeat;
height: 100%;
.mat-sidenav-container {
width: 100%;
height: 100%;
background: transparent;
md-sidenav {
display: flex;
flex-direction: column;
width: 400px;
max-width: 90%;
box-shadow: 0 0 1px rgba(0, 0, 0, .37);
overflow: hidden;
}
}
}
}
}

View File

@ -1,15 +1,25 @@
import { Component, OnInit } from '@angular/core';
import { Component, OnInit, ViewEncapsulation } from '@angular/core';
import { ChatService } from './chat.service';
@Component({
selector: 'fuse-chat',
templateUrl: './chat.component.html',
styleUrls: ['./chat.component.scss']
selector : 'fuse-chat',
templateUrl: './chat.component.html',
styleUrls : ['./chat.component.scss']
})
export class ChatComponent implements OnInit {
export class ChatComponent implements OnInit
{
selectedChat: any;
constructor() { }
constructor(private chatService: ChatService)
{
}
ngOnInit() {
}
ngOnInit()
{
this.chatService.onChatSelected
.subscribe(chatData => {
this.selectedChat = chatData;
});
}
}

View File

@ -2,10 +2,21 @@ import {NgModule} from '@angular/core';
import {SharedModule} from '../../../core/modules/shared.module';
import {RouterModule, Routes} from '@angular/router';
import {ChatComponent} from './chat.component';
import {ChatService} from './chat.service';
import { ChatViewComponent } from './chat-view/chat-view.component';
import { ChatStartComponent } from './chat-start/chat-start.component';
import {ChatsSidenavComponent} from './sidenavs/left/chats/chats.component';
import { UserSidenavComponent } from './sidenavs/left/user/user.component';
import { LeftSidenavComponent } from './sidenavs/left/left.component';
import { RightSidenavComponent } from './sidenavs/right/right.component';
import { ContactSidenavComponent } from './sidenavs/right/contact/contact.component';
const routes: Routes = [
{
path: 'apps/chat', component: ChatComponent, children: []
path : 'apps/chat', component: ChatComponent, children: [],
resolve: {
chat: ChatService
}
}
];
@ -15,7 +26,17 @@ const routes: Routes = [
RouterModule.forChild(routes)
],
declarations: [
ChatComponent
ChatComponent,
ChatViewComponent,
ChatStartComponent,
ChatsSidenavComponent,
UserSidenavComponent,
LeftSidenavComponent,
RightSidenavComponent,
ContactSidenavComponent
],
providers : [
ChatService
]
})
export class ChatModule

View File

@ -0,0 +1,259 @@
import { Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, Resolve, RouterStateSnapshot } from '@angular/router';
import { Observable } from 'rxjs/Observable';
import { Http } from '@angular/http';
import { Subject } from 'rxjs/Subject';
import { BehaviorSubject } from 'rxjs/BehaviorSubject';
@Injectable()
export class ChatService implements Resolve<any>
{
contacts: any[];
chats: any[];
user: any;
onChatSelected = new BehaviorSubject<any>(null);
onContactSelected = new BehaviorSubject<any>(null);
onChatsUpdated = new Subject<any>();
onUserUpdated = new Subject<any>();
onLeftSidenavViewChanged = new Subject<any>();
onRightSidenavViewChanged = new Subject<any>();
constructor(private http: Http)
{
}
/**
* Get chat
* @param contactId
* @returns {Promise<any>}
*/
getChat(contactId)
{
const chatItem = this.user.chatList.find((item) => {
return item.contactId === contactId;
});
/**
* Create new chat, if it's not created yet.
*/
if ( !chatItem )
{
this.createNewChat(contactId).then((newChats) => {
this.getChat(contactId);
});
return;
}
return new Promise((resolve, reject) => {
this.http.get('api/chat-chats/' + chatItem.id)
.subscribe(response => {
const chat = response.json().data;
const chatContact = this.contacts.find((contact) => {
return contact.id === contactId;
});
const chatData = {
chatId : chat.id,
dialog : chat.dialog,
contact: chatContact
};
this.onChatSelected.next({...chatData});
}, reject);
});
}
/**
* Create New Chat
* @param contactId
* @returns {Promise<any>}
*/
createNewChat(contactId)
{
return new Promise((resolve, reject) => {
const contact = this.contacts.find((item) => {
return item.id === contactId;
});
const chatId = this.guidGenerator();
const chat = {
id : chatId,
dialog: []
};
const chatListItem = {
'id' : chatId,
'contactId' : contactId,
'unread' : null,
'lastMessageTime': '2017-02-18T10:30:18.931Z'
};
/**
* Add new chat list item to the user's chat list
*/
this.user.chatList.push(chatListItem);
/**
* Post the created chat
*/
this.http.post('api/chat-chats', {...chat})
.subscribe(response => {
/**
* Post the new the user data
*/
this.http.post('api/chat-user/' + this.user.id, this.user)
.subscribe(addedlistItem => {
/**
* Update the user data from server
*/
this.getUser().then(updatedUser => {
this.onUserUpdated.next(updatedUser);
resolve(updatedUser);
});
});
}, reject);
});
}
/**
* Select Contact
* @param contact
*/
selectContact(contact)
{
this.onContactSelected.next(contact);
}
/**
* Set user status
* @param status
*/
setUserStatus(status)
{
this.user.status = status;
}
/**
* Update user data
* @param userData
*/
updateUserData(userData)
{
this.http.post('api/chat-user/' + this.user.id, userData)
.subscribe(response => {
this.user = userData;
}
);
}
/**
* Update the chat dialog
* @param chatId
* @param dialog
* @returns {Promise<any>}
*/
updateDialog(chatId, dialog): Promise<any>
{
return new Promise((resolve, reject) => {
const newData = {
id : chatId,
dialog: dialog
};
this.http.post('api/chat-chats/' + chatId, newData)
.subscribe(updatedChat => {
resolve(updatedChat);
}, reject);
});
}
/**
* The Mail App Main Resolver
* @param {ActivatedRouteSnapshot} route
* @param {RouterStateSnapshot} state
* @returns {Observable<any> | Promise<any> | any}
*/
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<any> | Promise<any> | any
{
return new Promise((resolve, reject) => {
Promise.all([
this.getContacts(),
this.getChats(),
this.getUser()
]).then(
([contacts, chats, user]) => {
this.contacts = contacts;
this.chats = chats;
this.user = user;
resolve();
},
reject
);
});
}
/**
* Get Contacts
* @returns {Promise<any>}
*/
getContacts(): Promise<any>
{
return new Promise((resolve, reject) => {
this.http.get('api/chat-contacts')
.subscribe(response => {
resolve(response.json().data);
}, reject);
});
}
/**
* Get Chats
* @returns {Promise<any>}
*/
getChats(): Promise<any>
{
return new Promise((resolve, reject) => {
this.http.get('api/chat-chats')
.subscribe(response => {
resolve(response.json().data);
}, reject);
});
}
/**
* Get User
* @returns {Promise<any>}
*/
getUser(): Promise<any>
{
return new Promise((resolve, reject) => {
this.http.get('api/chat-user')
.subscribe(response => {
resolve(response.json().data[0]);
}, reject);
});
}
/**
* Random ID Generator
* @returns {string}
*/
guidGenerator()
{
function S4()
{
return (((1 + Math.random()) * 0x10000) || 0).toString(16).substring(1);
}
return (S4() + S4());
}
}

View File

@ -0,0 +1,183 @@
<!-- SIDENAV HEADER -->
<div class="sidenav-header">
<!-- CHATS TOOLBAR -->
<md-toolbar>
<!-- TOOLBAR TOP -->
<div fxFlex fxLayout="row" fxLayoutAlign="space-between center">
<!-- USER AVATAR WRAPPER -->
<div class="avatar-wrapper">
<!-- USER AVATAR -->
<img (click)="changeLeftSidenavView('user')"
src="{{user.avatar}}"
class="md-avatar avatar"
alt="{{user.name}}"/>
<!-- / USER AVATAR -->
<md-icon class="s-16 status" [ngClass]="user.status" [mdMenuTriggerFor]="userStatusMenu"></md-icon>
<!-- USER STATUS -->
<md-menu id="user-status-menu" #userStatusMenu="mdMenu">
<button md-menu-item (click)="setUserStatus('online')">
<div fxLayout="row" fxLayoutAlign="start center">
<md-icon class="s-16 status online"></md-icon>
<span>Online</span>
</div>
</button>
<button md-menu-item (click)="setUserStatus('away')">
<div fxLayout="row" fxLayoutAlign="start center">
<md-icon class="s-16 status away"></md-icon>
<span>Away</span>
</div>
</button>
<button md-menu-item (click)="setUserStatus('do-not-disturb')">
<div fxLayout="row" fxLayoutAlign="start center">
<md-icon class="s-16 status do-not-disturb"></md-icon>
<span>Do not disturb</span>
</div>
</button>
<button md-menu-item (click)="setUserStatus('offline')">
<div fxLayout="row" fxLayoutAlign="start center">
<md-icon class="s-16 status offline"></md-icon>
<span>Offline</span>
</div>
</button>
</md-menu>
<!-- / USER STATUS -->
</div>
<!-- / USER AVATAR -->
<div>
<button md-button class="mat-icon-button"
[mdMenuTriggerFor]="userMenu"
aria-label="more">
<md-icon>more_vert</md-icon>
</button>
<md-menu #userMenu="mdMenu">
<button md-menu-item (click)="changeLeftSidenavView('user')">
Profile
</button>
<button md-menu-item (click)="logout()">
Logout
</button>
</md-menu>
</div>
</div>
<!-- / TOOLBAR TOP -->
<!-- TOOLBAR BOTTOM -->
<md-toolbar-row>
<!-- SEARCH -->
<div class="search-wrapper" fxFlex fxLayout="row" fxLayoutAlign="start center">
<div class="search" fxFlex fxLayout="row" fxLayoutAlign="start center">
<md-icon>search</md-icon>
<input [(ngModel)]="chatSearch.name" type="text" placeholder="Search or start new chat" fxFlex>
</div>
</div>
<!-- / SEARCH -->
</md-toolbar-row>
<!-- / TOOLBAR BOTTOM -->
</md-toolbar>
<!-- / CHATS TOOLBAR -->
</div>
<!-- / SIDENAV HEADER -->
<!-- SIDENAV CONTENT -->
<div class="sidenav-content" perfect-scrollbar fxFlex>
<!-- CHATS CONTENT -->
<div>
<!-- CHATS LIST-->
<div class="chat-list" fxLayout="column">
<div md-subheader *ngIf="(user.chatList | filterBy: chatSearch).length > 0">
Chats
</div>
<button md-button class="contact"
*ngFor="let chat of user.chatList | filterBy: chatSearch"
(click)="getChat(chat.contactId)" ngClass="{'unread':contact.unread}">
<div fxLayout="row" fxLayoutAlign="start center">
<div class="avatar-wrapper" fxFlex="0 1 auto" fxLayoutAlign="center center">
<img [src]="contacts |getById:chat.contactId:'avatar'"
class="avatar"
alt="{{contacts |getById:chat.contactId:'name'}}"/>
<md-icon class="s-16 status" [ngClass]="contacts |getById:chat.contactId:'status'"></md-icon>
</div>
<div fxLayout="row" fxFlex>
<div class="" fxFlex fxLayout="column" fxLayoutAlign="center start">
<div class="contact-name">{{contacts |getById:chat.contactId:'name'}}</div>
<p class="contact-last-message text-truncate">{{chat.lastMessage}}</p>
</div>
<div fxLayout="column" fxLayoutAlign="center end">
<div class="contact-last-message-time">
{{chat.lastMessageTime | date}}
</div>
<div *ngIf="chat.unread" class="unread-message-count">{{chat.unread}}</div>
</div>
</div>
</div>
</button>
</div>
<!-- / CHATS LIST-->
<!-- CONTACTS LIST-->
<div class="contact-list" fxLayout="column">
<div md-subheader *ngIf="(contacts| filterBy: chatSearch).length > 0">
Contacts
</div>
<button md-button class="contact"
ng-show="chatSearch"
*ngFor="let contact of contacts| filterBy: chatSearch"
(click)="getChat(contact.id)">
<div fxLayout="row" fxLayoutAlign="start center">
<div class="avatar-wrapper" fxFlex="0 1 auto">
<img src="{{contact.avatar}}" class="md-avatar avatar" alt="{{contact.name}}"/>
<md-icon class="s-16 status" [ngClass]="contact.status"></md-icon>
</div>
<div class="" fxLayout="column" fxLayoutAlign="center start">
<div class="contact-name">{{contact.name}}</div>
<p class="contact-mood">{{contact.mood}}</p>
</div>
</div>
</button>
</div>
<!-- / CONTACTS LIST-->
<!-- NO RESULTS MESSAGE -->
<div *ngIf="(contacts| filterBy: chatSearch).length === 0" class="no-results-message">
No results..
</div>
<!-- NO RESULTS MESSAGE-->
</div>
<!-- / CHATS CONTENT -->
</div>
<!-- / SIDENAV CONTENT -->

View File

@ -0,0 +1,110 @@
@import "src/app/core/scss/fuse";
:host {
display: flex;
flex: 1;
flex-direction: column;
.sidenav-header {
md-toolbar {
border-bottom: 1px solid rgba(0, 0, 0, .08);
.avatar-wrapper {
.avatar, .status {
cursor: pointer;
}
}
.search {
height: 36px;
line-height: 36px;
padding: 8px;
background: #FFFFFF;
font-size: 13px;
@include mat-elevation(1);
.icon {
margin: 0;
color: rgba(0, 0, 0, 0.54);
}
input {
padding-left: 12px;
height: 36px;
color: rgba(0, 0, 0, 0.54);
border: none;
}
}
}
}
.sidenav-content {
.contact-list, .chat-list {
.mat-subheader {
padding-left: 16px;
font-size: 20px;
font-weight: 300;
height: 88px;
line-height: 88px;
color: mat-color($accent);
}
.contact {
white-space: normal;
text-align: left;
letter-spacing: .010em;
min-height: 88px;
border-bottom: 1px solid rgba(0, 0, 0, 0.12);
padding: 16px;
font-weight: 400;
.avatar-wrapper {
.avatar {
margin-right: 16px;
}
}
.contact-name {
font-size: 16px;
white-space: nowrap;
text-overflow: ellipsis;
}
.contact-last-message {
line-height: 1.6em;
margin: 0;
font-weight: 500;
color: rgba(0, 0, 0, 0.54);
}
.contact-mood {
}
.unread-message-count {
border-radius: 50%;
text-align: center;
width: 24px;
height: 24px;
line-height: 24px;
background-color: mat-color($accent);
color: map-get($accent, default-contrast);
}
}
}
.no-results-message {
position: absolute;
width: 100%;
height: 88px;
padding: 16px;
background: #FFFFFF;
font-size: 15px;
font-weight: 400;
}
}
}

View File

@ -0,0 +1,66 @@
import { Component, OnInit } from '@angular/core';
import { ChatService } from '../../../chat.service';
import { FuseMdSidenavHelperService } from '../../../../../../core/directives/md-sidenav-helper/md-sidenav-helper.service';
import { ObservableMedia } from '@angular/flex-layout';
@Component({
selector : 'fuse-chat-chats-sidenav',
templateUrl: './chats.component.html',
styleUrls : ['./chats.component.scss']
})
export class ChatsSidenavComponent implements OnInit
{
user: any;
chats: any[];
contacts: any[];
chatSearch: any;
constructor(private chatService: ChatService,
private fuseMdSidenavService: FuseMdSidenavHelperService,
public media: ObservableMedia)
{
this.chatSearch = {
name: ''
};
}
ngOnInit()
{
this.user = this.chatService.user;
this.chats = this.chatService.chats;
this.contacts = this.chatService.contacts;
this.chatService.onChatsUpdated.subscribe(updatedChats => {
this.chats = updatedChats;
});
this.chatService.onUserUpdated.subscribe(updatedUser => {
this.user = updatedUser;
});
}
getChat(contact)
{
this.chatService.getChat(contact);
if ( !this.media.isActive('gt-md') )
{
this.fuseMdSidenavService.getSidenav('chat-left-sidenav').toggle();
}
}
setUserStatus(status)
{
this.chatService.setUserStatus(status);
}
changeLeftSidenavView(view)
{
this.chatService.onLeftSidenavViewChanged.next(view);
}
logout()
{
console.info('logout triggered');
}
}

View File

@ -0,0 +1,11 @@
<div [ngSwitch]="view" class="views">
<fuse-chat-chats-sidenav class="view"
*ngSwitchCase="'chats'"
[@slideInRight]>
</fuse-chat-chats-sidenav>
<fuse-chat-user-sidenav class="view"
*ngSwitchCase="'user'"
[@slideInLeft]>
</fuse-chat-user-sidenav>
</div>

View File

@ -0,0 +1,20 @@
:host {
display: flex;
flex-direction: column;
height: 100%;
.views {
display: flex;
flex-direction: column;
height: 100%;
.view {
position: absolute;
height: 100%;
bottom: 0;
left: 0;
right: 0;
top: 0;
}
}
}

View File

@ -0,0 +1,27 @@
import { Component, OnInit } from '@angular/core';
import { Animations } from '../../../../../core/animations';
import { ChatService } from '../../chat.service';
@Component({
selector : 'fuse-chat-left-sidenav',
templateUrl: './left.component.html',
styleUrls : ['./left.component.scss'],
animations : [Animations.slideInLeft, Animations.slideInRight]
})
export class LeftSidenavComponent implements OnInit
{
view: string;
constructor(private chatService: ChatService)
{
this.view = 'chats';
}
ngOnInit()
{
this.chatService.onLeftSidenavViewChanged.subscribe(view => {
this.view = view;
});
}
}

View File

@ -0,0 +1,78 @@
<!-- SIDENAV HEADER -->
<div class="sidenav-header">
<!-- USER TOOLBAR -->
<md-toolbar>
<!-- TOOLBAR TOP -->
<div fxFlex fxLayout="row" fxLayoutAlign="space-between center">
<button md-button class="mat-icon-button" (click)="changeLeftSidenavView('chats')" aria-label="back">
<md-icon>arrow_back</md-icon>
</button>
</div>
<!-- / TOOLBAR TOP -->
<!-- TOOLBAR BOTTOM -->
<md-toolbar-row class="toolbar-bottom" fxLayout="column" fxLayoutAlign="center center">
<img [src]="user.avatar" class="avatar user-avatar huge" alt="{{user.name}}"/>
<div class="user-name my-8">{{user.name}}</div>
</md-toolbar-row>
<!-- / TOOLBAR BOTTOM -->
</md-toolbar>
<!-- / USER TOOLBAR -->
</div>
<!-- SIDENAV CONTENT -->
<div class="sidenav-content p-16" perfect-scrollbar fxFlex>
<!-- USER MOOD -->
<md-card>
<form [formGroup]="userForm" fxLayout="column">
<md-input-container class="mb-24" fxFlex>
<textarea mdInput placeholder="Mood" name="mood"
formControlName="mood" rows="3"></textarea>
</md-input-container>
<md-radio-group formControlName="status" fxLayout="column">
<md-radio-button value="online" class="py-8">
<div fxLayout="row" fxLayoutAlign="start center">
<md-icon class="status online mr-8"></md-icon>
<span class="mat-h4 m-0">Online</span>
</div>
</md-radio-button>
<md-radio-button value="away" class="py-8">
<div fxLayout="row" fxLayoutAlign="start center">
<md-icon class="status away mr-8"></md-icon>
<span class="mat-h4 m-0">Away</span>
</div>
</md-radio-button>
<md-radio-button value="do-not-disturb" class="py-8">
<div fxLayout="row" fxLayoutAlign="start center">
<md-icon class="status do-not-disturb mr-8"></md-icon>
<span class="mat-h4 m-0">Do not disturb</span>
</div>
</md-radio-button>
<md-radio-button value="offline" class="py-8">
<div fxLayout="row" fxLayoutAlign="start center">
<md-icon class="status offline mr-8"></md-icon>
<span class="mat-h4 m-0">Offline</span>
</div>
</md-radio-button>
</md-radio-group>
</form>
</md-card>
<!-- / USER MOOD -->
</div>

View File

@ -0,0 +1,21 @@
@import "src/app/core/scss/fuse";
:host {
display: flex;
flex: 1;
flex-direction: column;
md-toolbar {
background-color: mat-color($accent);
color: map-get($accent, default-contrast);
.toolbar-bottom {
height: 240px;
}
}
.sidenav-content{
background: whitesmoke;
}
}

View File

@ -0,0 +1,47 @@
import { Component, OnDestroy, OnInit } from '@angular/core';
import { ChatService } from '../../../chat.service';
import { FormControl, FormGroup } from '@angular/forms';
import 'rxjs/Rx';
@Component({
selector : 'fuse-chat-user-sidenav',
templateUrl: './user.component.html',
styleUrls : ['./user.component.scss']
})
export class UserSidenavComponent implements OnInit, OnDestroy
{
user: any;
onFormChange: any;
userForm: FormGroup;
constructor(private chatService: ChatService)
{
this.user = this.chatService.user;
this.userForm = new FormGroup({
mood : new FormControl(this.user.mood),
status: new FormControl(this.user.status)
});
}
ngOnInit()
{
this.onFormChange = this.userForm.valueChanges
.debounceTime(500)
.distinctUntilChanged()
.subscribe(data => {
this.user.mood = data.mood;
this.user.status = data.status;
this.chatService.updateUserData(this.user);
});
}
changeLeftSidenavView(view)
{
this.chatService.onLeftSidenavViewChanged.next(view);
}
ngOnDestroy()
{
this.onFormChange.unsubscribe();
}
}

View File

@ -0,0 +1,47 @@
<!-- SIDENAV HEADER -->
<div class="sidenav-header" *ngIf="contact">
<!-- CONTACT TOOLBAR -->
<md-toolbar>
<!-- TOOLBAR TOP -->
<div fxFlex fxLayout="row" fxLayoutAlign="space-between center">
<div>Contact Info</div>
<button md-button class="mat-icon-button" fuseMdSidenavToggler="chat-right-sidenav" aria-label="close">
<md-icon>close</md-icon>
</button>
</div>
<!-- / TOOLBAR TOP -->
<!-- TOOLBAR BOTTOM -->
<md-toolbar-row class="toolbar-bottom" fxLayout="column" fxLayoutAlign="center center">
<img [src]="contact.avatar" class="avatar contact-avatar huge" alt="{{contact.name}}"/>
<div class="contact-name my-8">{{contact.name}}</div>
</md-toolbar-row>
<!-- / TOOLBAR BOTTOM -->
</md-toolbar>
<!-- / CONTACT TOOLBAR -->
</div>
<!-- SIDENAV CONTENT -->
<div class="sidenav-content p-16" perfect-scrollbar fxFlex *ngIf="contact">
<!-- CONTACT MOOD -->
<md-card>
<md-input-container fxFlex>
<textarea mdInput placeholder="Mood" name="mood"
[value]="contact.mood" rows="3" disabled>
</textarea>
</md-input-container>
</md-card>
<!-- / CONTACT MOOD -->
</div>

View File

@ -0,0 +1,21 @@
@import "src/app/core/scss/fuse";
:host {
display: flex;
flex: 1;
flex-direction: column;
md-toolbar {
background-color: mat-color($accent);
color: map-get($accent, default-contrast);
.toolbar-bottom {
height: 240px;
}
}
.sidenav-content{
background: whitesmoke;
}
}

View File

@ -0,0 +1,25 @@
import { Component, OnInit } from '@angular/core';
import { ChatService } from '../../../chat.service';
@Component({
selector : 'fuse-chat-contact-sidenav',
templateUrl: './contact.component.html',
styleUrls : ['./contact.component.scss']
})
export class ContactSidenavComponent implements OnInit
{
contact: any;
constructor(private chatService: ChatService)
{
}
ngOnInit()
{
this.chatService.onContactSelected.subscribe(contact => {
this.contact = contact;
});
}
}

View File

@ -0,0 +1,8 @@
<div [ngSwitch]="view" class="views">
<fuse-chat-contact-sidenav class="view"
*ngSwitchCase="'contact'"
[@slideInRight]>
</fuse-chat-contact-sidenav>
</div>

View File

@ -0,0 +1,20 @@
:host {
display: flex;
flex-direction: column;
height: 100%;
.views {
display: flex;
flex-direction: column;
height: 100%;
.view {
position: absolute;
height: 100%;
bottom: 0;
left: 0;
right: 0;
top: 0;
}
}
}

View File

@ -0,0 +1,27 @@
import { Component, OnInit } from '@angular/core';
import { Animations } from '../../../../../core/animations';
import { ChatService } from '../../chat.service';
@Component({
selector : 'fuse-chat-right-sidenav',
templateUrl: './right.component.html',
styleUrls : ['./right.component.scss'],
animations : [Animations.slideInLeft, Animations.slideInRight]
})
export class RightSidenavComponent implements OnInit
{
view: string;
constructor(private chatService: ChatService)
{
this.view = 'contact';
}
ngOnInit()
{
this.chatService.onRightSidenavViewChanged.subscribe(view => {
this.view = view;
});
}
}

View File

@ -26,7 +26,7 @@
"label-position": true,
"max-line-length": [
true,
120
180
],
"member-access": false,
"member-ordering": [