Skip to content

Commit

Permalink
Merge pull request #51 from Rikthepixel/feature/user-can-start-a-conv…
Browse files Browse the repository at this point in the history
…ersation

Feature/user can start a conversation
  • Loading branch information
Rikthepixel authored Jan 14, 2024
2 parents 8f9b682 + cc31403 commit 802275b
Show file tree
Hide file tree
Showing 102 changed files with 2,803 additions and 750 deletions.
2 changes: 2 additions & 0 deletions .github/workflows/pull-request-microservices.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ jobs:
- "apps/accounts/**"
advertisements:
- "apps/advertisements/**"
messages:
- "apps/messages/**"
gateway:
- "apps/gateway/**"
Expand Down
16 changes: 15 additions & 1 deletion .github/workflows/release-microservices.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ jobs:
environment: production
strategy:
matrix:
app: [advertisements, accounts, gateway]
app: [advertisements, accounts, messages, gateway]
steps:
- uses: actions/checkout@v4

Expand Down Expand Up @@ -53,6 +53,8 @@ jobs:
resource-group: ${{ vars.AZURE_RESOURCE_GROUP }}
cluster-name: ${{ vars.AZURE_CLUSTER_NAME }}

- name: "Ensure app routing"
run: az aks approuting enable -g ${{ vars.AZURE_RESOURCE_GROUP }} -n ${{ vars.AZURE_CLUSTER_NAME }}
- name: Delete auth secrets
continue-on-error: true
run: kubectl delete secret auth-secrets
Expand All @@ -65,6 +67,9 @@ jobs:
- name: Delete advertisements secrets
continue-on-error: true
run: kubectl delete secret advertisement-service-secrets
- name: Delete messages secrets
continue-on-error: true
run: kubectl delete secret messaging-service-secrets

- name: Set secrets
run: |
Expand All @@ -91,6 +96,15 @@ jobs:
--from-literal=STORAGE_DRIVER="azure" \
--from-literal=STORAGE_AZURE_CONNECTION_STRING="$(az storage account show-connection-string --name ${{ vars.ADVERTISEMENTS_AZURE_STORAGE_ACCOUNT }} -o tsv)"
kubectl create secret generic messaging-service-secrets \
--from-literal=DATABASE_USER="${{ secrets.DB_ADMIN_USER }}@${{ vars.MESSAGES_AZURE_DB_NAME }}" \
--from-literal=DATABASE_PASSWORD="${{ secrets.DB_ADMIN_PASSWORD }}" \
--from-literal=DATABASE_HOST="${{ vars.MESSAGES_AZURE_DB_NAME }}.mysql.database.azure.com" \
--from-literal=DATABASE_PORT="3306" \
--from-literal=DATABASE_NAME="messages" \
--from-literal=STORAGE_DRIVER="azure" \
--from-literal=STORAGE_AZURE_CONNECTION_STRING="$(az storage account show-connection-string --name ${{ vars.MESSAGES_AZURE_STORAGE_ACCOUNT }} -o tsv)"
- name: Deploys application
uses: Azure/k8s-deploy@v4
with:
Expand Down
2 changes: 1 addition & 1 deletion apps/advertisements/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,4 @@ STORAGE_DRIVER=azure
STORAGE_PATH=/storage # Used when STORAGE_DRIVER is set to 'file'

## Storage - STORAGE_DRIVER=azure
STORAGE_AZURE_CONNECTION_STRING=DefaultEndpointsProtocol=https;EndpointSuffix=core.windows.net;AccountName=squaremarketblobstorage;AccountKey=tEZpZsHtEg6AGukAoXTaa7A+xoSUyhMmeoDy4xlMKzS2oj3AUrwPX0q1mF3kwUXpVihLarWvz6wq+AStYwbReA==;BlobEndpoint=https://squaremarketblobstorage.blob.core.windows.net/;FileEndpoint=https://squaremarketblobstorage.file.core.windows.net/;QueueEndpoint=https://squaremarketblobstorage.queue.core.windows.net/;TableEndpoint=https://squaremarketblobstorage.table.core.windows.net/
STORAGE_AZURE_CONNECTION_STRING=
159 changes: 159 additions & 0 deletions apps/frontend/src/apis/messages/chats.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
import backend from '@/adapters/backend';
import { chatsResponseSchema } from '@/responses/messages/ChatsResponse';
import { getToken } from '@/lib/auth';
import {
ChatResponse,
chatResponseSchema,
} from '@/responses/messages/ChatResponse';
import {
ChatMessageResponse,
chatMessageResponseSchema,
} from '@/responses/messages/ChatMessageResponse';
import { chatCreatedResponse } from '@/responses/messages/ChatCreatedResponse';

