Skip to content

Commit

Permalink
wip(perf): pagination on mobile app
Browse files Browse the repository at this point in the history
  • Loading branch information
nikkothari22 committed Mar 20, 2024
1 parent 3c8eea0 commit 18ed2d6
Show file tree
Hide file tree
Showing 13 changed files with 545 additions and 101 deletions.
47 changes: 19 additions & 28 deletions mobile/src/components/features/chat-space/ChatInterface.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { Haptics, ImpactStyle } from '@capacitor/haptics'
import { UserContext } from '@/utils/auth/UserProvider'
import { ChatLoader } from '@/components/layout/loaders/ChatLoader'
import { MessageActionModal, useMessageActionModal } from './MessageActions/MessageActionModal'
import useChatStream from './useChatStream'

export type ChannelMembersMap = Record<string, UserFields>

Expand All @@ -32,6 +33,8 @@ export const ChatInterface = ({ channel }: { channel: ChannelListItem | DMChanne
scrollToBottom(0, 0)
})



const onNewMessageLoaded = useCallback(() => {
/**
* We need to scroll to the bottom of the chat interface if the user is already at the bottom.
Expand Down Expand Up @@ -65,27 +68,16 @@ export const ChatInterface = ({ channel }: { channel: ChannelListItem | DMChanne
*
* */
// Fetch all the messages in the channel
const { data: messages, error: messagesError, mutate: refreshMessages, isLoading: isMessageLoading } = useFrappeGetCall<{ message: MessagesWithDate }>("raven.api.raven_message.get_messages_with_dates", {
channel_id: channel.name
}, `get_messages_for_channel_${channel.name}`, {
keepPreviousData: true,
onSuccess: (data) => {
onNewMessageLoaded()
}
})

useFrappeDocumentEventListener('Raven Channel', channel.name, () => { })

/** Realtime event listener to update messages */
useFrappeEventListener('message_updated', (data) => {
//If the message is sent on the current channel
if (data.channel_id === channel.name) {
//If the sender is not the current user
if (data.sender !== currentUser) {
refreshMessages()
}
}
})
const { messages, error, isLoading } = useChatStream(channel.name, conRef)
// const { data: messages, error: messagesError, mutate: refreshMessages, isLoading: isMessageLoading } = useFrappeGetCall<{ message: MessagesWithDate }>("raven.api.raven_message.get_messages_with_dates", {
// channel_id: channel.name
// }, `get_messages_for_channel_${channel.name}`, {
// keepPreviousData: true,
// onSuccess: (data) => {
// onNewMessageLoaded()
// }
// })

const { data: channelMembers } = useFrappeGetCall<{ message: ChannelMembersMap }>('raven.api.chat.get_channel_members', {
channel_id: channel.name
Expand All @@ -97,10 +89,7 @@ export const ChatInterface = ({ channel }: { channel: ChannelListItem | DMChanne
Haptics.impact({
style: ImpactStyle.Light
})
refreshMessages()
.then(() => {
scrollToBottom(0, 100)
})
scrollToBottom(0, 100)
}

const { selectedMessage, onMessageSelected, onDismiss } = useMessageActionModal()
Expand Down Expand Up @@ -134,9 +123,11 @@ export const ChatInterface = ({ channel }: { channel: ChannelListItem | DMChanne
</IonToolbar>
</IonHeader>
<IonContent className='flex flex-col-reverse' fullscreen ref={conRef}>
{isMessageLoading && <ChatLoader />}
{messagesError && <ErrorBanner error={messagesError} />}
<ChatView messages={messages?.message ?? []} members={channelMembers?.message ?? {}} onMessageSelected={onMessageSelected} />
{isLoading && <ChatLoader />}
{error && <ErrorBanner error={error} />}
{messages &&
<ChatView messages={messages} members={channelMembers?.message ?? {}} onMessageSelected={onMessageSelected} />
}

{/* Commented out the button because it was unreliable. We only scroll to bottom when the user is at the bottom. */}
{/* <IonButton
Expand All @@ -157,7 +148,7 @@ export const ChatInterface = ({ channel }: { channel: ChannelListItem | DMChanne
</IonContent>

<IonFooter
hidden={!!messagesError}>
hidden={!!error}>
<div className='overflow-visible
text-slate-100
bg-[color:var(--ion-background-color)]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export const CopyAction = ({ message, onSuccess }: ActionProps) => {
// The copy action would only be available for text messages

//TODO: Extend this to other message types as well - one can have text content with image/file attachments as well
if (message.data.message_type !== 'Text') return null
if (message.message_type !== 'Text') return null

return <CopyActionItem message={message} onSuccess={onSuccess} />
}
Expand All @@ -22,13 +22,13 @@ const CopyActionItem = ({ message, onSuccess }: ActionProps) => {
const [loading, setLoading] = useState(false)

const writeToClipboard = () => {
if (message.data.message_type !== 'Text') {
if (message.message_type !== 'Text') {
return
}

setLoading(true)

let text = message.data.text
let text = message.text

// Remove all empty lines
text = text.replace(/^\s*[\r\n]/gm, "")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export const DeleteAction = ({ message, onSuccess }: ActionProps) => {
const [present] = useIonToast();

const deleteMessage = () => {
deleteDoc('Raven Message', message.data.name)
deleteDoc('Raven Message', message.name)

.then(() => {
return present({
Expand All @@ -32,7 +32,7 @@ export const DeleteAction = ({ message, onSuccess }: ActionProps) => {
message: "Error: Could not delete message",
})
})
.then(() => mutate(`get_messages_for_channel_${message.data.channel_id}`))
.then(() => mutate(`get_messages_for_channel_${message.channel_id}`))
.then(() => onSuccess())


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ interface EmojiActionProps extends ActionProps {
}
export const EmojiAction = ({ message, onSuccess, presentingElement }: EmojiActionProps) => {

const { data: { name: messageID } } = message
const { name: messageID } = message
const { call: reactToMessage } = useFrappePostCall('raven.api.reactions.react')

const { mutate } = useSWRConfig()
Expand All @@ -28,7 +28,7 @@ export const EmojiAction = ({ message, onSuccess, presentingElement }: EmojiActi
message_id: messageID,
reaction: emoji
})
.then(() => mutate(`get_messages_for_channel_${message.data.channel_id}`))
.then(() => mutate(`get_messages_for_channel_${message.channel_id}`))
.then(() => onSuccess())
}, [messageID, reactToMessage])

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useContext, useEffect, useRef, useState } from 'react'
import { MessageBlock } from '../../../../../../types/Messaging/Message'
import { Message } from '../../../../../../types/Messaging/Message'
import {
IonModal,
IonContent,
Expand All @@ -16,13 +16,13 @@ import { EmojiAction } from './EmojiAction';
import MessagePreview from './MessagePreview';

interface MessageActionModalProps {
selectedMessage?: MessageBlock,
selectedMessage?: Message,
onDismiss: VoidFunction,
}

export const MessageActionModal = ({ selectedMessage, onDismiss }: MessageActionModalProps) => {
const { currentUser } = useContext(UserContext)
const isOwnMessage = currentUser === selectedMessage?.data?.owner
const isOwnMessage = currentUser === selectedMessage?.owner

const modalRef = useRef<HTMLIonModalElement>(null)

Expand All @@ -45,7 +45,7 @@ export const MessageActionModal = ({ selectedMessage, onDismiss }: MessageAction
}
}, [selectedMessage])

const user = useGetUser(selectedMessage?.data.owner)
const user = useGetUser(selectedMessage?.owner)

return (
<IonModal
Expand Down Expand Up @@ -107,7 +107,7 @@ export const MessageActionModal = ({ selectedMessage, onDismiss }: MessageAction

export const useMessageActionModal = (mutate?: VoidFunction) => {

const [selectedMessage, setSelectedMessage] = useState<MessageBlock | undefined>(undefined)
const [selectedMessage, setSelectedMessage] = useState<Message | undefined>(undefined)

// const [present, dismiss] = useIonModal(MessageActionModal, {
// selectedMessage,
Expand All @@ -129,7 +129,7 @@ export const useMessageActionModal = (mutate?: VoidFunction) => {
// });
// }

const onMessageSelected = (m: MessageBlock) => {
const onMessageSelected = (m: Message) => {
setSelectedMessage(m)
// openModal()
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { MessageBlock } from '../../../../../../types/Messaging/Message'
import { Message } from '../../../../../../types/Messaging/Message'
import { UserFields } from '@/utils/users/UserListProvider'
import { UserAvatarBlock } from '../chat-view/MessageBlock'
import { IonText } from '@ionic/react'
import { DateObjectToTimeString } from '@/utils/operations/operations'
import parse from 'html-react-parser';

type MessagePreview = { message: MessageBlock, user?: UserFields }
type MessagePreview = { message: Message, user?: UserFields }

const MessagePreview = ({ message, user }: MessagePreview) => {
return (
Expand All @@ -15,16 +15,16 @@ const MessagePreview = ({ message, user }: MessagePreview) => {
</div>
<div className='overflow-x-clip'>
<div className='flex items-end pb-1.5'>
<IonText className='font-bold text-zinc-50 text-sm'>{user?.full_name ?? message.data.owner}</IonText>
<IonText className='text-xs pl-1.5 text-zinc-500'>{DateObjectToTimeString(message.data.creation)}</IonText>
<IonText className='font-bold text-zinc-50 text-sm'>{user?.full_name ?? message.owner}</IonText>
<IonText className='text-xs pl-1.5 text-zinc-500'>{DateObjectToTimeString(message.creation)}</IonText>
</div>
{message.data.message_type === 'Text' && <div className='text-base line-clamp-3 text-ellipsis'>{parse(message.data.content ?? '')}</div>}
{message.data.message_type === 'Image' && <div className='flex items-center space-x-2'>
<img src={message.data.file} alt={`Image`} className='inline-block w-10 h-10 rounded-md' />
{message.message_type === 'Text' && <div className='text-base line-clamp-3 text-ellipsis'>{parse(message.content ?? '')}</div>}
{message.message_type === 'Image' && <div className='flex items-center space-x-2'>
<img src={message.file} alt={`Image`} className='inline-block w-10 h-10 rounded-md' />
<p className='text-sm font-semibold'>📸 &nbsp;Image</p>
</div>}
{message.data.message_type === 'File' && <p
className='text-sm font-semibold'>📎 &nbsp;{message.data.file?.split('/')[3]}</p>}
{message.message_type === 'File' && <p
className='text-sm font-semibold'>📎 &nbsp;{message.file?.split('/')[3]}</p>}
</div>
</div>
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,13 @@ export const SaveMessageAction = ({ message, onSuccess }: ActionProps) => {
return JSON.parse(likedBy ?? '[]')?.length > 0 && JSON.parse(likedBy ?? '[]')?.includes(currentUser)
}

const isMessageLiked = checkLiked(message.data._liked_by)
const isMessageLiked = checkLiked(message._liked_by)
const handleLike = () => {

const action = isMessageLiked ? 'No' : 'Yes'
call({
doctype: 'Raven Message',
name: message.data.name,
name: message.name,
add: action
})
.then(() => {
Expand All @@ -41,7 +41,7 @@ export const SaveMessageAction = ({ message, onSuccess }: ActionProps) => {
message: "We ran into an error.",
})
})
.then(() => mutate(`get_messages_for_channel_${message.data.channel_id}`))
.then(() => mutate(`get_messages_for_channel_${message.channel_id}`))
.then(() => onSuccess())

}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,15 +42,15 @@ const ShareActionItem = ({ message, onSuccess }: ActionProps) => {

// If the message is a file, we need to download it first
let blob: Blob | null = null
if (message.data.message_type === 'Image' || message.data.message_type === 'File') {
if (message.message_type === 'Image' || message.message_type === 'File') {

blob = await downloadFile(message.data.file)
blob = await downloadFile(message.file)
}

if (webShareSupported) {
if (message.data.message_type === 'Image' || message.data.message_type === 'File') {
if (message.message_type === 'Image' || message.message_type === 'File') {

const fileName = getFileName(message.data.file)
const fileName = getFileName(message.file)

if (blob) {
shareOptions.files = [
Expand All @@ -60,8 +60,8 @@ const ShareActionItem = ({ message, onSuccess }: ActionProps) => {
]
shareOptions.title = fileName
}
} else if (message.data.message_type === 'Text') {
let text = message.data.text
} else if (message.message_type === 'Text') {
let text = message.text

// Remove all empty lines
text = text.replace(/^\s*[\r\n]/gm, "")
Expand Down Expand Up @@ -111,12 +111,12 @@ const ShareActionItem = ({ message, onSuccess }: ActionProps) => {
}
}
}
if (message.data.message_type === 'Image' || message.data.message_type === 'File') {
if (message.message_type === 'Image' || message.message_type === 'File') {
if (blob) {
// Fallback implementation.
console.log('Fallback to download', blob)
const a = document.createElement('a');
a.download = getFileName(message.data.file);
a.download = getFileName(message.file);
a.style.display = 'none';
a.href = URL.createObjectURL(blob);
a.addEventListener('click', () => {
Expand Down Expand Up @@ -152,7 +152,7 @@ const ShareActionItem = ({ message, onSuccess }: ActionProps) => {

};

const isFile = message.data.message_type !== 'Text'
const isFile = message.message_type !== 'Text'

const isDownload = isFile && !webShareSupported

Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { IonIcon, IonItem, IonLabel, IonSpinner } from '@ionic/react'
import React, { PropsWithChildren } from 'react'
import { MessageBlock } from '../../../../../../types/Messaging/Message'
import { Message } from '../../../../../../types/Messaging/Message'

export interface ActionProps {
message: MessageBlock,
message: Message,
onSuccess: () => void,
}
interface ActionItemProps {
Expand Down
20 changes: 11 additions & 9 deletions mobile/src/components/features/chat-space/chat-view/ChatView.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import { IonList } from '@ionic/react'
import { createContext } from 'react'
import { DateBlock, MessageBlock } from '../../../../../../types/Messaging/Message'
import { DateSeparator } from './DateSeparator'
import { ChannelMembersMap } from '../ChatInterface'
import { MessageBlockItem } from './MessageBlock'
import { MessageDateBlock } from '../useChatStream'
import { Message } from '../../../../../../types/Messaging/Message'

type ChatViewProps = {
messages: (DateBlock | MessageBlock)[],
messages: MessageDateBlock[],
members: ChannelMembersMap,
onMessageSelected: (message: MessageBlock) => void
onMessageSelected: (message: Message) => void
}

export const ChannelMembersContext = createContext<ChannelMembersMap>({})
Expand All @@ -21,14 +22,15 @@ export const ChatView = ({ messages, members, onMessageSelected }: ChatViewProps
return (
<ChannelMembersContext.Provider value={members}>
<IonList lines='none' className='flex flex-col'>
{messages.map((message: DateBlock | MessageBlock) => {
if (message.block_type === "date") {
return <DateSeparator key={message.data} date={message.data} />
}
if (message.block_type === "message")
{messages.map((message) => {

if (message.message_type === "date") {
return <DateSeparator key={`date-${message.creation}`} date={message.creation} />
} else {
return (
<MessageBlockItem key={message.data.name} message={message} onMessageSelect={onMessageSelected} />
<MessageBlockItem key={`${message.name}_${message.modified}`} message={message} onMessageSelect={onMessageSelected} />
)
}
}
)}
</IonList>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,22 +1,17 @@
import { DateObjectToFormattedDateString } from '@/utils/operations/operations'
import { IonText } from '@ionic/react'

type Props = {
date: string
}

const parseDateString = (date: string) => {
const dateObj = new Date(date)
return DateObjectToFormattedDateString(dateObj)
}
export const DateSeparator = ({ date }: Props) => {
return (
<div className='relative my-2 px-2'>
<div className="absolute inset-0 flex items-center mx-4" aria-hidden="true">
<div className="w-full border-t border-t-zinc-800" />
</div>
<div className="relative flex justify-center">
<IonText className="px-2 text-sm bg-[var(--ion-background-color)] font-bold text-zinc-300">{parseDateString(date)}</IonText>
<IonText className="px-2 text-sm bg-[var(--ion-background-color)] font-bold text-zinc-300">{date}</IonText>
</div>
</div>
)
Expand Down
Loading

0 comments on commit 18ed2d6

Please sign in to comment.