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 d40db126..1219fff7 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 a84a788d..07b92728 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/hooks/useActiveState.ts b/apps/mobile/hooks/useActiveState.ts
new file mode 100644
index 00000000..f6414ab6
--- /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 00000000..03102ff1
--- /dev/null
+++ b/apps/mobile/hooks/useFetchActiveUsers.ts
@@ -0,0 +1,54 @@
+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.
+ * 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 00000000..57e789ed
--- /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 caf0ddac..f2e22a1e 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 00000000..8411c31c
--- /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 f424f333..0425ba8d 100644
--- a/apps/mobile/package.json
+++ b/apps/mobile/package.json
@@ -58,6 +58,7 @@
"react-native-svg": "15.8.0",
"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",
"react-native-webview": "13.12.5",
"react-native-zoom-toolkit": "^4.0.0",
diff --git a/packages/lib/hooks/useBoolean.ts b/packages/lib/hooks/useBoolean.ts
new file mode 100644
index 00000000..b8f5078e
--- /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 84a060f0..b7bed558 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -8942,6 +8942,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"
@@ -10556,6 +10563,11 @@ use-sync-external-store@^1.2.2, use-sync-external-store@^1.4.0:
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"