Skip to content

Commit

Permalink
Further evolved chat app for the owner-app (#25)
Browse files Browse the repository at this point in the history
* Cleanup chat type of double items;

* Simplified structure;

* Properly display galleries of 3;

* Chat context WIP

* Added reply option;

* Fixed getFileHeader to avoid altering when parsing;

* Updated tryJsonParse to avoid returning empty objects, when the source is already parsed;

* Improved handling of conversations; And read receipts

* Fixed a bug gallery nav;

* Added delete chat support;

* Added delete ui state; Added delete of payload;

* Vite upgrade;

* Make gifs work, even when the tiny thumb isn't used;

* Added new conversation and new group conversation sidebars;

* Added a first version of group chat;

* Added a first version of group chat;

* Fix build provisioning;

* WIP: keep logged in state, when back-end isn't reachable;

* Added support for read reciepts on group chats;

* Change command processor to run sequentially;

* Add indication when not connected to group chat recipients;

* Add indication when not connected to group chat recipients;

* Better detection of emoji messages;

* Integrated the chat-app into the owner-app;

* Added message info;

* Moved components;

* Added deleted drive search result for queryModified;

* Extended queryBatch with dynamice return type;

* Increased contrast on the postTeaser; Added sr labels for the socials;

* Build fixes;

* Moved chat-app to run at identity.cloud/apps/chat

* Fixed bad merge of conversatinos;

* Rebuid package-lock;

* Upgrade node v;

* Explict set of rollup version;

* Attribute editor fixes;

* Updated chat link on sidenav;
  • Loading branch information
stef-coenen authored Nov 29, 2023
1 parent 87412ee commit eb852db
Show file tree
Hide file tree
Showing 84 changed files with 3,013 additions and 2,077 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ jobs:

- uses: actions/setup-node@v3
with:
node-version: '18.x'
node-version: '20.x'
registry-url: 'https://npm.pkg.github.com'
# Defaults to the user or organization that owns the workflow file
scope: '@octocat'
Expand Down
1,660 changes: 619 additions & 1,041 deletions package-lock.json

Large diffs are not rendered by default.

5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@
"prettier": "3.0.3",
"tslib": "^2.4.0",
"typescript": "5.2",
"vite": "4.5.0",
"vite-plugin-dts": "^3.6.0"
"vite": "5.0.4",
"vite-plugin-dts": "^3.6.3",
"rollup": "4.6.0"
}
}
2 changes: 1 addition & 1 deletion packages/chat-app/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<html lang="en">
<head>
<meta charset="utf-8" />
<!-- <base href="/chat/" /> -->
<base href="/apps/chat/" />
<meta name="viewport" content="width=device-width, initial-scale=1" />

