Skip to content

Commit

Permalink
feat(notifications): added field: native to Notification model
Browse files Browse the repository at this point in the history
  • Loading branch information
xmlking committed Nov 18, 2018
1 parent bacfc3e commit 12fa080
Show file tree
Hide file tree
Showing 23 changed files with 1,215 additions and 879 deletions.
12 changes: 6 additions & 6 deletions .deploy/api/Dockerfile
@@ -1,23 +1,23 @@
FROM node:11 as api-build-dependencies
FROM node:alpine as dev-dependencies
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci

FROM node:11 as api-production-dependencies
FROM node:alpine as prod-dependencies
WORKDIR /app
COPY package.api.json package.json
RUN npm i --production

FROM node:11 as api-builder
FROM node:alpine as builder
WORKDIR /app
COPY --from=api-build-dependencies /app /app
COPY --from=dev-dependencies /app /app
COPY apps/api apps/api
COPY angular.json nx.json tsconfig.json ./
ENV NODE_ENV production
RUN npm run api:build

FROM astefanutti/scratch-node:11
COPY --from=api-production-dependencies /app .
COPY --from=api-builder /app/dist/apps/api/main.js .
COPY --from=prod-dependencies /app .
COPY --from=builder /app/dist/apps/api/main.js .
EXPOSE 3000
ENTRYPOINT ["./node", "main.js"]
3 changes: 3 additions & 0 deletions .dockerignore
Expand Up @@ -3,6 +3,9 @@ dist
tmp
sme

.git/
.gitignore

# dependencies
node_modules
bower_components
Expand Down
5 changes: 2 additions & 3 deletions PLAYBOOK.md
Expand Up @@ -207,11 +207,10 @@ npm i -D lint-staged

