Skip to content

Commit

Permalink
feat(messages): add embed type image & gallery
Browse files Browse the repository at this point in the history
  • Loading branch information
antonstjernquist committed Sep 22, 2024
1 parent 82ce797 commit 01f4211
Show file tree
Hide file tree
Showing 28 changed files with 5,736 additions and 4,140 deletions.
8,897 changes: 4,982 additions & 3,915 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions src/server/database/schemas/Message.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ export const createMessageTable = async () => {
.references('id')
.inTable(`${DATABASE_PREFIX}sim_card`);
table.string('content').notNullable();
table.string('embed_type').nullable();
table.jsonb('embed_content').nullable();
table.dateTime('created_at').notNullable().defaultTo(DBInstance.fn.now());
});
};
7 changes: 6 additions & 1 deletion src/server/exports/Notifications.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
import { InsertNotification } from '../../shared/Types';
import BroadcastService from '../services/BroadcastService';

const _exports = global.exports;
const _exports =
typeof global.exports === 'function'
? global.exports
: (exportName: string) => {
console.log(`Creating export: ${exportName}`);
};

_exports('createNotification', (src: number, notification: InsertNotification) => {
BroadcastService.createNotification(src, notification);
Expand Down
43 changes: 35 additions & 8 deletions src/server/global.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import PlayerService from './services/PlayerService';
const isRunningInGame = typeof RegisterCommand !== 'undefined';

const isRunningIngame = typeof RegisterCommand !== 'undefined';

if (!isRunningIngame) {
if (!isRunningInGame) {
global.GetConvar = (_: string, defaultValue: string) => {
return defaultValue;
};
Expand Down Expand Up @@ -31,8 +29,37 @@ if (!isRunningIngame) {
},
} as unknown as CitizenExports;

const baseLicense = `license:ef0b12fd95e37572c24c00503c3fd02f3f9b99cb`;
PlayerService.selectDevice(1, `1:${baseLicense}`);
PlayerService.selectDevice(2, `2:${baseLicense}`);
PlayerService.selectDevice(3, `3:${baseLicense}`);
const initDevices = async () => {
const PlayerService = require('./services/PlayerService').default;
const DeviceService = require('./services/DeviceService').default;
const SimCardService = require('./services/SimCardService').default;
const baseLicense = `license:ef0b12fd95e37572c24c00503c3fd02f3f9b99cb`;

/** Check if device exists. otherwise create them */
const isDevicesCreated = await DeviceService.getDeviceByIdentifier(`1:${baseLicense}`);

if (!isDevicesCreated) {
console.log('Creating devices');

for (let i = 1; i <= 3; i++) {
console.log(`Creating device ${i}`);
const simcard = await SimCardService.createSimCard({
phone_number: `${i}${i}${i}`,
});
await await DeviceService.createDevice({
sim_card_id: simcard.id,
identifier: `${i}:${baseLicense}`,
});
}
}

PlayerService.selectDevice(1, `1:${baseLicense}`);
PlayerService.selectDevice(2, `2:${baseLicense}`);
PlayerService.selectDevice(3, `3:${baseLicense}`);
};

console.log('Initializing devices');
setTimeout(() => {
initDevices();
}, 2500);
}
8 changes: 2 additions & 6 deletions src/server/repositories/DeviceRepository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,7 @@ class DeviceRepository {
if (result) {
result.settings = JSON.parse(result.settings);
}
} catch (error) {
console.log('Error parsing settings', error);
}
} catch (error) {}

return result;
}
Expand All @@ -63,9 +61,7 @@ class DeviceRepository {
if (result) {
result.settings = JSON.parse(result.settings);
}
} catch (error) {
console.log('Error parsing settings', error);
}
} catch (error) {}

return result;
}
Expand Down
14 changes: 12 additions & 2 deletions src/server/router/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,16 +25,26 @@ messagesRouter.add('/', async (ctx) => {
});

