Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 22 additions & 21 deletions src/app/app.component.html
Original file line number Diff line number Diff line change
@@ -1,23 +1,24 @@
<mat-toolbar color="primary" class="flex flex-row justify-between items-center">
<span>Angular Knock Notifications</span>
<span class="example-spacer"></span>
<mat-icon *ngIf="count > 0" [matBadge]="count" matBadgeColor="warn" [matMenuTriggerFor]="menu">notifications</mat-icon>
<mat-icon *ngIf="count === 0"[matMenuTriggerFor]="menu">notifications</mat-icon>
</mat-toolbar>
<ng-container *ngIf="{
unread: (unreadNotificationCount$ | async),
notifications: (notifications$ | async)
} as data">
<mat-toolbar color="primary" class="flex flex-row justify-between items-center">
<span>Angular Knock Notifications</span>
<span class="example-spacer"></span>
<ng-container *ngIf="data.unread; else noNotifications">
<mat-icon [matBadge]="data.unread" matBadgeColor="warn" [matMenuTriggerFor]="menu">notifications</mat-icon>
</ng-container>
<ng-template #noNotifications>
<mat-icon [matMenuTriggerFor]="menu">notifications</mat-icon>
</ng-template>
</mat-toolbar>

<mat-menu #menu="matMenu" class="w-96 h-96" >
<div class="flex flex-col justify-start items-start p-2">
<div class="flex flex-row justify-between items-center w-full sticky top-0 z-10 bg-white">
<h2 class="!m-0">Notifications</h2>
<button *ngIf="notifications?.length" (click)="service.markAllNotificationsAsRead()" mat-button>Mark all as read</button>
</div>
<app-notifications-feed *ngIf="notifications?.length" [notifications]="notifications" class="w-full"></app-notifications-feed>
<div class="flex flex-col justify-center items-center p-2 w-full">
<h3>
No New Notifications
</h3>
<h4>Check back later</h4>
</div>
</div>
</mat-menu>
<mat-menu #menu="matMenu" class="w-96 h-96">
<ng-container>
<app-notifications-feed [notifications]="data.notifications ?? []" (markAllAsRead)="markAllAsRead()"
class="w-full"></app-notifications-feed>
</ng-container>
</mat-menu>


</ng-container>
33 changes: 9 additions & 24 deletions src/app/app.component.ts
Original file line number Diff line number Diff line change
@@ -1,37 +1,22 @@
import { Component, OnDestroy, OnInit } from '@angular/core';
import { Subject, takeUntil } from 'rxjs';
import { Notifications } from 'src/models/notifications.models';
import { Component, OnInit } from '@angular/core';
import { KnockNotificationService } from 'src/services/knock-notification.service';

