Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add message preview component #779

Merged
merged 42 commits into from
Oct 12, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
61a914a
feat: add message button to profile using xmtp
Sep 30, 2022
e0e0086
address review feedback
Oct 5, 2022
c0190d6
Merge remote-tracking branch 'origin/main' into ea/add-profile-msg-btn
Oct 5, 2022
44f4f42
added xmtp and added conversations and messages to the store
bhavya2611 Oct 4, 2022
15faa0d
added route for converations
bhavya2611 Oct 4, 2022
96bfd31
added hook for streaming messages
bhavya2611 Oct 4, 2022
5cc3373
added converation page and message page
bhavya2611 Oct 4, 2022
4b33e85
addressed review comments
bhavya2611 Oct 5, 2022
9f00d39
updated import for old messages page
bhavya2611 Oct 5, 2022
859fef7
conversation list component added
bhavya2611 Oct 5, 2022
c9248f0
message list component added
bhavya2611 Oct 5, 2022
eb72f88
Revert "message list component added"
bhavya2611 Oct 5, 2022
af107c9
Revert "conversation list component added"
bhavya2611 Oct 5, 2022
8f05e8f
put all new code behind feature flag check and removed old messages code
bhavya2611 Oct 5, 2022
3e09f12
added seo back and added bhavyas profile to featured flag list
bhavya2611 Oct 5, 2022
f3e44eb
fix spelling error and lint warnings
Oct 5, 2022
8318575
Merge remote-tracking branch 'origin/main' into feat/setup-xmtp-req
Oct 5, 2022
aec4aaf
Merge branch 'main' into feat/setup-xmtp-req
bigint Oct 5, 2022
0a4fed8
Revert "fix spelling error and lint warnings"
Oct 5, 2022
20bdb2b
fix spelling error and disable exhaustive hooks warning
Oct 5, 2022
8dba5af
Merge remote-tracking branch 'origin/feat/setup-xmtp-req' into feat/s…
Oct 5, 2022
cc29054
feat: add conversation preview component
Oct 6, 2022
e77f3a6
Merge remote-tracking branch 'origin/main' into ea/add-convo-preview
Oct 6, 2022
fe08afd
grab data from graphql for profiles
Oct 7, 2022
368b52e
add profilemessage data type to pair lens & xmtp data
Oct 10, 2022
ee05a90
Merge remote-tracking branch 'origin/main' into ea/add-convo-preview
Oct 10, 2022
e6f095d
populate message preview
Oct 10, 2022
52db08a
ensure a match between lens and xmtp accts before showing
Oct 10, 2022
71bb1f4
add todo to sort messages by descending order
Oct 10, 2022
ff951c8
tidy up before PR
Oct 10, 2022
9bb0bca
remove unnecessary navbar
Oct 10, 2022
3172dea
Merge remote-tracking branch 'origin/main' into ea/add-convo-preview
Oct 10, 2022
7f8b154
Merge branch 'main' into ea/add-convo-preview
bigint Oct 11, 2022
0e05b7a
Update src/components/Messages/Preview.tsx
Oct 11, 2022
4e1c08b
remove message preview from user profile
Oct 11, 2022
3ec751b
remove MessagePreview class
Oct 11, 2022
86e13c8
Merge remote-tracking branch 'origin/main' into ea/add-convo-preview
Oct 11, 2022
7881f0a
fix: store convo addresses as lowercase
Oct 11, 2022
9101ff2
Merge branch 'main' into ea/add-convo-preview
bigint Oct 12, 2022
4999475
Merge branch 'main' into ea/add-convo-preview
bigint Oct 12, 2022
baf0f38
Remove isDefault from profileFields fragment
Oct 12, 2022
c126e4b
Merge remote-tracking branch 'origin/main' into ea/add-convo-preview
Oct 12, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 13 additions & 18 deletions src/components/Messages/Message.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import Preview from '@components/Messages/Preview';
import MessageComposer from '@components/Shared/MessageComposer';
import MessagesList from '@components/Shared/MessagesList';
import { Card } from '@components/UI/Card';
Expand All @@ -14,19 +15,17 @@ import { useAppStore } from 'src/store/app';
import { useMessageStore } from 'src/store/message';

