From dce5388dbd7646767fc1df1b771d8930eb8cf27f Mon Sep 17 00:00:00 2001 From: pranavmene2000 Date: Fri, 27 Dec 2024 18:53:38 +0530 Subject: [PATCH 1/3] feat: DMs list --- .../(tabs)/direct-messages/_layout.tsx | 2 +- .../(tabs)/direct-messages/index.tsx | 46 ++++++++++++++-- apps/mobile/components/layout/UserAvatar.tsx | 25 +++------ .../nativewindui/useFetchActiveUsers.ts | 54 +++++++++++++++++++ apps/mobile/hooks/useActiveState.ts | 50 +++++++++++++++++ apps/mobile/hooks/useFetchActiveUsers.ts | 54 +++++++++++++++++++ apps/mobile/hooks/useIsUserActive.ts | 27 ++++++++++ apps/mobile/lib/Providers.tsx | 15 ++++-- apps/mobile/lib/UserInactivityProvider.tsx | 46 ++++++++++++++++ apps/mobile/package.json | 1 + packages/lib/hooks/useBoolean.ts | 19 +++++++ yarn.lock | 12 +++++ 12 files changed, 323 insertions(+), 28 deletions(-) create mode 100644 apps/mobile/components/nativewindui/useFetchActiveUsers.ts create mode 100644 apps/mobile/hooks/useActiveState.ts create mode 100644 apps/mobile/hooks/useFetchActiveUsers.ts create mode 100644 apps/mobile/hooks/useIsUserActive.ts create mode 100644 apps/mobile/lib/UserInactivityProvider.tsx create mode 100644 packages/lib/hooks/useBoolean.ts diff --git a/apps/mobile/app/[site_id]/(tabs)/direct-messages/_layout.tsx b/apps/mobile/app/[site_id]/(tabs)/direct-messages/_layout.tsx index d40db126d..1219fff7d 100644 --- a/apps/mobile/app/[site_id]/(tabs)/direct-messages/_layout.tsx +++ b/apps/mobile/app/[site_id]/(tabs)/direct-messages/_layout.tsx @@ -13,7 +13,7 @@ const DirectMessagesLayout = () => { ) diff --git a/apps/mobile/app/[site_id]/(tabs)/direct-messages/index.tsx b/apps/mobile/app/[site_id]/(tabs)/direct-messages/index.tsx index a84a788d5..07b927287 100644 --- a/apps/mobile/app/[site_id]/(tabs)/direct-messages/index.tsx +++ b/apps/mobile/app/[site_id]/(tabs)/direct-messages/index.tsx @@ -1,10 +1,48 @@ -import { View } from 'react-native'; +import { Link } from 'expo-router'; +import { Pressable, ScrollView, View } from 'react-native'; +import useGetDirectMessageChannels from '@raven/lib/hooks/useGetDirectMessageChannels'; +import { DMChannelListItem } from '@raven/types/common/ChannelListItem'; +import { useGetUser } from '@raven/lib/hooks/useGetUser'; +import UserAvatar from '@components/layout/UserAvatar'; import { Text } from '@components/nativewindui/Text'; +import { useColorScheme } from '@hooks/useColorScheme'; +import { useIsUserActive } from '@hooks/useIsUserActive'; export default function DirectMessages() { + const { colors } = useColorScheme() + + const { dmChannels } = useGetDirectMessageChannels() + + return ( + + {dmChannels.map((dm) => )} + + ) +} + +interface DMRowProps { + dm: DMChannelListItem +} +const DMRow = ({ dm }: DMRowProps) => { + const user = useGetUser(dm.peer_user_id) + + const isActive = useIsUserActive(dm.peer_user_id) + return ( - - DMs - + + + + + + {user?.full_name} + {dm.last_message_details ? JSON.parse(dm?.last_message_details)?.content?.substring(0, 35) + "..." : ""} + + + ) } \ No newline at end of file diff --git a/apps/mobile/components/layout/UserAvatar.tsx b/apps/mobile/components/layout/UserAvatar.tsx index a52c2b0ea..574f5c546 100644 --- a/apps/mobile/components/layout/UserAvatar.tsx +++ b/apps/mobile/components/layout/UserAvatar.tsx @@ -106,25 +106,14 @@ const styles = StyleSheet.create({ const ActiveIndicator = ({ isActive, availabilityStatus, isBot, botColor, indicatorProps }: Pick & { indicatorProps?: ViewProps, botColor?: string }) => { const dotColor = useMemo(() => { - - if (availabilityStatus) { - if (availabilityStatus === 'Away') { - return 'bg-yellow-500' - } else if (availabilityStatus === 'Do not disturb') { - return 'bg-red-500' - } else if (availabilityStatus === 'Invisible') { - return '' - } else if (availabilityStatus === 'Available') { - return 'bg-green-500' - } - } - if (isActive) { - return 'bg-green-500' - } else { - return '' + switch (availabilityStatus) { + case 'Away': return 'bg-yellow-500'; + case 'Do not disturb': return 'bg-red-500'; + case 'Available': + case 'Invisible': return isActive ? 'bg-green-500' : ''; + default: return ''; } - - }, [availabilityStatus, isActive]) + }, [availabilityStatus, isActive]); if (isBot) { return { + const res = useFrappeGetCall<{ message: string[] }>('raven.api.user_availability.get_active_users', + undefined, + 'active_users', + { + dedupingInterval: 1000 * 60 * 5, // 5 minutes - do not refetch if the data is fresh + } + ) + + return res +} + +/** + * Hook to listen to user_active_state_updated event and update the active_users list in realtime + * Also handles the user's active state via visibilty change and idle timer + */ +export const useFetchActiveUsersRealtime = () => { + const { currentUserInfo } = useGetCurrentUser(); + + const { mutate } = useSWRConfig() + + useActiveState() + + /** Hook to listen to user_active_state */ + useFrappeEventListener('raven:user_active_state_updated', (data) => { + if (data.user !== currentUserInfo?.name) { + // If the user is not the current user, update the active_users list + // No need to revalidate the data as the websocket event has emitted the new data for that user + mutate('active_users', (res?: { message: string[] }) => { + if (res) { + if (data.active) { + return { message: [...res.message, data.user] } + } else { + return { message: res.message.filter(user => user !== data.user) } + } + } else { + return undefined + } + }, { + revalidate: false + }) + } + }) +} + +export default useFetchActiveUsers \ No newline at end of file diff --git a/apps/mobile/hooks/useActiveState.ts b/apps/mobile/hooks/useActiveState.ts new file mode 100644 index 000000000..f6414ab6c --- /dev/null +++ b/apps/mobile/hooks/useActiveState.ts @@ -0,0 +1,50 @@ +import { useContext, useEffect } from "react"; +import { FrappeContext, FrappeConfig } from "frappe-react-sdk"; +import { useActiveUser } from "@lib/UserInactivityProvider"; +import { useBoolean } from "@raven/lib/hooks/useBoolean"; + +export type PresenceType = "active" | "idle"; + +interface Presence { + type: PresenceType; +} + +export const useActiveState = () => { + const { call } = useContext(FrappeContext) as FrappeConfig; + + const [isActive, { on: activate, off: deactivate }] = useBoolean(false); + + const { isActive: isUserActive } = useActiveUser(); + + const updateUserActiveState = async (deactivate = false) => { + return call + .get("raven.api.user_availability.refresh_user_active_state", { + deactivate, + }) + .catch(console.log); + }; + + const onPresenceChange = (presence: Presence) => { + if (presence.type === "active" && !isActive) { + updateUserActiveState().then(activate); + } else if (presence.type === "idle" && isActive) { + updateUserActiveState(true).then(deactivate); + } + }; + + useEffect(() => { + if (isUserActive) { + onPresenceChange({ type: "active" }); + } else { + onPresenceChange({ type: "idle" }); + } + + return () => { + if (isActive) { + updateUserActiveState(true).then(deactivate); + } + }; + }, [isUserActive, onPresenceChange]); + + return isActive; +}; diff --git a/apps/mobile/hooks/useFetchActiveUsers.ts b/apps/mobile/hooks/useFetchActiveUsers.ts new file mode 100644 index 000000000..d07c24c90 --- /dev/null +++ b/apps/mobile/hooks/useFetchActiveUsers.ts @@ -0,0 +1,54 @@ +import useCurrentRavenUser from '@raven/lib/hooks/useCurrentRavenUser' +import { useFrappeEventListener, useFrappeGetCall, useSWRConfig } from 'frappe-react-sdk' +import { useActiveState } from './useActiveState' + +/** + * Hook to fetch active users from the server. + * SWRKey: active_users + */ +const useFetchActiveUsers = () => { + const res = useFrappeGetCall<{ message: string[] }>('raven.api.user_availability.get_active_users', + undefined, + 'active_users', + { + dedupingInterval: 1000 * 60 * 5, // 5 minutes - do not refetch if the data is fresh + } + ) + + return res +} + +/** + * Hook to listen to user_active_state_updated event and update the active_users list in realtime + * Also handles the user's active state via visibilty change and idle timer + */ +export const useFetchActiveUsersRealtime = () => { + const { myProfile: currentUserInfo } = useCurrentRavenUser(); + + const { mutate } = useSWRConfig() + + useActiveState() + + /** Hook to listen to user_active_state */ + useFrappeEventListener('raven:user_active_state_updated', (data) => { + if (data.user !== currentUserInfo?.name) { + // If the user is not the current user, update the active_users list + // No need to revalidate the data as the websocket event has emitted the new data for that user + mutate('active_users', (res?: { message: string[] }) => { + if (res) { + if (data.active) { + return { message: [...res.message, data.user] } + } else { + return { message: res.message.filter(user => user !== data.user) } + } + } else { + return undefined + } + }, { + revalidate: false + }) + } + }) +} + +export default useFetchActiveUsers \ No newline at end of file diff --git a/apps/mobile/hooks/useIsUserActive.ts b/apps/mobile/hooks/useIsUserActive.ts new file mode 100644 index 000000000..57e789ed7 --- /dev/null +++ b/apps/mobile/hooks/useIsUserActive.ts @@ -0,0 +1,27 @@ +import { useMemo } from 'react'; +import useFetchActiveUsers from './useFetchActiveUsers'; +import useCurrentRavenUser from '@raven/lib/hooks/useCurrentRavenUser'; +import { useGetUser } from '@raven/lib/hooks/useGetUser'; + +export const useIsUserActive = (userID?: string): boolean => { + + const { myProfile: currentUserInfo } = useCurrentRavenUser() + const { data } = useFetchActiveUsers() + + const user = useGetUser(userID) + + const isActive = useMemo(() => { + // if user has explicitly set their status to invisible, do not show them as active + if (user?.availability_status === 'Invisible') { + return false + } else if (userID === currentUserInfo?.name) { + return true + } else if (userID) { + return data?.message.includes(userID) ?? false + } else { + return false + } + }, [userID, data]) + + return isActive +} \ No newline at end of file diff --git a/apps/mobile/lib/Providers.tsx b/apps/mobile/lib/Providers.tsx index 32abd37b3..8445ce590 100644 --- a/apps/mobile/lib/Providers.tsx +++ b/apps/mobile/lib/Providers.tsx @@ -3,6 +3,7 @@ import { Text } from '@components/nativewindui/Text' import { ChannelListContext, useChannelListProvider } from '@raven/lib/providers/ChannelListProvider' import { UserListContext, useUserListProvider } from '@raven/lib/providers/UserListProvider' import React, { PropsWithChildren } from 'react' +import { ActiveUserProvider } from './UserInactivityProvider' const Providers = (props: PropsWithChildren) => { @@ -16,11 +17,15 @@ const Providers = (props: PropsWithChildren) => { return Error loading users } - return - - {props.children} - - + return ( + + + + {props.children} + + + + ) } const ChannelListProvider = ({ children }: PropsWithChildren) => { diff --git a/apps/mobile/lib/UserInactivityProvider.tsx b/apps/mobile/lib/UserInactivityProvider.tsx new file mode 100644 index 000000000..8411c31ce --- /dev/null +++ b/apps/mobile/lib/UserInactivityProvider.tsx @@ -0,0 +1,46 @@ +import React, { createContext, useContext, useState, ReactNode } from "react"; +import UserInactivity from "react-native-user-inactivity"; + +interface ActiveUserContextType { + isActive: boolean; +} + +const ActiveUserContext = createContext( + undefined +); + +interface ActiveUserProviderProps { + children: ReactNode; + inactivityTimeout?: number; +} + +export const ActiveUserProvider = ({ + children, + inactivityTimeout = 1000 * 60 * 10, +}: ActiveUserProviderProps) => { + const [isActive, setIsActive] = useState(true); + + const handleUserActivity = (active: boolean) => { + setIsActive(active); + }; + + return ( + + + {children} + + + ); +}; + +export const useActiveUser = (): ActiveUserContextType => { + const context = useContext(ActiveUserContext); + if (context === undefined) { + throw new Error("useActiveUser must be used within an ActiveUserProvider"); + } + return context; +}; diff --git a/apps/mobile/package.json b/apps/mobile/package.json index 3f4d34cdd..6071bda31 100644 --- a/apps/mobile/package.json +++ b/apps/mobile/package.json @@ -56,6 +56,7 @@ "react-native-svg": "^15.10.1", "react-native-svg-transformer": "^1.5.0", "react-native-uitextview": "^1.4.0", + "react-native-user-inactivity": "^1.2.0", "react-native-web": "~0.19.13", "tailwind-merge": "^2.5.5" }, diff --git a/packages/lib/hooks/useBoolean.ts b/packages/lib/hooks/useBoolean.ts new file mode 100644 index 000000000..b8f5078e5 --- /dev/null +++ b/packages/lib/hooks/useBoolean.ts @@ -0,0 +1,19 @@ +import { useCallback, useState } from "react"; + +/** + * Simple hook to manage boolean (on - off) states + * @param initialState + * @returns + */ +export function useBoolean(initialState: boolean = false) { + + const [value, setValue] = useState(initialState); + + const on = useCallback(() => setValue(true), []); + + const off = useCallback(() => setValue(false), []); + + const toggle = useCallback(() => setValue(value => !value), []); + + return [value, { on, off, toggle }, setValue] as const; +} \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 1231c71e0..6c1bfac62 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9034,6 +9034,13 @@ react-native-uitextview@^1.4.0: resolved "https://registry.yarnpkg.com/react-native-uitextview/-/react-native-uitextview-1.4.0.tgz#d1b583cc173cec00f4fdd03744cca76c54a12fbb" integrity sha512-itm/frzkn/ma3+lwmKn2CkBOXPNo4bL8iVwQwjlzix5gVO59T2+axdfoj/Wi+Ra6F76KzNKxSah+7Y8dYmCHbQ== +react-native-user-inactivity@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/react-native-user-inactivity/-/react-native-user-inactivity-1.2.0.tgz#b0123951b89db9939eec07cffab724f7e76edcc6" + integrity sha512-VR+zv+cKBOSbJyHLJxvDUVluQ4OD/uW9FomAJVWrXCnSO4tYQieaAR3/5oNIfr6JXAJ/ChdJ/eyc06vHEBvlsA== + dependencies: + usetimeout-react-hook "^0.1.2" + react-native-web@~0.19.13: version "0.19.13" resolved "https://registry.yarnpkg.com/react-native-web/-/react-native-web-0.19.13.tgz#2d84849bf0251ec0e3a8072fda7f9a7c29375331" @@ -10614,6 +10621,11 @@ use-sync-external-store@^1.2.0, use-sync-external-store@^1.2.2: resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.4.0.tgz#adbc795d8eeb47029963016cefdf89dc799fcebc" integrity sha512-9WXSPC5fMv61vaupRkCKCxsPxBocVnwakBEkMIHHpkTTg6icbJtg6jzgtLDm4bl3cSHAca52rYWih0k4K3PfHw== +usetimeout-react-hook@^0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/usetimeout-react-hook/-/usetimeout-react-hook-0.1.2.tgz#fdeaff9b8fe2b5e5b8d29a3ef9c4054368711c03" + integrity sha512-uHc8QsWDznEhWkK+ygX0xWxyObRjy72685EZ5wr3/ipNsK0EYntGnbenKxLNirPKKsng+1KW5CPTAk7qQDY8VQ== + util-deprecate@^1.0.2, util-deprecate@~1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" From 9ae932552283dc33f0a4278fcceec86e514e6a33 Mon Sep 17 00:00:00 2001 From: pranavmene2000 Date: Fri, 27 Dec 2024 19:16:07 +0530 Subject: [PATCH 2/3] fix: revert user's status in UserAvatar --- apps/mobile/components/layout/UserAvatar.tsx | 25 ++++++++++++++------ 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/apps/mobile/components/layout/UserAvatar.tsx b/apps/mobile/components/layout/UserAvatar.tsx index 574f5c546..a52c2b0ea 100644 --- a/apps/mobile/components/layout/UserAvatar.tsx +++ b/apps/mobile/components/layout/UserAvatar.tsx @@ -106,14 +106,25 @@ const styles = StyleSheet.create({ const ActiveIndicator = ({ isActive, availabilityStatus, isBot, botColor, indicatorProps }: Pick & { indicatorProps?: ViewProps, botColor?: string }) => { const dotColor = useMemo(() => { - switch (availabilityStatus) { - case 'Away': return 'bg-yellow-500'; - case 'Do not disturb': return 'bg-red-500'; - case 'Available': - case 'Invisible': return isActive ? 'bg-green-500' : ''; - default: return ''; + + if (availabilityStatus) { + if (availabilityStatus === 'Away') { + return 'bg-yellow-500' + } else if (availabilityStatus === 'Do not disturb') { + return 'bg-red-500' + } else if (availabilityStatus === 'Invisible') { + return '' + } else if (availabilityStatus === 'Available') { + return 'bg-green-500' + } + } + if (isActive) { + return 'bg-green-500' + } else { + return '' } - }, [availabilityStatus, isActive]); + + }, [availabilityStatus, isActive]) if (isBot) { return Date: Fri, 10 Jan 2025 16:33:54 +0530 Subject: [PATCH 3/3] chore: moved useFetchActiveUsers to mobile hooks --- .../nativewindui/useFetchActiveUsers.ts | 54 ------------------- apps/mobile/hooks/useFetchActiveUsers.ts | 2 +- 2 files changed, 1 insertion(+), 55 deletions(-) delete mode 100644 apps/mobile/components/nativewindui/useFetchActiveUsers.ts diff --git a/apps/mobile/components/nativewindui/useFetchActiveUsers.ts b/apps/mobile/components/nativewindui/useFetchActiveUsers.ts deleted file mode 100644 index f4fd5c936..000000000 --- a/apps/mobile/components/nativewindui/useFetchActiveUsers.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { useFrappeEventListener, useFrappeGetCall, useSWRConfig } from 'frappe-react-sdk' -import { useActiveState } from './useActiveState' -import { useGetCurrentUser } from './useGetCurrentUser' - -/** - * Hook to fetch active users from the server. - * SWRKey: active_users - */ -const useFetchActiveUsers = () => { - const res = useFrappeGetCall<{ message: string[] }>('raven.api.user_availability.get_active_users', - undefined, - 'active_users', - { - dedupingInterval: 1000 * 60 * 5, // 5 minutes - do not refetch if the data is fresh - } - ) - - return res -} - -/** - * Hook to listen to user_active_state_updated event and update the active_users list in realtime - * Also handles the user's active state via visibilty change and idle timer - */ -export const useFetchActiveUsersRealtime = () => { - const { currentUserInfo } = useGetCurrentUser(); - - const { mutate } = useSWRConfig() - - useActiveState() - - /** Hook to listen to user_active_state */ - useFrappeEventListener('raven:user_active_state_updated', (data) => { - if (data.user !== currentUserInfo?.name) { - // If the user is not the current user, update the active_users list - // No need to revalidate the data as the websocket event has emitted the new data for that user - mutate('active_users', (res?: { message: string[] }) => { - if (res) { - if (data.active) { - return { message: [...res.message, data.user] } - } else { - return { message: res.message.filter(user => user !== data.user) } - } - } else { - return undefined - } - }, { - revalidate: false - }) - } - }) -} - -export default useFetchActiveUsers \ No newline at end of file diff --git a/apps/mobile/hooks/useFetchActiveUsers.ts b/apps/mobile/hooks/useFetchActiveUsers.ts index d07c24c90..03102ff13 100644 --- a/apps/mobile/hooks/useFetchActiveUsers.ts +++ b/apps/mobile/hooks/useFetchActiveUsers.ts @@ -1,6 +1,6 @@ -import useCurrentRavenUser from '@raven/lib/hooks/useCurrentRavenUser' import { useFrappeEventListener, useFrappeGetCall, useSWRConfig } from 'frappe-react-sdk' import { useActiveState } from './useActiveState' +import useCurrentRavenUser from '@raven/lib/hooks/useCurrentRavenUser' /** * Hook to fetch active users from the server.