Skip to content

Commit

Permalink
feat(matches): export chat messages from context-menu
Browse files Browse the repository at this point in the history
ref #1056
  • Loading branch information
akiver committed Jan 31, 2025
1 parent 07149e0 commit 73cb0b9
Show file tree
Hide file tree
Showing 20 changed files with 460 additions and 171 deletions.
14 changes: 14 additions & 0 deletions src/node/chat-messages/export-match-chat-messages.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { fetchChatMessages } from '../database/chat-messages/fetch-chat-messages';
import { writeChatMessagesToFile } from './write-chat-messages-to-file';
import type { ChatMessage } from 'csdm/common/types/chat-message';

export async function exportMatchChatMessages(checksum: string, filePath: string, messages?: ChatMessage[]) {
let chatMessages: ChatMessage[];
if (!messages) {
chatMessages = await fetchChatMessages(checksum);
} else {
chatMessages = messages;
}

return await writeChatMessagesToFile(chatMessages, filePath);
}
20 changes: 20 additions & 0 deletions src/node/chat-messages/export-matches-chat-messages.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { fetchChatMessages } from '../database/chat-messages/fetch-chat-messages';
import path from 'node:path';
import { writeChatMessagesToFile } from './write-chat-messages-to-file';

export async function exportMatchesChatMessages(checksums: string[], outputFolder: string) {
let hasExportedAtLeastOneMatch = false;

await Promise.all(
checksums.map(async (checksum) => {
const messages = await fetchChatMessages(checksum);
const filePath = path.join(outputFolder, `messages-${checksum}.txt`);
const hasWrittenFile = await writeChatMessagesToFile(messages, filePath);
if (hasWrittenFile) {
hasExportedAtLeastOneMatch = true;
}
}),
);

return hasExportedAtLeastOneMatch;
}
21 changes: 21 additions & 0 deletions src/node/chat-messages/write-chat-messages-to-file.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import fs from 'fs-extra';
import type { ChatMessage } from 'csdm/common/types/chat-message';

function formatChatMessage(chatMessage: ChatMessage) {
const { senderIsAlive, senderName, message } = chatMessage;
const playerStatus = senderIsAlive ? '' : '*DEAD* ';

return `${playerStatus}${senderName} : ${message}`;
}

export async function writeChatMessagesToFile(messages: ChatMessage[], filePath: string) {
if (messages.length === 0) {
return false;
}

const formattedChatMessages = messages.map(formatChatMessage);
const text = formattedChatMessages.join('\n');
await fs.writeFile(filePath, text);

return true;
}
8 changes: 7 additions & 1 deletion src/server/handlers/renderer-handlers-mapping.ts
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,10 @@ import { updateCurrent5EPlayAccountHandler } from './renderer-process/5eplay/upd
import type { FiveEPlayMatch } from 'csdm/common/types/5eplay-match';
import { fetchLast5EPlayMatchesHandler } from './renderer-process/5eplay/fetch-last-5eplay-matches-handler';
import { delete5EPlayAccountHandler } from './renderer-process/5eplay/delete-5eplay-account-handler';
import {
exportMatchesChatMessagesHandler,
type ExportMatchesChatMessagesPayload,
} from './renderer-process/match/export-matches-chat-messages-handler';

