Nounspace / nounspace.ts

The nounspace web client
https://nounspace.com
GNU General Public License v3.0
11 stars 10 forks source link

Bug: Tapping a name suggestion when tagging causes Cast Modal to close #476

Open willyogo opened 3 weeks ago

willyogo commented 3 weeks ago

Reported by a user on 11/14/24 (https://discord.com/channels/1224471875023802521/1251186884726493225/1295470564785393717)

Steps to reproduce:

  1. When casting, start tagging someone (ie. @nounspacetom) and tap one of the name suggestions that appears

Expected result:

  1. When a name suggestion is tapped, the tag is autocompleted with the username and highlighted blue

Actual result:

  1. When a name suggestion is tapped, the cast modal closes

AC:

  1. When a name suggestion is tapped, the tag is autocompleted with the username and highlighted blue; the modal does not close
sktbrd commented 1 week ago

There was initially 3 problems, 1) FocusMode prop in Modal component was causing it to fix when the focus shifted to mentionList that was imported from deprecated modProtocol

2) we had no control over mentionList so I brought it over, its still only responsive by keyboard command

3) we were passing mentions as simple text, when we needed to mount the cast using the text, the mentions and its positions as a separated object like this :

image

I am now stuck between the regex to grab the mention text position and placing it in the right position with all possible line breaks

This is the crucial code to solve the mention positions problem:

    const fetchMentionsAndSetDraft = async () => {
      const newEmbeds = initialEmbeds ? [...embeds, ...initialEmbeds] : embeds;

      // Regex to match pure @username mentions, ensuring it's not part of a URL
      const usernamePattern = /(?:^|\s|^)@([a-zA-Z0-9_.]+)(?=\s|$)/g;

      // The working copy of the text for position calculation
      const workingText = text;

      // Extract mentions and their positions from the original text
      const usernamesWithPositions = [
        ...workingText.matchAll(usernamePattern),
      ].map((match) => ({
        username: match[1],
        position: match.index! + match[0].indexOf("@"), // Adjust position to '@'
      }));

      const uniqueUsernames = Array.from(
        new Set(usernamesWithPositions.map((u) => u.username)),
      );

      if (uniqueUsernames.length > 0) {
        try {
          // Fetch the FIDs for the mentioned users
          const fetchedMentions =
            await getMentionFidsByUsernames(API_URL)(uniqueUsernames);

          const mentionsToFids = fetchedMentions.reduce(
            (acc, mention) => {
              if (mention && mention.username && mention.fid) {
                acc[mention.username] = mention.fid.toString(); // Convert fid to string
              }
              return acc;
            },
            {} as { [key: string]: string },
          );

          const mentionsPositions: number[] = [];

          // Traverse mentions and track positions while replacing them in the working text
          let currentTextIndex = 0;
          let finalText = text; // Keep the original text to display but update mention positions

          // for (const mention of usernamesWithPositions) {
          //   const { username, position } = mention;

          //   // As we find each mention, update the positions list
          //   const mentionIndex = finalText.indexOf(username, currentTextIndex);
          //   if (mentionIndex !== -1) {
          //     mentionsPositions.push(mentionIndex); // Log the position for each mention
          //     currentTextIndex = mentionIndex + username.length; // Move forward in the text

          //     // Optionally, remove the duplicate `@username` from the final text (visible text)
          //     finalText = finalText.replace(`@${username}`, ``); // Keep one `@`
          //   }
          // }

          for (const mention of usernamesWithPositions) {
            const { username, position } = mention;

            // As we find each mention, update the positions list
            const mentionIndex = finalText.indexOf(username, currentTextIndex);
            if (mentionIndex !== -1) {
              mentionsPositions.push(mentionIndex - 1); // Log the position for each mention
              currentTextIndex = mentionIndex + username.length; // Move forward in the text
            }
          }

          for (const mention of usernamesWithPositions) {
            const { username, position } = mention;
            // Optionally, remove the duplicate `@username` from the final text (visible text)
            finalText = finalText.replace(`@${username}`, ``); // Keep one `@`
          }

          if (Object.keys(mentionsToFids).length !== mentionsPositions.length) {
            console.error(
              "Mismatch between mentions and their positions:",
              mentionsToFids,
              mentionsPositions,
            );
          }

          // Update the draft with recalculated text and positions
          setDraft((prevDraft) => {
            const updatedDraft = {
              ...prevDraft,
              text: finalText, // Use the modified text for submission
              embeds: newEmbeds,
              parentUrl: channel?.parent_url || undefined,
              mentionsToFids, // Correct FIDs
              mentionsPositions, // Final recalculated positions
            };
            console.log("Updated Draft before posting:", updatedDraft);
            return updatedDraft;
          });
        } catch (error) {
          console.error("Error fetching FIDs:", error);
        }
      }
    };
and then we still need to refactor the original mentionList now in our code so it can handle Mouse commands.