From 8c5602b8091f7c681cba3b872ac756c0ddf44e1f Mon Sep 17 00:00:00 2001 From: Bagasbanuna02 Date: Wed, 24 Sep 2025 15:28:16 +0800 Subject: [PATCH] Collaboration Add: - Collaboration/GroupChatSection.tsx : fitur room chat Fix: - Clear code: Hapus console pada beberapa file ### No Issue --- app.config.js | 1 + .../(user)/collaboration/(tabs)/group.tsx | 4 +- .../collaboration/[id]/[detail]/room-chat.tsx | 173 +---------- .../(user)/collaboration/[id]/index.tsx | 2 +- .../[id]/list-of-participants.tsx | 2 +- .../(user)/collaboration/create.tsx | 1 - bun.lock | 3 + package.json | 1 + screens/Collaboration/GroupChatSection.back | 222 +++++++++++++ screens/Collaboration/GroupChatSection.tsx | 292 ++++++++++++++++++ service/api-client/api-collaboration.ts | 37 ++- utils/formatChatTime.ts | 35 +++ 12 files changed, 598 insertions(+), 175 deletions(-) create mode 100644 screens/Collaboration/GroupChatSection.back create mode 100644 screens/Collaboration/GroupChatSection.tsx create mode 100644 utils/formatChatTime.ts 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" +};