> update 3rd party modules/schematics
```bash
ng update @angular/core
ng update @angular/core@next
ng update @angular/cli@next
ng update @angular/core
ng update @angular/material --force
ng update @angular/pwa
ng update @angular/pwa@next
ng update @ngx-formly/schematics --ui-theme=material
ng update @nrwl/schematics --force
```
Expand Down
5 changes: 5 additions & 0 deletions angular.json
Expand Up @@ -254,6 +254,11 @@
"builder": "@nrwl/builders:node-execute",
"options": {
"buildTarget": "api:build"
},
"configurations": {
"production": {
"buildTarget": "api:build:production"
}
}
},
"lint": {
Expand Down
2 changes: 2 additions & 0 deletions apps/api/README.md
Expand Up @@ -46,6 +46,8 @@ ng serve api
# to turn on logging for `request`
NODE_DEBUG=request ng serve api
DEBUG=typeorm:* ng serve api
# optinally you can run with prod env(environment.prod.ts) for tesrting! use this for testing only.
ng serve api --prod
```

#### Run Prod Mode
Expand Down
5 changes: 5 additions & 0 deletions apps/api/src/notifications/dto/create-notification.dto.ts
Expand Up @@ -31,4 +31,9 @@ export class CreateNotificationDto {
@MinLength(3)
@MaxLength(20)
userId: string;

@ApiModelProperty({ type: Boolean, default: false })
@IsBoolean()
@IsNotEmpty()
public native = false;
}
9 changes: 8 additions & 1 deletion apps/api/src/notifications/notification.entity.ts
Expand Up @@ -36,7 +36,7 @@ export class Notification extends Base {
@CreateDateColumn()
createdAt?: Date;

@ApiModelProperty({ type: String })
@ApiModelProperty({ type: Boolean, default: false })
@IsBoolean()
@IsNotEmpty()
@Index()
Expand All @@ -57,4 +57,11 @@ export class Notification extends Base {
@Index()
@Column()
userId: string;

@ApiModelProperty({ type: Boolean, default: false })
@IsBoolean()
@IsNotEmpty()
@Index()
@Column({ default: false})
native: boolean;
}
2 changes: 0 additions & 2 deletions libs/core/src/lib/services/page-title.service.spec.ts
Expand Up @@ -14,7 +14,5 @@ describe('PageTitleService', () => {

it('should initialize title to empty string', inject([PageTitleService], (service: PageTitleService) => {
expect(service).toBeTruthy();
expect(service._title).toEqual('');
expect(service.title).toEqual('');
}));
});
3 changes: 1 addition & 2 deletions libs/core/src/lib/services/page-title.service.ts
@@ -1,6 +1,5 @@
import { Injectable } from '@angular/core';
import { Title } from '@angular/platform-browser';
import { Store } from '@ngxs/store';

/**
* EventBus will use this service
Expand All @@ -11,7 +10,7 @@ import { Store } from '@ngxs/store';
export class PageTitleService {
private readonly defaultTitle;

constructor(private store: Store, private bodyTitle: Title) {
constructor(private bodyTitle: Title) {
this.defaultTitle = bodyTitle.getTitle() || 'WebApp';
}

Expand Down
2 changes: 1 addition & 1 deletion libs/grid/src/lib/services/account.service.spec.ts
Expand Up @@ -5,7 +5,7 @@ import { AccountService } from './account.service';
describe('AccountService', () => {
beforeEach(() => {
TestBed.configureTestingModule({
providers: [Account1Service],
providers: [AccountService],
});
});

Expand Down
2 changes: 2 additions & 0 deletions libs/notifications/src/index.ts
@@ -1,3 +1,5 @@
export * from './lib/notifications.module';
export * from './lib/notifications.actions';
export * from './lib/notifications.service';
export * from './lib/app-notification.model';
export * from './lib/notifications.state';
@@ -1,11 +1,11 @@
import { Moment } from 'moment';
import { Entity } from '@ngx-starter-kit/shared';

export class Notification extends Entity {
public id: string;
export class AppNotification extends Entity {
public id: number;
public icon: 'notifications' | 'notifications_active' | 'shopping_basket' | 'eject' | 'cached' | 'code';
public message: string;
public createdAt: Moment;
public createdAt: Date;
public read: boolean;
public color: 'warn' | 'accent' | 'primary';
public native: boolean;
}
10 changes: 5 additions & 5 deletions libs/notifications/src/lib/notifications.actions.ts
@@ -1,24 +1,24 @@
import { Notification } from './notification.model';
import { AppNotification } from './app-notification.model';

// Actions
export class FetchNotifications {
static readonly type = '[Notifications] Fetch';
}

// new AddNotification({ id: 6, icon: 'notifications', message: 'sumo',createdAt: new Date(Date.now() - 864e5), read: false, native: true}),
export class AddNotification {
static readonly type = '[Notifications] Add';
constructor(public readonly payload: Notification) {}
constructor(public readonly payload: AppNotification) {}
}

export class DeleteNotification {
static readonly type = '[Notifications] Delete';

constructor(public readonly payload: Notification) {}
constructor(public readonly payload: AppNotification) {}
}

export class MarkAsRead {
static readonly type = '[Notifications] MarkAsRead';
constructor(public readonly payload: Notification) {}
constructor(public readonly payload: AppNotification) {}
}

export class MarkAllAsRead {
Expand Down
2 changes: 1 addition & 1 deletion libs/notifications/src/lib/notifications.component.html
Expand Up @@ -25,7 +25,7 @@
<ng-container *ngIf="notifications?.length !== 0; then thenBlock else elseBlock;"></ng-container>
<ng-template #thenBlock>
<perfect-scrollbar class="content">
<ng-container *ngFor="let notification of notifications; last as isLast">
<ng-container *ngFor="let notification of notifications; last as isLast; trackBy: trackById">
<div class="notification"
(click)="markAsRead(notification)"
[ngClass]="notification.color" [class.read]="notification.read"
Expand Down
16 changes: 9 additions & 7 deletions libs/notifications/src/lib/notifications.component.ts
Expand Up @@ -2,11 +2,11 @@ import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
import { Select, Store } from '@ngxs/store';
import { listFadeAnimation } from '@ngx-starter-kit/animations';

import { Observable } from 'rxjs';
import { Notification } from './notification.model';
import { NotificationsState } from './notifications.state';
import { DeleteNotification, FetchNotifications, MarkAllAsRead, MarkAsRead } from './notifications.actions';
import { SendWebSocketAction } from '@ngx-starter-kit/socketio-plugin';
import { Observable } from 'rxjs';
import { NotificationsState } from './notifications.state';
import { AppNotification } from './app-notification.model';

@Component({
selector: 'ngx-notifications',
Expand All @@ -16,10 +16,8 @@ import { SendWebSocketAction } from '@ngx-starter-kit/socketio-plugin';
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class NotificationsComponent implements OnInit {
@Select(NotificationsState)
notifications$: Observable<Notification>;
@Select(NotificationsState.unReadCount)
unReadCount$: Observable<number>;
@Select(NotificationsState) notifications$: Observable<AppNotification>;
@Select(NotificationsState.unReadCount) unReadCount$: Observable<number>;
isOpen: boolean;

constructor(private store: Store) {}
Expand Down Expand Up @@ -50,4 +48,8 @@ export class NotificationsComponent implements OnInit {
markAllAsRead() {
this.store.dispatch(new MarkAllAsRead());
}

trackById(index: number, item: AppNotification) {
return item.id;
}
}
2 changes: 1 addition & 1 deletion libs/notifications/src/lib/notifications.module.ts
Expand Up @@ -4,7 +4,7 @@ import { NgxsModule } from '@ngxs/store';

import { SharedModule } from '@ngx-starter-kit/shared';
import { NotificationsState } from './notifications.state';
import { NotificationsService } from './notifications.service';

@NgModule({
imports: [SharedModule, NgxsModule.forFeature([NotificationsState])],
declarations: [NotificationsComponent],
Expand Down
39 changes: 16 additions & 23 deletions libs/notifications/src/lib/notifications.service.ts
@@ -1,7 +1,7 @@
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { environment } from '@env/environment';
import { Notification as AppNotification } from './notification.model';
import { AppNotification } from './app-notification.model';
import { EntityService } from '@ngx-starter-kit/shared';
import { Observable } from 'rxjs';
import { catchError, finalize, map, retry } from 'rxjs/operators';
Expand All @@ -18,28 +18,6 @@ export class NotificationsService extends EntityService<AppNotification> {
super(httpClient);
}

// TODO: Move to state
// this.showNotification('PWA Workshop', 'Hello audience! Nice to meet you!', null, true);

async showNotification(title: string, message: string, button?: string, showNative = false) {
if (showNative && this.featureService.detectFeature(BrowserFeatureKey.NotificationsAPI).supported
&& !this.featureService.isMobileAndroid()) {
const permission = await Notification.requestPermission();
if (permission === 'granted') {
const notification = new Notification(title, {
body: message
});
notification.onclick = () => {
console.log('clicked todo: mark as read');
};
}
// this.showInAppNotification(title, message, button);
}
}
// private showInAppNotification(title: string, message: string, button: string) {
// this.notifications.next(new AppNotification(title, message, button, id));
// }

getAll(): Observable<AppNotification[]> {
this.loadingSubject.next(true);
return this.httpClient.get<[AppNotification[], number]>(`${this.baseUrl}/${this.entityPath}`).pipe(
Expand All @@ -50,4 +28,19 @@ export class NotificationsService extends EntityService<AppNotification> {
map(data => data[0]),
);
}

async showNativeNotification(noti: { title: string; options?: Partial<NotificationOptions> }) {
if (
this.featureService.detectFeature(BrowserFeatureKey.NotificationsAPI).supported &&
!this.featureService.isMobileAndroid()
) {
const permission = await Notification.requestPermission();
if (permission === 'granted') {
const notification = new Notification(noti.title, noti.options);
notification.onclick = () => {
console.log('clicked todo: mark as read');
};
}
}
}
}
26 changes: 16 additions & 10 deletions libs/notifications/src/lib/notifications.state.ts
@@ -1,5 +1,5 @@
import { Action, NgxsOnInit, Selector, State, StateContext } from '@ngxs/store';
import { Notification } from './notification.model';
import { AppNotification } from './app-notification.model';
import { tap } from 'rxjs/operators';
import { NotificationsService } from './notifications.service';
import {
Expand All @@ -10,41 +10,47 @@ import {
MarkAllAsRead,
} from './notifications.actions';

@State<Notification[]>({
@State<AppNotification[]>({
name: 'notifications',
defaults: [],
})
export class NotificationsState implements NgxsOnInit {
constructor(private notificationsService: NotificationsService) {}

@Selector()
static unReadCount(state: Notification[]) {
static unReadCount(state: AppNotification[]) {
return state.filter(note => !note.read).length;
}

ngxsOnInit(ctx: StateContext<Notification[]>) {
ngxsOnInit(ctx: StateContext<AppNotification[]>) {
console.log('State initialized, now getting Notifications.');
// well, this way be call be called before dashboard is routed due to preloadingStrategy. so lets use ngOnInit on component
/**
* well, this way, it will be called before dashboard is routed due to preloadingStrategy.
* we will loose lazy loading benefits. so lets use ngOnInit on component to load initial data.
*/
// ctx.dispatch(new FetchNotifications())
}

@Action(AddNotification)
add({ getState, setState, patchState }: StateContext<Notification[]>, { payload }: AddNotification) {
add({ getState, setState, patchState }: StateContext<AppNotification[]>, { payload }: AddNotification) {
setState([...getState(), payload]);
if (payload.native) {
return this.notificationsService.showNativeNotification({title: payload.message, options: { body: payload.message }});
}
}

@Action(FetchNotifications, { cancelUncompleted: true })
fetchNotifications({ getState, patchState, setState }: StateContext<Notification[]>) {
fetchNotifications({ getState, patchState, setState }: StateContext<AppNotification[]>) {
return this.notificationsService.getAll().pipe(tap(res => setState(res)));
}

@Action(DeleteNotification)
delete({ getState, setState, patchState }: StateContext<Notification[]>, { payload }: AddNotification) {
delete({ getState, setState, patchState }: StateContext<AppNotification[]>, { payload }: AddNotification) {
setState(getState().filter(note => note !== payload));
}

@Action(MarkAsRead)
markAsRead({ getState, setState, patchState }: StateContext<Notification[]>, { payload }: MarkAsRead) {
markAsRead({ getState, setState, patchState }: StateContext<AppNotification[]>, { payload }: MarkAsRead) {
setState(
getState().map(notification => {
if (notification === payload) {
Expand All @@ -56,7 +62,7 @@ export class NotificationsState implements NgxsOnInit {
}

@Action(MarkAllAsRead)
markAllAsRead({ getState, setState, patchState }: StateContext<Notification[]>) {
markAllAsRead({ getState, setState, patchState }: StateContext<AppNotification[]>) {
setState(
getState().map(notification => {
notification.read = true;
Expand Down
2 changes: 1 addition & 1 deletion libs/notifications/src/lib/push-notification.service.ts
Expand Up @@ -23,7 +23,7 @@ export class PushNotificationService {
}

// Key generation: https://web-push-codelab.glitch.me
const subscription = await this.swPush.requestSubscription({ serverPublicKey: environment.serverPublicKey });
const subscription = await this.swPush.requestSubscription({ serverPublicKey: environment.webPush.publicVapidKey });
console.log('Push subscription endpoint: ', subscription.endpoint);
this.pushSubscription = subscription;
// this.apiService.post('push/register', subscription).subscribe();
Expand Down
2 changes: 1 addition & 1 deletion libs/toolbar/src/lib/toolbar.component.html
Expand Up @@ -18,7 +18,7 @@

<ngx-notifications class="button" fxHide fxShow.gt-sm></ngx-notifications>

<!--<theme-picker class="button" fxShow fxHide.lt-md></theme-picker>-->
<!-- <theme-picker class="button" fxShow fxHide.lt-md></theme-picker> -->

<ngx-user-menu class="button" [currentUser]="profile$ | async"></ngx-user-menu>

Expand Down

0 comments on commit 12fa080

Please sign in to comment.