瀏覽代碼

@{uid} tags

Viktoriia 3 月之前
父節點
當前提交
eeebc0d98c

+ 69 - 0
src/screens/InAppScreens/MessagesScreen/Components/MentionsList.tsx

@@ -0,0 +1,69 @@
+import { TouchableOpacity, Text, Image, FlatList } from 'react-native';
+import { AvatarWithInitials } from 'src/components';
+import { API_HOST } from 'src/constants';
+import { Colors } from 'src/theme';
+
+const MentionsList = ({
+  mentionList,
+  inputHeight,
+  onMentionSelect
+}: {
+  mentionList: any[];
+  inputHeight: number;
+  onMentionSelect: (item: any) => void;
+}) => (
+  <FlatList
+    data={mentionList}
+    keyExtractor={(item) => item.uid?.toString()}
+    keyboardShouldPersistTaps="always"
+    style={{
+      position: 'absolute',
+      bottom: inputHeight,
+      backgroundColor: 'white',
+      width: '100%',
+      borderWidth: 1,
+      borderColor: '#ccc',
+      zIndex: 100,
+      maxHeight: 200
+    }}
+    renderItem={({ item }) => (
+      <TouchableOpacity
+        onPress={() => onMentionSelect(item)}
+        style={{
+          paddingVertical: 8,
+          paddingHorizontal: 10,
+          borderBottomWidth: 1,
+          borderColor: Colors.BORDER_LIGHT,
+          flexDirection: 'row',
+          alignItems: 'center',
+          gap: 4
+        }}
+      >
+        {item.avatar ? (
+          <Image
+            source={{ uri: API_HOST + item.avatar }}
+            style={{
+              width: 28,
+              height: 28,
+              borderWidth: 1,
+              borderColor: Colors.BORDER_LIGHT,
+              borderRadius: 14
+            }}
+          />
+        ) : (
+          <AvatarWithInitials
+            text={item.name?.split(' ')[0][0] + (item.name?.split(' ')[1][0] ?? '')}
+            flag={API_HOST + item.homebase_flag}
+            size={28}
+            fontSize={12}
+            borderColor={Colors.BORDER_LIGHT}
+            borderWidth={1}
+          />
+        )}
+        <Text style={{ color: Colors.DARK_BLUE, fontWeight: '500' }}>@{item.name}</Text>
+      </TouchableOpacity>
+    )}
+  />
+);
+
+export default MentionsList;

+ 126 - 20
src/screens/InAppScreens/MessagesScreen/GroupChatScreen/index.tsx

@@ -27,7 +27,8 @@ import {
   Actions,
   isSameUser,
   isSameDay,
-  SystemMessage
+  SystemMessage,
+  MessageText
 } from 'react-native-gifted-chat';
 import { MaterialCommunityIcons } from '@expo/vector-icons';
 import { GestureHandlerRootView, Swipeable } from 'react-native-gesture-handler';
@@ -52,7 +53,8 @@ import {
   usePostDeleteGroupMessageMutation,
   usePostGetPinnedGroupMessageQuery,
   usePostSetPinGroupMessageMutation,
-  usePostGetGroupSettingsQuery
+  usePostGetGroupSettingsQuery,
+  usePostGetGroupMembersQuery
 } from '@api/chat';
 import { CustomMessage, GroupMessage, Reaction } from '../types';
 import { API_HOST, WEBSOCKET_URL } from 'src/constants';
@@ -81,6 +83,7 @@ import GroupIcon from 'assets/icons/messages/group-chat.svg';
 import { CACHED_ATTACHMENTS_DIR } from 'src/constants/constants';
 import GroupStatusModal from '../Components/GroupStatusModal';
 import PinIcon from 'assets/icons/messages/pin.svg';