export interface RendererMessageHandlers {
[RendererClientMessageName.InitializeApplication]: Handler<void, InitializeApplicationSuccessPayload>;
Expand Down Expand Up @@ -282,7 +286,8 @@ export interface RendererMessageHandlers {
WatchPlayerAsSuspectPayload,
WatchDemoErrorPayload | undefined
>;
[RendererClientMessageName.ExportMatchChatMessages]: Handler<ExportChatMessagesPayload>;
[RendererClientMessageName.ExportMatchChatMessages]: Handler<ExportChatMessagesPayload, boolean>;
[RendererClientMessageName.ExportMatchesChatMessages]: Handler<ExportMatchesChatMessagesPayload, boolean>;
[RendererClientMessageName.WriteBase64File]: Handler<WriteBase64FilePayload>;
[RendererClientMessageName.FetchPlayerStats]: Handler<FetchPlayerFilters, PlayerProfile>;
[RendererClientMessageName.InsertTag]: Handler<Omit<Tag, 'id'>, Tag>;
Expand Down Expand Up @@ -383,6 +388,7 @@ export const rendererHandlers: RendererMessageHandlers = {
[RendererClientMessageName.WatchPlayerLowlights]: watchPlayerLowlightsHandler,
[RendererClientMessageName.WatchPlayerAsSuspect]: watchPlayerAsSuspectHandler,
[RendererClientMessageName.ExportMatchChatMessages]: exportMatchChatMessagesHandler,
[RendererClientMessageName.ExportMatchesChatMessages]: exportMatchesChatMessagesHandler,
[RendererClientMessageName.WriteBase64File]: writeBase64FileHandler,
[RendererClientMessageName.FetchPlayerStats]: fetchPlayerHandler,
[RendererClientMessageName.InsertTag]: insertTagHandler,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
import fs from 'fs-extra';
import type { ChatMessage } from 'csdm/common/types/chat-message';
import { handleError } from '../../handle-error';

import { exportMatchChatMessages } from 'csdm/node/chat-messages/export-match-chat-messages';

export type ExportChatMessagesPayload = {
checksum: string;
filePath: string;
messages: string[];
messages?: ChatMessage[];
};

export async function exportMatchChatMessagesHandler({ filePath, messages }: ExportChatMessagesPayload) {
export async function exportMatchChatMessagesHandler({ checksum, filePath, messages }: ExportChatMessagesPayload) {
try {
const text = messages.join('\n');
await fs.writeFile(filePath, text);
return await exportMatchChatMessages(checksum, filePath, messages);
} catch (error) {
handleError(error, 'Error while exporting chat messages');
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { exportMatchesChatMessages } from 'csdm/node/chat-messages/export-matches-chat-messages';
import { handleError } from '../../handle-error';

export type ExportMatchesChatMessagesPayload = { outputFolderPath: string; checksums: string[] };

export async function exportMatchesChatMessagesHandler({
checksums,
outputFolderPath,
}: ExportMatchesChatMessagesPayload) {
try {
return await exportMatchesChatMessages(checksums, outputFolderPath);
} catch (error) {
handleError(error, 'Error while exporting matches chat messages');
}
}
1 change: 1 addition & 0 deletions src/server/renderer-client-message-name.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export const RendererClientMessageName = {
UpdateMatchDemoLocation: 'update-match-demo-location',
InitializeVideo: 'initialize-video',
ExportMatchChatMessages: 'export-match-chat-messages',
ExportMatchesChatMessages: 'export-matches-chat-messages',
FetchDemosTable: 'fetch-demos-table',
LoadDemoByPath: 'load-demo-by-path',
NavigateToDemoOrMatch: 'navigate-to-demo-or-match',
Expand Down
3 changes: 3 additions & 0 deletions src/ui/changelog/changelog-dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,9 @@ export function ChangelogDialog() {
<li>
<Category>MATCH</Category> Added search input in chat tab to filter messages.
</li>
<li>
<Category>MATCHES</Category> Added context-menu item to export chat messages.
</li>
<li>
<Category>SEARCH</Category> Added no scope kills event.
</li>
Expand Down
76 changes: 76 additions & 0 deletions src/ui/components/context-menu/items/export-chat-messages-item.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import React from 'react';
import { Trans, useLingui } from '@lingui/react/macro';
import { ContextMenuItem } from 'csdm/ui/components/context-menu/context-menu-item';
import { useWebSocketClient } from 'csdm/ui/hooks/use-web-socket-client';
import { useShowToast } from '../../toasts/use-show-toast';
import { RendererClientMessageName } from 'csdm/server/renderer-client-message-name';
import type { ExportMatchesChatMessagesPayload } from 'csdm/server/handlers/renderer-process/match/export-matches-chat-messages-handler';
import { useExportMatchChatMessages } from 'csdm/ui/hooks/use-export-match-chat-messages';

type Props = {
checksums: string[];
};

export function ExportChatMessagesItem({ checksums }: Props) {
const { t } = useLingui();
const client = useWebSocketClient();
const showToast = useShowToast();
const exportChatMessages = useExportMatchChatMessages();

const onClick = async () => {
if (checksums.length === 1) {
return exportChatMessages(checksums[0]);
}

const { filePaths, canceled } = await window.csdm.showOpenDialog({
buttonLabel: t({
context: 'Button label',
message: 'Select',
}),
properties: ['openDirectory'],
});

if (canceled || filePaths.length === 0) {
return;
}

const outputFolderPath = filePaths[0];

try {
const payload: ExportMatchesChatMessagesPayload = {
checksums,
outputFolderPath,
};
const hasExportedAtLeastOneMatch = await client.send({
name: RendererClientMessageName.ExportMatchesChatMessages,
payload,
});

if (hasExportedAtLeastOneMatch) {
showToast({
content: <Trans context="Toast">Chat messages exported, click here to reveal the folder</Trans>,
type: 'success',
onClick: () => {
window.csdm.browseToFolder(outputFolderPath);
},
});
} else {
showToast({
content: <Trans>No chat messages to export</Trans>,
type: 'warning',
});
}
} catch (error) {
showToast({
content: <Trans>An error occurred</Trans>,
type: 'error',
});
}
};

return (
<ContextMenuItem onClick={onClick} isDisabled={checksums.length === 0}>
<Trans context="Context menu">Chat messages</Trans>
</ContextMenuItem>
);
}
2 changes: 2 additions & 0 deletions src/ui/components/context-menu/items/export-matches-item.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { SubContextMenu } from 'csdm/ui/components/context-menu/sub-context-menu
import { Trans } from '@lingui/react/macro';
import { ExportMatchesToXlsxItem } from './export-matches-to-xlsx-item';
import { ExportPlayersVoiceItem } from './export-players-voice-item';
import { ExportChatMessagesItem } from './export-chat-messages-item';

type Props = {
matches: MatchTable[];
Expand Down Expand Up @@ -37,6 +38,7 @@ export function ExportMatchesItem({ matches }: Props) {
<ExportMatchesToXlsxItem onClick={onExportToXlsxClick} />
<ExportMatchesToJsonItem checksums={checksums} />
<ExportPlayersVoiceItem demoPaths={filepaths} />
<ExportChatMessagesItem checksums={checksums} />
</SubContextMenu>
);
}
61 changes: 61 additions & 0 deletions src/ui/hooks/use-export-match-chat-messages.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import React from 'react';
import { Trans, useLingui } from '@lingui/react/macro';
import { useShowToast } from '../components/toasts/use-show-toast';
import { useWebSocketClient } from './use-web-socket-client';
import type { SaveDialogOptions, SaveDialogReturnValue } from 'electron';
import type { ExportChatMessagesPayload } from 'csdm/server/handlers/renderer-process/match/export-match-chat-messages-handler';
import type { ChatMessage } from 'csdm/common/types/chat-message';
import { RendererClientMessageName } from 'csdm/server/renderer-client-message-name';

export function useExportMatchChatMessages() {
const client = useWebSocketClient();
const showToast = useShowToast();
const { t } = useLingui();

return async (checksum: string, messages?: ChatMessage[]) => {
const options: SaveDialogOptions = {
defaultPath: `messages-${checksum}.txt`,
title: t({
context: 'OS save dialog title',
message: 'Export',
}),
filters: [{ name: 'TXT', extensions: ['txt'] }],
};
const { canceled, filePath }: SaveDialogReturnValue = await window.csdm.showSaveDialog(options);
if (canceled || filePath === undefined) {
return;
}

try {
const payload: ExportChatMessagesPayload = {
checksum,
filePath,
messages,
};
const hasMessages = await client.send({
name: RendererClientMessageName.ExportMatchChatMessages,
payload,
});

if (hasMessages) {
showToast({
content: <Trans>Chat messages exported, click here to reveal the file</Trans>,
type: 'success',
onClick: () => {
window.csdm.browseToFile(filePath);
},
});
} else {
showToast({
content: <Trans>No chat messages to export</Trans>,
type: 'warning',
});
}
} catch (error) {
showToast({
content: <Trans>An error occurred</Trans>,
type: 'error',
});
}
};
}
2 changes: 1 addition & 1 deletion src/ui/match/chat-messages/chat-messages.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export function ChatMessages() {
return (
<>
<ActionBar
left={<ExportChatMessagesButton checksum={checksum} chatMessages={visibleChatMessages} />}
left={<ExportChatMessagesButton checksum={checksum} messages={visibleChatMessages} />}
right={<TextInputFilter value={fuzzySearchText} onChange={setFuzzySearchText} />}
/>
<ChatMessagesList chatMessages={visibleChatMessages} />
Expand Down
67 changes: 8 additions & 59 deletions src/ui/match/chat-messages/export-chat-messages-button.tsx
Original file line number Diff line number Diff line change
@@ -1,74 +1,23 @@
import React from 'react';
import { Trans, useLingui } from '@lingui/react/macro';
import type { SaveDialogOptions, SaveDialogReturnValue } from 'electron';
import { Trans } from '@lingui/react/macro';
import { Button } from 'csdm/ui/components/buttons/button';
import type { ChatMessage } from 'csdm/common/types/chat-message';
import { RendererClientMessageName } from 'csdm/server/renderer-client-message-name';
import { useWebSocketClient } from 'csdm/ui/hooks/use-web-socket-client';
import type { ExportChatMessagesPayload } from 'csdm/server/handlers/renderer-process/match/export-match-chat-messages-handler';
import { useShowToast } from 'csdm/ui/components/toasts/use-show-toast';

function formatChatMessage(chatMessage: ChatMessage) {
const { senderIsAlive, senderName, message } = chatMessage;
const playerStatus = senderIsAlive ? '' : '*DEAD* ';

return `${playerStatus}${senderName} : ${message}`;
}
import { useExportMatchChatMessages } from 'csdm/ui/hooks/use-export-match-chat-messages';

type Props = {
checksum: string;
chatMessages: ChatMessage[];
messages: ChatMessage[];
};

export function ExportChatMessagesButton({ checksum, chatMessages }: Props) {
const client = useWebSocketClient();
const showToast = useShowToast();
const { t } = useLingui();

const onClick = async () => {
const options: SaveDialogOptions = {
defaultPath: `messages-${checksum}.txt`,
title: t({
context: 'OS save dialog title',
message: 'Export',
}),
filters: [{ name: 'TXT', extensions: ['txt'] }],
};
const { canceled, filePath }: SaveDialogReturnValue = await window.csdm.showSaveDialog(options);
if (canceled || filePath === undefined) {
return;
}

try {
const messages = chatMessages.map(formatChatMessage);
const payload: ExportChatMessagesPayload = {
filePath,
messages,
};
await client.send({
name: RendererClientMessageName.ExportMatchChatMessages,
payload,
});
export function ExportChatMessagesButton({ checksum, messages }: Props) {
const exportMessages = useExportMatchChatMessages();

showToast({
content: <Trans>Chat messages exported</Trans>,
id: 'chat-messages-export',
type: 'success',
onClick: () => {
window.csdm.browseToFile(filePath);
},
});
} catch (error) {
showToast({
content: <Trans>An error occurred</Trans>,
id: 'chat-messages-export',
type: 'error',
});
}
const onClick = () => {
exportMessages(checksum, messages);
};

return (
<Button onClick={onClick} isDisabled={chatMessages.length === 0}>
<Button onClick={onClick} isDisabled={messages.length === 0}>
<Trans context="Button">Export</Trans>
</Button>
);
Expand Down
Loading

0 comments on commit 73cb0b9

Please sign in to comment.