Skip to content

Commit

Permalink
Optimistic UI
Browse files Browse the repository at this point in the history
  • Loading branch information
kamilkisiela committed Jan 14, 2019
1 parent d9e24db commit b25d0b2
Show file tree
Hide file tree
Showing 5 changed files with 139 additions and 19 deletions.
10 changes: 7 additions & 3 deletions client/src/app/graphql/chat.state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,15 @@ export class ChatState {
if (!data.messages || !data.messages.length) {
data.messages = [];
}

data.messages.push({
const message = {
id,
__typename: 'Message',
});
};


data.messages.push(message);
data.recentMessage = message;
},
);
}
Expand Down
31 changes: 25 additions & 6 deletions client/src/app/graphql/graphql-root.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
MessageEvent,
ID,
pickOtherUser,
pickOwner,
} from '../whatsapp';
import GetChats from './queries/get-chats.graphql';
import GetChat from './queries/get-chat.graphql';
Expand Down Expand Up @@ -54,15 +55,33 @@ export class GraphQLRootComponent {
onMessage(event: MessageEvent) {
const text = event.text;
const recipient = pickOtherUser(event.chat);
const sender = pickOwner(event.chat);

this.loona
.mutate(NewMessage, {
id: event.chat.id,
input: {
text,
recipient: recipient.id,
.mutate(
NewMessage,
{
id: event.chat.id,
input: {
text,
recipient: recipient.id,
},
},
})
{
optimisticResponse: {
newMessage: {
__typename: 'Message',
id: Math.random()
.toString(16)
.substr(2),
text,
createdAt: new Date(),
sender,
recipient,
},
},
},
)
.subscribe();
}

Expand Down
14 changes: 14 additions & 0 deletions client/src/app/ngrx/state/chat.actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export enum ActionTypes {
LoadMessagesFailure = '[Chat] Load messages Failure',

SendMessage = '[Chat] Send message',
SendMessageOptimistic = '[Chat] Send message Optimistic',
SendMessageSuccess = '[Chat] Send message Success',
SendMessageFailure = '[Chat] Send message Failure',
}
Expand Down Expand Up @@ -74,11 +75,22 @@ export class SendMessage implements Action {
) {}
}

export class SendMessageOptimistic implements Action {
readonly type = ActionTypes.SendMessageOptimistic;
constructor(
public payload: {
chatId: ID;
message: Message;
},
) {}
}

export class SendMessageSuccess implements Action {
readonly type = ActionTypes.SendMessageSuccess;
constructor(
public payload: {
chatId: ID;
tempId: ID;
message: Message;
},
) {}
Expand All @@ -89,6 +101,7 @@ export class SendMessageFailure implements Action {
constructor(
public payload: {
chatId: ID;
tempId: ID;
},
) {}
}
Expand All @@ -101,5 +114,6 @@ export type ChatAction =
| LoadMessagesSuccess
| LoadMessagesFailure
| SendMessage
| SendMessageOptimistic
| SendMessageSuccess
| SendMessageFailure;
62 changes: 54 additions & 8 deletions client/src/app/ngrx/state/chat.effects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
LoadMessagesFailure,
SendMessageSuccess,
SendMessageFailure,
SendMessageOptimistic,
} from './chat.actions';
import { User } from '../../whatsapp';
import { ChatsService } from '../chats.service';
Expand Down Expand Up @@ -57,21 +58,66 @@ export class ChatEffects {
);

@Effect()
send$: Observable<ChatAction> = this.actions$.pipe(
optimistic$: Observable<ChatAction> = this.actions$.pipe(
ofType(ActionTypes.SendMessage),
mergeMap(action => {
const { chatId, text, recipient } = action.payload;

return this.chats.sendMessage(chatId, text, recipient).pipe(
mergeMap(message => of(new SendMessageSuccess({ chatId, message }))),
catchError(() =>
of(
new SendMessageFailure({
function selectMemberIf(condition: (m: User) => boolean) {
return (state: AppState) =>
state.chats.find(c => c.id === chatId).members.find(condition);
}

const recipient$ = this.store$
.select(selectMemberIf(m => m.id === recipient))
.pipe(take(1));
const sender$ = this.store$
.select(selectMemberIf(m => m.id !== recipient))
.pipe(take(1));

return combineLatest([recipient$, sender$]).pipe(
mergeMap(([recipient, sender]) => {
return of(
new SendMessageOptimistic({
chatId,
message: {
id: Math.random()
.toString(16)
.substr(2),
createdAt: new Date().toString(),
text,
sender,
recipient,
},
}),
),
),
);
}),
);
}),
);

@Effect()
send$: Observable<ChatAction> = this.actions$.pipe(
ofType(ActionTypes.SendMessageOptimistic),
mergeMap(action => {
const { chatId, message } = action.payload;
const tempId = message.id;

return this.chats
.sendMessage(chatId, message.text, message.recipient.id)
.pipe(
mergeMap(message =>
of(new SendMessageSuccess({ chatId, tempId, message })),
),
catchError(() =>
of(
new SendMessageFailure({
chatId,
tempId,
}),
),
),
);
}),
);
}
41 changes: 39 additions & 2 deletions client/src/app/ngrx/state/chat.reducer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,14 +29,51 @@ export function chatReducer(
});
}

case ActionTypes.SendMessageSuccess: {
case ActionTypes.SendMessageOptimistic: {
const { chatId, message } = action.payload;

return state.map(chat => {
if (chat.id === chatId) {
const messages = chat.messages || [];

return {
...chat,
messages: [...messages, message],
};
}

return chat;
});
}

case ActionTypes.SendMessageSuccess: {
const { chatId, tempId, message } = action.payload;

return state.map(chat => {
if (chat.id === chatId) {
const messages = chat.messages.map(m =>
m.id === tempId ? message : m,
);

return {
...chat,
messages,
recentMessage: message,
};
}

return chat;
});
}

case ActionTypes.SendMessageFailure: {
const { chatId, tempId } = action.payload;

return state.map(chat => {
if (chat.id === chatId) {
return {
...chat,
messages: [...chat.messages, message],
messages: chat.messages.filter(m => m.id !== tempId),
};
}

Expand Down

0 comments on commit b25d0b2

Please sign in to comment.