+import MentionsList from '../Components/MentionsList';
 
 const options = {
   enableVibrateFallback: true,
@@ -120,12 +123,19 @@ const GroupChatScreen = ({ route }: { route: any }) => {
     refetch: refetch,
     isFetching: isFetching
   } = usePostGetGroupChatQuery(token, group_token, 50, prevThenMessageId, true);
+  const [canSeeMembers, setCanSeeMembers] = useState(false);
+
   const { data: pinData, refetch: refetchPinned } = usePostGetPinnedGroupMessageQuery(
     token,
     group_token,
     true
   );
   const { data } = usePostGetGroupSettingsQuery(token, group_token, true);
+  const { data: members, refetch: refetchMembers } = usePostGetGroupMembersQuery(
+    token,
+    group_token,
+    canSeeMembers
+  );
 
   const { mutateAsync: sendMessage } = usePostSendGroupMessageMutation();
 
@@ -176,6 +186,11 @@ const GroupChatScreen = ({ route }: { route: any }) => {
   const updateUnreadMessagesCount = useMessagesStore((state) => state.updateUnreadMessagesCount);
   const [insetsColor, setInsetsColor] = useState(Colors.FILL_LIGHT);
 
+  const [text, setText] = useState('');
+  const [mentionList, setMentionList] = useState<any>([]);
+  const [showMentions, setShowMentions] = useState(false);
+  const [inputHeight, setInputHeight] = useState(45);
+
   const appState = useRef(AppState.currentState);
   const textInputRef = useRef<TextInput>(null);
 
@@ -589,6 +604,12 @@ const GroupChatScreen = ({ route }: { route: any }) => {
 
   const onShareLiveLocation = useCallback(() => {}, []);
 
+  useEffect(() => {
+    if (data && data.settings) {
+      setCanSeeMembers(data.settings.members_can_see_members === 1 || data.settings.admin === 1);
+    }
+  }, [data]);
+
   useEffect(() => {
     let unsubscribe: any;
 
@@ -1419,7 +1440,7 @@ const GroupChatScreen = ({ route }: { route: any }) => {
     (newMessages: CustomMessage[] = []) => {
       if (replyMessage) {
         newMessages[0].replyMessage = {
-          text: replyMessage.text,
+          text: transformMessageForServer(replyMessage.text),
           id: replyMessage._id,
           name: replyMessage.user._id !== +currentUserId ? (replyMessage.user.name as string) : 'Me'
         };
@@ -1431,13 +1452,17 @@ const GroupChatScreen = ({ route }: { route: any }) => {
       };
       const message = { ...newMessages[0], pending: true, isSending: true, user };
 
-      setMessages((previousMessages) => GiftedChat.append(previousMessages ?? [], [message]));
+      setMessages((previousMessages) =>
+        GiftedChat.append(previousMessages ?? [], [
+          { ...message, text: transformMessageForServer(newMessages[0].text) }
+        ])
+      );
 
       sendMessage(
         {
           token,
           to_group_token: group_token,
-          text: message.text,
+          text: transformMessageForServer(message.text),
           reply_to_id: replyMessage ? (replyMessage._id as number) : -1
         },
         {
@@ -1856,20 +1881,37 @@ const GroupChatScreen = ({ route }: { route: any }) => {
     if (!chatData?.can_send_messages) return null;
 
     return (
-      <InputToolbar
-        {...props}
-        renderActions={() =>
-          userType === 'normal' ? (
-            <Actions
-              icon={() => <MaterialCommunityIcons name="plus" size={28} color={Colors.DARK_BLUE} />}
-              onPressActionButton={openAttachmentsModal}
-            />
-          ) : null
-        }
-        containerStyle={{
-          backgroundColor: Colors.FILL_LIGHT
-        }}
-      />
+      <>
+        {showMentions && canSeeMembers ? (
+          <MentionsList
+            mentionList={mentionList}
+            inputHeight={inputHeight}
+            onMentionSelect={onMentionSelect}
+          />
+        ) : null}
+        <View
+          onLayout={(e) => {
+            setInputHeight(e.nativeEvent.layout.height);
+          }}
+        >
+          <InputToolbar
+            {...props}
+            renderActions={() =>
+              userType === 'normal' ? (
+                <Actions
+                  icon={() => (
+                    <MaterialCommunityIcons name="plus" size={28} color={Colors.DARK_BLUE} />
+                  )}
+                  onPressActionButton={openAttachmentsModal}
+                />
+              ) : null
+            }
+            containerStyle={{
+              backgroundColor: Colors.FILL_LIGHT
+            }}
+          />
+        </View>
+      </>
     );
   };
 
@@ -1897,6 +1939,42 @@ const GroupChatScreen = ({ route }: { route: any }) => {
     return currentId === highlightedMessageId;
   };
 
+  const onInputTextChanged = (value: string) => {
+    handleTyping(value.length > 0);
+
+    setText(value);
+
+    const mentionMatch = value.match(/(^|\s)(@\w*)$/);
+
+    if (mentionMatch) {
+      setShowMentions(true);
+      const searchText = mentionMatch[2].slice(1).toLowerCase();
+      setMentionList(
+        (members?.settings ?? [])?.filter(
+          (m) => m.name.toLowerCase().includes(searchText) && m.uid !== +currentUserId
+        )
+      );
+    } else {
+      setShowMentions(false);
+    }
+  };
+
+  const onMentionSelect = (member: { uid: number; name: string }) => {
+    const words = text.split(' ');
+    words[words.length - 1] = `@${member.name} `;
+    setText(words.join(' '));
+    setShowMentions(false);
+  };
+
+  const transformMessageForServer = (text: string) => {
+    let transformedText = text;
+    members?.settings?.forEach((member) => {
+      const mentionRegex = new RegExp(`@${member.name}\\b`, 'g');
+      transformedText = transformedText.replace(mentionRegex, `@{${member.uid}}`);
+    });
+    return transformedText;
+  };
+
   return (
     <SafeAreaView
       edges={['top']}
@@ -1983,6 +2061,7 @@ const GroupChatScreen = ({ route }: { route: any }) => {
         {messages ? (
           <GiftedChat
             messages={messages as CustomMessage[]}
+            text={text}
             listViewProps={{
               ref: flatList,
               showsVerticalScrollIndicator: false,
@@ -2016,7 +2095,7 @@ const GroupChatScreen = ({ route }: { route: any }) => {
             isCustomViewBottom={false}
             messageContainerRef={messageContainerRef}
             minComposerHeight={34}
-            onInputTextChanged={(text) => handleTyping(text.length > 0)}
+            onInputTextChanged={onInputTextChanged}
             textInputRef={textInputRef}
             isTyping={isTyping ? true : false}
             renderTypingIndicator={() => <TypingIndicator isTyping={isTyping} />}
@@ -2081,6 +2160,33 @@ const GroupChatScreen = ({ route }: { route: any }) => {
                   Clipboard.setString(url ?? '');
                   Alert.alert('Link copied');
                 }
+              },
+              {
+                pattern: /@\{(\d+)\}/g,
+                renderText: (messageText: string) => {
+                  const tagId = messageText.slice(2, messageText.length - 1);
+                  const user = (members?.settings ?? [])?.find((m) => m.uid === +tagId);
+
+                  if (user) {
+                    return (
+                      <Text
+                        style={{ color: Colors.ORANGE }}
+                        onPress={() =>
+                          navigation.navigate(
+                            ...([
+                              NAVIGATION_PAGES.PUBLIC_PROFILE_VIEW,
+                              { userId: user.uid }
+                            ] as never)
+                          )
+                        }
+                      >
+                        @{user.name}
+                      </Text>
+                    );
+                  } else {
+                    return messageText;
+                  }
+                }
               }
             ]}
             infiniteScroll={true}