diff --git a/apps/mobile/src/assets/icons.tsx b/apps/mobile/src/assets/icons.tsx index 81ff8227..552d6bdc 100644 --- a/apps/mobile/src/assets/icons.tsx +++ b/apps/mobile/src/assets/icons.tsx @@ -23,6 +23,16 @@ export const AdminIcon: React.FC = (props) => ( /> ); + +export const EditIcon: React.FC = (props) => ( + + + +); + export const CrownIcon: React.FC = (props) => ( = (props) => ( ); +export const TrashIcon: React.FC = (props) => ( + + + + + + + +); + export const RemoveIcon: React.FC = (props) => ( = (props) => { ); }; + export const LikeIcon: React.FC = (props) => { return ( diff --git a/apps/mobile/src/modules/Group/addGroup/AddGroup.tsx b/apps/mobile/src/modules/Group/addGroup/AddGroup.tsx index 03b71221..3e1c09e7 100644 --- a/apps/mobile/src/modules/Group/addGroup/AddGroup.tsx +++ b/apps/mobile/src/modules/Group/addGroup/AddGroup.tsx @@ -1,49 +1,79 @@ -import {useState} from 'react'; +import {useQueryClient} from '@tanstack/react-query'; +import {useCreateGroup} from 'afk_nostr_sdk'; +import {Formik} from 'formik'; import {Text, View} from 'react-native'; import {SafeAreaView} from 'react-native-safe-area-context'; import {Picker} from '../../../components'; import {Button, Input} from '../../../components'; import {useStyles} from '../../../hooks'; +import {useToast} from '../../../hooks/modals'; import stylesheet from './styles'; export const CreateGroup: React.FC = () => { const styles = useStyles(stylesheet); - const [groupName, setGroupName] = useState(''); - const [groupType, setGroupType] = useState(''); + const {showToast} = useToast(); + const queryClient = useQueryClient(); + const {mutate} = useCreateGroup(); - const handleSubmit = () => { - // Here you would typically handle the form submission, - // e.g., sending the data to an API - console.log('Submitted:', {groupName, groupType}); + const initialValues = { + groupName: '', + groupType: 'private', }; return ( - - - Create a New Group - Add a new group and set its privacy level. - - - - setGroupType(itemValue)} - label="" - > - - - - - - + { + mutate( + { + groupType: 'private', + groupName: values.groupName, + }, + { + onSuccess() { + showToast({type: 'success', title: 'Group Created successfully'}); + queryClient.invalidateQueries({queryKey: ['getAllGroups']}); + }, + onError() { + showToast({ + type: 'error', + title: 'Error! Group could not be created. Please try again later.', + }); + }, + }, + ); + }} + > + {({handleChange, handleBlur, handleSubmit, values}) => ( + + + Create a New Group + Add a new group and set its privacy level. + + + + handleChange('groupType')} + label="" + > + + + + + + + )} + ); }; diff --git a/apps/mobile/src/modules/Group/all/AllGroup.tsx b/apps/mobile/src/modules/Group/all/AllGroup.tsx index 83f0cb0c..2f367b54 100644 --- a/apps/mobile/src/modules/Group/all/AllGroup.tsx +++ b/apps/mobile/src/modules/Group/all/AllGroup.tsx @@ -1,21 +1,18 @@ import {useNavigation} from '@react-navigation/native'; +import {useAuth, useGetGroupList} from 'afk_nostr_sdk'; import {FlatList, SafeAreaView, Text, TouchableOpacity, View} from 'react-native'; -import {GlobeIcon, PadlockIcon, SlantedArrowIcon} from '../../../assets/icons'; +import {PadlockIcon, SlantedArrowIcon} from '../../../assets/icons'; import {useStyles} from '../../../hooks'; import {MainStackNavigationProps} from '../../../types'; import stylesheet from './styles'; -// Mock data for the groups -const groups = [ - {id: '1', name: 'Book Club', type: 'public'}, - {id: '2', name: 'Family', type: 'private'}, - {id: '3', name: 'Work Team', type: 'private'}, - {id: '4', name: 'Hiking Enthusiasts', type: 'public'}, - {id: '5', name: 'Local Community', type: 'public'}, -]; - export default function AllGroupListComponent() { + const {publicKey: pubKey} = useAuth(); + const data = useGetGroupList({ + pubKey, + }); + const styles = useStyles(stylesheet); const navigation = useNavigation(); @@ -25,21 +22,19 @@ export default function AllGroupListComponent() { My Groups ( + data={data.data.pages.flat()} + renderItem={({item}: any) => ( navigation.navigate('GroupChat', {groupId: item.id})} + onPress={() => + navigation.navigate('GroupChat', {groupId: item.id, groupName: item.content}) + } style={styles.groupItem} > - {item.name} + {item.content || 'No Name'} - {item.type === 'private' ? ( - - ) : ( - - )} - {item.type} + + Private @@ -47,7 +42,7 @@ export default function AllGroupListComponent() { )} - keyExtractor={(item) => item.id} + keyExtractor={(item: any) => item.id} contentContainerStyle={styles.listContent} /> diff --git a/apps/mobile/src/modules/Group/all/styles.ts b/apps/mobile/src/modules/Group/all/styles.ts index 590c9fb7..92b2249d 100644 --- a/apps/mobile/src/modules/Group/all/styles.ts +++ b/apps/mobile/src/modules/Group/all/styles.ts @@ -43,9 +43,9 @@ export default ThemedStyleSheet((theme) => ({ }, groupName: { color: theme.colors.text, - fontSize: 18, + fontSize: 16, fontWeight: '600', - marginBottom: 4, + marginBottom: 7, }, groupType: { color: theme.colors.text, diff --git a/apps/mobile/src/modules/Group/groupDetail/GroupChatDetail.tsx b/apps/mobile/src/modules/Group/groupDetail/GroupChatDetail.tsx index 2c0710cb..1fe4b840 100644 --- a/apps/mobile/src/modules/Group/groupDetail/GroupChatDetail.tsx +++ b/apps/mobile/src/modules/Group/groupDetail/GroupChatDetail.tsx @@ -1,67 +1,146 @@ +import {useQueryClient} from '@tanstack/react-query'; +import {useAuth, useDeleteGroup, useGetGroupMemberList} from 'afk_nostr_sdk'; import React, {useRef, useState} from 'react'; -import {FlatList, SafeAreaView, Text, TouchableOpacity, View} from 'react-native'; +import {FlatList, Pressable, SafeAreaView, TouchableOpacity, View} from 'react-native'; -import {BackIcon} from '../../../assets/icons'; -import {IconButton, Modalize} from '../../../components'; -import {useStyles} from '../../../hooks'; +import {AddPostIcon, BackIcon, EditIcon, TrashIcon, UserPlusIcon} from '../../../assets/icons'; +import {IconButton, Modalize, Text} from '../../../components'; +import {useStyles, useTheme} from '../../../hooks'; +import {useToast} from '../../../hooks/modals'; import {GroupChatDetailScreenProps} from '../../../types'; +import AddMemberView from '../memberAction/addMember'; +import {EditGroup} from '../memberAction/editGroup'; import GroupAdminActions from '../memberAction/groupAction'; import stylesheet from './styles'; -const data = [ - {id: '1', name: 'Alice Johnson', role: 'Admin'}, - {id: '2', name: 'Bob Smith', role: 'Member'}, - {id: '3', name: 'Charlie Brown', role: 'Member'}, - {id: '4', name: 'Diana Prince', role: 'Member'}, - {id: '5', name: 'Ethan Hunt', role: 'Member'}, -]; - const GroupChatDetail: React.FC = ({navigation, route}) => { + const theme = useTheme(); + const {publicKey: pubKey} = useAuth(); + const queryClient = useQueryClient(); + const {showToast} = useToast(); + const datas = useGetGroupMemberList({ + groupId: route.params.groupId, + }); + const {mutate} = useDeleteGroup(); const modalizeRef = useRef(null); + const addMemberModalizeRef = useRef(null); + const editGroupModalizeRef = useRef(null); + const menuModalizeRef = useRef(null); + const styles = useStyles(stylesheet); + const [selectedMember, setSelectedMember] = useState(); - const onOpen = () => { + const onOpen = (selected: any) => { modalizeRef.current?.open(); + setSelectedMember(selected); + }; + + const onOpenAddMember = () => { + addMemberModalizeRef.current?.open(); + menuModalizeRef.current?.close(); + }; + const onOpenEditGroup = () => { + editGroupModalizeRef.current?.open(); + menuModalizeRef.current?.close(); + }; + + const onOpenMenu = (selected: any) => { + setSelectedMember(selected); + menuModalizeRef.current?.open(); }; - const styles = useStyles(stylesheet); - const [groupName] = useState('Project Team'); - const [members] = useState(data); return ( <> - + modalizeRef.current?.close()} + /> + + + editGroupModalizeRef.current?.close()} + groupId={route.params.groupId ? route.params.groupId : ''} + /> + + + addMemberModalizeRef.current?.close()} + groupId={route.params.groupId ? route.params.groupId : ''} + /> - - navigation.navigate('GroupChat', {groupId: route.params.groupId})} - > - - - - {groupName} - {members.length} members + + + { + mutate( + { + groupId: route.params.groupId, + }, + { + onSuccess: () => { + showToast({type: 'success', title: 'Group Deleted Successfully'}); + queryClient.invalidateQueries({queryKey: ['getAllGroups', pubKey]}); + menuModalizeRef.current?.close(); + navigation.navigate('Tips'); + }, + onError: () => { + showToast({ + type: 'error', + title: 'Error! Couldnt Delete Group. Please try again later.', + }); + }, + }, + ); + }} + onOpenAddMember={() => onOpenAddMember()} + /> + + + + + + navigation.navigate('GroupChat', { + groupId: route.params.groupId, + groupName: route.params.groupName, + }) + } + > + + + + {route.params.groupName} + {datas.data.pages.flat().length} members + onOpen()} />} - keyExtractor={(item) => item.id} + data={datas.data.pages.flat()} + renderItem={({item}: any) => onOpen(item)} />} + keyExtractor={(item: any) => item.id} contentContainerStyle={styles.memberList} /> + + + ); }; const MemberCard = ({item, handleOpen}: {item: any; handleOpen: () => void}) => { + const pub = item?.tags.find((tag: any) => tag[0] === 'p')?.[1]; const styles = useStyles(stylesheet); return ( - {item.name} + + {pub} + {item.role} @@ -74,5 +153,35 @@ const MemberCard = ({item, handleOpen}: {item: any; handleOpen: () => void}) => ); }; +const MenuBubble = ({ + onOpenAddMember, + onDeleteGroup, + onEditGroup, +}: { + onOpenAddMember: () => void; + onDeleteGroup: () => void; + onEditGroup: () => void; +}) => { + const styles = useStyles(stylesheet); + const theme = useTheme(); + + return ( + + Member Actions + + + Add Member + + + + Edit Group + + + + Delete Group + + + ); +}; export default GroupChatDetail; diff --git a/apps/mobile/src/modules/Group/groupDetail/styles.ts b/apps/mobile/src/modules/Group/groupDetail/styles.ts index 2bf5a848..c91d8676 100644 --- a/apps/mobile/src/modules/Group/groupDetail/styles.ts +++ b/apps/mobile/src/modules/Group/groupDetail/styles.ts @@ -47,7 +47,7 @@ export default ThemedStyleSheet((theme) => ({ flex: 1, }, memberName: { - fontSize: 16, + fontSize: 14, fontWeight: '500', color: theme.colors.text, }, @@ -63,4 +63,35 @@ export default ThemedStyleSheet((theme) => ({ backgroundColor: theme.colors.buttonDisabledBackground, padding: Spacing.small, }, + addMemberButton: { + position: 'absolute', + bottom: Spacing.large, + right: Spacing.pagePadding, + }, + + //Menu + menuContainer: { + padding: 16, + }, + title: { + fontSize: 18, + fontWeight: 'bold', + marginBottom: 16, + color: theme.colors.text, + }, + actionButton: { + flexDirection: 'row', + alignItems: 'center', + padding: 16, + borderBottomWidth: 1, + borderBottomColor: theme.colors.divider, + }, + actionText: { + marginLeft: 16, + fontSize: 16, + color: theme.colors.text, + }, + deleteText: { + color: theme.colors.red, + }, })); diff --git a/apps/mobile/src/modules/Group/memberAction/addMember.tsx b/apps/mobile/src/modules/Group/memberAction/addMember.tsx new file mode 100644 index 00000000..7fea6686 --- /dev/null +++ b/apps/mobile/src/modules/Group/memberAction/addMember.tsx @@ -0,0 +1,89 @@ +import {useQueryClient} from '@tanstack/react-query'; +import {useAddMember, useGetGroupMemberList} from 'afk_nostr_sdk'; +import {Formik} from 'formik'; +import {Text, View} from 'react-native'; + +import {Button, Input} from '../../../components'; +import {useStyles} from '../../../hooks'; +import {useToast} from '../../../hooks/modals'; +import stylesheet from './styles'; + +export default function AddMemberView({ + groupId, + handleClose, +}: { + groupId: string; + handleClose: () => void; +}) { + const groupMembers = useGetGroupMemberList({ + groupId, + }); + const {mutate} = useAddMember(); + const queryClient = useQueryClient(); + const {showToast} = useToast(); + const styles = useStyles(stylesheet); + + const initialValues = { + pubKey: '', + groupId, + }; + + const checkIfMemberExists = (pubKey: string) => { + return groupMembers?.data.pages.some((page: any) => + page.some((event: any) => event.tags.some((tag: any) => tag[0] === 'p' && tag[1] === pubKey)), + ); + }; + + return ( + + Add New Member + Enter user public key. + + { + //Check if the pubKey that wants to be added exist + if (checkIfMemberExists(values.pubKey)) { + showToast({ + type: 'error', + title: 'Error! This public key is already a member of the group.', + }); + } else { + mutate( + { + pubkey: values.pubKey, + groupId, + }, + { + onSuccess(data) { + console.log(data); + showToast({type: 'success', title: 'Member Added successfully'}); + queryClient.invalidateQueries({queryKey: ['getAllGroupMember']}); + handleClose(); + }, + onError() { + showToast({ + type: 'error', + title: 'Error! Member could not be added. Please try again later.', + }); + }, + }, + ); + } + }} + > + {({handleChange, handleBlur, handleSubmit, values}) => ( + + + + + )} + + + ); +} diff --git a/apps/mobile/src/modules/Group/memberAction/editGroup.tsx b/apps/mobile/src/modules/Group/memberAction/editGroup.tsx new file mode 100644 index 00000000..e0eff6a2 --- /dev/null +++ b/apps/mobile/src/modules/Group/memberAction/editGroup.tsx @@ -0,0 +1,82 @@ +import {useQueryClient} from '@tanstack/react-query'; +import {useGroupEditMetadata} from 'afk_nostr_sdk'; +import {Formik} from 'formik'; +import {Text, View} from 'react-native'; +import {SafeAreaView} from 'react-native-safe-area-context'; + +import {Button, Input, SquareInput} from '../../../components'; +import {useStyles} from '../../../hooks'; +import {useToast} from '../../../hooks/modals'; +import stylesheet from '../addGroup/styles'; + +export const EditGroup = ({groupId, handleClose}: {groupId: string; handleClose: () => void}) => { + const styles = useStyles(stylesheet); + const {showToast} = useToast(); + const queryClient = useQueryClient(); + const {mutate} = useGroupEditMetadata(); + + const initialValues = { + name: '', + about: '', + }; + + return ( + + { + mutate( + { + groupId, + meta: { + name: values.name, + about: values.about, + }, + }, + { + onSuccess() { + showToast({type: 'success', title: 'Group Edited successfully'}); + queryClient.invalidateQueries({queryKey: ['getAllGroups']}); + handleClose(); + }, + onError() { + showToast({ + type: 'error', + title: 'Error! Group could not be edited. Please try again later.', + }); + }, + }, + ); + }} + > + {({handleChange, handleBlur, handleSubmit, values}) => ( + + + Edit Group + + + + + + + + )} + + + ); +}; diff --git a/apps/mobile/src/modules/Group/memberAction/groupAction.tsx b/apps/mobile/src/modules/Group/memberAction/groupAction.tsx index e978374a..1ee8bd85 100644 --- a/apps/mobile/src/modules/Group/memberAction/groupAction.tsx +++ b/apps/mobile/src/modules/Group/memberAction/groupAction.tsx @@ -1,56 +1,135 @@ +import {useQueryClient} from '@tanstack/react-query'; +import {useAddPermissions, useRemoveMember} from 'afk_nostr_sdk'; import React, {useState} from 'react'; -import {Switch, Text, TouchableOpacity, View} from 'react-native'; +import {ScrollView, Switch, Text, TouchableOpacity, View} from 'react-native'; import {CrownIcon, RemoveIcon} from '../../../assets/icons'; import {useStyles} from '../../../hooks'; +import {useToast} from '../../../hooks/modals'; import stylesheet from './styles'; -const GroupAdminActions = () => { - const [canManageMembers, setCanManageMembers] = useState(false); - const [canEditGroup, setCanEditGroup] = useState(false); +export enum AdminGroupPermission { + AddMember = 'add-user', + EditMetadata = 'edit-metadata', + DeleteEvent = 'delete-event', + RemoveUser = 'remove-user', + AddPermission = 'add-permission', + RemovePermission = 'remove-permission', + EditGroupStatus = 'edit-group-status', + DeleteGroup = 'delete-group', +} + +const GroupAdminActions = ({ + selectedMember, + handleClose, +}: { + selectedMember: any; + handleClose: () => void; +}) => { + const {mutate: removeMember} = useRemoveMember(); + const {mutate: addPermissions} = useAddPermissions(); + const queryClient = useQueryClient(); + const {showToast} = useToast(); const styles = useStyles(stylesheet); + const [permissions, setPermissions] = useState({ + [AdminGroupPermission.AddMember]: false, + [AdminGroupPermission.EditMetadata]: false, + [AdminGroupPermission.DeleteEvent]: false, + [AdminGroupPermission.RemoveUser]: false, + [AdminGroupPermission.AddPermission]: false, + [AdminGroupPermission.RemovePermission]: false, + [AdminGroupPermission.EditGroupStatus]: false, + [AdminGroupPermission.DeleteGroup]: false, + }); + + const groupId = selectedMember?.tags.find((tag: any) => tag[0] === 'd')?.[1]; + const memberPubKey = selectedMember?.tags.find((tag: any) => tag[0] === 'p')?.[1]; + + const togglePermission = (permission: AdminGroupPermission) => { + setPermissions((prev) => ({ + ...prev, + [permission]: !prev[permission], + })); + }; + + const handleMakeAdmin = () => { + const activePermissions = Object.entries(permissions) + .filter(([_, value]) => value) + .map(([key]) => key); + + addPermissions( + { + groupId, + pubkey: memberPubKey, + permissionName: activePermissions as any, + }, + { + onSuccess: () => { + showToast({type: 'success', title: 'Permissions updated successfully'}); + queryClient.invalidateQueries({queryKey: ['getAllGroupMember']}); + handleClose(); + }, + onError: () => { + showToast({ + type: 'error', + title: 'Error! Permissions could not be updated. Please try again later.', + }); + }, + }, + ); + }; + + const handleRemoveMember = () => { + removeMember( + { + groupId, + pubkey: memberPubKey, + }, + { + onSuccess: (data) => { + console.log(data); + showToast({type: 'success', title: 'Member removed successfully'}); + queryClient.invalidateQueries({queryKey: ['getAllGroupMember']}); + handleClose(); + }, + onError: () => { + showToast({ + type: 'error', + title: 'Error! Member could not be removed. Please try again later.', + }); + }, + }, + ); + }; + return ( - + - Make Group Admin - - - Can manage members - - - - Can edit group - + Admin Permissions - - Make Admin + {Object.entries(permissions).map(([permission, value]) => ( + + {permission} + togglePermission(permission as AdminGroupPermission)} + trackColor={{ + false: styles.switchTrack.backgroundColor, + true: styles.switchTrackActive.backgroundColor, + }} + thumbColor={ + value + ? styles.switchThumbActive.backgroundColor + : styles.switchThumb.backgroundColor + } + /> + + ))} + + Update Permissions @@ -59,11 +138,14 @@ const GroupAdminActions = () => { Remove from Group - + Remove - + ); }; diff --git a/apps/mobile/src/modules/Group/memberAction/styles.ts b/apps/mobile/src/modules/Group/memberAction/styles.ts index 144d86e2..504c0b97 100644 --- a/apps/mobile/src/modules/Group/memberAction/styles.ts +++ b/apps/mobile/src/modules/Group/memberAction/styles.ts @@ -3,8 +3,8 @@ import {Spacing, ThemedStyleSheet} from '../../../styles'; export default ThemedStyleSheet((theme) => ({ container: { flex: 1, - backgroundColor: theme.colors.background, - padding: Spacing.medium, + // backgroundColor: theme.colors.background, + padding: Spacing.small, }, card: { backgroundColor: theme.colors.background, @@ -66,4 +66,16 @@ export default ThemedStyleSheet((theme) => ({ switchThumbActive: { backgroundColor: theme.colors.buttonDisabledBackground, }, + + //Add Member Styles + title: { + fontSize: 20, + fontWeight: 'bold', + color: theme.colors.text, + marginBottom: Spacing.xsmall, + }, + text: { + fontSize: 14, + color: theme.colors.text, + }, })); diff --git a/apps/mobile/src/modules/Group/message/GroupMessage.tsx b/apps/mobile/src/modules/Group/message/GroupMessage.tsx index 0b5c4c7a..0b4bb8fe 100644 --- a/apps/mobile/src/modules/Group/message/GroupMessage.tsx +++ b/apps/mobile/src/modules/Group/message/GroupMessage.tsx @@ -1,40 +1,81 @@ +import {useQueryClient} from '@tanstack/react-query'; +import { + useAuth, + useGetGroupMemberList, + useGetGroupMessages, + useProfile, + useSendGroupMessages, +} from 'afk_nostr_sdk'; import React, {useState} from 'react'; -import {FlatList, SafeAreaView, Text, TouchableOpacity, View} from 'react-native'; +import {FlatList, Modal, Pressable, SafeAreaView, Text, TouchableOpacity, View} from 'react-native'; import {BackIcon, MenuIcons} from '../../../assets/icons'; import {IconButton, Input, KeyboardFixedView} from '../../../components'; import {useStyles} from '../../../hooks'; +import {useToast} from '../../../hooks/modals'; import {GroupChatScreenProps} from '../../../types'; import stylesheet from './styles'; -const data = [ - {id: '1', text: 'Hello everyone!', sender: 'Alice'}, - {id: '2', text: 'Hi Alice, how are you?', sender: 'Bob'}, - {id: '3', text: 'Im doing great, thanks!', sender: 'Alice'}, - { - id: '4', - text: 'Whats the plan for today? Whats the plan for today Whats the plan for todayWhats the plan for today', - sender: 'Charlie', - }, - {id: '5', text: 'Whats the plan for today?', sender: 'Charlie'}, - {id: '6', text: 'Whats the plan for today?', sender: 'Charlie'}, - {id: '7', text: 'Whats the plan for today?', sender: 'Charlie'}, - {id: '8', text: 'Whats the plan for today?', sender: 'Charlie'}, - {id: '9', text: 'Whats the plan for today?', sender: 'Charlie'}, -]; - -const groupName = 'Project Team'; -const memberCount = 15; - const GroupChat: React.FC = ({navigation, route}) => { + const {publicKey} = useAuth(); + const datas = useGetGroupMemberList({ + groupId: route.params.groupId, + }); + const profile = useProfile({publicKey}); + const [menuVisible, setMenuVisible] = useState(false); + const [replyToId, setReplyToId] = useState(null); + const [replyToContent, setReplyToContent] = useState(''); + const [selectedMessageId, setSelectedMessageId] = useState(null); + const queryClient = useQueryClient(); + const {showToast} = useToast(); + const {data: messageData} = useGetGroupMessages({ + groupId: route.params.groupId, + authors: publicKey, + }); + const {mutate} = useSendGroupMessages(); const styles = useStyles(stylesheet); const [message, setMessage] = useState(''); - const [messages, setMessages] = useState(data); + + const handleLongPress = (messageId: any, messageContent: string) => { + setSelectedMessageId(messageId); + setReplyToContent(messageContent); + setMenuVisible(true); + }; + const handleReply = () => { + setReplyToId(selectedMessageId); + setMenuVisible(false); + }; + const cancelReply = () => { + setReplyToId(null); + setReplyToContent(''); + }; const sendMessage = () => { - if (message.trim() === '') return; - setMessages([...messages, {id: Date.now().toString(), text: message, sender: 'You'}]); - setMessage(''); + if (!message) return; + mutate( + { + content: message, + groupId: route.params.groupId, + pubkey: publicKey, + name: profile.data?.nip05, + replyId: replyToId ?? (null as any), + }, + { + onSuccess() { + showToast({type: 'success', title: 'Message sent successfully'}); + queryClient.invalidateQueries({queryKey: ['getGroupMessages', route.params.groupId]}); + setMessage(''); + setReplyToId(null); + setReplyToContent(''); + }, + onError() { + showToast({ + type: 'error', + title: 'Error! Comment could not be sent. Please try again later.', + }); + }, + }, + ); }; return ( @@ -44,54 +85,127 @@ const GroupChat: React.FC = ({navigation, route}) => { - {groupName} - {memberCount} members + {route.params.groupName} + + {datas.data.pages.flat().length} members navigation.navigate('GroupChatDetail', {groupId: route.params.groupId})} + onPress={() => + navigation.navigate('GroupChatDetail', { + groupId: route.params.groupId, + groupName: route.params.groupName, + }) + } style={styles.headerButton} > } - keyExtractor={(item) => item.id} + data={messageData.pages.flat()} + renderItem={({item}: any) => } + keyExtractor={(item: any) => item.id} contentContainerStyle={styles.messageList} inverted /> + {replyToId && } + - + sendMessage()} icon="SendIcon" size={24} /> + setMenuVisible(false)} + onReply={handleReply} + /> ); }; // TODO: MOVE TO COMPONENT -const MessageCard = ({item}: {item: (typeof data)[0]}) => { +const MessageCard = ({ + item, + handleLongPress, +}: { + item: any; + handleLongPress: (val: any, content: string) => void; +}) => { + const styles = useStyles(stylesheet); + const memberNip = item?.tags.find((tag: any) => tag[0] === 'name')?.[1]; + const replymemberNip = item?.reply + ? item?.reply.tags.find((tag: any) => tag[0] === 'name')?.[1] + : ''; + + return ( + handleLongPress(item.id, item.content)} delayLongPress={500}> + + {item.reply && {memberNip || 'Nil'}} + {item.reply && ( + + {replymemberNip || 'Nil'} + + {item.reply.content} + + + )} + {!item.reply && {memberNip || 'Nil'}} + + {item.content} + + + ); +}; + +const ReplyIndicator = ({message, onCancel}: {onCancel: () => void; message: string}) => { const styles = useStyles(stylesheet); return ( - - {item.sender} - {item.text} + + + + Replying to: {message} + + + + + ); }; +const LongPressMenu = ({ + visible, + onClose, + onReply, +}: { + visible: boolean; + onClose: () => void; + onReply: any; +}) => { + const styles = useStyles(stylesheet); + return ( + + + + + Reply Message + + + + + ); +}; + export default GroupChat; diff --git a/apps/mobile/src/modules/Group/message/styles.ts b/apps/mobile/src/modules/Group/message/styles.ts index 6b2613a9..696a5c69 100644 --- a/apps/mobile/src/modules/Group/message/styles.ts +++ b/apps/mobile/src/modules/Group/message/styles.ts @@ -41,13 +41,33 @@ export default ThemedStyleSheet((theme) => ({ paddingBottom: 10, }, messageBubble: { - maxWidth: '90%', + maxWidth: '70%', color: theme.colors.messageCardText, backgroundColor: theme.colors.messageCard, padding: 10, borderRadius: 10, marginVertical: 5, }, + replyContainer: { + backgroundColor: theme.colors.messageReplyCard, + borderRadius: 5, + borderWidth: 1, + borderColor: theme.colors.divider, + padding: 10, + marginVertical: 5, + borderLeftColor: theme.colors.blue, + borderLeftWidth: 3, + }, + replySender: { + fontWeight: 'bold', + fontSize: 12, + color: theme.colors.messageReplyCardText, + marginBottom: 2, + }, + replyContentHighlight: { + fontSize: 12, + color: theme.colors.messageReplyCardText, + }, yourMessage: { alignSelf: 'flex-end', borderWidth: 1, @@ -82,6 +102,7 @@ export default ThemedStyleSheet((theme) => ({ input: { flex: 1, width: 'auto', + paddingTop: 10, }, sendButton: { justifyContent: 'center', @@ -96,4 +117,54 @@ export default ThemedStyleSheet((theme) => ({ fontWeight: 'bold', }, //End of All Group Styling + + // Long Press Menu + modalOverlay: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + backgroundColor: 'rgba(0, 0, 0, 0.8)', // Keeping this as a non-theme-based color for overlay + }, + menuContainer: { + backgroundColor: theme.colors.background, + borderWidth: 1, + borderColor: theme.colors.text, + borderRadius: Spacing.xsmall, + color: theme.colors.text, + padding: Spacing.small, + minWidth: '70%', + }, + menuItem: { + padding: Spacing.small, + color: theme.colors.text, + }, + + // Reply Indicator + replyIndicator: { + flexDirection: 'row', + alignItems: 'center', + backgroundColor: theme.colors.messageCard, + padding: Spacing.small, + borderRadius: Spacing.xsmall, + marginBottom: Spacing.small, + borderWidth: 1, + borderColor: theme.colors.text, + margin: Spacing.small, + }, + replyContent: { + flex: 1, + color: theme.colors.text, + }, + replyText: { + fontSize: 14, + color: theme.colors.text, + }, + cancelButton: { + marginLeft: Spacing.small, + padding: Spacing.xsmall, + }, + cancelButtonText: { + fontSize: 16, + color: theme.colors.text, + }, })); diff --git a/apps/mobile/src/styles/Colors.tsx b/apps/mobile/src/styles/Colors.tsx index 940523d5..472f50ed 100644 --- a/apps/mobile/src/styles/Colors.tsx +++ b/apps/mobile/src/styles/Colors.tsx @@ -21,6 +21,9 @@ export const LightTheme = { messageCard: '#FFFFFF', messageCardText: '#14142C', + messageReplyCard: '#E0E0E0', + messageReplyCardText: '#14142C', + // primary: '#EC796B', primary: '#4FA89B', primaryLight: 'rgba(236,185,107, 0.1)', @@ -76,6 +79,9 @@ export const DarkTheme = { messageCard: '#2C2C2C', messageCardText: '#FFFFFF', + messageReplyCard: '#1F1F1F', + messageReplyCardText: '#E0E0E0', + // primary: '#EC796B', primary: '#4FA89B', primaryLight: 'rgba(236,185,107, 0.1)', diff --git a/apps/mobile/src/types/routes.ts b/apps/mobile/src/types/routes.ts index 46f620e9..50551c9f 100644 --- a/apps/mobile/src/types/routes.ts +++ b/apps/mobile/src/types/routes.ts @@ -27,8 +27,8 @@ export type MainStackParams = { CreatePost: undefined; Profile: {publicKey: string}; PostDetail: {postId: string; post?: NDKEvent}; - GroupChat: {groupId: string; post?: NDKEvent}; - GroupChatDetail: {groupId: string; post?: NDKEvent}; + GroupChat: {groupId: string; post?: NDKEvent; groupName: string}; + GroupChatDetail: {groupId: string; groupName: string; post?: NDKEvent}; EditProfile: undefined; Search: undefined; CreateChannel: undefined; @@ -56,7 +56,7 @@ export type MainStackParams = { PrivateGroupDetails: {postId: string; post?: NDKEvent}; Lightning: undefined; Whatever: undefined; - RightDrawer:undefined; + RightDrawer: undefined; }; export type DegensAppStackParams = { diff --git a/packages/afk_nostr_sdk/src/hooks/group/private/useAddMember.ts b/packages/afk_nostr_sdk/src/hooks/group/private/useAddMember.ts index c8df41be..eb5dbfec 100644 --- a/packages/afk_nostr_sdk/src/hooks/group/private/useAddMember.ts +++ b/packages/afk_nostr_sdk/src/hooks/group/private/useAddMember.ts @@ -1,12 +1,12 @@ +import {NDKEvent, NDKKind} from '@nostr-dev-kit/ndk'; import {useMutation} from '@tanstack/react-query'; + import {useNostrContext} from '../../../context/NostrContext'; -import {NDKEvent, NDKKind} from '@nostr-dev-kit/ndk'; -import { useAuth } from '../../../store'; +import {useAuth} from '../../../store'; // TODO export const useAddMember = () => { const {ndk} = useNostrContext(); - const {publicKey} = useAuth(); return useMutation({ mutationKey: ['addMemberGroup', ndk], diff --git a/packages/afk_nostr_sdk/src/hooks/group/private/useAddPermissions.ts b/packages/afk_nostr_sdk/src/hooks/group/private/useAddPermissions.ts index 02a59045..218f2697 100644 --- a/packages/afk_nostr_sdk/src/hooks/group/private/useAddPermissions.ts +++ b/packages/afk_nostr_sdk/src/hooks/group/private/useAddPermissions.ts @@ -1,6 +1,7 @@ -import {useMutation, useQuery} from '@tanstack/react-query'; +import {NDKEvent} from '@nostr-dev-kit/ndk'; +import {useMutation} from '@tanstack/react-query'; + import {useNostrContext} from '../../../context/NostrContext'; -import {NDKEvent, NDKKind} from '@nostr-dev-kit/ndk'; export enum AdminGroupPermission { AddMember = 'add-user', @@ -12,6 +13,7 @@ export enum AdminGroupPermission { EditGroupStatus = 'edit-group-status', DeleteGroup = 'delete-group', } +type IAdminGroupPermission = `${AdminGroupPermission}`; export const useAddPermissions = () => { const {ndk} = useNostrContext(); @@ -20,14 +22,14 @@ export const useAddPermissions = () => { mutationKey: ['addPermissions', ndk], mutationFn: async (data: { pubkey: string; - permissionName: AdminGroupPermission[]; + permissionName: IAdminGroupPermission[]; groupId: string; }) => { const event = new NDKEvent(ndk); - event.kind = 9003 // NDKKind.GroupAdminAddPermission; + event.kind = 9003; // NDKKind.GroupAdminAddPermission; event.tags = [ + // ['h', data.groupId], ['h', data.groupId], - ['d', data.groupId], ['p', data.pubkey, ...data.permissionName], ]; return event.publish(); diff --git a/packages/afk_nostr_sdk/src/hooks/group/private/useCreateGroup.ts b/packages/afk_nostr_sdk/src/hooks/group/private/useCreateGroup.ts index c5b5ec05..5b61324f 100644 --- a/packages/afk_nostr_sdk/src/hooks/group/private/useCreateGroup.ts +++ b/packages/afk_nostr_sdk/src/hooks/group/private/useCreateGroup.ts @@ -1,6 +1,7 @@ +import {NDKEvent, NDKKind} from '@nostr-dev-kit/ndk'; import {useMutation} from '@tanstack/react-query'; + import {useNostrContext} from '../../../context/NostrContext'; -import {NDKEvent, NDKKind} from '@nostr-dev-kit/ndk'; // TODO export const useCreateGroup = () => { @@ -8,10 +9,11 @@ export const useCreateGroup = () => { return useMutation({ mutationKey: ['createGroup', ndk], - mutationFn: async (data?: {groupType: 'private' | 'public'}) => { + mutationFn: async (data?: {groupType: 'private' | 'public'; groupName: string}) => { const event = new NDKEvent(ndk); event.kind = NDKKind.GroupAdminCreateGroup; - event.tags = [[data.groupType || 'private']]; + event.content = data.groupName; + event.tags = [[data.groupType || 'private'], ['name', data.groupName]]; return event.publish(); }, }); diff --git a/packages/afk_nostr_sdk/src/hooks/group/private/useDeleteEvent.ts b/packages/afk_nostr_sdk/src/hooks/group/private/useDeleteEvent.ts index 00917512..645fb265 100644 --- a/packages/afk_nostr_sdk/src/hooks/group/private/useDeleteEvent.ts +++ b/packages/afk_nostr_sdk/src/hooks/group/private/useDeleteEvent.ts @@ -1,9 +1,10 @@ +import {NDKEvent} from '@nostr-dev-kit/ndk'; import {useMutation} from '@tanstack/react-query'; + import {useNostrContext} from '../../../context/NostrContext'; -import {NDKEvent, NDKKind} from '@nostr-dev-kit/ndk'; import {useAuth} from '../../../store'; -import {checkGroupPermission} from './useGetPermission'; import {AdminGroupPermission} from './useAddPermissions'; +import {checkGroupPermission} from './useGetPermission'; // TODO export const useDeleteEvent = () => { @@ -23,7 +24,7 @@ export const useDeleteEvent = () => { throw new Error('You do not have permission to delete this event'); } const event = new NDKEvent(ndk); - event.kind = 9005 //NDKKind.GroupAdminDeleteEvent; + event.kind = 9005; //NDKKind.GroupAdminDeleteEvent; event.tags = [ ['d', data.groupId], ['e', data.id], diff --git a/packages/afk_nostr_sdk/src/hooks/group/private/useDeleteGroup.ts b/packages/afk_nostr_sdk/src/hooks/group/private/useDeleteGroup.ts index 09dcc7da..1f82ce61 100644 --- a/packages/afk_nostr_sdk/src/hooks/group/private/useDeleteGroup.ts +++ b/packages/afk_nostr_sdk/src/hooks/group/private/useDeleteGroup.ts @@ -1,9 +1,10 @@ +import {NDKEvent} from '@nostr-dev-kit/ndk'; import {useMutation} from '@tanstack/react-query'; + import {useNostrContext} from '../../../context/NostrContext'; -import {NDKEvent, NDKKind} from '@nostr-dev-kit/ndk'; -import {checkGroupPermission} from './useGetPermission'; import {useAuth} from '../../../store'; import {AdminGroupPermission} from './useAddPermissions'; +import {checkGroupPermission} from './useGetPermission'; // TODO export const useDeleteGroup = () => { @@ -24,7 +25,7 @@ export const useDeleteGroup = () => { throw new Error('You do not have permission to delete group'); } const event = new NDKEvent(ndk); - event.kind = 9008 // NDKKind.GroupAdminDeleteGroup; + event.kind = 9008; // NDKKind.GroupAdminDeleteGroup; event.tags = [['d', data.groupId]]; return event.publish(); }, diff --git a/packages/afk_nostr_sdk/src/hooks/group/private/useEditGroupStatus.ts b/packages/afk_nostr_sdk/src/hooks/group/private/useEditGroupStatus.ts index 17e9e496..8d6ae55c 100644 --- a/packages/afk_nostr_sdk/src/hooks/group/private/useEditGroupStatus.ts +++ b/packages/afk_nostr_sdk/src/hooks/group/private/useEditGroupStatus.ts @@ -1,10 +1,11 @@ +import {NDKEvent, NDKKind} from '@nostr-dev-kit/ndk'; import {useMutation} from '@tanstack/react-query'; + import {useNostrContext} from '../../../context/NostrContext'; -import {NDKEvent, NDKKind} from '@nostr-dev-kit/ndk'; -import {objectToTagArray} from './util'; import {useAuth} from '../../../store'; -import {checkGroupPermission} from './useGetPermission'; import {AdminGroupPermission} from './useAddPermissions'; +import {checkGroupPermission} from './useGetPermission'; +import {objectToTagArray} from './util'; type GroupStatus = { groupVisibility: 'public' | 'private'; diff --git a/packages/afk_nostr_sdk/src/hooks/group/private/useGetGroupMember.ts b/packages/afk_nostr_sdk/src/hooks/group/private/useGetGroupMember.ts new file mode 100644 index 00000000..d6020196 --- /dev/null +++ b/packages/afk_nostr_sdk/src/hooks/group/private/useGetGroupMember.ts @@ -0,0 +1,68 @@ +import {NDKKind} from '@nostr-dev-kit/ndk'; +import {useInfiniteQuery} from '@tanstack/react-query'; + +import {useNostrContext} from '../../../context/NostrContext'; +import {useAuth} from '../../../store'; + +interface UseGetGroupListOptions { + limit?: number; + search?: string; + groupId: string; +} + +interface UseGetGroupListOptions { + limit?: number; + search?: string; + groupId: string; +} + +export const useGetGroupMemberList = (options: UseGetGroupListOptions) => { + const {ndk} = useNostrContext(); + const {publicKey} = useAuth(); + + return useInfiniteQuery({ + queryKey: ['getAllGroupMember', publicKey, options.search, options.groupId], + initialPageParam: 0, + getNextPageParam: (lastPage, allPages, lastPageParam) => { + if (!lastPage?.length) return undefined; + const pageParam = lastPage[lastPage.length - 1].created_at - 1; + if (!pageParam || pageParam === lastPageParam) return undefined; + return pageParam; + }, + queryFn: async ({pageParam}) => { + const events = await ndk.fetchEvents({ + kinds: [NDKKind.GroupAdminAddUser, NDKKind.GroupAdminRemoveUser], + authors: [publicKey], + '#d': [options.groupId], + until: pageParam || Math.round(Date.now() / 1000), + limit: options?.limit || 20, + search: options?.search, + }); + + const memberMap = new Map(); + + [...events] + .sort((a, b) => a.created_at - b.created_at) + .forEach((event) => { + const pubkey = event.tags.find((tag) => tag[0] === 'p')?.[1]; + if (!pubkey) return; + + if (event.kind === NDKKind.GroupAdminAddUser) { + memberMap.set(pubkey, {...event, isRemoved: false}); + } else if (event.kind === NDKKind.GroupAdminRemoveUser) { + const existingMember = memberMap.get(pubkey); + if (existingMember) { + memberMap.set(pubkey, {...existingMember, isRemoved: true}); + } + } + }); + + const currentMembers = Array.from(memberMap.values()) + .filter((member) => !member.isRemoved) + .sort((a, b) => b.created_at - a.created_at); + + return currentMembers; + }, + placeholderData: {pages: [], pageParams: []}, + }); +}; diff --git a/packages/afk_nostr_sdk/src/hooks/group/private/useGetGroupMessage.ts b/packages/afk_nostr_sdk/src/hooks/group/private/useGetGroupMessage.ts new file mode 100644 index 00000000..e2e61ede --- /dev/null +++ b/packages/afk_nostr_sdk/src/hooks/group/private/useGetGroupMessage.ts @@ -0,0 +1,73 @@ +import {NDKKind} from '@nostr-dev-kit/ndk'; +import {useInfiniteQuery} from '@tanstack/react-query'; + +import {useNostrContext} from '../../../context/NostrContext'; + +interface UseGetActiveGroupListOptions { + search?: string; + limit?: number; + groupId: string; + authors: string; + content?: string; +} + +export const useGetGroupMessages = (options: UseGetActiveGroupListOptions) => { + const {ndk} = useNostrContext(); + + return useInfiniteQuery({ + queryKey: ['getGroupMessages', options.groupId, options?.search], + initialPageParam: 0, + getNextPageParam: (lastPage, allPages, lastPageParam) => { + if (!lastPage?.length) return undefined; + + const pageParam = lastPage[lastPage.length - 1].created_at - 1; + if (!pageParam || pageParam === lastPageParam) return undefined; + return pageParam; + }, + queryFn: async ({pageParam}) => { + const events = await ndk.fetchEvents({ + '#h': [options.groupId], + kinds: [NDKKind.GroupNote, NDKKind.GroupReply], + authors: [options?.authors], + search: options?.search, + until: pageParam || Math.round(Date.now() / 1000), + limit: options?.limit || 20, + }); + + const eventMap = new Map(); + const replyMap = new Map(); + + // Single pass: Store all events and process replies + events.forEach((event) => { + eventMap.set(event.id, event); + + const replyTag = event.tags.find((tag) => tag[0] === 'e' && tag[3] === 'reply'); + if (replyTag) { + const rootId = replyTag[1]; + const rootMessage = eventMap.get(rootId); + if (rootMessage) { + event['reply'] = rootMessage; + } else { + if (!replyMap.has(rootId)) { + replyMap.set(rootId, []); + } + replyMap.get(rootId).push(event); + } + } + }); + + // Process any remaining replies + replyMap.forEach((replies, rootId) => { + const rootMessage = eventMap.get(rootId); + if (rootMessage) { + replies.forEach((reply) => { + reply['reply'] = rootMessage; + }); + } + }); + + return [...eventMap.values()]; + }, + placeholderData: {pages: [], pageParams: []}, + }); +}; diff --git a/packages/afk_nostr_sdk/src/hooks/group/private/useGetGroups.ts b/packages/afk_nostr_sdk/src/hooks/group/private/useGetGroups.ts new file mode 100644 index 00000000..b978421d --- /dev/null +++ b/packages/afk_nostr_sdk/src/hooks/group/private/useGetGroups.ts @@ -0,0 +1,60 @@ +import {NDKEvent, NDKKind} from '@nostr-dev-kit/ndk'; +import {useInfiniteQuery} from '@tanstack/react-query'; + +import {useNostrContext} from '../../../context/NostrContext'; + +interface UseGetActiveGroupListOptions { + pubKey: string; + search?: string; + limit?: number; +} + +export const useGetGroupList = (options: UseGetActiveGroupListOptions) => { + const {ndk} = useNostrContext(); + const GroupAdminDeleteGroup: any = 9008; + const groupMap = new Map(); + return useInfiniteQuery({ + queryKey: ['getAllGroups', options.pubKey, options?.search], + initialPageParam: 0, + getNextPageParam: (lastPage, allPages, lastPageParam) => { + if (!lastPage?.length) return undefined; + + const pageParam = lastPage[lastPage.length - 1].created_at - 1; + if (!pageParam || pageParam === lastPageParam) return undefined; + return pageParam; + }, + queryFn: async ({pageParam}) => { + const events = await ndk.fetchEvents({ + kinds: [NDKKind.GroupAdminCreateGroup, GroupAdminDeleteGroup, 39000], + authors: [options.pubKey], + until: pageParam || Math.round(Date.now() / 1000), + limit: options?.limit || 20, + search: options?.search, + }); + + [...events] + .sort((a, b) => a.created_at - b.created_at) + .forEach((event) => { + if (event.kind === NDKKind.GroupAdminCreateGroup) { + const groupId = event.tags.find((tag) => tag[0] === 'd')?.[1] || event?.id; + if (groupId) { + groupMap.set(groupId, event); + } + } else if (event.kind === GroupAdminDeleteGroup) { + const groupId = event.tags.find((tag) => tag[0] === 'd')?.[1]; + if (groupId) { + groupMap.delete(groupId); + } + } + }); + + const activeGroups = Array.from(groupMap.values()).sort( + (a, b) => b.created_at - a.created_at, + ); + + return activeGroups; + }, + + placeholderData: {pages: [], pageParams: []}, + }); +}; diff --git a/packages/afk_nostr_sdk/src/hooks/group/private/useGetPermission.ts b/packages/afk_nostr_sdk/src/hooks/group/private/useGetPermission.ts index a19a6764..ec226b3b 100644 --- a/packages/afk_nostr_sdk/src/hooks/group/private/useGetPermission.ts +++ b/packages/afk_nostr_sdk/src/hooks/group/private/useGetPermission.ts @@ -1,6 +1,7 @@ +import NDK, {NDKEvent, NDKKind} from '@nostr-dev-kit/ndk'; import {useQuery} from '@tanstack/react-query'; + import {useNostrContext} from '../../../context/NostrContext'; -import NDK, {NDKEvent, NDKKind} from '@nostr-dev-kit/ndk'; import {useAuth} from '../../../store'; import {AdminGroupPermission} from './useAddPermissions'; @@ -28,7 +29,7 @@ export const useGetPermissionsByUserConnected = (groupId: string) => { }; // Function for fetching permissions -const fetchPermissions = async ({ +export const fetchPermissions = async ({ ndk, groupId, pubkey, diff --git a/packages/afk_nostr_sdk/src/hooks/group/private/useGroupEditMetadata.ts b/packages/afk_nostr_sdk/src/hooks/group/private/useGroupEditMetadata.ts index 316747a5..ad481196 100644 --- a/packages/afk_nostr_sdk/src/hooks/group/private/useGroupEditMetadata.ts +++ b/packages/afk_nostr_sdk/src/hooks/group/private/useGroupEditMetadata.ts @@ -1,10 +1,11 @@ +import {NDKEvent, NDKKind} from '@nostr-dev-kit/ndk'; import {useMutation} from '@tanstack/react-query'; + import {useNostrContext} from '../../../context/NostrContext'; -import {NDKEvent, NDKKind} from '@nostr-dev-kit/ndk'; -import {objectToTagArray} from './util'; import {useAuth} from '../../../store'; -import {checkGroupPermission} from './useGetPermission'; import {AdminGroupPermission} from './useAddPermissions'; +import {checkGroupPermission} from './useGetPermission'; +import {objectToTagArray} from './util'; type UpdateMetaData = { name?: string; @@ -12,7 +13,6 @@ type UpdateMetaData = { picture?: string; }; -// TODO export const useGroupEditMetadata = () => { const {ndk} = useNostrContext(); const {publicKey: pubkey} = useAuth(); @@ -30,6 +30,7 @@ export const useGroupEditMetadata = () => { if (!hasPermission) { throw new Error('You do not have permission to edit metadata'); } + const event = new NDKEvent(ndk); event.kind = NDKKind.GroupAdminEditMetadata; event.tags = [['d', data.groupId], objectToTagArray(data.meta)[0]]; diff --git a/packages/afk_nostr_sdk/src/hooks/group/private/useJoinRequest.ts b/packages/afk_nostr_sdk/src/hooks/group/private/useJoinRequest.ts index 0f9cc885..dcc57d51 100644 --- a/packages/afk_nostr_sdk/src/hooks/group/private/useJoinRequest.ts +++ b/packages/afk_nostr_sdk/src/hooks/group/private/useJoinRequest.ts @@ -1,6 +1,7 @@ +import {NDKEvent, NDKKind} from '@nostr-dev-kit/ndk'; import {useMutation} from '@tanstack/react-query'; + import {useNostrContext} from '../../../context/NostrContext'; -import {NDKEvent, NDKKind} from '@nostr-dev-kit/ndk'; export const useJoinGroupRequest = () => { const {ndk} = useNostrContext(); diff --git a/packages/afk_nostr_sdk/src/hooks/group/private/useLeaveRequest.ts b/packages/afk_nostr_sdk/src/hooks/group/private/useLeaveRequest.ts index d6caa5c7..70ee8966 100644 --- a/packages/afk_nostr_sdk/src/hooks/group/private/useLeaveRequest.ts +++ b/packages/afk_nostr_sdk/src/hooks/group/private/useLeaveRequest.ts @@ -1,6 +1,7 @@ +import {NDKEvent} from '@nostr-dev-kit/ndk'; import {useMutation} from '@tanstack/react-query'; + import {useNostrContext} from '../../../context/NostrContext'; -import {NDKEvent, NDKKind} from '@nostr-dev-kit/ndk'; // TODO export const useLeaveGroupRequest = () => { @@ -10,7 +11,7 @@ export const useLeaveGroupRequest = () => { mutationKey: ['leaveGroupRequest', ndk], mutationFn: async (data: {groupId: string; content?: string}) => { const event = new NDKEvent(ndk); - event.kind = 9022 // NDKKind.GroupAdminRequestLeave; + event.kind = 9022; // NDKKind.GroupAdminRequestLeave; event.content = data?.content || ''; event.tags = [['h', data.groupId]]; return event.publish(); diff --git a/packages/afk_nostr_sdk/src/hooks/group/private/useRemoveMember.ts b/packages/afk_nostr_sdk/src/hooks/group/private/useRemoveMember.ts index b454524f..f023ed0b 100644 --- a/packages/afk_nostr_sdk/src/hooks/group/private/useRemoveMember.ts +++ b/packages/afk_nostr_sdk/src/hooks/group/private/useRemoveMember.ts @@ -1,17 +1,17 @@ +import {NDKEvent, NDKKind} from '@nostr-dev-kit/ndk'; import {useMutation} from '@tanstack/react-query'; + import {useNostrContext} from '../../../context/NostrContext'; -import {NDKEvent, NDKKind} from '@nostr-dev-kit/ndk'; import {useAuth} from '../../../store'; -import {checkGroupPermission} from './useGetPermission'; import {AdminGroupPermission} from './useAddPermissions'; +import {checkGroupPermission} from './useGetPermission'; -// TODO export const useRemoveMember = () => { const {ndk} = useNostrContext(); const {publicKey: pubkey} = useAuth(); return useMutation({ - mutationKey: ['removeMemberGroup', ndk], + mutationKey: ['removeMemberGroup'], mutationFn: async (data: {pubkey: string; groupId: string}) => { const hasPermission = checkGroupPermission({ groupId: data.groupId, diff --git a/packages/afk_nostr_sdk/src/hooks/group/private/useRemovePermissions.ts b/packages/afk_nostr_sdk/src/hooks/group/private/useRemovePermissions.ts index 6c4e0c0b..bd6410b5 100644 --- a/packages/afk_nostr_sdk/src/hooks/group/private/useRemovePermissions.ts +++ b/packages/afk_nostr_sdk/src/hooks/group/private/useRemovePermissions.ts @@ -1,13 +1,13 @@ +import {NDKEvent} from '@nostr-dev-kit/ndk'; import {useMutation} from '@tanstack/react-query'; + import {useNostrContext} from '../../../context/NostrContext'; -import {NDKEvent, NDKKind} from '@nostr-dev-kit/ndk'; +import {useAuth} from '../../../store'; import {AdminGroupPermission} from './useAddPermissions'; -import { useAuth } from '../../../store'; // TODO export const useRemovePermissions = () => { const {ndk} = useNostrContext(); - const {publicKey} = useAuth(); return useMutation({ mutationKey: ['removePermissions', ndk], @@ -17,7 +17,7 @@ export const useRemovePermissions = () => { groupId: string; }) => { const event = new NDKEvent(ndk); - event.kind = 9004 // NDKKind.GroupAdminRemovePermission; + event.kind = 9004; // NDKKind.GroupAdminRemovePermission; event.tags = [ ['d', data.groupId], diff --git a/packages/afk_nostr_sdk/src/hooks/group/private/useSendGroupMessage.ts b/packages/afk_nostr_sdk/src/hooks/group/private/useSendGroupMessage.ts index 576e457a..78d16a8b 100644 --- a/packages/afk_nostr_sdk/src/hooks/group/private/useSendGroupMessage.ts +++ b/packages/afk_nostr_sdk/src/hooks/group/private/useSendGroupMessage.ts @@ -1,12 +1,12 @@ +import {NDKEvent, NDKKind} from '@nostr-dev-kit/ndk'; import {useMutation} from '@tanstack/react-query'; + import {useNostrContext} from '../../../context/NostrContext'; -import {NDKEvent, NDKKind} from '@nostr-dev-kit/ndk'; import {useAuth} from '../../../store'; // TODO export const useSendGroupMessages = () => { const {ndk} = useNostrContext(); - const {publicKey} = useAuth(); return useMutation({ mutationKey: ['sendGroupMessage', ndk], @@ -14,13 +14,26 @@ export const useSendGroupMessages = () => { pubkey: string; content: string; groupId: string; - tag?: string[]; + name?: string; + replyId: string; }) => { const event = new NDKEvent(ndk); - event.kind = NDKKind.GroupNote; event.content = data.content; + // Set the kind based on whether it's a reply or not + event.kind = data.replyId ? NDKKind.GroupReply : NDKKind.GroupNote; // Using literal kind values + + // Base tags + event.tags = [ + ['h', data.groupId], + ['p', data.pubkey], + ['name', data.name], + ]; + + // Check if it's a reply and append NIP-10 markers + if (data.replyId) { + event.tags.push(['e', data.replyId, '', 'reply']); + } - event.tags = [['h', data.groupId], data.tag]; return event.publish(); }, }); diff --git a/packages/afk_nostr_sdk/src/hooks/index.ts b/packages/afk_nostr_sdk/src/hooks/index.ts index cdf4c4a7..ab1ddce9 100644 --- a/packages/afk_nostr_sdk/src/hooks/index.ts +++ b/packages/afk_nostr_sdk/src/hooks/index.ts @@ -20,13 +20,16 @@ export {useRootNotes} from './useRootNotes'; export {useSearchNotes} from './useSearchNotes'; export {useSendNote} from './useSendNote'; export {useAddMember} from './group/private/useAddMember'; +export {useGetGroupMemberList} from './group/private/useGetGroupMember'; export {useAddPermissions} from './group/private/useAddPermissions'; export {useCreateGroup} from './group/private/useCreateGroup'; +export {useGetGroupList} from './group/private/useGetGroups'; export {useDeleteEvent} from './group/private/useDeleteEvent'; export {useGroupEditMetadata} from './group/private/useGroupEditMetadata'; export {useRemovePermissions} from './group/private/useRemovePermissions'; export {useRemoveMember} from './group/private/useRemoveMember'; export {useSendGroupMessages} from './group/private/useSendGroupMessage'; +export {useGetGroupMessages} from './group/private/useGetGroupMessage'; export {useGroupEditStatus} from './group/private/useEditGroupStatus'; export {useDeleteGroup} from './group/private/useDeleteGroup'; export {useJoinGroupRequest} from './group/private/useJoinRequest'; @@ -36,4 +39,3 @@ export {useSendPrivateMessage} from './messages/useSendPrivateMessage'; export {useMyGiftWrapMessages} from './messages/useMyGiftWrapMessages'; export {useMyMessagesSent} from './messages/useMyMessagesSent'; export {useBookmark} from './useBookmark'; -