const sendMessageSchema = z.object({
embedType: z.enum(['image', 'video', 'audio']).nullable(),
embedContent: z.string().nullable(),
content: z.string().min(1).max(255),
phoneNumber: z.string().min(2).max(15),
});

messagesRouter.add('/send', async (ctx, next) => {
try {
const { content, phoneNumber } = sendMessageSchema.parse(ctx.request.body);
const { content, phoneNumber, embedContent, embedType } = sendMessageSchema.parse(
ctx.request.body,
);

// Send message to phoneNumber
const message = await MessageService.sendMessage(ctx, content, phoneNumber);
const message = await MessageService.sendMessage({
ctx,
content,
phoneNumber,
embedContent,
embedType,
});

ctx.body = {
ok: true,
Expand Down
22 changes: 13 additions & 9 deletions src/server/services/MessageService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
UnauthorizedError,
} from '../../shared/Errors';
import DeviceRepository from '../repositories/DeviceRepository';
import { Message } from '../../shared/Types';
import { InsertMessage, Message } from '../../shared/Types';
import { handleError } from '../utils/errors';
import BroadcastService from './BroadcastService';

Expand All @@ -27,11 +27,15 @@ class MessageService {
this.deviceRepository = deviceRepository;
}

public async sendMessage(
ctx: RouterContext,
content: string,
receiverPhoneNumber: string,
): Promise<Message> {
public async sendMessage(params: {
ctx: RouterContext;
phoneNumber: string;
content: string;
embedType: InsertMessage['embed_type'];
embedContent: InsertMessage['embed_content'];
}): Promise<Message> {
const { ctx, phoneNumber, content } = params;

if (!ctx.device.sim_card_id) {
throw new SimcardNotFoundError('SENDER');
}
Expand All @@ -40,9 +44,7 @@ class MessageService {
throw new SimCardNotActiveError('SENDER');
}

const receiverSimcard = await this.simCardRepository.getSimCardByPhoneNumber(
receiverPhoneNumber,
);
const receiverSimcard = await this.simCardRepository.getSimCardByPhoneNumber(phoneNumber);

if (!receiverSimcard) {
throw new SimcardNotFoundError('RECEIVER');
Expand All @@ -56,6 +58,8 @@ class MessageService {
sender_id: ctx.device.sim_card_id,
receiver_id: receiverSimcard.id,
content,
embed_type: params.embedType,
embed_content: params.embedContent,
});

this.broadcastNewMessage(ctx, message);
Expand Down
1 change: 1 addition & 0 deletions src/server/services/PlayerService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ class PlayerService {
}

public async selectDevice(src: number, deviceIdentifier: string) {
console.log(`Selecting device for ${src}: ${deviceIdentifier}`);
const isAuthorized = await this.authorizeDevice(src, deviceIdentifier);

if (!isAuthorized) {
Expand Down
7 changes: 6 additions & 1 deletion src/shared/Types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,11 +57,16 @@ export interface Message extends Record<string, unknown> {
sender_id: number;
receiver_id: number;
content: string;
embed_type: 'image' | 'video' | 'audio' | null;
embed_content: string | null;
created_at: Date;
updated_at: Date;
}

export type InsertMessage = Pick<Message, 'receiver_id' | 'sender_id' | 'content'>;
export type InsertMessage = Pick<
Message,
'receiver_id' | 'sender_id' | 'content' | 'embed_type' | 'embed_content'
>;

export interface MessageWithPhoneNumbers extends Message {
sender_phone_number: string;
Expand Down
2 changes: 1 addition & 1 deletion src/ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
"react-router-dom": "^6.26.1",
"tailwind-merge": "^2.5.2",
"tailwindcss-animate": "^1.0.7",
"vaul": "^0.9.3",
"vaul": "^0.9.4",
"vite-plugin-top-level-await": "^1.4.4"
},
"devDependencies": {
Expand Down
2 changes: 1 addition & 1 deletion src/ui/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ function App() {
});

useKeys({
Escape: () => {
Backspace: () => {
if (isNavigationDisabled) return;
closePhone();
navigate(-1);
Expand Down
68 changes: 68 additions & 0 deletions src/ui/src/Apps/Calls/Messages/Conversation/ConversationImages.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { ImageViewer } from '@/components/Gallery/ImageViewer';
import { clsx } from 'clsx';
import { AnimatePresence } from 'framer-motion';
import { useState } from 'react';
import { motion } from 'framer-motion';

interface ConversationImageProps {
images: string[];
isReceiver: boolean;
}
export const ConversationImages = ({ images, isReceiver }: ConversationImageProps) => {
const [selectedImageIndex, setSelectedImageIndex] = useState(-1);

return (
<>
<AnimatePresence>
{selectedImageIndex !== -1 && (
<ImageViewer
images={images}
initialIndex={selectedImageIndex}
onClose={() => {
setSelectedImageIndex(-1);
}}
/>
)}
</AnimatePresence>

<div className="flex flex-col gap-1 mt-2 w-full flex-1">
{images.length > 1 && (
<span
className={clsx(
'font-bold text-xs text-secondary',
isReceiver ? 'text-left' : 'text-right',
)}
>
{images.length} images
</span>
)}

<div className="flex overflow-auto justify-start max-w-80 isolate border rounded-2xl">
{images.map((src, index) => (
<motion.div
key={src}
className={clsx(
'h-40 rounded-xl flex-none object-cover cursor-pointer shadow-2xl drop-shadow-2xl',
)}
style={{
zIndex: -index,
marginLeft: index !== 0 ? `-45px` : 0,
}}
onClick={() => setSelectedImageIndex(index)}
animate={{ opacity: 1 }}
>
<img
draggable={false}
src={src}
alt="Image"
className={clsx(
'h-40 rounded-xl flex-none object-cover cursor-pointer shadow-2xl drop-shadow-2xl',
)}
/>
</motion.div>
))}
</div>
</div>
</>
);
};
59 changes: 59 additions & 0 deletions src/ui/src/Apps/Calls/Messages/Conversation/Message.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { Fragment } from 'react';
import { Message as MessageType } from '../../../../../../shared/Types';
import { DateTime } from 'luxon';
import { StringifyDates } from '../../../../../../shared/TypeUtils';
import { clsx } from 'clsx';
import { ConversationImages } from './ConversationImages';

interface MessageProps {
isReceiver: boolean;
message: StringifyDates<MessageType>;
previousMessage: StringifyDates<MessageType>;
}
export const Message = ({ isReceiver, message, previousMessage }: MessageProps) => {
const timeDiffSincePrevMessage = previousMessage
? new Date(message.created_at).getTime() - new Date(previousMessage?.created_at).getTime()
: 0;

const shouldDisplayDate = !previousMessage || timeDiffSincePrevMessage > 60 * 5 * 1000;
const isPreviousMessageDifferentSender = previousMessage?.sender_id !== message.sender_id;
const luxonDate = DateTime.fromISO(message.created_at);

const images = (message.embed_type === 'image' && message.embed_content?.split(',')) || [];

return (
<Fragment>
{shouldDisplayDate && (
<div className="flex gap-1 m-auto text-xs my-4">
<span className="font-medium">Today</span>
<span className="">{luxonDate?.toFormat('HH:mm')}</span>
</div>
)}

{images.length > 0 && (
<div
className={clsx('flex gap-1', {
'self-end': !isReceiver,
'self-start': isReceiver,
})}
>
<ConversationImages images={images} isReceiver={isReceiver} />
</div>
)}

<li
className={clsx(
'p-4 rounded-lg max-w-[80%] shadow-sm text-ellipsis flex-1 break-words',
{
'self-end': !isReceiver,
'self-start': isReceiver,
},
!isReceiver ? 'dark:bg-cyan-800 bg-cyan-100' : 'bg-secondary',
!shouldDisplayDate && isPreviousMessageDifferentSender && 'mt-2',
)}
>
{message.content}
</li>
</Fragment>
);
};
Loading

0 comments on commit 01f4211

Please sign in to comment.