<link rel="apple-touch-icon" sizes="180x180" href="/icons/apple-touch-icon.png" />
Expand Down
2 changes: 1 addition & 1 deletion packages/chat-app/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1 @@
export * from './src/templates/Conversations/Conversations';
export * from './src/templates/Chat/ChatHome';
3 changes: 2 additions & 1 deletion packages/chat-app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
"files": [
"src"
],
"type": "module",
"module": "./index.ts",
"types": "./index.ts",
"scripts": {
Expand Down Expand Up @@ -35,7 +36,7 @@
"@types/lodash-es": "^4.17.6",
"@types/react": "^18.0.26",
"@types/react-dom": "^18.0.9",
"@vitejs/plugin-react": "^4.1.0",
"@vitejs/plugin-react": "^4.2.0",
"autoprefixer": "10.4.16",
"postcss": "8.4.31",
"prettier-plugin-tailwindcss": "0.5.5",
Expand Down
8 changes: 6 additions & 2 deletions packages/chat-app/src/app/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,15 @@ const About = lazy(() => import('../templates/About/About'));
const Auth = lazy(() => import('../templates/Auth/Auth'));
const FinalizeAuth = lazy(() => import('../templates/Auth/FinalizeAuth'));

const ChatHome = lazy(() => import('../templates/Chat/ChatHome'));
const ChatHome = lazy(() =>
import('../templates/Chat/ChatHome').then((chatApp) => ({ default: chatApp.ChatHome }))
);

import '@youfoundation/ui-lib/dist/style.css';
import './App.css';
import { useAuth } from '../hooks/auth/useAuth';

export const ROOT_PATH = '';
export const ROOT_PATH = '/apps/chat';
const AUTH_PATH = ROOT_PATH + '/auth';

import { ErrorBoundary, NotFound } from '@youfoundation/common-app';
Expand Down Expand Up @@ -61,6 +63,8 @@ function App() {
>
<Route index={true} element={<ChatHome />} />
<Route path={':conversationKey'} element={<ChatHome />} />
<Route path={'new'} element={<ChatHome />} />
<Route path={'new-group'} element={<ChatHome />} />
<Route path={':conversationKey/:chatMessageKey'} element={<ChatHome />} />
<Route path={':conversationKey/:chatMessageKey/:mediaKey'} element={<ChatHome />} />
</Route>
Expand Down
60 changes: 60 additions & 0 deletions packages/chat-app/src/components/Chat/ChatHistory.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { ErrorNotification } from '@youfoundation/common-app';
import { DriveSearchResult } from '@youfoundation/js-lib/core';
import { useChatMessages } from '../../hooks/chat/useChatMessages';
import { useMarkMessagesAsRead } from '../../hooks/chat/useMarkMessagesAsRead';
import { ChatMessage } from '../../providers/ChatProvider';
import { Conversation } from '../../providers/ConversationProvider';
import { ChatMessageItem } from './Detail/ChatMessageItem';
import { ChatActions } from './Detail/ContextMenu';
import { useMemo } from 'react';

export const ChatHistory = ({
conversation,
setReplyMsg,
}: {
conversation: DriveSearchResult<Conversation> | undefined;
setReplyMsg: (msg: DriveSearchResult<ChatMessage>) => void;
}) => {
const {
all: { data: messages },
delete: { mutate: deleteMessages, error: deleteMessagesError },
} = useChatMessages({ conversationId: conversation?.fileMetadata?.appData?.uniqueId });
const flattenedMsgs = useMemo(
() =>
(messages?.pages.flatMap((page) => page.searchResults).filter(Boolean) ||
[]) as DriveSearchResult<ChatMessage>[],
[messages]
);

useMarkMessagesAsRead({ conversation, messages: flattenedMsgs });

const chatActions: ChatActions = {
doReply: (msg: DriveSearchResult<ChatMessage>) => setReplyMsg(msg),
doDelete: async (msg: DriveSearchResult<ChatMessage>) => {
if (!conversation || !msg) return;
await deleteMessages({
conversation: conversation,
messages: [msg],
deleteForEveryone: true,
});
},
};

return (
<>
<ErrorNotification error={deleteMessagesError} />
<div className="flex h-full flex-grow flex-col-reverse gap-2 overflow-auto p-5">
{flattenedMsgs?.map((msg) =>
msg ? (
<ChatMessageItem
key={msg.fileId}
msg={msg}
conversation={conversation}
chatActions={chatActions}
/>
) : null
)}
</div>
</>
);
};
132 changes: 132 additions & 0 deletions packages/chat-app/src/components/Chat/Composer/ChatComposer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
import {
ErrorNotification,
FileOverview,
EmojiSelector,
FileSelector,
ImageIcon,
VolatileInput,
ActionButton,
t,
Times,
} from '@youfoundation/common-app';
import { DriveSearchResult } from '@youfoundation/js-lib/core';
import { NewMediaFile } from '@youfoundation/js-lib/dist';
import { useChatMessage } from '../../../hooks/chat/useChatMessage';
import { ChatMessage } from '../../../providers/ChatProvider';
import {
Conversation,
GroupConversation,
SingleConversation,
} from '../../../providers/ConversationProvider';
import { useState, useEffect } from 'react';
import { EmbeddedMessage } from '../Detail/EmbeddedMessage';

export const ChatComposer = ({
conversation,
replyMsg,
clearReplyMsg,
}: {
conversation: DriveSearchResult<Conversation> | undefined;
replyMsg: DriveSearchResult<ChatMessage> | undefined;
clearReplyMsg: () => void;
}) => {
const [stateIndex, setStateIndex] = useState(0); // Used to force a re-render of the component, to reset the input
const [message, setMessage] = useState<string | undefined>();
const [files, setFiles] = useState<NewMediaFile[]>();

const {
mutate: sendMessage,
status: sendMessageState,
reset: resetState,
error: sendMessageError,
} = useChatMessage().send;

const conversationContent = conversation?.fileMetadata.appData.content;
const doSend = () => {
if ((!message && !files) || !conversationContent || !conversation.fileMetadata.appData.uniqueId)
return;
sendMessage({
conversationId: conversation.fileMetadata.appData.uniqueId as string,
message: message || '',
replyId: replyMsg?.fileMetadata?.appData?.uniqueId,
files,
recipients: (conversationContent as GroupConversation).recipients || [
(conversationContent as SingleConversation).recipient,
],
});
};

// Reset state, when the message was sent successfully
useEffect(() => {
if (sendMessageState === 'success') {
setMessage('');
setStateIndex((oldIndex) => oldIndex + 1);
setFiles([]);
clearReplyMsg();
resetState();
}
}, [sendMessageState]);

useEffect(() => {
if (replyMsg) setFiles([]);
}, [replyMsg]);

useEffect(() => {
if (files?.length) clearReplyMsg();
}, [files]);

return (
<>
<ErrorNotification error={sendMessageError} />
<div className="bg-page-background">
<FileOverview files={files} setFiles={setFiles} className="mt-2" />
{replyMsg ? <MessageForReply msg={replyMsg} onClear={clearReplyMsg} /> : null}
<div className="flex flex-shrink-0 flex-row gap-2 px-5 py-3">
<div className="my-auto flex flex-row items-center gap-1">
<EmojiSelector
size="none"
className="px-1 py-1 text-foreground text-opacity-30 hover:text-opacity-100"
onInput={(val) => setMessage((oldVal) => (oldVal ? `${oldVal} ${val}` : val))}
/>
<FileSelector
onChange={(files) => setFiles(files.map((file) => ({ file })))}
className="text-foreground text-opacity-30 hover:text-opacity-100"
>
<ImageIcon className="h-5 w-5" />
</FileSelector>
</div>

<VolatileInput
key={stateIndex}
placeholder="Your message"
defaultValue={message}
className="rounded-md border bg-background p-2 dark:border-slate-800"
onChange={setMessage}
onSubmit={(val) => {
setMessage(val);
doSend();
}}
/>
<ActionButton type="secondary" onClick={doSend} state={sendMessageState}>
{t('Send')}
</ActionButton>
</div>
</div>
</>
);
};

const MessageForReply = ({
msg,
onClear,
}: {
msg: DriveSearchResult<ChatMessage>;
onClear: () => void;
}) => {
return (
<div className="flex flex-row gap-2 px-5 py-3">
<EmbeddedMessage msg={msg} />
<ActionButton icon={Times} type="mute" onClick={onClear}></ActionButton>
</div>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { useDotYouClient, SubtleCheck } from '@youfoundation/common-app';
import { DriveSearchResult } from '@youfoundation/js-lib/core';
import { ChatMessage, ChatDeliveryStatus } from '../../../providers/ChatProvider';

export const ChatDeliveryIndicator = ({ msg }: { msg: DriveSearchResult<ChatMessage> }) => {
const identity = useDotYouClient().getIdentity();
const content = msg.fileMetadata.appData.content;
const authorOdinId = msg.fileMetadata.senderOdinId;
const messageFromMe = !authorOdinId || authorOdinId === identity;

if (!messageFromMe) return null;
return <InnerDeliveryIndicator state={content.deliveryStatus} />;
};

export const InnerDeliveryIndicator = ({ state }: { state?: ChatDeliveryStatus }) => {
const isDelivered = state && state >= ChatDeliveryStatus.Delivered;
const isRead = state === ChatDeliveryStatus.Read;

return (
<div
className={`${isDelivered ? '-ml-2' : ''} flex flex-row drop-shadow-md ${
isRead ? 'text-blue-600 ' : 'text-foreground/60'
}`}
>
{isDelivered ? <SubtleCheck className="relative -right-2 z-10 h-4 w-4" /> : null}
<SubtleCheck className="h-4 w-4" />
</div>
);
};
60 changes: 60 additions & 0 deletions packages/chat-app/src/components/Chat/Detail/ChatMessageInfo.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { createPortal } from 'react-dom';
import { DriveSearchResult } from '@youfoundation/js-lib/core';
import { ChatMessage } from '../../../providers/ChatProvider';
import {
ConnectionImage,
ConnectionName,
DialogWrapper,
t,
usePortal,
} from '@youfoundation/common-app';
import {
Conversation,
GroupConversation,
SingleConversation,
} from '../../../providers/ConversationProvider';
import { InnerDeliveryIndicator } from './ChatDeliveryIndicator';

export const ChatMessageInfo = ({
msg,
conversation,
onClose,
}: {
msg: DriveSearchResult<ChatMessage>;
conversation: DriveSearchResult<Conversation>;
onClose: () => void;
}) => {
const target = usePortal('modal-container');
const messageContent = msg.fileMetadata.appData.content;
const conversationContent = conversation.fileMetadata.appData.content;
const recipients = (conversationContent as GroupConversation).recipients || [
(conversationContent as SingleConversation).recipient,
];

const dialog = (
<DialogWrapper onClose={onClose} title={t('Message info')}>
<div>
<p className="mb-2 text-lg">{t('Recipients')}</p>
<div className="flex flex-col gap-4">
{recipients.map((recipient) => (
<div className="flex flex-row items-center justify-between" key={recipient}>
<div className="flex flex-row items-center gap-2">
<ConnectionImage
odinId={recipient}
className="border border-neutral-200 dark:border-neutral-800"
size="sm"
/>
<ConnectionName odinId={recipient} />
</div>
<InnerDeliveryIndicator
state={messageContent.deliveryDetails?.[recipient] || messageContent.deliveryStatus}
/>
</div>
))}
</div>
</div>
</DialogWrapper>
);

return createPortal(dialog, target);
};
Loading

0 comments on commit eb852db

Please sign in to comment.