diff --git a/mobile/index.html b/mobile/index.html index ba56143d0..b74d0c2a7 100644 --- a/mobile/index.html +++ b/mobile/index.html @@ -5,7 +5,7 @@ - Raven + {{ app_name }} diff --git a/mobile/package.json b/mobile/package.json index 29b1b2e7b..68a7bc11b 100644 --- a/mobile/package.json +++ b/mobile/package.json @@ -37,7 +37,7 @@ "clsx": "^2.1.0", "emoji-picker-element": "^1.21.1", "firebase": "^10.9.0", - "frappe-react-sdk": "^1.3.11", + "frappe-react-sdk": "^1.5.1", "highlight.js": "^11.9.0", "html-react-parser": "^5.1.8", "input-otp": "^1.2.2", diff --git a/mobile/public/sw.js b/mobile/public/sw.js index 5db9fdbb8..66978fb40 100644 --- a/mobile/public/sw.js +++ b/mobile/public/sw.js @@ -28,19 +28,26 @@ try { if (payload.data.notification_icon) { notificationOptions["icon"] = payload.data.notification_icon } + + if (payload.data.raven_message_type === "Image") { + notificationOptions["image"] = payload.data.content + } + + if (payload.data.creation) { + notificationOptions["timestamp"] = payload.data.creation + } + const url = `${payload.data.base_url}/raven_mobile/channel/${payload.data.channel_id}` if (isChrome()) { notificationOptions["data"] = { - url: payload.data.click_action, + url: url, } } else { - if (payload.data.click_action) { - notificationOptions["actions"] = [ - { - action: payload.data.click_action, - title: "View", - }, - ] - } + notificationOptions["actions"] = [ + { + action: url, + title: "View", + }, + ] } self.registration.showNotification(notificationTitle, notificationOptions) }) diff --git a/mobile/src/App.tsx b/mobile/src/App.tsx index 00bde79ae..6f72656b2 100644 --- a/mobile/src/App.tsx +++ b/mobile/src/App.tsx @@ -59,13 +59,6 @@ function App() { } - useEffect(() => { - //@ts-expect-error - window?.frappePushNotification?.onMessage((payload) => { - showNotification(payload) - }) - }, []) - return ( void, } -export const ChatInput = ({ channelID, allChannels, allMembers, onMessageSend }: Props) => { +export const ChatInput = ({ channelID, allChannels, allMembers }: Props) => { const { call, loading } = useFrappePostCall('raven.api.raven_message.send_message') const [files, setFiles] = useState([]) + const onMessageSend = useCallback(() => { + Haptics.impact({ + style: ImpactStyle.Light + }) + }, []) + const onSubmit = async (message: string, json: any) => { return call({ channel_id: channelID, diff --git a/mobile/src/components/features/chat-space/ChatInterface.tsx b/mobile/src/components/features/chat-space/ChatInterface.tsx index c1a75488f..242143056 100644 --- a/mobile/src/components/features/chat-space/ChatInterface.tsx +++ b/mobile/src/components/features/chat-space/ChatInterface.tsx @@ -1,81 +1,77 @@ -import { IonBackButton, IonButton, IonButtons, IonContent, IonFooter, IonHeader, IonIcon, IonToolbar, useIonViewWillEnter } from '@ionic/react' -import { useFrappeDocumentEventListener, useFrappeEventListener, useFrappeGetCall } from 'frappe-react-sdk' -import { useCallback, useContext, useMemo, useRef } from 'react' -import { MessagesWithDate } from '../../../../../types/Messaging/Message' +import { IonBackButton, IonButton, IonButtons, IonContent, IonFooter, IonHeader, IonIcon, IonSpinner, IonToolbar, useIonViewWillEnter } from '@ionic/react' +import { useFrappeGetCall } from 'frappe-react-sdk' +import { createContext, useMemo, useRef } from 'react' import { ErrorBanner } from '../../layout' import { ChatInput } from '../chat-input' -import { ChatView } from './chat-view/ChatView' import { ChatHeader } from './chat-header' import { ChannelListItem, DMChannelListItem, useChannelList } from '@/utils/channel/ChannelListProvider' import { UserFields } from '@/utils/users/UserListProvider' -import { peopleOutline } from 'ionicons/icons' -import { Haptics, ImpactStyle } from '@capacitor/haptics' -import { UserContext } from '@/utils/auth/UserProvider' +import { arrowDownOutline, peopleOutline } from 'ionicons/icons' import { ChatLoader } from '@/components/layout/loaders/ChatLoader' import { MessageActionModal, useMessageActionModal } from './MessageActions/MessageActionModal' import useChatStream from './useChatStream' +import { useInView } from 'react-intersection-observer' +import { DateSeparator } from './chat-view/DateSeparator' +import { MessageBlockItem } from './chat-view/MessageBlock' +import ChatViewFirstMessage from './chat-view/ChatViewFirstMessage' export type ChannelMembersMap = Record +export const ChannelMembersContext = createContext({}) export const ChatInterface = ({ channel }: { channel: ChannelListItem | DMChannelListItem }) => { - const { currentUser } = useContext(UserContext) - const initialDataLoaded = useRef(false) const conRef = useRef(null); - const scrollToBottom = useCallback((duration = 0, delay = 0) => { - setTimeout(() => { - conRef.current?.scrollToBottom(duration) - }, delay) - }, []) - useIonViewWillEnter(() => { - scrollToBottom(0, 0) + conRef.current?.scrollToBottom() }) - const onNewMessageLoaded = useCallback(() => { - /** - * We need to scroll to the bottom of the chat interface if the user is already at the bottom. - * If the user is not at the bottom, we need to show a button to scroll to the bottom. - */ - if (conRef.current && !initialDataLoaded.current) { - scrollToBottom(0, 100) - initialDataLoaded.current = true - } else { - conRef.current?.getScrollElement().then((scrollElement) => { - - const scrollHeight = scrollElement.scrollHeight - const clientHeight = scrollElement.clientHeight - const scrollTop = scrollElement.scrollTop - const isAtBottom = scrollHeight <= scrollTop + clientHeight - if (isAtBottom) { - scrollToBottom(0, 100) + const { + messages, + hasOlderMessages, + loadOlderMessages, + hasNewMessages, + loadNewerMessages, + loadingOlderMessages, + highlightedMessage, + scrollToMessage, + goToLatestMessages, + error, + isLoading } = useChatStream(channel.name, conRef) + + + const onReplyMessageClick = (messageID: string) => { + scrollToMessage(messageID) + } + + const { ref: oldLoaderRef } = useInView({ + fallbackInView: true, + initialInView: false, + skip: !hasOlderMessages || loadingOlderMessages, + onChange: (async (inView) => { + if (inView && hasOlderMessages) { + const lastMessage = messages ? messages[0] : null; + await loadOlderMessages() + // Restore the scroll position to the last message before loading more + if (lastMessage?.message_type === 'date') { + document.getElementById(`date-${lastMessage?.creation}`)?.scrollIntoView() } else { - // setNewMessagesAvailable(true) + document.getElementById(`message-${lastMessage?.name}`)?.scrollIntoView() } - }) + } + }) + }); + + const { ref: newLoaderRef } = useInView({ + fallbackInView: true, + skip: !hasNewMessages, + initialInView: false, + onChange: (inView) => { + if (inView && hasNewMessages) { + loadNewerMessages() + } } - - }, [scrollToBottom, conRef]) - - /** - * We have the channel data. We also have the channel list in a global context. - * Now we need to fetch: - * 1. All the messages in the channel - this is done outside and then sent to the ChatHistory component - * 2. All the users in the channel - * - * */ - // Fetch all the messages in the channel - - 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 @@ -85,13 +81,6 @@ export const ChatInterface = ({ channel }: { channel: ChannelListItem | DMChanne revalidateOnReconnect: false }) - const onMessageSend = () => { - Haptics.impact({ - style: ImpactStyle.Light - }) - scrollToBottom(0, 100) - } - const { selectedMessage, onMessageSelected, onDismiss } = useMessageActionModal() const { channels } = useChannelList() @@ -106,6 +95,7 @@ export const ChatInterface = ({ channel }: { channel: ChannelListItem | DMChanne } return [] }, [channelMembers]) + return ( <> @@ -122,27 +112,62 @@ export const ChatInterface = ({ channel }: { channel: ChannelListItem | DMChanne - + + +
+ {hasOlderMessages && !isLoading &&
+ +
} +
+ {!isLoading && !hasOlderMessages && } {isLoading && } {error && } + + {messages && - + +
+ {messages.map((message) => { + + if (message.message_type === "date") { + return + } else { + return ( + + ) + } + } + )} +
+
} + {hasNewMessages &&
+
+ +
+
} + {/* Commented out the button because it was unreliable. We only scroll to bottom when the user is at the bottom. */} - {/* scrollToBottom(200, 0)} - hidden={!newMessagesAvailable} + onClick={goToLatestMessages} shape='round' // fill="outline" className="fixed bottom-24 left-1/2 -translate-x-1/2 " > - New messages - */} + + }
@@ -155,7 +180,7 @@ export const ChatInterface = ({ channel }: { channel: ChannelListItem | DMChanne border-t-zinc-900 border-t-[1px] pb-6 pt-1'> - + diff --git a/mobile/src/components/features/chat-space/chat-view/ChatView.tsx b/mobile/src/components/features/chat-space/chat-view/ChatView.tsx deleted file mode 100644 index 64c1791c2..000000000 --- a/mobile/src/components/features/chat-space/chat-view/ChatView.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import { IonList } from '@ionic/react' -import { createContext } from 'react' -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: MessageDateBlock[], - members: ChannelMembersMap, - onMessageSelected: (message: Message) => void -} - -export const ChannelMembersContext = createContext({}) -export const ChatView = ({ messages, members, onMessageSelected }: ChatViewProps) => { - - /** The ChatHistory component renders all the messages in the Chat. - * It receives a messages array - which consists of two blocks: a Date Block (to show a date separator) and a Message Block (to show a message) - */ - - return ( - - - {messages.map((message) => { - - if (message.message_type === "date") { - return - } else { - return ( - - ) - } - } - )} - - - ) -} \ No newline at end of file diff --git a/mobile/src/components/features/chat-space/chat-view/ChatViewFirstMessage.tsx b/mobile/src/components/features/chat-space/chat-view/ChatViewFirstMessage.tsx new file mode 100644 index 000000000..780881fee --- /dev/null +++ b/mobile/src/components/features/chat-space/chat-view/ChatViewFirstMessage.tsx @@ -0,0 +1,83 @@ +import { SquareAvatar } from '@/components/common/UserAvatar' +import { useGetUser } from '@/hooks/useGetUser' +import { ChannelListItem, DMChannelListItem } from '@/utils/channel/ChannelListProvider' +import { BiGlobe, BiHash, BiLock } from 'react-icons/bi' + +type Props = { + channel: ChannelListItem | DMChannelListItem +} + +const ChatViewFirstMessage = ({ channel }: Props) => { + + if (channel.is_direct_message) { + return + } else { + return + } +} + +const ICON_SIZE = '24px' + +const parseDateString = (date: string) => { + const dateObj = new Date(date) + return dateObj.toLocaleDateString('en-GB', { day: 'numeric', month: 'long', year: 'numeric' }) +} + +const ChannelHeader = ({ channel }: { channel: ChannelListItem }) => { + + const owner = useGetUser(channel.owner) + + return
+
+
+ {channel.type === 'Private' ? : channel.type === 'Public' ? : } +
+

{channel.channel_name}

+ +
+
+
+

{channel.channel_description}

+

{owner?.full_name ?? channel.owner} created this channel on {parseDateString(channel.creation)}.
This is the very beginning of the {channel.channel_name} channel.

+ +
+
+
+} + +const DirectMessageHeader = ({ channel }: { channel: DMChannelListItem }) => { + + const peer = channel.peer_user_id + + const peerUser = useGetUser(peer) + + if (peer && peerUser) { + return
+
+
+ +
+

{peerUser.full_name}

+

{peerUser.name}

+
+
+ {channel.is_self_message == 1 ? +
+

This space is all yours. Draft messages, list your to-dos, or keep links and files handy.

+

And if you ever feel like talking to yourself, don't worry, we won't judge - just remember to bring your own banter to the table.

+
+ : +
+

This is a Direct Message channel between you and {peerUser.full_name ?? peer}.

+
+ } +
+
+ } else { + return null + } + + +} + +export default ChatViewFirstMessage \ No newline at end of file diff --git a/mobile/src/components/features/chat-space/chat-view/DateSeparator.tsx b/mobile/src/components/features/chat-space/chat-view/DateSeparator.tsx index d2ace3a86..07d20d1be 100644 --- a/mobile/src/components/features/chat-space/chat-view/DateSeparator.tsx +++ b/mobile/src/components/features/chat-space/chat-view/DateSeparator.tsx @@ -6,7 +6,7 @@ type Props = { export const DateSeparator = ({ date }: Props) => { return ( -
+