From 1d10e553a4958009075b2d35fe8cf8ad67ea3700 Mon Sep 17 00:00:00 2001 From: Fergal Moran Date: Sun, 18 Mar 2018 19:56:00 +0000 Subject: [PATCH] Push notifications added --- client/src/app/app.component.ts | 25 +-- client/src/app/app.module.ts | 4 +- .../app/components/debug/debug.component.ts | 24 +-- .../components/podcast/podcast.component.ts | 1 + .../services/push-notifications.service.ts | 36 +++- server/Controllers/ChatterController.cs | 31 +++ .../20180316223756_UidToUser.Designer.cs | 197 ++++++++++++++++++ server/Migrations/20180316223756_UidToUser.cs | 25 +++ .../20180317002411_UidLenIncrease.Designer.cs | 197 ++++++++++++++++++ .../20180317002411_UidLenIncrease.cs | 33 +++ server/Services/Hubs/ChatterHub.cs | 14 ++ 11 files changed, 547 insertions(+), 40 deletions(-) create mode 100644 server/Controllers/ChatterController.cs create mode 100644 server/Migrations/20180316223756_UidToUser.Designer.cs create mode 100644 server/Migrations/20180316223756_UidToUser.cs create mode 100644 server/Migrations/20180317002411_UidLenIncrease.Designer.cs create mode 100644 server/Migrations/20180317002411_UidLenIncrease.cs create mode 100644 server/Services/Hubs/ChatterHub.cs diff --git a/client/src/app/app.component.ts b/client/src/app/app.component.ts index 26f1a58..751db97 100755 --- a/client/src/app/app.component.ts +++ b/client/src/app/app.component.ts @@ -5,6 +5,7 @@ import { AuthService } from 'app/services/auth.service'; import { AppInsightsService } from 'app/services/app-insights.service'; import { SignalRService } from 'app/services/signalr.service'; import { ProfileService } from './services/profile.service'; +import { PushNotificationsService } from 'app/services/push-notifications.service'; @Component({ selector: 'app-root', @@ -27,9 +28,9 @@ export class AppComponent implements OnInit { } ngOnInit() { - if (this.loggedIn()) { - this._pushNotifications.requestPermission(); + this._pushNotifications.requestPermissions(); + if (this.loggedIn()) { const user = this._profileService.getProfile().subscribe(u => { if (u) { const chatterChannel = `${u.uid}_chatter`; @@ -44,22 +45,10 @@ export class AppComponent implements OnInit { this._signalrService.connection.on( chatterChannel, result => { - this._pushNotifications - .create('PodNoms', { body: result }) - .subscribe( - res => - console.log( - 'app.component', - '_pushNotifications', - res - ), - err => - console.log( - 'app.component', - '_pushNotifications', - err - ) - ); + this._pushNotifications.createNotification( + 'PodNoms', + result + ); } ); }) diff --git a/client/src/app/app.module.ts b/client/src/app/app.module.ts index 322974d..6864314 100755 --- a/client/src/app/app.module.ts +++ b/client/src/app/app.module.ts @@ -51,6 +51,7 @@ import { AboutComponent } from './components/about/about.component'; import { FooterComponent } from './components/footer/footer.component'; import { JobsService } from 'app/services/jobs.service'; import { AppInsightsService } from 'app/services/app-insights.service'; +import { PushNotificationsService } from './services/push-notifications.service'; export function authHttpServiceFactory(http: Http, options: RequestOptions) { return new AuthHttp( @@ -100,7 +101,6 @@ export function authHttpServiceFactory(http: Http, options: RequestOptions) { ToastyModule.forRoot(), DropzoneModule, ClipboardModule, - PushNotificationsModule, StoreModule.forRoot(reducers), @@ -124,9 +124,9 @@ export function authHttpServiceFactory(http: Http, options: RequestOptions) { SignalRService, ProfileService, PodcastService, - PushNotificationsService, ImageService, DebugService, + PushNotificationsService, ChatterService, AppInsightsService, JobsService, diff --git a/client/src/app/components/debug/debug.component.ts b/client/src/app/components/debug/debug.component.ts index a428ffe..545ad9d 100755 --- a/client/src/app/components/debug/debug.component.ts +++ b/client/src/app/components/debug/debug.component.ts @@ -4,8 +4,8 @@ import { Component, OnInit } from '@angular/core'; import { DebugService } from 'app/services/debug.service'; import { environment } from 'environments/environment'; import { JobsService } from 'app/services/jobs.service'; -import { PushNotificationsService } from 'ng-push'; import { ChatterService } from 'app/services/chatter.service'; +import { PushNotificationsService } from 'app/services/push-notifications.service'; @Component({ selector: 'app-debug', @@ -57,7 +57,7 @@ export class DebugComponent implements OnInit { } sendChatter() { this._chatterService.ping('Pong').subscribe(r => { - this._pushNotifications.create('PodNoms', { body: r }); + this._pushNotifications.createNotification('PodNoms', r); }); } sendDesktopNotification() { @@ -66,22 +66,10 @@ export class DebugComponent implements OnInit { 'sendDesktopFunction', this.notificationMessage ); - this._pushNotifications - .create('PodNoms', { body: this.notificationMessage }) - .subscribe( - res => - console.log( - 'debug.component', - 'sendDesktopNotification', - res - ), - err => - console.log( - 'debug.component', - 'sendDesktopNotification', - err - ) - ); + this._pushNotifications.createNotification( + 'PodNoms', + this.notificationMessage + ); } processOrphans() { this._jobsService diff --git a/client/src/app/components/podcast/podcast.component.ts b/client/src/app/components/podcast/podcast.component.ts index 1c2ce01..489b385 100755 --- a/client/src/app/components/podcast/podcast.component.ts +++ b/client/src/app/components/podcast/podcast.component.ts @@ -14,6 +14,7 @@ import * as fromPodcast from 'app/reducers'; import * as fromPodcastActions from 'app/actions/podcast.actions'; import * as fromEntriesActions from 'app/actions/entries.actions'; import { PodcastService } from 'app/services/podcast.service'; +import { PushNotificationsService } from 'app/services/push-notifications.service'; @Component({ selector: 'app-podcast', diff --git a/client/src/app/services/push-notifications.service.ts b/client/src/app/services/push-notifications.service.ts index e3fb95c..4d602c1 100644 --- a/client/src/app/services/push-notifications.service.ts +++ b/client/src/app/services/push-notifications.service.ts @@ -1,8 +1,40 @@ import { Injectable } from '@angular/core'; +export type Permission = 'denied' | 'granted' | 'default'; + @Injectable() export class PushNotificationsService { + permission: Permission; - constructor() { } - + constructor() {} + requestPermissions() { + if ('Notification' in window) { + Notification.requestPermission((status: any) => { + console.log( + 'push-notifications.service', + 'requestPermissions', + status + ); + this.permission = status; + }); + } + } + isSupported() { + return 'Notification' in window && this.permission == 'granted'; + } + createNotification(subject: string, text: string) { + if (this.isSupported()) { + const options = { + body: text, + icon: 'https://podnoms.com/assets/img/logo-icon.png' + }; + const n = new Notification(subject, options); + } else { + console.error( + 'push-notifications.service', + 'createNotification', + 'Notifications are not supported' + ); + } + } } diff --git a/server/Controllers/ChatterController.cs b/server/Controllers/ChatterController.cs new file mode 100644 index 0000000..60a320d --- /dev/null +++ b/server/Controllers/ChatterController.cs @@ -0,0 +1,31 @@ +using System.Linq; +using System.Security.Claims; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.SignalR; +using PodNoms.Api.Persistence; +using PodNoms.Api.Services.Hubs; + +namespace PodNoms.Api.Controllers { + [Authorize] + [Route("[controller]")] + public class ChatterController : Controller { + private readonly IUserRepository _repository; + private readonly HubLifetimeManager _chatterHub; + public ChatterController(IUserRepository repository, HubLifetimeManager chatterHub) { + this._chatterHub = chatterHub; + this._repository = repository; + } + [HttpPost("ping")] + public async Task> Ping([FromBody] string message) { + var email = User.Claims.FirstOrDefault(c => c.Type == ClaimTypes.Email)?.Value; + + var user = await this._repository.GetAsync(email); + await _chatterHub.SendAllAsync( + $"{user.Uid}_chatter", + new object[] { message }); + return Ok(message); + } + } +} \ No newline at end of file diff --git a/server/Migrations/20180316223756_UidToUser.Designer.cs b/server/Migrations/20180316223756_UidToUser.Designer.cs new file mode 100644 index 0000000..239aa8f --- /dev/null +++ b/server/Migrations/20180316223756_UidToUser.Designer.cs @@ -0,0 +1,197 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Metadata.Internal; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage; +using Microsoft.EntityFrameworkCore.Storage.Internal; +using PodNoms.Api.Models; +using PodNoms.Api.Persistence; + +namespace PodNoms.Api.Migrations +{ + [DbContext(typeof(PodnomsDbContext))] + [Migration("20180316223756_UidToUser")] + partial class UidToUser + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "2.1.0-preview1-28290") + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + modelBuilder.Entity("PodNoms.Api.Models.Playlist", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("CreateDate"); + + b.Property("PodcastId"); + + b.Property("SourceUrl"); + + b.Property("UpdateDate"); + + b.HasKey("Id"); + + b.HasIndex("PodcastId"); + + b.ToTable("Playlists"); + }); + + modelBuilder.Entity("PodNoms.Api.Models.Podcast", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("CreateDate") + .ValueGeneratedOnAdd() + .HasDefaultValueSql("getdate()"); + + b.Property("Description"); + + b.Property("ImageUrl"); + + b.Property("Slug") + .IsUnicode(true); + + b.Property("Title"); + + b.Property("Uid"); + + b.Property("UpdateDate"); + + b.Property("UserId"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("Podcasts"); + }); + + modelBuilder.Entity("PodNoms.Api.Models.PodcastEntry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AudioFileSize"); + + b.Property("AudioLength"); + + b.Property("AudioUrl"); + + b.Property("Author"); + + b.Property("CreateDate") + .ValueGeneratedOnAdd() + .HasDefaultValueSql("getdate()"); + + b.Property("Description"); + + b.Property("ImageUrl"); + + b.Property("PlaylistId"); + + b.Property("PodcastId"); + + b.Property("Processed"); + + b.Property("ProcessingPayload"); + + b.Property("ProcessingStatus"); + + b.Property("SourceUrl"); + + b.Property("Title"); + + b.Property("Uid"); + + b.Property("UpdateDate"); + + b.HasKey("Id"); + + b.HasIndex("PlaylistId"); + + b.HasIndex("PodcastId"); + + b.ToTable("PodcastEntries"); + }); + + modelBuilder.Entity("PodNoms.Api.Models.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ApiKey") + .HasMaxLength(50); + + b.Property("CreateDate"); + + b.Property("EmailAddress") + .HasMaxLength(100); + + b.Property("FullName") + .HasMaxLength(100); + + b.Property("ProfileImage"); + + b.Property("ProviderId") + .HasMaxLength(50); + + b.Property("RefreshToken"); + + b.Property("Sid") + .HasMaxLength(50); + + b.Property("Slug") + .HasMaxLength(50); + + b.Property("Uid") + .HasMaxLength(32); + + b.Property("UpdateDate"); + + b.HasKey("Id"); + + b.HasIndex("Slug") + .IsUnique() + .HasFilter("[Slug] IS NOT NULL"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("PodNoms.Api.Models.Playlist", b => + { + b.HasOne("PodNoms.Api.Models.Podcast", "Podcast") + .WithMany() + .HasForeignKey("PodcastId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("PodNoms.Api.Models.Podcast", b => + { + b.HasOne("PodNoms.Api.Models.User", "User") + .WithMany() + .HasForeignKey("UserId"); + }); + + modelBuilder.Entity("PodNoms.Api.Models.PodcastEntry", b => + { + b.HasOne("PodNoms.Api.Models.Playlist") + .WithMany("PodcastEntries") + .HasForeignKey("PlaylistId"); + + b.HasOne("PodNoms.Api.Models.Podcast", "Podcast") + .WithMany("PodcastEntries") + .HasForeignKey("PodcastId") + .OnDelete(DeleteBehavior.Cascade); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/server/Migrations/20180316223756_UidToUser.cs b/server/Migrations/20180316223756_UidToUser.cs new file mode 100644 index 0000000..3ace503 --- /dev/null +++ b/server/Migrations/20180316223756_UidToUser.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace PodNoms.Api.Migrations +{ + public partial class UidToUser : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "Uid", + table: "Users", + maxLength: 32, + nullable: true); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "Uid", + table: "Users"); + } + } +} diff --git a/server/Migrations/20180317002411_UidLenIncrease.Designer.cs b/server/Migrations/20180317002411_UidLenIncrease.Designer.cs new file mode 100644 index 0000000..b298b79 --- /dev/null +++ b/server/Migrations/20180317002411_UidLenIncrease.Designer.cs @@ -0,0 +1,197 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Metadata.Internal; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage; +using Microsoft.EntityFrameworkCore.Storage.Internal; +using PodNoms.Api.Models; +using PodNoms.Api.Persistence; + +namespace PodNoms.Api.Migrations +{ + [DbContext(typeof(PodnomsDbContext))] + [Migration("20180317002411_UidLenIncrease")] + partial class UidLenIncrease + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "2.1.0-preview1-28290") + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + modelBuilder.Entity("PodNoms.Api.Models.Playlist", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("CreateDate"); + + b.Property("PodcastId"); + + b.Property("SourceUrl"); + + b.Property("UpdateDate"); + + b.HasKey("Id"); + + b.HasIndex("PodcastId"); + + b.ToTable("Playlists"); + }); + + modelBuilder.Entity("PodNoms.Api.Models.Podcast", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("CreateDate") + .ValueGeneratedOnAdd() + .HasDefaultValueSql("getdate()"); + + b.Property("Description"); + + b.Property("ImageUrl"); + + b.Property("Slug") + .IsUnicode(true); + + b.Property("Title"); + + b.Property("Uid"); + + b.Property("UpdateDate"); + + b.Property("UserId"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("Podcasts"); + }); + + modelBuilder.Entity("PodNoms.Api.Models.PodcastEntry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AudioFileSize"); + + b.Property("AudioLength"); + + b.Property("AudioUrl"); + + b.Property("Author"); + + b.Property("CreateDate") + .ValueGeneratedOnAdd() + .HasDefaultValueSql("getdate()"); + + b.Property("Description"); + + b.Property("ImageUrl"); + + b.Property("PlaylistId"); + + b.Property("PodcastId"); + + b.Property("Processed"); + + b.Property("ProcessingPayload"); + + b.Property("ProcessingStatus"); + + b.Property("SourceUrl"); + + b.Property("Title"); + + b.Property("Uid"); + + b.Property("UpdateDate"); + + b.HasKey("Id"); + + b.HasIndex("PlaylistId"); + + b.HasIndex("PodcastId"); + + b.ToTable("PodcastEntries"); + }); + + modelBuilder.Entity("PodNoms.Api.Models.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ApiKey") + .HasMaxLength(50); + + b.Property("CreateDate"); + + b.Property("EmailAddress") + .HasMaxLength(100); + + b.Property("FullName") + .HasMaxLength(100); + + b.Property("ProfileImage"); + + b.Property("ProviderId") + .HasMaxLength(50); + + b.Property("RefreshToken"); + + b.Property("Sid") + .HasMaxLength(50); + + b.Property("Slug") + .HasMaxLength(50); + + b.Property("Uid") + .HasMaxLength(50); + + b.Property("UpdateDate"); + + b.HasKey("Id"); + + b.HasIndex("Slug") + .IsUnique() + .HasFilter("[Slug] IS NOT NULL"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("PodNoms.Api.Models.Playlist", b => + { + b.HasOne("PodNoms.Api.Models.Podcast", "Podcast") + .WithMany() + .HasForeignKey("PodcastId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("PodNoms.Api.Models.Podcast", b => + { + b.HasOne("PodNoms.Api.Models.User", "User") + .WithMany() + .HasForeignKey("UserId"); + }); + + modelBuilder.Entity("PodNoms.Api.Models.PodcastEntry", b => + { + b.HasOne("PodNoms.Api.Models.Playlist") + .WithMany("PodcastEntries") + .HasForeignKey("PlaylistId"); + + b.HasOne("PodNoms.Api.Models.Podcast", "Podcast") + .WithMany("PodcastEntries") + .HasForeignKey("PodcastId") + .OnDelete(DeleteBehavior.Cascade); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/server/Migrations/20180317002411_UidLenIncrease.cs b/server/Migrations/20180317002411_UidLenIncrease.cs new file mode 100644 index 0000000..32b6267 --- /dev/null +++ b/server/Migrations/20180317002411_UidLenIncrease.cs @@ -0,0 +1,33 @@ +using System; +using System.Collections.Generic; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace PodNoms.Api.Migrations +{ + public partial class UidLenIncrease : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "Uid", + table: "Users", + maxLength: 50, + nullable: true, + oldClrType: typeof(string), + oldMaxLength: 32, + oldNullable: true); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "Uid", + table: "Users", + maxLength: 32, + nullable: true, + oldClrType: typeof(string), + oldMaxLength: 50, + oldNullable: true); + } + } +} diff --git a/server/Services/Hubs/ChatterHub.cs b/server/Services/Hubs/ChatterHub.cs new file mode 100644 index 0000000..9b7509d --- /dev/null +++ b/server/Services/Hubs/ChatterHub.cs @@ -0,0 +1,14 @@ +using System; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.SignalR; + +namespace PodNoms.Api.Services.Hubs { + [Authorize] + public class ChatterHub : Hub { + public async Task SendMessage(string user, string message) { + string timestamp = DateTime.Now.ToShortTimeString(); + await Clients.All.SendAsync($"{user}_chatter", timestamp, user, message); + } + } +} \ No newline at end of file