Merge pull request 'upd: aksi komentar diskusi' (#51) from amalia/14-okt-25 into join

Reviewed-on: bip/mobile-darmasaba#51
This commit is contained in:
2025-10-14 17:35:47 +08:00
6 changed files with 366 additions and 100 deletions

View File

@@ -14,10 +14,10 @@ import { ColorsStatus } from "@/constants/ColorsStatus";
import { ConstEnv } from "@/constants/ConstEnv"; import { ConstEnv } from "@/constants/ConstEnv";
import { regexOnlySpacesOrEnter } from "@/constants/OnlySpaceOrEnter"; import { regexOnlySpacesOrEnter } from "@/constants/OnlySpaceOrEnter";
import Styles from "@/constants/Styles"; import Styles from "@/constants/Styles";
import { apiGetDiscussionGeneralOne, apiSendDiscussionGeneralCommentar } from "@/lib/api"; import { apiDeleteDiscussionGeneralCommentar, apiGetDiscussionGeneralOne, apiSendDiscussionGeneralCommentar, apiUpdateDiscussionGeneralCommentar } from "@/lib/api";
import { getDB } from "@/lib/firebaseDatabase"; import { getDB } from "@/lib/firebaseDatabase";
import { useAuthSession } from "@/providers/AuthProvider"; import { useAuthSession } from "@/providers/AuthProvider";
import { Ionicons, MaterialCommunityIcons, MaterialIcons } from "@expo/vector-icons"; import { Feather, Ionicons, MaterialCommunityIcons, MaterialIcons } from "@expo/vector-icons";
import { ref } from '@react-native-firebase/database'; import { ref } from '@react-native-firebase/database';
import { useHeaderHeight } from '@react-navigation/elements'; import { useHeaderHeight } from '@react-navigation/elements';
import { router, Stack, useLocalSearchParams } from "expo-router"; import { router, Stack, useLocalSearchParams } from "expo-router";
@@ -49,6 +49,7 @@ type PropsKomentar = {
export default function DetailDiscussionGeneral() { export default function DetailDiscussionGeneral() {
const { token, decryptToken } = useAuthSession() const { token, decryptToken } = useAuthSession()
const entityUser = useSelector((state: any) => state.user) const entityUser = useSelector((state: any) => state.user)
const entities = useSelector((state: any) => state.entities)
const { id } = useLocalSearchParams<{ id: string }>(); const { id } = useLocalSearchParams<{ id: string }>();
const [data, setData] = useState<Props>() const [data, setData] = useState<Props>()
const [dataKomentar, setDataKomentar] = useState<PropsKomentar[]>([]) const [dataKomentar, setDataKomentar] = useState<PropsKomentar[]>([])
@@ -67,6 +68,8 @@ export default function DetailDiscussionGeneral() {
id: '', id: '',
comment: '' comment: ''
}) })
const [viewEdit, setViewEdit] = useState(false)
useEffect(() => { useEffect(() => {
@@ -138,7 +141,7 @@ export default function DetailDiscussionGeneral() {
setKomentar('') setKomentar('')
updateTrigger() updateTrigger()
} else { } else {
Toast.show({ type: 'small', text1: 'Gagal menambahkan komentar' }) Toast.show({ type: 'small', text1: response.message })
} }
} }
} catch (error) { } catch (error) {
@@ -148,11 +151,52 @@ export default function DetailDiscussionGeneral() {
} }
} }
async function handleEditKomentar() {
try {
setLoadingSendKomentar(true)
const hasil = await decryptToken(String(token?.current))
const response = await apiUpdateDiscussionGeneralCommentar({ id: selectKomentar.id, data: { desc: selectKomentar.comment, user: hasil } })
if (response.success) {
updateTrigger()
} else {
Toast.show({ type: 'small', text1: response.message })
}
} catch (error) {
console.error(error)
} finally {
setLoadingSendKomentar(false)
handleViewEditKomentar()
}
}
async function handleDeleteKomentar() {
try {
setLoadingSendKomentar(true)
const hasil = await decryptToken(String(token?.current))
const response = await apiDeleteDiscussionGeneralCommentar({ id: selectKomentar.id, data: { user: hasil } })
if (response.success) {
updateTrigger()
} else {
Toast.show({ type: 'small', text1: response.message })
}
} catch (error) {
console.error(error)
} finally {
setLoadingSendKomentar(false)
setVisible(false)
}
}
function handleMenuKomentar(id: string, comment: string) { function handleMenuKomentar(id: string, comment: string) {
setSelectKomentar({ id, comment }) setSelectKomentar({ id, comment })
setVisible(true) setVisible(true)
} }
function handleViewEditKomentar() {
setVisible(false)
setViewEdit(!viewEdit)
}
return ( return (
<> <>
<Stack.Screen <Stack.Screen
@@ -213,6 +257,7 @@ export default function DetailDiscussionGeneral() {
<BorderBottomItem <BorderBottomItem
key={i} key={i}
borderType="bottom" borderType="bottom"
colorPress
icon={ icon={
<ImageUser src={`${ConstEnv.url_storage}/files/${item.img}`} size="xs" /> <ImageUser src={`${ConstEnv.url_storage}/files/${item.img}`} size="xs" />
} }
@@ -231,7 +276,7 @@ export default function DetailDiscussionGeneral() {
}) })
}} }}
onLongPress={() => { onLongPress={() => {
handleMenuKomentar(item.id, item.comment) item.idUser == entities.id && data?.status != 2 && data?.isActive && handleMenuKomentar(item.id, item.comment)
}} }}
/> />
) )
@@ -248,8 +293,45 @@ export default function DetailDiscussionGeneral() {
Styles.contentItemCenter, Styles.contentItemCenter,
Styles.w100, Styles.w100,
{ backgroundColor: "#f4f4f4" }, { backgroundColor: "#f4f4f4" },
viewEdit && Styles.borderTop
]}> ]}>
{ {
viewEdit ?
<>
<View style={[Styles.w90, Styles.rowSpaceBetween, Styles.pv05]}>
<View style={[Styles.rowItemsCenter]}>
<Feather name="edit-3" color="black" size={22} style={[Styles.mh05]} />
<Text style={[Styles.textMediumSemiBold]}>Edit Komentar</Text>
</View>
<Pressable onPress={() => handleViewEditKomentar()}>
<MaterialIcons name="close" color="black" size={22} />
</Pressable>
</View>
<InputForm
disable={(data?.status === 2 || !data?.isActive || (!memberDiscussion && (entityUser.role == "user" || entityUser.role == "coadmin")))}
type="default"
round
placeholder="Kirim Komentar"
bg="white"
onChange={(val: string) => setSelectKomentar({ ...selectKomentar, comment: val })}
value={selectKomentar.comment}
multiline
focus={viewEdit}
itemRight={
<Pressable onPress={() => {
(!loadingSendKomentar && selectKomentar.comment != '' && !regexOnlySpacesOrEnter.test(selectKomentar.comment) && data?.status === 1 && data?.isActive && (memberDiscussion || (entityUser.role != "user" && entityUser.role != "coadmin")))
&& handleEditKomentar()
}}
style={[
Platform.OS == 'android' && Styles.mb12,
]}
>
<MaterialIcons name="send" size={25} style={(loadingSendKomentar || selectKomentar.comment == '' || regexOnlySpacesOrEnter.test(selectKomentar.comment) || data?.status === 2 || !data?.isActive || (!memberDiscussion && (entityUser.role == "user" || entityUser.role == "coadmin"))) ? Styles.cGray : Styles.cDefault} />
</Pressable>
}
/>
</>
:
data?.status != 2 && data?.isActive && ((entityUser.role != "user" && entityUser.role != "coadmin") || memberDiscussion) data?.status != 2 && data?.isActive && ((entityUser.role != "user" && entityUser.role != "coadmin") || memberDiscussion)
? ?
<InputForm <InputForm
@@ -261,6 +343,7 @@ export default function DetailDiscussionGeneral() {
onChange={setKomentar} onChange={setKomentar}
value={komentar} value={komentar}
multiline multiline
focus={viewEdit}
itemRight={ itemRight={
<Pressable onPress={() => { <Pressable onPress={() => {
(!loadingSendKomentar && komentar != '' && !regexOnlySpacesOrEnter.test(komentar) && data?.status === 1 && data?.isActive && (memberDiscussion || (entityUser.role != "user" && entityUser.role != "coadmin"))) (!loadingSendKomentar && komentar != '' && !regexOnlySpacesOrEnter.test(komentar) && data?.status === 1 && data?.isActive && (memberDiscussion || (entityUser.role != "user" && entityUser.role != "coadmin")))
@@ -293,9 +376,7 @@ export default function DetailDiscussionGeneral() {
<MenuItemRow <MenuItemRow
icon={<MaterialCommunityIcons name="pencil-outline" color="black" size={25} />} icon={<MaterialCommunityIcons name="pencil-outline" color="black" size={25} />}
title="Edit" title="Edit"
onPress={() => { onPress={() => { handleViewEditKomentar() }}
setVisible(false)
}}
/> />
<MenuItemRow <MenuItemRow
icon={<MaterialIcons name="delete" color="black" size={25} />} icon={<MaterialIcons name="delete" color="black" size={25} />}
@@ -304,7 +385,9 @@ export default function DetailDiscussionGeneral() {
AlertKonfirmasi({ AlertKonfirmasi({
title: 'Konfirmasi', title: 'Konfirmasi',
desc: 'Apakah anda yakin ingin menghapus komentar?', desc: 'Apakah anda yakin ingin menghapus komentar?',
onPress: () => { setVisible(false) } onPress: () => {
handleDeleteKomentar()
}
}) })
}} }}
/> />

View File

@@ -1,9 +1,12 @@
import AlertKonfirmasi from "@/components/alertKonfirmasi";
import BorderBottomItem from "@/components/borderBottomItem"; import BorderBottomItem from "@/components/borderBottomItem";
import ButtonBackHeader from "@/components/buttonBackHeader"; import ButtonBackHeader from "@/components/buttonBackHeader";
import HeaderRightDiscussionDetail from "@/components/discussion/headerDiscussionDetail"; import HeaderRightDiscussionDetail from "@/components/discussion/headerDiscussionDetail";
import DrawerBottom from "@/components/drawerBottom";
import ImageUser from "@/components/imageNew"; import ImageUser from "@/components/imageNew";
import { InputForm } from "@/components/inputForm"; import { InputForm } from "@/components/inputForm";
import LabelStatus from "@/components/labelStatus"; import LabelStatus from "@/components/labelStatus";
import MenuItemRow from "@/components/menuItemRow";
import Skeleton from "@/components/skeleton"; import Skeleton from "@/components/skeleton";
import SkeletonContent from "@/components/skeletonContent"; import SkeletonContent from "@/components/skeletonContent";
import Text from "@/components/Text"; import Text from "@/components/Text";
@@ -11,18 +14,21 @@ import { ConstEnv } from "@/constants/ConstEnv";
import { regexOnlySpacesOrEnter } from "@/constants/OnlySpaceOrEnter"; import { regexOnlySpacesOrEnter } from "@/constants/OnlySpaceOrEnter";
import Styles from "@/constants/Styles"; import Styles from "@/constants/Styles";
import { import {
apiDeleteDiscussionCommentar,
apiEditDiscussionCommentar,
apiGetDiscussionOne, apiGetDiscussionOne,
apiGetDivisionOneFeature, apiGetDivisionOneFeature,
apiSendDiscussionCommentar, apiSendDiscussionCommentar,
} from "@/lib/api"; } from "@/lib/api";
import { getDB } from "@/lib/firebaseDatabase"; import { getDB } from "@/lib/firebaseDatabase";
import { useAuthSession } from "@/providers/AuthProvider"; import { useAuthSession } from "@/providers/AuthProvider";
import { Ionicons, MaterialIcons } from "@expo/vector-icons"; import { Feather, Ionicons, MaterialCommunityIcons, MaterialIcons } from "@expo/vector-icons";
import { ref } from "@react-native-firebase/database"; import { ref } from "@react-native-firebase/database";
import { useHeaderHeight } from '@react-navigation/elements'; import { useHeaderHeight } from '@react-navigation/elements';
import { router, Stack, useLocalSearchParams } from "expo-router"; import { router, Stack, useLocalSearchParams } from "expo-router";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { KeyboardAvoidingView, Platform, Pressable, RefreshControl, ScrollView, View } from "react-native"; import { KeyboardAvoidingView, Platform, Pressable, RefreshControl, ScrollView, View } from "react-native";
import Toast from "react-native-toast-message";
import { useSelector } from "react-redux"; import { useSelector } from "react-redux";
type Props = { type Props = {
@@ -44,6 +50,9 @@ type PropsComment = {
createdAt: string; createdAt: string;
username: string; username: string;
img: string; img: string;
idUser: string;
isEdited: boolean;
updatedAt: string;
}; };
export default function DiscussionDetail() { export default function DiscussionDetail() {
@@ -65,6 +74,14 @@ export default function DiscussionDetail() {
const [refreshing, setRefreshing] = useState(false) const [refreshing, setRefreshing] = useState(false)
const headerHeight = useHeaderHeight(); const headerHeight = useHeaderHeight();
const [detailMore, setDetailMore] = useState<any>([]) 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)
useEffect(() => { useEffect(() => {
@@ -172,6 +189,59 @@ export default function DiscussionDetail() {
} }
} }
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) {
console.error(error);
} 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) {
console.error(error);
} 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 () => { const handleRefresh = async () => {
setRefreshing(true) setRefreshing(true)
@@ -268,6 +338,7 @@ export default function DiscussionDetail() {
<BorderBottomItem <BorderBottomItem
key={index} key={index}
borderType="bottom" borderType="bottom"
colorPress
icon={ icon={
<ImageUser <ImageUser
src={`${ConstEnv.url_storage}/files/${item.img}`} src={`${ConstEnv.url_storage}/files/${item.img}`}
@@ -277,6 +348,7 @@ export default function DiscussionDetail() {
title={item.username} title={item.username}
rightTopInfo={item.createdAt} rightTopInfo={item.createdAt}
desc={item.comment} desc={item.comment}
rightBottomInfo={item.isEdited ? "Edited" : ""}
descEllipsize={detailMore.includes(item.id) ? false : true} descEllipsize={detailMore.includes(item.id) ? false : true}
onPress={() => { onPress={() => {
setDetailMore((prev: any) => { setDetailMore((prev: any) => {
@@ -287,6 +359,9 @@ export default function DiscussionDetail() {
} }
}) })
}} }}
onLongPress={() => {
item.idUser == entities.id && data?.status != 2 && data?.isActive && handleMenuKomentar(item.id, item.comment)
}}
/> />
)) ))
} }
@@ -303,18 +378,69 @@ export default function DiscussionDetail() {
Styles.contentItemCenter, Styles.contentItemCenter,
Styles.w100, Styles.w100,
{ backgroundColor: "#f4f4f4" }, { backgroundColor: "#f4f4f4" },
viewEdit && Styles.borderTop
]} ]}
> >
{ {
viewEdit ?
<>
<View style={[Styles.w90, Styles.rowSpaceBetween, Styles.pv05]}>
<View style={[Styles.rowItemsCenter]}>
<Feather name="edit-3" color="black" size={22} style={[Styles.mh05]} />
<Text style={[Styles.textMediumSemiBold]}>Edit Komentar</Text>
</View>
<Pressable onPress={() => handleViewEditKomentar()}>
<MaterialIcons name="close" color="black" size={22} />
</Pressable>
</View>
<InputForm
bg="white"
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)
? Styles.cGray
: Styles.cDefault,
]
}
/>
</Pressable>
}
/>
</>
:
data?.status != 2 && data?.isActive && ((entityUser.role != "user" && entityUser.role != "coadmin") || data?.status != 2 && data?.isActive && ((entityUser.role != "user" && entityUser.role != "coadmin") ||
isMemberDivision) isMemberDivision)
? ?
<InputForm <InputForm
// disable={
// data?.status == 2 ||
// data?.isActive == false ||
// ((entityUser.role == "user" || entityUser.role == "coadmin") && !isMemberDivision)
// }
bg="white" bg="white"
type="default" type="default"
round round
@@ -368,8 +494,29 @@ export default function DiscussionDetail() {
</View> </View>
</KeyboardAvoidingView> </KeyboardAvoidingView>
</View> </View>
<DrawerBottom animation="slide" isVisible={isVisible} setVisible={setVisible} title="Komentar">
<View style={Styles.rowItemsCenter}>
<MenuItemRow
icon={<MaterialCommunityIcons name="pencil-outline" color="black" size={25} />}
title="Edit"
onPress={() => { handleViewEditKomentar() }}
/>
<MenuItemRow
icon={<MaterialIcons name="delete" color="black" size={25} />}
title="Hapus"
onPress={() => {
AlertKonfirmasi({
title: 'Konfirmasi',
desc: 'Apakah anda yakin ingin menghapus komentar?',
onPress: () => {
handleDeleteKomentar()
}
})
}}
/>
</View>
</DrawerBottom>
</> </>
); );
} }