const Message: FC = () => {
const { push, query } = useRouter();
const address = query.address as string;
const { query } = useRouter();
const address = (query.address as string).toLowerCase();
bigint marked this conversation as resolved.
Show resolved Hide resolved
const currentProfile = useAppStore((state) => state.currentProfile);
const messageState = useMessageStore((state) => state);
const { conversations } = messageState;
const conversations = useMessageStore((state) => state.conversations);
const selectedConversation = conversations.get(address);
// TODO(elise): Move messageProfiles and previewMessages to their own ConversationList component.
const messageProfiles = useMessageStore((state) => state.messageProfiles);
const previewMessages = useMessageStore((state) => state.previewMessages);
const { messages } = useGetMessages(selectedConversation);
const { sendMessage } = useSendMessage(selectedConversation);

const onConversationSelected = (address: string) => {
push(address ? `/messages/${address}` : '/messages/');
};

if (!isFeatureEnabled('messages', currentProfile?.id)) {
return <Custom404 />;
}
Expand All @@ -47,16 +46,12 @@ const Message: FC = () => {
<div className="text-xs">All messages</div>
</div>
<div>
{Array.from(conversations.keys()).map((address: string) => {
return (
<div
onClick={() => onConversationSelected(address)}
key={`convo_${address}`}
className="border p-5 text-xs"
>
{address}
</div>
);
{Array.from(messageProfiles.values()).map((profile, index) => {
const message = previewMessages.get(profile.ownedBy.toLowerCase());
if (!message) {
return null;
}
return <Preview key={`${profile.ownedBy}_${index}`} profile={profile} message={message} />;
})}
</div>
</Card>
Expand Down
60 changes: 60 additions & 0 deletions src/components/Messages/Preview.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import type { Profile } from '@generated/types';
import { BadgeCheckIcon } from '@heroicons/react/solid';
import getAvatar from '@lib/getAvatar';
import isVerified from '@lib/isVerified';
import type { Message } from '@xmtp/xmtp-js';
import clsx from 'clsx';
import dayjs from 'dayjs';
import relativeTime from 'dayjs/plugin/relativeTime';
import Link from 'next/link';
import { useRouter } from 'next/router';
import type { FC } from 'react';
import React from 'react';

dayjs.extend(relativeTime);

interface Props {
profile: Profile;
message: Message;
}

const Preview: FC<Props> = ({ profile, message }) => {
const router = useRouter();

const onConversationSelected = (address: string) => {
router.push(address ? `/messages/${address}` : '/messages');
};

return (
<div onClick={() => onConversationSelected(profile.ownedBy)}>
<div className="flex justify-between pb-4 space-x-1.5">
<div className="flex justify-between items-center">
<div className="flex items-center space-x-3">
<Link href={`/u/${profile?.handle}`}>
<img
src={getAvatar(profile)}
loading="lazy"
className={clsx('w-10 h-10', 'bg-gray-200 rounded-full border dark:border-gray-700/80')}
height={40}
width={40}
alt={profile?.handle}
/>
</Link>
<div>
<div className="flex gap-1 items-center max-w-sm truncate">
<div className={clsx('text-md')}>{profile?.name ?? profile?.handle}</div>
{isVerified(profile?.id) && <BadgeCheckIcon className="w-4 h-4 text-brand" />}
</div>
<span className="text-sm text-gray-500">{message.content}</span>
</div>
</div>
</div>
{message.sent && (
<span className="text-xs text-gray-500">{dayjs(new Date(message.sent)).fromNow()}</span>
)}
</div>
</div>
);
};

export default Preview;
137 changes: 80 additions & 57 deletions src/components/Messages/index.tsx
Original file line number Diff line number Diff line change
@@ -1,26 +1,56 @@
import { useQuery } from '@apollo/client';
import Preview from '@components/Messages/Preview';
import { Card } from '@components/UI/Card';
import { GridItemEight, GridItemFour, GridLayout } from '@components/UI/GridLayout';
import MetaTags from '@components/utils/MetaTags';
import type { Profile } from '@generated/types';
import { ProfilesDocument } from '@generated/types';
import isFeatureEnabled from '@lib/isFeatureEnabled';
import type { Conversation, Stream } from '@xmtp/xmtp-js';
import type { Conversation, Message } from '@xmtp/xmtp-js';
import { Client } from '@xmtp/xmtp-js';
import { useRouter } from 'next/router';
import type { FC } from 'react';
import { useEffect, useState } from 'react';
import { useEffect } from 'react';
import { APP_NAME } from 'src/constants';
import Custom404 from 'src/pages/404';
import Custom500 from 'src/pages/500';
import { useAppStore } from 'src/store/app';
import { useMessageStore } from 'src/store/message';
import { useSigner } from 'wagmi';

const Messages: FC = () => {
const { data: signer } = useSigner();
const currentProfile = useAppStore((state) => state.currentProfile);
const [stream, setStream] = useState<Stream<Conversation>>();
const messageState = useMessageStore((state) => state);
const { client, setClient, conversations, setConversations, messages, setMessages, setLoading } =
messageState;
const router = useRouter();
const client = useMessageStore((state) => state.client);
const setClient = useMessageStore((state) => state.setClient);
const isMessagesEnabled = isFeatureEnabled('messages', currentProfile?.id);

const conversations = useMessageStore((state) => state.conversations);
const setConversations = useMessageStore((state) => state.setConversations);
const messageProfiles = useMessageStore((state) => state.messageProfiles);
const setMessageProfiles = useMessageStore((state) => state.setMessageProfiles);
const previewMessages = useMessageStore((state) => state.previewMessages);
const setPreviewMessages = useMessageStore((state) => state.setPreviewMessages);

const peerAddresses = Array.from(conversations.keys());
const { error: profilesError } = useQuery(ProfilesDocument, {
elisealix22 marked this conversation as resolved.
Show resolved Hide resolved
// TODO(elise): Right now this is capped at 50 profiles. We'll want to paginate.
variables: {
request: { ownedBy: peerAddresses, limit: 50 }
},
skip: !currentProfile?.id || peerAddresses.length === 0,
onCompleted: (data) => {
if (!data?.profiles?.items?.length) {
return;
}
const profiles = data.profiles.items as Profile[];
const newMessageProfiles = new Map(messageProfiles);
for (const profile of profiles) {
const peerAddress = (profile.ownedBy as string).toLowerCase();
newMessageProfiles.set(peerAddress, profile);
}
setMessageProfiles(newMessageProfiles);
}
});

useEffect(() => {
const initXmtpClient = async () => {
Expand All @@ -29,67 +59,64 @@ const Messages: FC = () => {
setClient(xmtp);
}
};
if (isFeatureEnabled('messages', currentProfile?.id)) {
if (isMessagesEnabled) {
initXmtpClient();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [signer]);

useEffect(() => {
if (!client || !isFeatureEnabled('messages', currentProfile?.id)) {
if (!isMessagesEnabled || !client) {
return;
}

async function listConversations() {
setLoading(true);
const fetchMostRecentMessage = async (
convo: Conversation
): Promise<{ address: string; message?: Message }> => {
const peerAddress = convo.peerAddress.toLowerCase();
// TODO(elise): Add sort direction on XMTP's side so we can grab only the most recent message.
const newMessages = await convo.messages({ limit: 1 });
if (newMessages.length <= 0) {
return { address: peerAddress };
}
return { address: peerAddress, message: newMessages[0] };
};

const listConversations = async () => {
const newPreviewMessages = new Map(previewMessages);
const newConversations = new Map(conversations);
const convos = (await client?.conversations?.list()) || [];
Promise.all(
const previews = await Promise.all(
convos.map(async (convo) => {
if (convo.peerAddress !== currentProfile?.ownedBy) {
conversations.set(convo.peerAddress, convo);
setConversations(new Map(conversations));
}
newConversations.set(convo.peerAddress.toLowerCase(), convo);
return await fetchMostRecentMessage(convo);
})
).then(() => {
setLoading(false);
});
}
const streamConversations = async () => {
elisealix22 marked this conversation as resolved.
Show resolved Hide resolved
const newStream = (await client?.conversations?.stream()) || [];
setStream(newStream);
for await (const convo of newStream) {
if (convo.peerAddress !== currentProfile?.ownedBy) {
const newMessages = await convo.messages();
messages.set(convo.peerAddress, newMessages);
setMessages(new Map(messages));
conversations.set(convo.peerAddress, convo);
setConversations(new Map(conversations));
);
for (const preview of previews) {
if (preview.message) {
newPreviewMessages.set(preview.address, preview.message);
}
}
setPreviewMessages(newPreviewMessages);
setConversations(newConversations);
};
listConversations();
streamConversations();

return () => {
const closeStream = async () => {
if (!stream) {
return;
}
await stream.return();
};
closeStream();
};
listConversations();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [client]);

const onConversationSelected = (address: string) => {
router.push(address ? `/messages/${address}` : '/messages/');
};

if (!isFeatureEnabled('messages', currentProfile?.id)) {
if (!isMessagesEnabled) {
return <Custom404 />;
}

if (profilesError) {
return <Custom500 />;
}

if (previewMessages?.size <= 0 || messageProfiles?.size <= 0) {
return null;
elisealix22 marked this conversation as resolved.
Show resolved Hide resolved
}

return (
<GridLayout>
<MetaTags title={`Messages • ${APP_NAME}`} />
Expand All @@ -108,16 +135,12 @@ const Messages: FC = () => {
<div className="text-xs">All messages</div>
</div>
<div>
{Array.from(conversations.keys()).map((convo: string) => {
return (
<div
onClick={() => onConversationSelected(convo)}
key={`convo_${convo}`}
className="border p-5 text-xs"
>
{convo}
</div>
);
{Array.from(messageProfiles.values()).map((profile, index) => {
const message = previewMessages.get(profile.ownedBy.toLowerCase());
if (!message) {
return null;
}
return <Preview key={`${profile.ownedBy}_${index}`} profile={profile} message={message} />;
})}
</div>
</Card>
Expand Down
2 changes: 1 addition & 1 deletion src/components/utils/hooks/useGetMessages.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ const useGetMessages = (conversation?: Conversation) => {
}
const loadMessages = async () => {
const newMessages = await conversation.messages();
messages.set(conversation.peerAddress, newMessages);
messages.set(conversation.peerAddress.toLowerCase(), newMessages);
setMessages(new Map(messages));
};
loadMessages();
Expand Down
Loading