feat: redesign input komentar diskusi divisi menggunakan komponen messaging app

This commit is contained in:
2026-06-10 14:28:52 +08:00
parent 4ab8422808
commit 1c495541b5
2 changed files with 80 additions and 138 deletions

View File

@@ -2,15 +2,13 @@ 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 DiscussionCommentInput from "@/components/discussion_general/discussionCommentInput";
import DiscussionCommentList, { CommentFile, CommentItem } from "@/components/discussion_general/discussionCommentList";
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,
@@ -18,6 +16,7 @@ import {
apiGetDiscussionOne,
apiGetDivisionOneFeature,
apiSendDiscussionCommentar,
apiSendDiscussionCommentarWithFile,
} from "@/lib/api";
import { getDB } from "@/lib/firebaseDatabase";
import { useAuthSession } from "@/providers/AuthProvider";
@@ -30,6 +29,7 @@ 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";
import ImageUser from "@/components/imageNew";
type Props = {
id: string;
@@ -44,17 +44,6 @@ type Props = {
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;
@@ -66,10 +55,11 @@ 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 [dataComment, setDataComment] = useState<CommentItem[]>([]);
const [fileDiscussion, setFileDiscussion] = useState<PropsFile[]>([])
const { token, decryptToken } = useAuthSession();
const [komentar, setKomentar] = useState("");
const [commentFiles, setCommentFiles] = useState<{ uri: string; name: string }[]>([])
const [loadingSend, setLoadingSend] = useState(false);
const update = useSelector((state: any) => state.discussionUpdate);
const entityUser = useSelector((state: any) => state.user);
@@ -78,16 +68,15 @@ export default function DiscussionDetail() {
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 [selectKomentar, setSelectKomentar] = useState<{ id: string; comment: string; files: CommentFile[] }>({ id: '', comment: '', files: [] })
const [viewEdit, setViewEdit] = useState(false)
const [showDeleteModal, setShowDeleteModal] = useState(false)
const [removedFileIds, setRemovedFileIds] = useState<string[]>([])
useEffect(() => {
const onValueChange = reference.on('value', snapshot => {
@@ -152,8 +141,22 @@ export default function DiscussionDetail() {
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() }
let response
if (commentFiles.length > 0) {
const fd = new FormData()
fd.append('data', JSON.stringify({ comment: komentar, user: hasil }))
commentFiles.forEach((f, i) => {
fd.append(`file${i}`, { uri: f.uri, name: f.name, type: 'application/octet-stream' } as any)
})
response = await apiSendDiscussionCommentarWithFile(detail, fd)
} else {
response = await apiSendDiscussionCommentar({ id: detail, data: { comment: komentar, user: hasil } });
}
if (response.success) {
setKomentar("");
setCommentFiles([])
updateTrigger()
}
} catch (error: any) {
console.error(error);
Toast.show({ type: 'small', text1: error?.response?.data?.message || "Gagal menambahkan komentar" })
@@ -166,7 +169,10 @@ export default function DiscussionDetail() {
try {
setLoadingSend(true);
const hasil = await decryptToken(String(token?.current));
const response = await apiEditDiscussionCommentar({ id: selectKomentar.id, data: { comment: selectKomentar.comment, user: hasil } });
const response = await apiEditDiscussionCommentar({
id: selectKomentar.id,
data: { comment: selectKomentar.comment, user: hasil, filesToRemove: removedFileIds }
});
if (response.success) { updateTrigger() } else { Toast.show({ type: 'small', text1: response.message }) }
} catch (error: any) {
console.error(error);
@@ -192,14 +198,20 @@ export default function DiscussionDetail() {
}
}
function handleMenuKomentar(id: string, comment: string) {
setSelectKomentar({ id, comment })
function handleMenuKomentar(id: string, comment: string, files: CommentFile[]) {
setSelectKomentar({ id, comment, files })
setRemovedFileIds([])
setVisible(true)
}
function handleViewEditKomentar() {
setVisible(false)
setViewEdit(!viewEdit)
if (viewEdit) setRemovedFileIds([])
}
function handleRemoveExistingFile(fileId: string) {
setRemovedFileIds(prev => prev.includes(fileId) ? prev.filter(id => id !== fileId) : [...prev, fileId])
}
const handleRefresh = async () => {
@@ -212,6 +224,9 @@ export default function DiscussionDetail() {
const canWrite = data?.status != 2 && data?.isActive && ((entityUser.role != "user" && entityUser.role != "coadmin") || isMemberDivision)
const isOpen = data?.status === 1
const canSend = (entityUser.role == "admin" || entityUser.role == "supadmin" || entityUser.role == "developer" || entityUser.role == "cosupadmin") || isMemberDivision
const existingFilesForEdit = selectKomentar.files.filter(f => !removedFileIds.includes(f.id))
return (
<>
@@ -273,123 +288,43 @@ export default function DiscussionDetail() {
/>
)}
<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>
<DiscussionCommentList
data={dataComment}
loading={loadingKomentar}
myId={entities.id}
canInteract={data?.status != 2 && data?.isActive == true}
onLongPress={handleMenuKomentar}
/>
</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>
<DiscussionCommentInput
mode={
viewEdit ? 'edit'
: canWrite ? 'new'
: 'locked'
}
lockedReason={
data?.status == 2 ? "Diskusi telah ditutup"
: data?.isActive == false ? "Diskusi telah diarsipkan"
: "Hanya anggota divisi yang dapat memberikan komentar"
}
value={viewEdit ? selectKomentar.comment : komentar}
onChange={viewEdit
? (val) => setSelectKomentar(prev => ({ ...prev, comment: val }))
: setKomentar
}
loading={loadingSend}
onSend={viewEdit ? handleEditKomentar : handleKomentar}
onCancelEdit={handleViewEditKomentar}
files={viewEdit ? [] : commentFiles}
onAddFile={viewEdit ? undefined : (f) => setCommentFiles(prev => [...prev, ...f])}
onRemoveFile={viewEdit ? undefined : (idx) => setCommentFiles(prev => prev.filter((_, i) => i !== idx))}
existingFiles={viewEdit ? existingFilesForEdit : []}
onRemoveExistingFile={viewEdit ? handleRemoveExistingFile : undefined}
canSend={canSend}
/>
</KeyboardAvoidingView>
</View>

View File

@@ -105,7 +105,14 @@ export const apiSendDiscussionCommentar = async ({ data, id }: { data: { user: s
return response.data;
};
export const apiEditDiscussionCommentar = async ({ data, id }: { data: { user: string, comment: string }, id: string }) => {
export const apiSendDiscussionCommentarWithFile = async (id: string, data: FormData) => {
const response = await api.post(`/mobile/discussion/${id}/comment`, data, {
headers: { 'Content-Type': 'multipart/form-data' },
})
return response.data;
};
export const apiEditDiscussionCommentar = async ({ data, id }: { data: { user: string, comment: string, filesToRemove?: string[] }, id: string }) => {
const response = await api.put(`/mobile/discussion/${id}/comment`, data)
return response.data;
};