View File

@@ -19,16 +19,28 @@ type Props = {
bgColor?: 'white' | 'transparent' bgColor?: 'white' | 'transparent'
width?: number width?: number
descEllipsize?: boolean descEllipsize?: boolean
textColor?: string textColor?: string,
colorPress?: boolean
} }
export default function BorderBottomItem({ title, subtitle, icon, desc, onPress, onLongPress, rightTopInfo, borderType, leftBottomInfo, rightBottomInfo, titleWeight, bgColor, width, descEllipsize, textColor }: Props) { export default function BorderBottomItem({ title, subtitle, icon, desc, onPress, onLongPress, rightTopInfo, borderType, leftBottomInfo, rightBottomInfo, titleWeight, bgColor, width, descEllipsize, textColor, colorPress }: Props) {
const lebarDim = Dimensions.get("window").width; const lebarDim = Dimensions.get("window").width;
const lebar = width ? lebarDim * width / 100 : 'auto'; const lebar = width ? lebarDim * width / 100 : 'auto';
const textColorFix = textColor ? textColor : 'black'; const textColorFix = textColor ? textColor : 'black';
return ( return (
<Pressable onLongPress={onLongPress} style={[borderType == 'bottom' ? Styles.wrapItemBorderBottom : borderType == 'all' ? Styles.wrapItemBorderAll : Styles.wrapItemBorderNone, bgColor && bgColor == 'white' && ColorsStatus.white]} onPress={onPress}> <Pressable onLongPress={onLongPress} onPress={onPress}
style={({ pressed }) => [
borderType == 'bottom'
? Styles.wrapItemBorderBottom
: borderType == 'all'
? Styles.wrapItemBorderAll
: Styles.wrapItemBorderNone,
bgColor && bgColor == 'white' && ColorsStatus.white,
// efek warna saat ditekan (sementara)
pressed && colorPress && ColorsStatus.pressedGray,
]}
>
<View style={[Styles.rowItemsCenter]}> <View style={[Styles.rowItemsCenter]}>
{icon} {icon}
<View style={[Styles.rowSpaceBetween, width ? { width: lebar } : { width: '88%' }]}> <View style={[Styles.rowSpaceBetween, width ? { width: lebar } : { width: '88%' }]}>

View File

@@ -20,10 +20,11 @@ type Props = {
disable?: boolean disable?: boolean
multiline?: boolean multiline?: boolean
mb?: boolean mb?: boolean
focus?: boolean
}; };
export function InputForm({ label, value, placeholder, onChange, info, disable, error, errorText, required, itemLeft, itemRight, type, round, width, bg, multiline, mb = true }: Props) { export function InputForm({ label, value, placeholder, onChange, info, disable, error, errorText, required, itemLeft, itemRight, type, round, width, bg, multiline, mb = true, focus }: Props) {
const lebar = Dimensions.get("window").width; const lebar = Dimensions.get("window").width;
if (itemLeft != undefined || itemRight != undefined) { if (itemLeft != undefined || itemRight != undefined) {

View File

@@ -31,5 +31,8 @@ export const ColorsStatus = {
}, },
lightRed: { lightRed: {
backgroundColor: '#ffcdcd' backgroundColor: '#ffcdcd'
},
pressedGray: {
backgroundColor: '#e4e4e4'
} }
} }

View File

@@ -182,6 +182,16 @@ export const apiSendDiscussionGeneralCommentar = async ({ id, data }: { id: stri
return response.data; return response.data;
}; };
export const apiDeleteDiscussionGeneralCommentar = async ({ id, data }: { id: string, data: { user: string } }) => {
const response = await api.delete(`/mobile/discussion-general/${id}/comment`, { data })
return response.data;
};
export const apiUpdateDiscussionGeneralCommentar = async ({ id, data }: { id: string, data: { desc: string, user: string } }) => {
const response = await api.put(`/mobile/discussion-general/${id}/comment`, data)
return response.data;
};
export const apiDeleteMemberDiscussionGeneral = async (data: { user: string, idUser: string }, id: string) => { export const apiDeleteMemberDiscussionGeneral = async (data: { user: string, idUser: string }, id: string) => {
await api.delete(`mobile/discussion-general/${id}/member`, { data }).then(response => { await api.delete(`mobile/discussion-general/${id}/member`, { data }).then(response => {
@@ -445,6 +455,16 @@ export const apiSendDiscussionCommentar = async ({ data, id }: { data: { user: s
return response.data; return response.data;
}; };
export const apiEditDiscussionCommentar = async ({ data, id }: { data: { user: string, comment: string }, id: string }) => {
const response = await api.put(`/mobile/discussion/${id}/comment`, data)
return response.data;
};
export const apiDeleteDiscussionCommentar = async ({ data, id }: { data: { user: string }, id: string }) => {
const response = await api.delete(`/mobile/discussion/${id}/comment`, { data })
return response.data;
};
export const apiEditDiscussion = async ({ data, id }: { data: { user: string, desc: string }, id: string }) => { export const apiEditDiscussion = async ({ data, id }: { data: { user: string, desc: string }, id: string }) => {
const response = await api.post(`/mobile/discussion/${id}`, data) const response = await api.post(`/mobile/discussion/${id}`, data)
return response.data; return response.data;