Skip to content
This repository was archived by the owner on Apr 21, 2026. It is now read-only.
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
115 changes: 45 additions & 70 deletions app/(tabs)/_layout.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
import { Slot, Tabs } from "expo-router";
import { Tabs } from "expo-router";
import React from "react";
import { Platform, View } from "react-native";
import { Platform } from "react-native";

import { HapticTab } from "@/components/HapticTab";
import { IconSymbol } from "@/components/ui/IconSymbol";
import TabBarBackground from "@/components/ui/TabBarBackground";
import { Colors } from "@/constants/Colors";
import { useColorScheme } from "@/hooks/useColorScheme";
import { useAuth } from "@/contexts/AuthContext";
import AuthScreen from "@/components/AuthScreen";
import { SafeAreaView } from "react-native-safe-area-context";
import TopNavbar from "@/components/TopNavBar";
import { useColorScheme } from "@/hooks/useColorScheme";
import { Colors } from "@/constants/Colors";
import { HapticTab } from "@/components/HapticTab";
import TabBarBackground from "@/components/ui/TabBarBackground";
import { IconSymbol } from "@/components/ui/IconSymbol";
import { ActiveUserProvider } from "@/contexts/ActiveUserContext";
import { FriendStatusProvider } from "@/contexts/FriendStatusContext";

type IconName = React.ComponentProps<typeof IconSymbol>["name"];

Expand All @@ -21,73 +21,48 @@ interface NavItem {
}

const navigations: NavItem[] = [
{
name: "index",
title: "Home",
icon: "house.fill",
},
// {
// name: "explore",
// title: "Explore",
// icon: "paperplane.fill",
// },
{
name: "training",
title: "Training",
icon: "figure.walk",
},
{
name: "multiPlayer",
title: "Multiplayer",
icon: "person.3.fill",
},
{
name: "lockerRoom",
title: "Locker Room",
icon: "lock.fill",
},
{
name: "shop",
title: "Shop",
icon: "cart.fill",
},
{ name: "index", title: "Home", icon: "house.fill" },
{ name: "training", title: "Training", icon: "figure.walk" },
{ name: "multiPlayer", title: "Multiplayer", icon: "person.3.fill" },
{ name: "lockerRoom", title: "Locker Room", icon: "lock.fill" },
{ name: "shop", title: "Shop", icon: "cart.fill" },
];

