From c84fb5888d9aea5e5fc072dfaaa0cb8eb6577ee7 Mon Sep 17 00:00:00 2001 From: Kamil Kisiela Date: Mon, 14 Jan 2019 19:24:34 +0100 Subject: [PATCH] Load Chats --- .../src/app/graphql/graphql-root.component.ts | 13 +++- client/src/app/graphql/graphql.module.ts | 2 + .../app/graphql/queries/fragments.graphql.ts | 23 +++++++ .../app/graphql/queries/get-chats.graphql.ts | 19 ++++++ client/src/app/ngrx/chats.service.ts | 68 +++++++++++++++++++ client/src/app/ngrx/ngrx-root.component.ts | 11 ++- client/src/app/ngrx/ngrx.module.ts | 14 +++- client/src/app/ngrx/state/chat.actions.ts | 29 ++++++++ client/src/app/ngrx/state/chat.effects.ts | 34 ++++++++++ client/src/app/ngrx/state/chat.reducer.ts | 20 ++++++ 10 files changed, 225 insertions(+), 8 deletions(-) create mode 100644 client/src/app/graphql/queries/fragments.graphql.ts create mode 100644 client/src/app/graphql/queries/get-chats.graphql.ts create mode 100644 client/src/app/ngrx/chats.service.ts create mode 100644 client/src/app/ngrx/state/chat.actions.ts create mode 100644 client/src/app/ngrx/state/chat.effects.ts create mode 100644 client/src/app/ngrx/state/chat.reducer.ts diff --git a/client/src/app/graphql/graphql-root.component.ts b/client/src/app/graphql/graphql-root.component.ts index 3580bde..f84b697 100644 --- a/client/src/app/graphql/graphql-root.component.ts +++ b/client/src/app/graphql/graphql-root.component.ts @@ -4,6 +4,7 @@ import { Observable } from 'rxjs'; import { pluck } from 'rxjs/operators'; import { Chat, Pages, PageChangeEvent, MessageEvent, ID } from '../whatsapp'; +import GetChats from './queries/get-chats.graphql'; @Component({ selector: 'app-graphql-root', @@ -15,7 +16,9 @@ import { Chat, Pages, PageChangeEvent, MessageEvent, ID } from '../whatsapp'; (message)="onMessage($event)" (star)="toggleStar($event)" (page)="onPage($event)" - > + > + + `, }) export class GraphQLRootComponent { @@ -23,7 +26,7 @@ export class GraphQLRootComponent { chats: Observable; chat: Observable; - constructor() {} + constructor(private loona: Loona) {} onPage(event: PageChangeEvent) { this.page = event.page; @@ -43,7 +46,11 @@ export class GraphQLRootComponent { toggleStar(chatId: ID) {} - loadChats() {} + loadChats() { + this.chats = this.loona + .query(GetChats) + .valueChanges.pipe(pluck('data', 'chats')); + } loadChat(id: ID) {} } diff --git a/client/src/app/graphql/graphql.module.ts b/client/src/app/graphql/graphql.module.ts index fe02042..d23b5b3 100644 --- a/client/src/app/graphql/graphql.module.ts +++ b/client/src/app/graphql/graphql.module.ts @@ -8,6 +8,7 @@ import { RestLink } from 'apollo-link-rest'; import { GraphQLRootComponent } from './graphql-root.component'; import { WhatsappModule } from '../whatsapp'; +import { SharedModule } from '../shared/shared.module'; const routes: Routes = [ { @@ -22,6 +23,7 @@ const routes: Routes = [ CommonModule, RouterModule.forChild(routes), WhatsappModule, + SharedModule, ApolloModule, LoonaModule.forRoot(), ], diff --git a/client/src/app/graphql/queries/fragments.graphql.ts b/client/src/app/graphql/queries/fragments.graphql.ts new file mode 100644 index 0000000..18b9c5c --- /dev/null +++ b/client/src/app/graphql/queries/fragments.graphql.ts @@ -0,0 +1,23 @@ +import gql from 'graphql-tag'; + +export const UserFragment = gql` + fragment UserFragment on User { + id + name + } +`; + +export const MessageFragment = gql` + fragment MessageFragment on Message { + id + text + createdAt + sender @type(name: "User") { + ...UserFragment + } + recipient @type(name: "User") { + ...UserFragment + } + } + ${UserFragment} +`; diff --git a/client/src/app/graphql/queries/get-chats.graphql.ts b/client/src/app/graphql/queries/get-chats.graphql.ts new file mode 100644 index 0000000..fa7f9fa --- /dev/null +++ b/client/src/app/graphql/queries/get-chats.graphql.ts @@ -0,0 +1,19 @@ +import gql from 'graphql-tag'; + +import { UserFragment, MessageFragment } from './fragments.graphql'; + +export default gql` + { + chats @rest(type: "Chat", path: "/chats") { + id @export(as: "chatId") + members @rest(type: "[User]", path: "/chats/:chatId/members") { + ...UserFragment + } + recentMessage @type(name: "Message") { + ...MessageFragment + } + } + } + ${UserFragment} + ${MessageFragment} +`; \ No newline at end of file diff --git a/client/src/app/ngrx/chats.service.ts b/client/src/app/ngrx/chats.service.ts new file mode 100644 index 0000000..b6deab7 --- /dev/null +++ b/client/src/app/ngrx/chats.service.ts @@ -0,0 +1,68 @@ +import { Injectable } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { of, combineLatest } from 'rxjs'; +import { map, mergeMap } from 'rxjs/operators'; + +function api(path: string) { + return `http://localhost:4000${path}`; +} + +@Injectable() +export class ChatsService { + constructor(private http: HttpClient) {} + + getChats() { + this.fetchChats().pipe( + mergeMap(chats => + combineLatest(chats.map(chat => this.resolveChat(chat))), + ), + ); + } + + private fetchChats() { + return this.http.get(api('/chats')); + } + + private fetchUser(link: string) { + return this.http.get(link); + } + + private resolveChat(chat: ChatResponse) { + return combineLatest(chat.members.map(link => this.fetchUser(link))).pipe( + map(members => { + return { + ...chat, + messages: [], + members, + }; + }), + ); + } +} + +export type Link = string; +export type ID = string; + +export interface ChatResponse { + id: ID; + members: Link[]; + messages: Link; + recentMessage: MessageResponse; +} + +export type ChatsResponse = ChatResponse[]; + +export interface UserResponse { + id: ID; + name: string; +} + +export type ChatMessagesResponse = MessageResponse[]; + +export interface MessageResponse { + id: ID; + text: string; + createdAt: string; + sender: UserResponse; + recipient: UserResponse; +} diff --git a/client/src/app/ngrx/ngrx-root.component.ts b/client/src/app/ngrx/ngrx-root.component.ts index 779ca8f..535ba26 100644 --- a/client/src/app/ngrx/ngrx-root.component.ts +++ b/client/src/app/ngrx/ngrx-root.component.ts @@ -1,6 +1,10 @@ import { Component } from '@angular/core'; import { Observable } from 'rxjs'; +import { Store } from '@ngrx/store'; + import { Chat, Pages, PageChangeEvent, MessageEvent, ID } from '../whatsapp'; +import { LoadChats } from './state/chat.actions'; +import { AppState } from './app.state'; @Component({ selector: 'app-ngrx-root', @@ -22,7 +26,7 @@ export class NgRxRootComponent { chats: Observable; chat: Observable; - constructor() {} + constructor(private store: Store) {} onPage(event: PageChangeEvent) { this.page = event.page; @@ -46,7 +50,10 @@ export class NgRxRootComponent { toggleStar(chatId: ID) {} - loadChats() {} + loadChats() { + this.store.dispatch(new LoadChats()); + this.chats = this.store.select(state => state.chats); + } loadChat(id: ID) {} } diff --git a/client/src/app/ngrx/ngrx.module.ts b/client/src/app/ngrx/ngrx.module.ts index dfb2f33..6105d9d 100644 --- a/client/src/app/ngrx/ngrx.module.ts +++ b/client/src/app/ngrx/ngrx.module.ts @@ -5,8 +5,12 @@ import { StoreModule } from '@ngrx/store'; import { EffectsModule } from '@ngrx/effects'; import { NgRxRootComponent } from './ngrx-root.component'; -import { SharedModule } from '../shared/shared.module'; import { WhatsappModule } from '../whatsapp'; +import { SharedModule } from '../shared/shared.module'; +import { chatReducer } from './state/chat.reducer'; +import { ChatEffects } from './state/chat.effects'; +import { ChatsService } from './chats.service'; +import { AppState } from './app.state'; const routes: Routes = [ { @@ -20,9 +24,13 @@ const routes: Routes = [ imports: [ CommonModule, RouterModule.forChild(routes), - SharedModule, WhatsappModule, + SharedModule, + StoreModule.forRoot({ + chats: chatReducer, + }), + EffectsModule.forRoot([ChatEffects]), ], - providers: [], + providers: [ChatsService], }) export class NgRxModule {} diff --git a/client/src/app/ngrx/state/chat.actions.ts b/client/src/app/ngrx/state/chat.actions.ts new file mode 100644 index 0000000..3b79409 --- /dev/null +++ b/client/src/app/ngrx/state/chat.actions.ts @@ -0,0 +1,29 @@ +import { Action } from '@ngrx/store'; +import { Chat, Message, ID } from '../../whatsapp'; + +export enum ActionTypes { + LoadChats = '[Chats] Load', + LoadChatsSuccess = '[Chats] Load Success', + LoadChatsFailure = '[Chats] Load Failure', +} + +// All chats + +export class LoadChats implements Action { + readonly type = ActionTypes.LoadChats; +} + +export class LoadChatsSuccess implements Action { + readonly type = ActionTypes.LoadChatsSuccess; + constructor( + public payload: { + chats: Chat[]; + }, + ) {} +} + +export class LoadChatsFailure implements Action { + readonly type = ActionTypes.LoadChatsFailure; +} + +export type ChatAction = LoadChats | LoadChatsSuccess | LoadChatsFailure; diff --git a/client/src/app/ngrx/state/chat.effects.ts b/client/src/app/ngrx/state/chat.effects.ts new file mode 100644 index 0000000..a3eed00 --- /dev/null +++ b/client/src/app/ngrx/state/chat.effects.ts @@ -0,0 +1,34 @@ +import { Injectable } from '@angular/core'; +import { Effect, Actions, ofType } from '@ngrx/effects'; +import { Observable, of, combineLatest } from 'rxjs'; +import { mergeMap, catchError, first, take } from 'rxjs/operators'; +import { Store } from '@ngrx/store'; +import { AppState } from '../app.state'; +import { + ChatAction, + ActionTypes, + LoadChatsSuccess, + LoadChatsFailure, +} from './chat.actions'; +import { User } from '../../whatsapp'; +import { ChatsService } from '../chats.service'; + +@Injectable() +export class ChatEffects { + constructor( + protected actions$: Actions, + protected store$: Store, + protected chats: ChatsService, + ) {} + + @Effect() + loadChats$: Observable = this.actions$.pipe( + ofType(ActionTypes.LoadChats), + mergeMap(_ => { + return this.chats.getChats().pipe( + mergeMap(chats => of(new LoadChatsSuccess({ chats }))), + catchError(() => of(new LoadChatsFailure())), + ); + }), + ); +} diff --git a/client/src/app/ngrx/state/chat.reducer.ts b/client/src/app/ngrx/state/chat.reducer.ts new file mode 100644 index 0000000..f77c08d --- /dev/null +++ b/client/src/app/ngrx/state/chat.reducer.ts @@ -0,0 +1,20 @@ +import { ChatAction, ActionTypes } from './chat.actions'; +import { ChatState } from './chat.state'; + +export const initialState: ChatState = []; + +export function chatReducer( + state = initialState, + action: ChatAction, +): ChatState { + switch (action.type) { + case ActionTypes.LoadChatsSuccess: { + const { chats } = action.payload; + + return chats; + } + + default: + return state; + } +}