diff --git a/app.config.js b/app.config.js
index 08ac960..a3d5836 100644
--- a/app.config.js
+++ b/app.config.js
@@ -26,6 +26,7 @@ export default {
},
edgeToEdgeEnabled: true,
package: 'com.bip.hipmimobileapp',
+ // softwareKeyboardLayoutMode: 'resize', // option: untuk mengatur keyboard pada room chst collaboration
},
web: {
diff --git a/app/(application)/(user)/collaboration/(tabs)/group.tsx b/app/(application)/(user)/collaboration/(tabs)/group.tsx
index 3cb28c0..895a51d 100644
--- a/app/(application)/(user)/collaboration/(tabs)/group.tsx
+++ b/app/(application)/(user)/collaboration/(tabs)/group.tsx
@@ -34,9 +34,7 @@ export default function CollaborationGroup() {
category: "group",
authorId: user?.id,
});
-
- console.log("[RES >>]", JSON.stringify(response.data, null, 2));
-
+
if (response.success) {
setListData(response.data);
}
diff --git a/app/(application)/(user)/collaboration/[id]/[detail]/room-chat.tsx b/app/(application)/(user)/collaboration/[id]/[detail]/room-chat.tsx
index 1e3c9c7..b9d1602 100644
--- a/app/(application)/(user)/collaboration/[id]/[detail]/room-chat.tsx
+++ b/app/(application)/(user)/collaboration/[id]/[detail]/room-chat.tsx
@@ -1,70 +1,13 @@
-
-import {
- BackButton,
- BoxButtonOnFooter,
- Grid,
- TextCustom,
- TextInputCustom,
- ViewWrapper,
-} from "@/components";
-import { AccentColor, MainColor } from "@/constants/color-palet";
+import { BackButton } from "@/components";
+import { MainColor } from "@/constants/color-palet";
import { ICON_SIZE_SMALL } from "@/constants/constans-value";
+import ChatScreen from "@/screens/Collaboration/GroupChatSection";
import { Feather } from "@expo/vector-icons";
import { router, Stack, useLocalSearchParams } from "expo-router";
-import { StyleSheet, TouchableOpacity, View } from "react-native";
export default function CollaborationRoomChat() {
const { id, detail } = useLocalSearchParams();
-
- const inputChat = () => {
- return (
- <>
-
- {/*
-
- console.log("Send")}
- style={{
- backgroundColor: AccentColor.blue,
- padding: 10,
- borderRadius: 50,
- }}
- >
-
-
- */}
-
-
-
-
-
-
-
-
- console.log("Send")}
- style={{
- backgroundColor: AccentColor.blue,
- padding: 10,
- borderRadius: 50,
- }}
- >
-
-
-
-
-
- >
- );
- };
-
return (
<>
-
- {dummyData.map((item, index) => (
-
-
- {item.nama}
- {item.chat}
-
- {new Date(item.time).toLocaleTimeString([], {
- hour: "2-digit",
- minute: "2-digit",
- })}
-
-
-
- ))}
- {/*
- */}
-
+
+
>
);
}
-
-const dummyData = [
- {
- nama: "Dina",
- role: 1,
- chat: "Hai! Kamu udah lihat dokumen proyek yang baru?",
- time: "2025-07-24T09:01:15Z",
- },
- {
- nama: "Rafi",
- role: 2,
- chat: "Halo! Iya, aku baru aja baca. Kayaknya kita harus revisi bagian akhir deh.",
- time: "2025-07-24T09:02:03Z",
- },
- {
- nama: "Dina",
- role: 1,
- chat: "Setuju. Aku juga kurang sreg sama penutupnya.",
- time: "2025-07-24T09:02:45Z",
- },
- {
- nama: "Rafi",
- role: 2,
- chat: "Oke, aku coba edit malam ini ya. Nanti aku share ulang versinya.",
- time: "2025-07-24T09:03:10Z",
- },
- {
- nama: "Dina",
- role: 1,
- chat: "Siap, makasih ya. Jangan begadang!",
- time: "2025-07-24T09:03:30Z",
- },
-];
-
-const styles = StyleSheet.create({
- container: {
- paddingVertical: 10,
- paddingHorizontal: 12,
- },
- messageRow: {
- flexDirection: "row",
- marginBottom: 12,
- },
- rightAlign: {
- justifyContent: "flex-end",
- },
- leftAlign: {
- justifyContent: "flex-start",
- },
- bubble: {
- maxWidth: "75%",
- padding: 10,
- borderRadius: 12,
- },
- bubbleRight: {
- backgroundColor: "#DCF8C6", // hijau muda
- borderTopRightRadius: 0,
- },
- bubbleLeft: {
- backgroundColor: "#F0F0F0", // abu-abu terang
- borderTopLeftRadius: 0,
- },
- sender: {
- fontSize: 12,
- fontWeight: "bold",
- marginBottom: 2,
- color: "#555",
- },
- message: {
- fontSize: 15,
- color: "#000",
- },
- time: {
- fontSize: 10,
- color: "#888",
- textAlign: "right",
- marginTop: 4,
- },
-});
diff --git a/app/(application)/(user)/collaboration/[id]/index.tsx b/app/(application)/(user)/collaboration/[id]/index.tsx
index 43cb50a..5d5800d 100644
--- a/app/(application)/(user)/collaboration/[id]/index.tsx
+++ b/app/(application)/(user)/collaboration/[id]/index.tsx
@@ -89,7 +89,7 @@ export default function CollaborationDetail() {
{user?.id === data?.Author?.id && (
)}
diff --git a/app/(application)/(user)/collaboration/[id]/list-of-participants.tsx b/app/(application)/(user)/collaboration/[id]/list-of-participants.tsx
index 2844bc5..dddb768 100644
--- a/app/(application)/(user)/collaboration/[id]/list-of-participants.tsx
+++ b/app/(application)/(user)/collaboration/[id]/list-of-participants.tsx
@@ -51,7 +51,7 @@ export default function CollaborationListOfParticipants() {
{loadingGetData ? (
) : _.isEmpty(listData) ? (
- Tidak ada data
+ Tidak ada partisipan
) : (
listData?.map((item: any, index: number) => (
diff --git a/app/(application)/(user)/collaboration/create.tsx b/app/(application)/(user)/collaboration/create.tsx
index beb24e5..06736ee 100644
--- a/app/(application)/(user)/collaboration/create.tsx
+++ b/app/(application)/(user)/collaboration/create.tsx
@@ -81,7 +81,6 @@ export default function CollaborationCreate() {
try {
setIsLoading(true);
- console.log("[DATA]>>", newData);
const response = await apiCollaborationCreate({ data: newData });
if (response.success) {
diff --git a/bun.lock b/bun.lock
index fec53a9..1b7878a 100644
--- a/bun.lock
+++ b/bun.lock
@@ -42,6 +42,7 @@
"react-native-dotenv": "^3.4.11",
"react-native-gesture-handler": "~2.28.0",
"react-native-international-phone-number": "^0.9.3",
+ "react-native-keyboard-controller": "^1.18.6",
"react-native-maps": "1.20.1",
"react-native-otp-entry": "^1.8.5",
"react-native-pager-view": "6.9.1",
@@ -1876,6 +1877,8 @@
"react-native-is-edge-to-edge": ["react-native-is-edge-to-edge@1.2.1", "", { "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-FLbPWl/MyYQWz+KwqOZsSyj2JmLKglHatd3xLZWskXOpRaio4LfEDEz8E/A6uD8QoTHW6Aobw1jbEwK7KMgR7Q=="],
+ "react-native-keyboard-controller": ["react-native-keyboard-controller@1.18.6", "", { "dependencies": { "react-native-is-edge-to-edge": "^1.2.1" }, "peerDependencies": { "react": "*", "react-native": "*", "react-native-reanimated": ">=3.0.0" } }, "sha512-K/RMw3MdtuykkACFN5d9RTapAcO0v4T34gmSyHkEraU5UsX+fxEHd6j4MvL7KUihvmLLod0NV/mQC0nL4cOurw=="],
+
"react-native-maps": ["react-native-maps@1.20.1", "", { "dependencies": { "@types/geojson": "^7946.0.13" }, "peerDependencies": { "react": ">= 17.0.1", "react-native": ">= 0.64.3", "react-native-web": ">= 0.11" }, "optionalPeers": ["react-native-web"] }, "sha512-NZI3B5Z6kxAb8gzb2Wxzu/+P2SlFIg1waHGIpQmazDSCRkNoHNY4g96g+xS0QPSaG/9xRBbDNnd2f2/OW6t6LQ=="],
"react-native-otp-entry": ["react-native-otp-entry@1.8.5", "", { "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-TZNkIuUzZKAAWrC8X/A22ZHJdycLysxUNysrGf0yTmDLRUyf4zLXwVFcDYUcRNe763Hjaf5qvtKGILb6lDGzoA=="],
diff --git a/package.json b/package.json
index 575c881..f9a495a 100644
--- a/package.json
+++ b/package.json
@@ -49,6 +49,7 @@
"react-native-dotenv": "^3.4.11",
"react-native-gesture-handler": "~2.28.0",
"react-native-international-phone-number": "^0.9.3",
+ "react-native-keyboard-controller": "^1.18.6",
"react-native-maps": "1.20.1",
"react-native-otp-entry": "^1.8.5",
"react-native-pager-view": "6.9.1",
diff --git a/screens/Collaboration/GroupChatSection.back b/screens/Collaboration/GroupChatSection.back
new file mode 100644
index 0000000..09b24df
--- /dev/null
+++ b/screens/Collaboration/GroupChatSection.back
@@ -0,0 +1,222 @@
+// ChatScreen.tsx
+import React, { useEffect, useRef, useState } from "react";
+import {
+ Dimensions,
+ Keyboard,
+ Platform,
+ ScrollView,
+ StyleSheet,
+ Text,
+ TextInput,
+ TouchableOpacity,
+ View,
+} from "react-native";
+import {
+ SafeAreaView
+} from "react-native-safe-area-context";
+
+type Message = {
+ id: string;
+ text: string;
+ sender: "me" | "other";
+ timestamp: Date;
+};
+
+const { height: SCREEN_HEIGHT } = Dimensions.get("window");
+
+const ChatScreen: React.FC = () => {
+ const [messages, setMessages] = useState([
+ {
+ id: "1",
+ text: "Hai!",
+ sender: "other",
+ timestamp: new Date(Date.now() - 300000),
+ },
+ {
+ id: "2",
+ text: "Halo juga!",
+ sender: "me",
+ timestamp: new Date(Date.now() - 240000),
+ },
+ {
+ id: "3",
+ text: "Apa kabar?",
+ sender: "other",
+ timestamp: new Date(Date.now() - 180000),
+ },
+ ]);
+ const [inputText, setInputText] = useState("");
+ const [keyboardHeight, setKeyboardHeight] = useState(0);
+ const scrollViewRef = useRef(null);
+
+ useEffect(() => {
+ const show = Keyboard.addListener("keyboardDidShow", (e) => {
+ let kbHeight = e.endCoordinates.height;
+ // Di Android dengan edge-to-edge, kadang tinggi termasuk navigation bar
+ if (Platform.OS === "android") {
+ // Batasi maksimal 60% layar
+ kbHeight = Math.min(kbHeight, SCREEN_HEIGHT * 2);
+ }
+ setKeyboardHeight(kbHeight);
+ });
+
+ const hide = Keyboard.addListener("keyboardDidHide", () => {
+ setKeyboardHeight(0);
+ });
+
+ return () => {
+ show.remove();
+ hide.remove();
+ };
+ }, []);
+
+ useEffect(() => {
+ // Scroll ke bawah setelah pesan baru atau keyboard muncul
+ const timer = setTimeout(() => {
+ scrollViewRef.current?.scrollToEnd({ animated: true });
+ }, 100); // delay kecil untuk pastikan layout stabil
+ return () => clearTimeout(timer);
+ }, [messages, keyboardHeight]);
+
+ const handleSend = () => {
+ if (!inputText.trim()) return;
+ setMessages((prev) => [
+ ...prev,
+ {
+ id: Date.now().toString(),
+ text: inputText.trim(),
+ sender: "me",
+ timestamp: new Date(),
+ },
+ ]);
+ setInputText("");
+ };
+
+
+ return (
+
+ {/* Kontainer utama dengan padding bottom = tinggi keyboard */}
+
+
+ {messages.map((msg) => (
+
+ {msg.text}
+
+ {msg.timestamp.toLocaleTimeString([], {
+ hour: "2-digit",
+ minute: "2-digit",
+ })}
+
+
+ ))}
+
+
+
+
+
+ Kirim
+
+
+
+
+ );
+};
+
+const styles = StyleSheet.create({
+ safeArea: {
+ flex: 1,
+ backgroundColor: "#e5ddd5",
+ },
+ container: {
+ flex: 1,
+ backgroundColor: "#e5ddd5",
+ },
+ messagesContainer: {
+ flex: 1,
+ paddingHorizontal: 10,
+ },
+ messagesContent: {
+ paddingBottom: 10,
+ },
+ messageBubble: {
+ maxWidth: "80%",
+ padding: 10,
+ marginVertical: 4,
+ borderRadius: 12,
+ },
+ myMessage: {
+ alignSelf: "flex-end",
+ backgroundColor: "#dcf8c6",
+ },
+ otherMessage: {
+ alignSelf: "flex-start",
+ backgroundColor: "#ffffff",
+ },
+ messageText: {
+ fontSize: 16,
+ color: "#000",
+ },
+ timestamp: {
+ fontSize: 10,
+ color: "#666",
+ textAlign: "right",
+ marginTop: 4,
+ },
+ inputContainer: {
+ flexDirection: "row",
+ alignItems: "flex-end",
+ paddingHorizontal: 10,
+ paddingTop: 8,
+ backgroundColor: "#f0f0f0",
+ borderTopWidth: 1,
+ borderTopColor: "#ddd",
+ },
+ textInput: {
+ flex: 1,
+ borderWidth: 1,
+ borderColor: "#ccc",
+ borderRadius: 20,
+ paddingHorizontal: 12,
+ paddingVertical: 8,
+ fontSize: 16,
+ backgroundColor: "#fff",
+ maxHeight: 100,
+ },
+ sendButton: {
+ marginLeft: 10,
+ backgroundColor: "#34b7f1",
+ paddingHorizontal: 16,
+ paddingVertical: 10,
+ borderRadius: 20,
+ },
+ sendButtonText: {
+ color: "#fff",
+ fontWeight: "bold",
+ },
+});
+
+export default ChatScreen;
diff --git a/screens/Collaboration/GroupChatSection.tsx b/screens/Collaboration/GroupChatSection.tsx
new file mode 100644
index 0000000..281628e
--- /dev/null
+++ b/screens/Collaboration/GroupChatSection.tsx
@@ -0,0 +1,292 @@
+/* eslint-disable react-hooks/exhaustive-deps */
+// ChatScreen.tsx
+import { LoaderCustom } from "@/components";
+import { AccentColor } from "@/constants/color-palet";
+import { ICON_SIZE_SMALL } from "@/constants/constans-value";
+import { useAuth } from "@/hooks/use-auth";
+import {
+ apiCollaborationGroupMessage,
+ apiCollaborationGroupMessageCreate,
+} from "@/service/api-client/api-collaboration";
+import { formatChatTime } from "@/utils/formatChatTime";
+import { AntDesign } from "@expo/vector-icons";
+import dayjs from "dayjs";
+import { useFocusEffect } from "expo-router";
+import _ from "lodash";
+import React, { useCallback, useEffect, useRef, useState } from "react";
+import {
+ Dimensions,
+ Keyboard,
+ Platform,
+ ScrollView,
+ StyleSheet,
+ Text,
+ TextInput,
+ TouchableOpacity,
+ View,
+} from "react-native";
+import { ActivityIndicator } from "react-native-paper";
+import { SafeAreaView } from "react-native-safe-area-context";
+
+type IMessage = {
+ id: string;
+ createdAt: Date;
+ isActive: boolean;
+ message: string;
+ isFile: boolean;
+ userId: string;
+ User: {
+ select: {
+ id: true;
+ username: true;
+ };
+ };
+};
+
+const { height: SCREEN_HEIGHT } = Dimensions.get("window");
+
+const ChatScreen: React.FC<{ id: string }> = ({ id }) => {
+ const { user } = useAuth();
+ const [messages, setMessages] = useState(null);
+ const [loadingMessage, setLoadingMessage] = useState(false);
+ const [inputText, setInputText] = useState("");
+ const [keyboardHeight, setKeyboardHeight] = useState(0);
+ const scrollViewRef = useRef(null);
+
+ useFocusEffect(
+ useCallback(() => {
+ onLoadMessage();
+ }, [id])
+ );
+
+ const onLoadMessage = async () => {
+ try {
+ setLoadingMessage(true);
+ const response = await apiCollaborationGroupMessage({ id: id as string });
+ if (response.success) {
+ setMessages(response.data);
+ }
+ } catch (error) {
+ console.log("[ERROR]", error);
+ } finally {
+ setLoadingMessage(false);
+ }
+ };
+
+ useEffect(() => {
+ const show = Keyboard.addListener("keyboardDidShow", (e) => {
+ let kbHeight = e.endCoordinates.height;
+ // Di Android dengan edge-to-edge, kadang tinggi termasuk navigation bar
+ if (Platform.OS === "android") {
+ // Batasi maksimal 60% layar
+ kbHeight = Math.min(kbHeight, SCREEN_HEIGHT * 0.6);
+ }
+ setKeyboardHeight(kbHeight);
+ });
+
+ const hide = Keyboard.addListener("keyboardDidHide", () => {
+ setKeyboardHeight(0);
+ });
+
+ return () => {
+ show.remove();
+ hide.remove();
+ };
+ }, []);
+
+ useEffect(() => {
+ // Scroll ke bawah setelah pesan baru atau keyboard muncul
+ const timer = setTimeout(() => {
+ scrollViewRef.current?.scrollToEnd({ animated: true });
+ }, 100); // delay kecil untuk pastikan layout stabil
+ return () => clearTimeout(timer);
+ }, [messages, keyboardHeight]);
+
+ const handleSend = async () => {
+ if (!inputText.trim()) return;
+
+ const newData = {
+ userId: user?.id,
+ message: inputText.trim(),
+ };
+
+ try {
+ const response = await apiCollaborationGroupMessageCreate({
+ id: id as string,
+ data: newData,
+ });
+
+ if (response.success) {
+ setMessages((prev: IMessage | any) => [
+ ...prev,
+ {
+ id: Date.now().toString(),
+ createdAt: new Date(),
+ isActive: true,
+ message: inputText.trim(),
+ isFile: false,
+ userId: user?.id,
+ User: {
+ username: user?.username,
+ },
+ },
+ ]);
+ setInputText("");
+ }
+ } catch (error) {
+ console.log("[ERROR]", error);
+ }
+ };
+
+ return (
+
+ {/* Kontainer utama dengan padding bottom = tinggi keyboard */}
+
+
+ {loadingMessage ? (
+
+ ) : _.isEmpty(messages) ? (
+
+ Belum ada pesan
+
+ ) : (
+ messages?.map((item: any) => (
+
+ {item?.User?.username}
+ {item.message}
+
+ {formatChatTime(item.createdAt)}
+
+
+ ))
+ )}
+
+
+
+
+
+
+ {/* Kirim */}
+
+
+
+
+ );
+};
+
+const styles = StyleSheet.create({
+ safeArea: {
+ flex: 1,
+ backgroundColor: "#e5ddd5",
+ },
+ container: {
+ flex: 1,
+ backgroundColor: "#e5ddd5",
+ },
+ isEmptyMessage: {
+ alignSelf: "center",
+ color: "#666",
+ marginTop: 20,
+ },
+ messagesContainer: {
+ flex: 1,
+ paddingHorizontal: 10,
+ },
+ messagesContent: {
+ paddingBottom: 10,
+ },
+ messageBubble: {
+ maxWidth: "80%",
+ padding: 10,
+ marginVertical: 4,
+ borderRadius: 12,
+ },
+ name: {
+ fontSize: 12,
+ color: "#666",
+ marginBottom: 4,
+ },
+ myMessage: {
+ alignSelf: "flex-end",
+ backgroundColor: "#dcf8c6",
+ },
+ otherMessage: {
+ alignSelf: "flex-start",
+ backgroundColor: "#ffffff",
+ },
+ messageText: {
+ fontSize: 16,
+ color: "#000",
+ },
+ timestamp: {
+ fontSize: 10,
+ color: "#666",
+ textAlign: "right",
+ marginTop: 4,
+ },
+ inputContainer: {
+ flexDirection: "row",
+ alignItems: "flex-end",
+ paddingHorizontal: 10,
+ paddingTop: 8,
+ backgroundColor: "#f0f0f0",
+ borderTopWidth: 1,
+ borderTopColor: "#ddd",
+ },
+ textInput: {
+ flex: 1,
+ borderWidth: 1,
+ borderColor: "#ccc",
+ borderRadius: 20,
+ paddingHorizontal: 12,
+ paddingVertical: 8,
+ fontSize: 16,
+ backgroundColor: "#fff",
+ maxHeight: 100,
+ },
+ sendButton: {
+ marginLeft: 10,
+ backgroundColor: AccentColor.blue,
+ // paddingHorizontal: 16,
+ // paddingVertical: 10,
+ borderRadius: "100%",
+ width: 40,
+ height: 40,
+ justifyContent: "center",
+ alignContent: "center",
+ alignItems: "center",
+ },
+ sendButtonText: {
+ color: "#fff",
+ fontWeight: "bold",
+ },
+});
+
+export default ChatScreen;
diff --git a/service/api-client/api-collaboration.ts b/service/api-client/api-collaboration.ts
index 9e69456..f3eb5e6 100644
--- a/service/api-client/api-collaboration.ts
+++ b/service/api-client/api-collaboration.ts
@@ -95,7 +95,13 @@ export async function apiCollaborationCreateGroup({
}
}
-export async function apiCollaborationEditData({ id, data }: { id: string; data: any }) {
+export async function apiCollaborationEditData({
+ id,
+ data,
+}: {
+ id: string;
+ data: any;
+}) {
try {
const response = await apiConfig.put(`/mobile/collaboration/${id}`, {
data: data,
@@ -114,3 +120,32 @@ export async function apiCollaborationGroup({ id }: { id: string }) {
throw error;
}
}
+
+export async function apiCollaborationGroupMessage({ id }: { id: string }) {
+ try {
+ const response = await apiConfig.get(`/mobile/collaboration/${id}/message`);
+ return response.data;
+ } catch (error) {
+ throw error;
+ }
+}
+
+export async function apiCollaborationGroupMessageCreate({
+ id,
+ data,
+}: {
+ id: string;
+ data: any;
+}) {
+ try {
+ const response = await apiConfig.post(
+ `/mobile/collaboration/${id}/message`,
+ {
+ data: data,
+ }
+ );
+ return response.data;
+ } catch (error) {
+ throw error;
+ }
+}
diff --git a/utils/formatChatTime.ts b/utils/formatChatTime.ts
new file mode 100644
index 0000000..9111588
--- /dev/null
+++ b/utils/formatChatTime.ts
@@ -0,0 +1,35 @@
+// utils/formatChatTime.ts
+import dayjs from 'dayjs';
+import relativeTime from 'dayjs/plugin/relativeTime';
+import 'dayjs/locale/id';
+
+dayjs.extend(relativeTime);
+dayjs.locale('id');
+
+/**
+ * Format waktu pesan untuk tampilan chat
+ * @param date ISO string atau Date object
+ * @returns string formatted time
+ */
+export const formatChatTime = (date: string | Date): string => {
+ const messageDate = dayjs(date);
+ const now = dayjs();
+
+ // Jika hari ini
+ if (messageDate.isSame(now, 'day')) {
+ return messageDate.format('HH.mm'); // contoh: "14.30"
+ }
+
+ // Jika kemarin
+ if (messageDate.isSame(now.subtract(1, 'day'), 'day')) {
+ return 'Kemarin';
+ }
+
+ // Jika dalam 7 hari terakhir (tapi bukan kemarin/ hari ini)
+ if (now.diff(messageDate, 'day') < 7) {
+ return messageDate.format('dddd HH:mm'); // contoh: "Senin 14:30"
+ }
+
+ // Lebih dari seminggu lalu → tampilkan tanggal
+ return messageDate.format('D MMM YYYY'); // contoh: "12 Mei 2024"
+};