export function startChat(userUid: string) {
return backend
.post('v1/chats', { json: { user: userUid } })
.then(async (res) => chatCreatedResponse.parse(await res.json()));
}

export async function getChats(signal?: AbortSignal) {
return await backend
.get('v1/chats', { signal })
.then(async (res) => chatsResponseSchema.parse(await res.json()));
}

export class ChatConnection {
constructor(
private target: EventTarget,
private socket: WebSocket,
) {}

addEventListener(
type: 'open',
callback: (event: OpenedEvent) => void | null,
options?: boolean | AddEventListenerOptions | undefined,
): void;
addEventListener(
type: 'chat-message',
callback: (event: ChatMessageEvent) => void | null,
options?: boolean | AddEventListenerOptions | undefined,
): void;
addEventListener(
type: 'chat-init',
callback: (event: ChatInitEvent) => void | null,
options?: boolean | AddEventListenerOptions | undefined,
): void;
addEventListener(
type: 'close',
callback: (event: ClosedEvent) => void | null,
options?: boolean | AddEventListenerOptions | undefined,
): void;
addEventListener(
type: string,
callback: (event: any) => void | null,
options?: boolean | AddEventListenerOptions | undefined,
): void {
this.target.addEventListener(type, callback, options);
}

removeEventListener(
type: 'open' | 'close',
callback: EventListenerOrEventListenerObject | null,
options?: boolean | EventListenerOptions | undefined,
): void {
this.target.removeEventListener(type, callback, options);
}

sendMessage(content: string) {
this.socket.send(
JSON.stringify({
type: 'chat-message',
content: content,
}),
);
}

disconnect() {
this.socket.close();
}
}

class OpenedEvent extends Event {
constructor() {
super('open');
}
}
class ClosedEvent extends Event {
constructor(public closedByClient: boolean) {
super('close');
}
}

class ChatInitEvent extends Event {
constructor(public data: ChatResponse) {
super('chat-init');
}
}

class ChatMessageEvent extends Event {
constructor(public data: ChatMessageResponse) {
super('chat-message');
}
}

const events = {
'chat-message': (data: unknown) =>
new ChatMessageEvent(chatMessageResponseSchema.parse(data)),
'chat-init': (data: unknown) =>
new ChatInitEvent(chatResponseSchema.parse(data)),
} as const;

export async function connectToChat(uid: string, signal?: AbortSignal) {
const token = await getToken();
if (!token) {
throw new Error('User must be authenticated to connect to this chat');
}

const url = new URL(import.meta.env.VITE_BACKEND_URL);
url.pathname = `v1/chats/${uid}`;
url.protocol = url.protocol === 'http:' ? 'ws:' : 'wss:';
url.searchParams.set('token', token);

const socket = new WebSocket(url);
const target = new EventTarget();

signal?.addEventListener('abort', function () {
socket.close();
});

socket.onopen = function () {
if (signal?.aborted) return socket.close();
target.dispatchEvent(new OpenedEvent());
};

socket.onmessage = function (e) {
if (signal?.aborted) return socket.close();
const data = JSON.parse(e.data.toString()) as unknown;
if (
typeof data !== 'object' ||
!data ||
!('type' in data) ||
typeof data.type !== 'string'
) {
return;
}
const event = Object.keys(events).includes(data.type)
? events[data.type as keyof typeof events]
: null;

if (!event) return;
target.dispatchEvent(event(data));
};

socket.onclose = function (e) {
target.dispatchEvent(new ClosedEvent(e.code === 1000));
};

return new ChatConnection(target, socket);
}
2 changes: 1 addition & 1 deletion apps/frontend/src/components/page/Container.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { Container } from '@mantine/core';
type PageContainerProps = Parameters<typeof Container>[0];

const PageContainer = (props: PageContainerProps) => (
<Container maw="1280px" w="100%" style={{ flex: 1 }} {...props} />
<Container maw="1280px" w="100%" style={{ flex: 1, flexDirection: "column" }} {...props} />
);