@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss']
})
export class AppComponent implements OnInit, OnDestroy {
public count = 0;
public notifications?: Notifications[];
private onDestroy$ = new Subject();
constructor(public service: KnockNotificationService){
this.service.connectToKnockFeed('USER_ID')
}
export class AppComponent implements OnInit {
unreadNotificationCount$ = this.knockService.unreadNotificationCount$;
notifications$ = this.knockService.notifications$.asObservable();

ngOnInit(): void {
this.service.notificationCount$.pipe(takeUntil(this.onDestroy$)).subscribe(response => {
if (response) {
this.count = response;
}
});
constructor(public knockService: KnockNotificationService){}

this.service.notifications$.pipe(takeUntil(this.onDestroy$)).subscribe(response => {
if (response) {
this.notifications = response;
}
});
ngOnInit() {
this.knockService.connectToKnockFeed('USER_ID');
}

ngOnDestroy(): void {
this.onDestroy$.next(true);
this.onDestroy$.complete();
markAllAsRead() {
this.knockService.markAllNotificationsAsRead();
}
}
2 changes: 1 addition & 1 deletion src/app/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { BrowserModule } from '@angular/platform-browser';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { NotificationsFeedComponent } from './notifications-feed/notifications-feed.component';
import { NotificationsFeedComponent } from './components/notifications-feed/notifications-feed.component';
import { MatToolbarModule } from '@angular/material/toolbar';
import { MatIconModule } from '@angular/material/icon';
import { MatBadgeModule } from '@angular/material/badge';
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
import { KnockBasicNotification } from '../../../models/notifications.models';
import { NgIf } from '@angular/common';
import { MatCardModule } from '@angular/material/card';

@Component({
standalone: true,
selector: 'app-notification-feed-item',
template: `
<mat-card class="w-full" *ngIf="notification">
<mat-card-content>
<div class="flex flex-row justify-start items-center p-2 w-full">
<div class="flex flex-col justify-start items-start p-2 w-full">
<h3>
{{ notification.action || 'Action here' }}
</h3>
<div class="w-full italic">{{ notification.timeStamp }}</div>
</div>

<div *ngIf="!notification.readAt" class="circle"></div>

</div>
</mat-card-content>
</mat-card>`,
styles: [
`.circle {
width: 15px;
height: 15px;
border-radius: 50%;
background-color: #f39c12;
}`
],
imports: [
MatCardModule,
NgIf
],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class NotificationFeedItemComponent {
@Input() notification: KnockBasicNotification | undefined;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<div class="w-full flex flex-col justify-start items-start p-2">
<div class="flex flex-row justify-between items-center w-full sticky top-0 z-10 bg-white">
<h2 class="!m-0">Notifications</h2>
<button *ngIf="notifications?.length" (click)="markAllAsRead.emit()" mat-button>Mark all as read</button>
</div>

<div class="feed-container w-full">
<ng-container *ngIf="notifications.length > 0; else noNotifications">
<app-notification-feed-item
class="w-full"
*ngFor="let notification of notifications"
[notification]="notification"
></app-notification-feed-item>
</ng-container>
</div>

<ng-template #noNotifications>
<div class="flex flex-col justify-center items-center p-2 w-full">
<h3>
No New Notifications
</h3>
<h4>Check back later</h4>
</div>
</ng-template>
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { MatButtonModule } from '@angular/material/button';
import { MatIconModule } from '@angular/material/icon';
import { CommonModule } from '@angular/common';
import { Component, EventEmitter, Input, Output } from '@angular/core';
import { MatCardModule } from '@angular/material/card';
import { KnockBasicNotification } from 'src/models/notifications.models';
import { NotificationFeedItemComponent } from "../notification-feed-item/notification-feed-item.component";

@Component({
standalone: true,
imports: [CommonModule, MatCardModule, MatIconModule, MatButtonModule, NotificationFeedItemComponent],
selector: 'app-notifications-feed',
templateUrl: './notifications-feed.component.html',
styles: [`
app-notification-feed-item {
margin-bottom: 10px;
display: block;
}
`]
})
export class NotificationsFeedComponent {
@Input() notifications: KnockBasicNotification[] = [];

@Output() markAllAsRead = new EventEmitter();
}
17 changes: 0 additions & 17 deletions src/app/notifications-feed/notifications-feed.component.html

This file was deleted.

6 changes: 0 additions & 6 deletions src/app/notifications-feed/notifications-feed.component.scss

This file was deleted.

17 changes: 0 additions & 17 deletions src/app/notifications-feed/notifications-feed.component.ts

This file was deleted.

Binary file added src/assets/icons/favicon-16x16.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/assets/icons/favicon-32x32.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/assets/icons/favicon.ico
Binary file not shown.
6 changes: 5 additions & 1 deletion src/environments/environment.prod.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
export const environment = {
production: true
production: true,
knock: {
clientId: 'CLIENT_KEY',
feedKey: 'FEED_KEY'
},
};
6 changes: 5 additions & 1 deletion src/environments/environment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,11 @@
// The list of file replacements can be found in `angular.json`.

export const environment = {
production: false
production: false,
knock: {
clientId: 'CLIENT_KEY',
feedKey: 'FEED_KEY'
},
};

/*
Expand Down
29 changes: 29 additions & 0 deletions src/functions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import {KnockBasicNotification, KnockDetailedNotification} from "./models/notifications.models";

// Helps you to create an example Notification for testing purposes.
export const createMockNotification = (partial: Partial<KnockBasicNotification>): KnockBasicNotification => ({
...partial,
action: '',
id: '',
timeStamp: '',
readAt: '',
})

export const convertDetailedToBasic = (notification: KnockDetailedNotification): KnockBasicNotification => ({
id: notification.id,
timeStamp: getTimeInterval(new Date(notification.inserted_at)),
action: notification.data?.action ?? '',
readAt: notification.read_at
});

export const getTimeInterval = (date: Date): string => {
const diff: number = Math.floor((new Date().getTime() - date.getTime()) / 1000);
const intervals: [number, string][] = [
[60, `${diff} second${diff === 1 ? '' : 's'} ago`],
[3600, `${Math.floor(diff / 60)} minute${Math.floor(diff / 60) === 1 ? '' : 's'} ago`],
[86400, `${Math.floor(diff / 3600)} hour${Math.floor(diff / 3600) === 1 ? '' : 's'} ago`],
[Infinity, `${Math.floor(diff / 86400)} day${Math.floor(diff / 86400) === 1 ? '' : 's'} ago`]
];
const [interval, label]: [number, string] = intervals.find(([interval, _]) => diff < interval)!;
return label;
}
19 changes: 11 additions & 8 deletions src/index.html
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>KnockAngularInAppFeed</title>
<base href="/">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/x-icon" href="favicon.ico">
<link rel="preconnect" href="https://fonts.gstatic.com">
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
<meta charset="utf-8">
<title>Angular In-App Notification | Knock</title>
<base href="/">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/x-icon" href="assets/icons/favicon.ico">
<link rel="preconnect" href="https://fonts.gstatic.com">
<link rel="icon" type="image/png" href="assets/icons/favicon-32x32.png" sizes="32x32"/>
<link rel="icon" type="image/png" href="assets/icons/favicon-16x16.png" sizes="16x16"/>

<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
</head>
<body class="mat-typography">
<app-root></app-root>
Expand Down
78 changes: 76 additions & 2 deletions src/models/notifications.models.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,80 @@
export interface Notifications {
export interface KnockBasicNotification<T = string> {
id: string;
action: string;
timeStamp: string;
readAt: boolean;
readAt: T | null;
}

export interface KnockDetailedNotification<T = string> extends KnockBasicNotification<T> {
activities: KnockActivity[];
actors: KnockActor[];
archived_at: T | null;
seen_at: T | null;
inserted_at: T;
total_activities: number;
total_actors: number;
read_at: T | null;
link_clicked_at: T | null;
blocks: KnockBlock[];
tenant?: string;
data: KnockData;
}

export interface KnockBlock {
content: string;
name: string;
rendered: string;
type: 'markdown' | 'html';
}

export interface KnockData {
action?: string;
}

export interface KnockActivity<T = string> {
id: string;
inserted_at: T;
updated_at: T | null;
data: KnockData;
recipient: KnockActor;
}

export interface KnockActor<T = string> {
id: string;
name: string;
userProfilePicture: string | null;
locale: string;
created_at: T | null;
updated_at: T | null;
}

export interface KnockFeedResponse {
status: 'ok' | 'error';
data: KnockFeedData;
}

export interface KnockFeedData {
entries?: KnockDetailedNotification[];
items?: KnockDetailedNotification[];
meta: KnockFeedMeta;
page_info: KnockFeedPageInfo;
}

export interface KnockFeedMeta {
total_count: number;
unread_count: number;
unseen_count: number;
}

export interface KnockEvent {
event: string;
items: KnockDetailedNotification[];
metadata: KnockFeedMeta;
}

export interface KnockFeedPageInfo {
after: number | null;
before: number | null;
page_size: number;
total_count: number;
}
Loading