Files
mobile-darmasaba/app/(application)/division/[id]/(fitur-division)/discussion/[detail]/index.tsx
2026-05-08 16:52:26 +08:00

415 lines
19 KiB
TypeScript

import AppHeader from "@/components/AppHeader";
import BorderBottomItem2 from "@/components/borderBottomItem2";
import HeaderRightDiscussionDetail from "@/components/discussion/headerDiscussionDetail";
import DrawerBottom from "@/components/drawerBottom";
import ImageUser from "@/components/imageNew";
import { InputForm } from "@/components/inputForm";
import MenuItemRow from "@/components/menuItemRow";
import ModalConfirmation from "@/components/ModalConfirmation";
import Skeleton from "@/components/skeleton";
import SkeletonContent from "@/components/skeletonContent";
import Text from "@/components/Text";
import { ConstEnv } from "@/constants/ConstEnv";
import { regexOnlySpacesOrEnter } from "@/constants/OnlySpaceOrEnter";
import Styles from "@/constants/Styles";
import {
apiDeleteDiscussionCommentar,
apiEditDiscussionCommentar,
apiGetDiscussionOne,
apiGetDivisionOneFeature,
apiSendDiscussionCommentar,
} from "@/lib/api";
import { getDB } from "@/lib/firebaseDatabase";
import { useAuthSession } from "@/providers/AuthProvider";
import { useTheme } from "@/providers/ThemeProvider";
import { Feather, MaterialCommunityIcons, MaterialIcons } from "@expo/vector-icons";
import { ref } from "@react-native-firebase/database";
import { useHeaderHeight } from '@react-navigation/elements';
import { router, Stack, useLocalSearchParams } from "expo-router";
import { useEffect, useState } from "react";
import { KeyboardAvoidingView, Platform, Pressable, RefreshControl, ScrollView, View } from "react-native";
import Toast from "react-native-toast-message";
import { useSelector } from "react-redux";
type Props = {
id: string;
title: string;
desc: string;
status: number;
createdAt: string;
createdBy: string;
username: string;
user_img: string;
isCreator: boolean;
isActive: boolean;
};
type PropsComment = {
id: string;
comment: string;
createdAt: string;
username: string;
img: string;
idUser: string;
isEdited: boolean;
updatedAt: string;
};
type PropsFile = {
id: string;
idStorage: string;
name: string;
extension: string
}
export default function DiscussionDetail() {
const { colors } = useTheme();
const { id, detail } = useLocalSearchParams<{ id: string; detail: string }>();
const [data, setData] = useState<Props>();
const [dataComment, setDataComment] = useState<PropsComment[]>([]);
const [fileDiscussion, setFileDiscussion] = useState<PropsFile[]>([])
const { token, decryptToken } = useAuthSession();
const [komentar, setKomentar] = useState("");
const [loadingSend, setLoadingSend] = useState(false);
const update = useSelector((state: any) => state.discussionUpdate);
const entityUser = useSelector((state: any) => state.user);
const [isMemberDivision, setIsMemberDivision] = useState(false);
const [isAdminDivision, setIsAdminDivision] = useState(false);
const [isCreator, setIsCreator] = useState(false);
const [loading, setLoading] = useState(true)
const [loadingKomentar, setLoadingKomentar] = useState(true)
const arrSkeleton = Array.from({ length: 3 })
const reference = ref(getDB(), `/discussion-division/${detail}`);
const [refreshing, setRefreshing] = useState(false)
const headerHeight = useHeaderHeight();
const [detailMore, setDetailMore] = useState<any>([])
const entities = useSelector((state: any) => state.entities)
const [isVisible, setVisible] = useState(false)
const [selectKomentar, setSelectKomentar] = useState({ id: '', comment: '' })
const [viewEdit, setViewEdit] = useState(false)
const [showDeleteModal, setShowDeleteModal] = useState(false)
useEffect(() => {
const onValueChange = reference.on('value', snapshot => {
if (snapshot.val() == null) { reference.set({ trigger: true }) }
handleLoadComment(false)
});
return () => reference.off('value', onValueChange);
}, []);
function updateTrigger() {
reference.once('value', snapshot => {
const data = snapshot.val();
reference.update({ trigger: !data.trigger });
});
}
async function handleLoad(loading: boolean) {
try {
setLoading(loading)
const hasil = await decryptToken(String(token?.current));
const response = await apiGetDiscussionOne({ id: detail, user: hasil, cat: "data" });
const responseFile = await apiGetDiscussionOne({ id: detail, user: hasil, cat: "file" });
setData(response.data);
setFileDiscussion(responseFile.data)
setIsCreator(response.data.createdBy == hasil);
} catch (error) {
console.error(error);
} finally {
setLoading(false)
}
}
async function handleLoadComment(loading: boolean) {
try {
setLoadingKomentar(loading)
const hasil = await decryptToken(String(token?.current));
const response = await apiGetDiscussionOne({ id: detail, user: hasil, cat: "comment" });
setDataComment(response.data);
} catch (error) {
console.error(error);
} finally {
setLoadingKomentar(false)
}
}
async function handleCheckMember() {
try {
const hasil = await decryptToken(String(token?.current));
const response = await apiGetDivisionOneFeature({ id, user: hasil, cat: "check-member" });
const response2 = await apiGetDivisionOneFeature({ id, user: hasil, cat: "check-admin" });
setIsMemberDivision(response.data);
setIsAdminDivision(response2.data);
} catch (error) {
console.error(error);
}
}
useEffect(() => { handleLoad(false); }, [update.data]);
useEffect(() => { handleLoad(true); handleLoadComment(true); handleCheckMember(); }, []);
async function handleKomentar() {
try {
setLoadingSend(true);
const hasil = await decryptToken(String(token?.current));
const response = await apiSendDiscussionCommentar({ id: detail, data: { comment: komentar, user: hasil } });
if (response.success) { setKomentar(""); updateTrigger() }
} catch (error: any) {
console.error(error);
Toast.show({ type: 'small', text1: error?.response?.data?.message || "Gagal menambahkan komentar" })
} finally {
setLoadingSend(false);
}
}
async function handleEditKomentar() {
try {
setLoadingSend(true);
const hasil = await decryptToken(String(token?.current));
const response = await apiEditDiscussionCommentar({ id: selectKomentar.id, data: { comment: selectKomentar.comment, user: hasil } });
if (response.success) { updateTrigger() } else { Toast.show({ type: 'small', text1: response.message }) }
} catch (error: any) {
console.error(error);
Toast.show({ type: 'small', text1: error?.response?.data?.message || "Gagal mengedit komentar" })
} finally {
setLoadingSend(false);
handleViewEditKomentar()
}
}
async function handleDeleteKomentar() {
try {
setLoadingSend(true);
const hasil = await decryptToken(String(token?.current));
const response = await apiDeleteDiscussionCommentar({ id: selectKomentar.id, data: { user: hasil } });
if (response.success) { updateTrigger() } else { Toast.show({ type: 'small', text1: response.message }) }
} catch (error: any) {
console.error(error);
Toast.show({ type: 'small', text1: error?.response?.data?.message || "Gagal menghapus komentar" })
} finally {
setLoadingSend(false)
setVisible(false)
}
}
function handleMenuKomentar(id: string, comment: string) {
setSelectKomentar({ id, comment })
setVisible(true)
}
function handleViewEditKomentar() {
setVisible(false)
setViewEdit(!viewEdit)
}
const handleRefresh = async () => {
setRefreshing(true)
handleLoad(false)
handleLoadComment(false)
await new Promise(resolve => setTimeout(resolve, 2000));
setRefreshing(false)
};
const canWrite = data?.status != 2 && data?.isActive && ((entityUser.role != "user" && entityUser.role != "coadmin") || isMemberDivision)
const isOpen = data?.status === 1
return (
<>
<Stack.Screen
options={{
headerTitle: "Diskusi",
headerTitleAlign: "center",
header: () => (
<AppHeader
title="Diskusi"
showBack={true}
onPressLeft={() => router.back()}
right={
((entityUser.role != "user" && entityUser.role != "coadmin") || isAdminDivision || isCreator) ?
<HeaderRightDiscussionDetail id={detail} status={data?.status} isActive={data?.isActive} /> : undefined
}
/>
)
}}
/>
<View style={[Styles.flex1, { backgroundColor: colors.background }]}>
<ScrollView
showsVerticalScrollIndicator={false}
refreshControl={<RefreshControl refreshing={refreshing} onRefresh={handleRefresh} tintColor={colors.icon} />}
>
<View style={[Styles.p15]}>
{loading ? (
<SkeletonContent />
) : (
<BorderBottomItem2
dataFile={fileDiscussion}
descEllipsize={false}
borderType="all"
bgColor="white"
icon={
<ImageUser src={`${ConstEnv.url_storage}/files/${data?.user_img}`} size="sm" />
}
title={data?.username}
titleShowAll={true}
subtitle={
<View style={[Styles.discussionStatusPill, {
borderColor: !data?.isActive ? '#F59E0B' : isOpen ? '#10B981' : colors.dimmed + '80',
}]}>
<Text style={[Styles.discussionStatusText, {
color: !data?.isActive ? '#F59E0B' : isOpen ? '#10B981' : colors.dimmed,
}]}>
{!data?.isActive ? 'Arsip' : isOpen ? 'Buka' : 'Tutup'}
</Text>
</View>
}
desc={data?.desc}
leftBottomInfo={
<View style={Styles.rowItemsCenter}>
<Feather name="message-square" size={14} color={colors.dimmed} style={Styles.mr05} />
<Text style={[Styles.textInformation, { color: colors.dimmed }]}>{dataComment.length} Komentar</Text>
</View>
}
rightBottomInfo={<Text style={[Styles.textInformation, { color: colors.dimmed }]}>{data?.createdAt}</Text>}
/>
)}
<View style={Styles.mt10}>
{loadingKomentar ? (
arrSkeleton.map((_, i) => (
<Skeleton key={i} width={100} widthType="percent" height={40} borderRadius={5} />
))
) : (
dataComment.map((item, i) => (
<Pressable
key={i}
onPress={() => {
setDetailMore((prev: any) =>
prev.includes(item.id) ? prev.filter((id: string) => id !== item.id) : [...prev, item.id]
)
}}
onLongPress={() => {
item.idUser == entities.id && data?.status != 2 && data?.isActive && handleMenuKomentar(item.id, item.comment)
}}
style={({ pressed }) => [
Styles.discussionCommentCard,
{ backgroundColor: pressed ? colors.icon + '10' : colors.card, borderColor: colors.icon + '20' }
]}
>
<View style={Styles.flex1}>
<View style={[Styles.rowSpaceBetween, Styles.itemsCenter, Styles.mb05]}>
<View style={[Styles.rowItemsCenter, { gap: 8, flex: 1, marginRight: 8 }]}>
<ImageUser src={`${ConstEnv.url_storage}/files/${item.img}`} size="xs" />
<Text style={[Styles.textMediumSemiBold, { color: colors.text }]} numberOfLines={1}>
{item.username}
</Text>
{item.isEdited && (
<Text style={[Styles.discussionEditedText, { color: colors.dimmed }]}>diedit</Text>
)}
</View>
<Text style={[Styles.discussionDateText, { color: colors.dimmed, flexShrink: 0 }]}>
{item.createdAt}
</Text>
</View>
<Text style={[Styles.textDefault, { color: colors.text }]} numberOfLines={detailMore.includes(item.id) ? 0 : 3}>
{item.comment}
</Text>
</View>
</Pressable>
))
)}
</View>
</View>
</ScrollView>
<KeyboardAvoidingView behavior={Platform.OS === 'ios' ? 'padding' : undefined} keyboardVerticalOffset={headerHeight}>
<View style={[Styles.contentItemCenter, Styles.w100, { backgroundColor: colors.background }, viewEdit && Styles.borderTop]}>
{viewEdit ? (
<>
<View style={[Styles.w90, Styles.rowSpaceBetween, Styles.pv05]}>
<View style={Styles.rowItemsCenter}>
<Feather name="edit-3" color={colors.text} size={22} style={Styles.mh05} />
<Text style={Styles.textMediumSemiBold}>Edit Komentar</Text>
</View>
<Pressable onPress={() => handleViewEditKomentar()}>
<MaterialIcons name="close" color={colors.text} size={22} />
</Pressable>
</View>
<InputForm
bg={colors.card}
type="default" round multiline
placeholder="Kirim Komentar"
onChange={(val: string) => setSelectKomentar({ ...selectKomentar, comment: val })}
value={selectKomentar.comment}
itemRight={
<Pressable
onPress={() => {
selectKomentar.comment != "" && !regexOnlySpacesOrEnter.test(selectKomentar.comment) && !loadingSend && data?.status != 2 && data?.isActive
&& (((entityUser.role == "user" || entityUser.role == "coadmin") && isMemberDivision) || entityUser.role == "admin" || entityUser.role == "supadmin" || entityUser.role == "developer" || entityUser.role == "cosupadmin")
&& handleEditKomentar();
}}
style={[Platform.OS == 'android' && Styles.mb12]}
>
<MaterialIcons name="send" size={25}
style={[
selectKomentar.comment == "" || regexOnlySpacesOrEnter.test(selectKomentar.comment) || loadingSend || ((entityUser.role == "user" || entityUser.role == "coadmin") && !isMemberDivision)
? { color: colors.dimmed } : { color: colors.tint },
]}
/>
</Pressable>
}
/>
</>
) : canWrite ? (
<InputForm
type="default" round multiline
placeholder="Kirim Komentar"
onChange={setKomentar} value={komentar}
itemRight={
<Pressable
onPress={() => {
komentar != "" && !regexOnlySpacesOrEnter.test(komentar) && !loadingSend && data?.status != 2 && data?.isActive
&& (((entityUser.role == "user" || entityUser.role == "coadmin") && isMemberDivision) || entityUser.role == "admin" || entityUser.role == "supadmin" || entityUser.role == "developer" || entityUser.role == "cosupadmin")
&& handleKomentar();
}}
style={[Platform.OS == 'android' && Styles.mb12]}
>
<MaterialIcons name="send" size={25}
style={[
komentar == "" || regexOnlySpacesOrEnter.test(komentar) || loadingSend || ((entityUser.role == "user" || entityUser.role == "coadmin") && !isMemberDivision)
? { color: colors.dimmed } : { color: colors.tint },
]}
/>
</Pressable>
}
/>
) : (
<View style={[Styles.pv20, Styles.itemsCenter]}>
<Text style={[Styles.textInformation, { color: colors.dimmed }]}>
{data?.status == 2 ? "Diskusi telah ditutup" : data?.isActive == false ? "Diskusi telah diarsipkan" : "Hanya anggota divisi yang dapat memberikan komentar"}
</Text>
</View>
)}
</View>
</KeyboardAvoidingView>
</View>
<DrawerBottom animation="slide" isVisible={isVisible} setVisible={setVisible} title="Komentar">
<View style={Styles.rowItemsCenter}>
<MenuItemRow icon={<MaterialCommunityIcons name="pencil-outline" color={colors.text} size={25} />} title="Edit" onPress={() => handleViewEditKomentar()} />
<MenuItemRow icon={<MaterialIcons name="delete-outline" color={colors.text} size={25} />} title="Hapus" onPress={() => { setVisible(false); setTimeout(() => setShowDeleteModal(true), 600) }} />
</View>
</DrawerBottom>
<ModalConfirmation
visible={showDeleteModal}
title="Konfirmasi"
message="Apakah anda yakin ingin menghapus komentar?"
onConfirm={() => { setShowDeleteModal(false); handleDeleteKomentar() }}
onCancel={() => setShowDeleteModal(false)}
confirmText="Hapus"
cancelText="Batal"
/>
</>
);
}