export default PageContainer;
9 changes: 7 additions & 2 deletions apps/frontend/src/helpers/Resource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,15 @@ export default class Resource<
TError = Error,
TLoading = undefined,
TIdle = undefined,
>(promise: Promise<TValue>): Promise<Resource<TValue, TError, TLoading, TIdle>> {
>(
promise: Promise<TValue>,
): Promise<Resource<TValue, TError, TLoading, TIdle>> {
return promise
.then((val) => Resource.wrapValue<TValue, TError, TLoading, TIdle>(val))
.catch((err: TError) => Resource.wrapError<TValue, TError, TLoading, TIdle>(err));
.catch((err: TError) =>{
console.log()
return Resource.wrapError<TValue, TError, TLoading, TIdle>(err);
});
}

/**
Expand Down
11 changes: 5 additions & 6 deletions apps/frontend/src/layouts/MainLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import {
} from '@mantine/core';
import {
MdClose,
MdPerson,
MdHome,
MdMessage,
MdLogin,
Expand Down Expand Up @@ -67,11 +66,11 @@ export default function MainLayout({ children }: React.PropsWithChildren) {
icon: IoMdPricetag,
text: `Dashboard`,
},
{
action: '/profile',
icon: MdPerson,
text: 'Profile',
},
// {
// action: '/profile',
// icon: MdPerson,
// text: 'Profile',
// },
{
action: auth.logout,
icon: MdLogout,
Expand Down
3 changes: 3 additions & 0 deletions apps/frontend/src/lib/auth/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import useAuth from "./stores/useAuth";

export const getToken = () => useAuth.getState().getToken();
36 changes: 21 additions & 15 deletions apps/frontend/src/lib/auth/models/authenticated-user.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,21 @@
import { User as Auth0User } from '@auth0/auth0-spa-js';

export default class AuthenticatedUser<TParent extends object> {
constructor(
public parent: TParent,
public displayName: string,
) {}

public static fromAuth0(user: Auth0User) {
return new AuthenticatedUser(
user,
user.name ?? user.nickname ?? 'unknown name',
);
}
}
import { User as Auth0User } from '@auth0/auth0-spa-js';

export default class AuthenticatedUser<TParent extends object> {
constructor(
public parent: TParent,
public username: string,
public providerId: string,
) {}

public static fromAuth0(user: Auth0User) {
if (!user.sub) {
throw new Error('Malformed authenticated user');
}

return new AuthenticatedUser(
user,
user.name ?? user.nickname ?? 'unknown name',
user.sub,
);
}
}
24 changes: 23 additions & 1 deletion apps/frontend/src/pages/ads/[uid]/index.page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ import { getImageUrl } from '@/apis/ads/images';
import PageContainer from '@/components/page/Container';
import useCurrencyFormatter from '@/hooks/useCurrencyFormatter';
import useTypedParams from '@/hooks/useTypedParams';
import useAuth from '@/lib/auth/stores/useAuth';
import useAdvertisements from '@/stores/useAdvertisements';
import useChats from '@/stores/useChats';
import { Carousel } from '@mantine/carousel';
import {
Image,
Expand All @@ -14,9 +16,11 @@ import {
Group,
SimpleGrid,
Skeleton,
Button,
} from '@mantine/core';
import { useEffect } from 'react';
import { MdPerson } from 'react-icons/md';
import { MdMessage, MdPerson } from 'react-icons/md';
import { useLocation } from 'wouter';
import { z } from 'zod';

const PARAMS_SCHEMA = z.object({
Expand All @@ -26,6 +30,10 @@ const PARAMS_SCHEMA = z.object({
export default function AdPage() {
const { uid } = useTypedParams(PARAMS_SCHEMA) ?? {};
const { advertisement, getAdvertisement } = useAdvertisements();
const { startChat } = useChats();
const { user } = useAuth();
const [, setLocation] = useLocation();

const currencyFormatter = useCurrencyFormatter(
advertisement.unwrapValue()?.currency ?? 'EUR',
);
Expand Down Expand Up @@ -75,6 +83,20 @@ export default function AdPage() {
<MdPerson />
<Text>{advertisement.user.name}</Text>
</Group>
{user && advertisement.user.uid !== user.providerId && (
<Button
onClick={() =>
startChat(advertisement.user.uid).then((uid) =>
setLocation(`/messages/${uid}`),
)
}
>
<Group gap="sm">
<MdMessage />
Contact seller
</Group>
</Button>
)}
</Stack>
<Stack gap="sm">
<Text fz="sm" fw={700}>
Expand Down
Loading

0 comments on commit 802275b

Please sign in to comment.