export default function TabLayout() {
const { user } = useAuth();
const { user: authUser } = useAuth();
const colorScheme = useColorScheme();

if (!user) {
return <AuthScreen />;
}
if (!authUser) return <AuthScreen />;

return (
<Tabs
screenOptions={{
tabBarActiveTintColor: Colors[colorScheme ?? "light"].tint,
headerShown: false,
tabBarButton: HapticTab,
tabBarBackground: TabBarBackground,
tabBarStyle: Platform.select({
ios: {
// Use a transparent background on iOS to show the blur effect
position: "absolute",
},
default: {},
}),
}}
>
{navigations.map((nav) => (
<Tabs.Screen
key={nav.name}
name={nav.name}
options={{
title: nav.title,
tabBarIcon: ({ color }) => (
<IconSymbol size={28} name={nav.icon} color={color} />
),
<ActiveUserProvider>
<FriendStatusProvider>
<Tabs
screenOptions={{
tabBarActiveTintColor: Colors[colorScheme ?? "light"]?.tint,
headerShown: false,
tabBarButton: HapticTab,
tabBarBackground: TabBarBackground,
tabBarStyle: Platform.select({
ios: { position: "absolute" },
default: {},
}),
}}
/>
))}
</Tabs>
>
{navigations.map(({ name, title, icon }) => (
<Tabs.Screen
key={name}
name={name}
options={{
title,
tabBarIcon: ({ color }) => (
<IconSymbol size={28} name={icon} color={color} />
),
}}
/>
))}
</Tabs>
</FriendStatusProvider>
</ActiveUserProvider>
);
}
6 changes: 5 additions & 1 deletion app/(tabs)/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@ import { ActivityIndicator, Text, TouchableOpacity, View } from "react-native";
import { SafeAreaView } from "react-native-safe-area-context";
import MaterialCommunityIcons from "@expo/vector-icons/MaterialCommunityIcons";
import FontAwesome5 from "@expo/vector-icons/FontAwesome5";
import { useFriendStatusContext } from "@/contexts/FriendStatusContext";

export default function HomeScreen() {
const { user, account, loading } = useAuth();
const { getStatus, onlineCount } = useFriendStatusContext();

if (loading) {
return (
Expand Down Expand Up @@ -46,7 +48,9 @@ export default function HomeScreen() {
</View>
<TouchableOpacity className="flex-row items-center gap-1">
<FontAwesome5 name="user-friends" size={24} color="black" />
<Text className="text-lg font-semibold italic">COUNT</Text>
<Text className="text-lg font-semibold italic">
{onlineCount}
</Text>
</TouchableOpacity>
</View>
<View className="w-full flex-1 flex items-center justify-center bg-white rounded-lg shadow-lg">
Expand Down
51 changes: 38 additions & 13 deletions components/FriendList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import { supabase } from "@/lib/supabase";
import { FlatList, Image, Text, TouchableOpacity, View } from "react-native";
import AntDesign from "@expo/vector-icons/AntDesign";
import { getInitials } from "@/lib/utils";
import { StatusDot } from "./StatusDot";
import { FontAwesome5 } from "@expo/vector-icons";

async function getFriends(userId: number, requestedCols: string | null = null) {
// 1) grab all accepted friendships where this user is either requester or addressee
Expand Down Expand Up @@ -44,7 +46,7 @@ export default function FriendList({
loading,
error,
reset,
} = useFetch(() => getFriends(account.id));
} = useFetch(() => getFriends(account.last_active_user_id));
if (loading) {
return (
<View className="flex-1 justify-center items-center">
Expand All @@ -56,7 +58,25 @@ export default function FriendList({
}

if (!friends || friends.length === 0) {
return null;
return (
<View className="flex-1 flex-col w-full p-8 pb-16">
<TouchableOpacity
className="px-4 py-2 bg-gray-300 rounded-lg mb-4 self-end flex-row items-center justify-between gap-2"
onPress={toggleFriendMenu}
>
<Text className="text-gray-700">Close</Text>
<AntDesign name="right" size={16} color="black" />
</TouchableOpacity>

<Text className="text-xl font-bold mb-4">List of Friends</Text>
<View className="w-full items-center rounded-2xl bg-gray-100 p-8">
<FontAwesome5 name="user-friends" size={24} color="black" />
<Text className="mt-3 text-center text-lg">
There is no friend yet, Let make some friends 👋
</Text>
</View>
</View>
);
}

return (
Expand All @@ -81,18 +101,23 @@ export default function FriendList({
renderItem={({ item }) => (
<View className="border-b border-gray-200 w-full min-w-[200px] flex-row items-center pb-2">
<View className="flex-1 flex-row items-center gap-2">
{item.profile_picture ? (
<Image
source={{ uri: item.profile_picture }}
className="w-14 h-14 rounded-full"
/>
) : (
<View className="w-14 h-14 bg-gray-300 rounded-full flex items-center justify-center">
<Text className="text-center text-gray-500 text-2xl font-bold">
{getInitials(item.name)}
</Text>
<View className="relative">
{item.profile_picture ? (
<Image
source={{ uri: item.profile_picture }}
className="w-14 h-14 rounded-full"
/>
) : (
<View className="w-14 h-14 bg-gray-300 rounded-full flex items-center justify-center">
<Text className="text-center text-gray-500 text-2xl font-bold">
{getInitials(item.name)}
</Text>
</View>
)}
<View className="absolute bottom-0 right-0">
<StatusDot userId={item.id} sizeClass="w-4 h-4" />
</View>
)}
</View>
<View className="flex-col flex-1 gap-1 h-14">
<Text className="text-lg font-semibold">{item.name}</Text>
<Text className="text-gray-600 max-w-[200px]" numberOfLines={1}>
Expand Down
28 changes: 28 additions & 0 deletions components/StatusDot.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// src/components/StatusDot.tsx
import React from "react";
import { View } from "react-native";
import { useFriendStatusContext } from "@/contexts/FriendStatusContext";

const COLOR_CLASSES: Record<string, string> = {
online: "bg-green-500",
"recently active": "bg-yellow-400",
offline: "bg-gray-400",
};

interface StatusDotProps {
userId: number;
/** Tailwind size key: 'w-2 h-2', 'w-3 h-3', 'w-4 h-4', etc. */
sizeClass?: string;
}

export function StatusDot({ userId, sizeClass = "w-3 h-3" }: StatusDotProps) {
const { getStatus } = useFriendStatusContext();
const status = getStatus(userId);
const colorClass = COLOR_CLASSES[status] || COLOR_CLASSES.offline;

return (
<View
className={`${colorClass} ${sizeClass} rounded-full border border-white`}
/>
);
}
120 changes: 120 additions & 0 deletions contexts/ActiveUserContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import React, {
createContext,
useContext,
useEffect,
useState,
ReactNode,
} from "react";
import { supabase } from "@/lib/supabase";
import { useAuth } from "@/contexts/AuthContext";
import { User } from "@/interfaces/interfaces";

interface ActiveUserContextType {
/** The currently active DB user */
activeUser: User | null;
/** Manually switch active user (and persist to account) */
setActiveUser: (user: User) => Promise<void>;
}

const ActiveUserContext = createContext<ActiveUserContextType | null>(null);

export function useActiveUser() {
const ctx = useContext(ActiveUserContext);
if (!ctx)
throw new Error("useActiveUser must be used within ActiveUserProvider");
return ctx;
}

/**
* ActiveUserProvider:
* - Reads account.last_active_user_id
* - If null, picks the first user of the account and writes it back
* - Exposes that user as activeUser, plus setter to switch and persist
*/
export const ActiveUserProvider: React.FC<{ children: ReactNode }> = ({
children,
}) => {
const { user: authUser } = useAuth();
const [activeUser, setActive] = useState<User | null>(null);

useEffect(() => {
if (!authUser) {
setActive(null);
return;
}

async function loadActive() {
// 1) fetch the account row, including last_active_user_id
const { data: account, error: accErr } = await supabase
.from("accounts")
.select("id, last_active_user_id")
.eq("auth_user_id", authUser?.id)
.single();
if (accErr || !account) {
console.error("Error loading account", accErr);
return;
}

let userId = account.last_active_user_id;

if (!userId) {
// 2) no active set: fetch first child user of account
const { data: users, error: uErr } = await supabase
.from("users")
.select("id")
.eq("account_id", account.id)
.order("id", { ascending: true })
.limit(1);
if (uErr || !users || users.length === 0) {
console.error("No users found for account", uErr);
return;
}
userId = users[0].id;
// 3) persist back to account
await supabase
.from("accounts")
.update({ last_active_user_id: userId })
.eq("id", account.id);
}

// 4) load the active user's full record
const { data: userRec, error: uRecErr } = await supabase
.from("users")
.select("*")
.eq("id", userId)
.single();
if (uRecErr) console.error("Error loading active user", uRecErr);
setActive(userRec ?? null);
}

loadActive();
}, [authUser]);

/**
* Switch active user and persist on accounts table
*/
const setActiveUser = async (user: User) => {
const { data: account, error: accErr } = await supabase
.from("accounts")
.select("id")
.eq("auth_user_id", authUser?.id)
.single();
if (accErr || !account) {
console.error("Error finding account for switch", accErr);
return;
}
// persist
const { error: updErr } = await supabase
.from("accounts")
.update({ last_active_user_id: user.id })
.eq("id", account.id);
if (updErr) console.error("Error updating last_active_user_id", updErr);
setActive(user);
};

return (
<ActiveUserContext.Provider value={{ activeUser, setActiveUser }}>
{children}
</ActiveUserContext.Provider>
);
};
Loading