• XSS.stack #1 – первый литературный журнал от юзеров форума

Статья Создаем приложение чата в Web3 на основе XMTP

tabac

CPU register
Пользователь
Регистрация
30.09.2018
Сообщения
1 610
Решения
1
Реакции
3 332
Источник
Автор перевода @xss.pro, специально для xss.pro


Связь между адресами кошельков уже давно является мечтой для многих людей в пространстве web3. Черт возьми, мой второй стартап в сфере web3 был полностью построен на концепции сегментации и общения с пользователями на основе адресов их кошельков. Если бы только у меня и моей команды были те инструменты, которые существуют сегодня.

Сегодня мы собираемся реализовать эти ранние мечты о Web3, создав приложение для чата Web3, которое позволит вам искать пользователей по их социальному профилю Web3 и отправлять сообщения в режиме реального времени. Мы будем отслеживать прошлые разговоры, чтобы вы могли вернуться к ним и продолжить их в любое время. Мы сделаем это благодаря API протокола XMTP и API GraphQL, созданному Airstack .

Airstack создает инструменты для разработчиков, которые упрощают поиск данных на основе блокчейна. Их API и помощник AI полезны при создании реальных приложений, которые можно масштабировать.

Протокол обмена сообщениями будет работать на базе XMTP . XMTP позиционирует себя как «Открытый протокол и сеть для безопасного обмена сообщениями Web3». Протокол передает сообщения напрямую между адресами кошельков, позволяя общаться в режиме реального времени с кем угодно по всему миру.

Чтобы помочь нам запустить проект, мы воспользуемся примером приложения, уже созданным командой XMTP. В этом примере приложения отсутствует аутентификация кошелька (любезно предоставленная Thirdweb) и создается базовый интерфейс для общения с ботом. Но мы хотим и будем делать гораздо больше.

Начиная​

Пример приложения, с которого мы начнем, находится здесь:

Мы клонируем его, установим зависимости и запустим его, но сначала проведем некоторую служебную работу. Чтобы следовать этому руководству, вам понадобится следующее:
  • Node.js v16 или выше
  • Редактор кода
  • Бесплатный ключ API Airstack.
Чтобы получить ключ API Airstack, зарегистрируйтесь бесплатно на их веб-сайте , затем нажмите на имя своего профиля при входе в систему. Отсюда выберите «Просмотреть ключи API»:

1*YF8ycv-0X0KcPotCuVq6YQ@2x.png

Хорошо, мы можем клонировать демонстрационное приложение и установить наши зависимости. Из терминала/командной строки переключитесь в каталог, в котором вы храните свои проекты разработки, затем запустите следующее:
Код:
git clone https://github.com/fabriguespe/xmtp-quickstart-nextjs
Теперь перейдите в новый каталог проекта:
Код:
cd xmtp-quickstart-nextjs
Затем установите зависимости и запустите приложение:
Код:
npm install && npm run dev
Откройте проект в редакторе кода и приступайте к работе!

Создание приложения​

Базовое приложение — отличный пример, но мы хотим не просто общаться с ботом. Мы хотим иметь возможность общаться с любым количеством контактов и искать новые контакты. Давайте начнем с создания этого интерфейса.

Мы можем сделать это, обновив Home.js файл, чтобы обеспечить возможность переключения между списком контактов и интерфейсом обмена сообщениями. Первое, что мы сделаем, это обновим PEER_ADDRESS переменную, чтобы она была более информативной для этого приложения. Измените переменную и все ее ссылки в файле на BOT_ADDRESS.

Теперь давайте добавим новую переменную состояния для управления переключением между экраном контактов и экраном чата. В верхней части тела функции компонента добавьте следующее:
Код:
...
const [showContactsList, setShowContactList] = useState(false);
Теперь найдите код, который показывает этот Chat компонент. Он выглядит так:
Код:
{isConnected && isOnNetwork && messages && (
  <Chat
    client={clientRef.current}
    conversation={convRef.current}
    messageHistory={messages}
  />
)}
Давайте обновим код, чтобы он отображал либо Chat компонент, либо новые Contacts компоненты, например:
Код:
{isConnected && isOnNetwork && messages && !showContactsList ? (
  <Chat
    client={clientRef.current}
    conversation={convRef.current}
    messageHistory={messages}
  />
) :
  (
    <Contacts setShowContactList={setShowContactList} />
  )
}
Мы еще не создали Contacts компонент, поэтому давайте сделаем это, а затем импортируем его в наш Home.js файл. В свою папку components добавьте файл Contacts.js. Затем добавьте следующее:
Код:
import React, { useState } from 'react'
import styles from "./Chat.module.css";

const Contacts = (props) => {
  const [contacts, setContacts] = useState([]);
  const [profileName, setProfileName] = useState("");
  const [results, setResults] = useState([]);
 
  const searchForUsers = async function(name) {
    console.log(name);
  };

  const handleInputChange = (e) => {
    setProfileName(e.target.value);
  }

  const SearchResults = () => {
   return (
    <div>
      <div>
        <h3>{profileName}</h3>
        {
          results.map(r => {
            return (
              <p key={r}>{r}</p>
            )
          })
        }
      </div>        
    </div>
   )
  }

  return (
    <div className={styles.Contacts}>
      <div className={styles.searchInput}>
        <input
          type="text"
          className={styles.inputField}
          onChange={handleInputChange}
          value={profileName}
          placeholder="Search for new Lens or Farcaster contacts"
        />
        <button onClick={searchForUsers}>
          <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className="w-6 h-6">
            <path strokeLinecap="round" strokeLinejoin="round" d="M21 21l-5.197-5.197m0 0A7.5 7.5 0 105.196 5.196a7.5 7.5 0 0010.607 10.607z" />
          </svg>
        </button>
      </div>
        {
          results.length > 0 && profileName &&
          <SearchResults />
        }
        <div>
          {
            contacts?.map(() => {
              return (
                <div>
               
                </div>
              )
            })
          }
        </div>
    </div>
  )
}

export default Contacts
Давайте пройдемся по этому новому файлу. Мы повторно используем существующий Chat.module.css файл. Позже мы добавим в этот файл несколько классов для наших новых компонентов Contacts.

Теперь внутри тела компонента мы используем две переменные состояния. У нас также есть функция поиска, которая срабатывает при нажатии кнопки поиска.

В основной части нашего компонента у нас есть поле поиска и заполнитель для отображения всех выбранных нами контактов. Пока ничего особенного не происходит, но это хорошая основа.

Давайте добавим эти новые классы в файл Chat.module.css:
Код:
.backButton {
  border-radius: 4px;
  padding: 12px;
  font-size: 16px;
  color: #555;
  cursor: pointer;
  margin-left: 10px;
  outline: none;
}

.Contacts{
  background-color: white;
  margin: 0;
  color:black;
  padding: 0;
  font-family: Arial, sans-serif;
  display: flex;
  flex-direction: column;
  height: 100vh;
  width:100%;
  margin: 0;
}

.searchInput {
  display: flex;
  flex-direction: row;
  padding-right: 10px;
  padding-left: 10px;
  margin-top: 10px;
}

.searchInput button {
  margin-left: 15px;
}
Я думаю, нам следует подключить функцию поиска, прежде чем мы выясним, как мы собираемся отправлять сообщения не только нашему другу-боту. Используем Airstack SDK для преобразования имен пользователей Farcaster и Lens в 0x.. адреса. С помощью этого адреса мы можем отправлять сообщения через XMTP.

Давайте импортируем Airstack SDK в ваш файл Contacts.js и инициализируем его следующим образом:
Код:
import { init, useLazyQueryWithPagination } from "@airstack/airstack-react";
init("YOUR AIRSTACK API KEY");
Теперь давайте установим два useQueryWithPagination хука. Мы настраиваем два, потому что мы собираемся сделать немного другой запрос в зависимости от того, является ли искомый пользователь пользователем Lens или пользователем Farcaster. Под переменными состояния в теле компонента добавьте следующее:
Код:
const [fetchLensUser, { data: lensData, loading: lensLoading, pagination: lensPagination }] = useLazyQueryWithPagination(
  lensQuery, variables
);

const [fetchFCUser, { data: fcData, loading: fcLoading, pagination: fcPagination }] = useLazyQueryWithPagination(
  fcQuery, variables
);
Вы заметите, что мы передаем запрос и переменные обеим функциям выборки. Мы будем хранить наши переменные в состоянии и жестко закодировать наши запросы. У Airstack есть отличная документация о том, как это работает с хуками, здесь . По сути, переменные будут автоматически переданы в ваш запрос во время запроса.

Обязательно добавьте этот новый useState хук к этим двум запросам над вашими двумя useLazyQueryWithPagination хуками.
Код:
const [variables, setVariables] = useState({
  name: ""
})

const lensQuery = `query LensUser($name: Identity!) {
  Wallet(input: {identity: $name, blockchain: ethereum}) {
    addresses
  }
}`

const fcQuery = `query FarcasterUser($name: Identity!)  {
  Wallet(input: {identity: $name, blockchain: ethereum}) {
    addresses
  }
}`
Как уже упоминалось, useState хук будет использоваться для хранения переменной, которую мы передаем в наш запрос. В данном случае это имя профиля пользователя в социальной сети. Эти два запроса немного отличаются из-за различий в том, как искать пользователей Lens и пользователей Farcaster.

Если вам интересно, как я пришел к этим двум запросам GraphQL, я открою вам секрет. Я жульничал. Я использовал AI Assistant от Airstack . Мы можем использовать Airstack Explorer и AI Assistant, чтобы написать запрос GraphQL.

В текстовом поле AI Assistant введите текстовый поиск. Вот мой и полученный запрос:

1*HsYSSA0TTsHf7KuB0-uXWA@2x.png


Вышло хорошо, но как это работает, если мы ищем пользователей Farcaster?
1*JWy3SHj-4Q0DMtAuMYTZdg@2x.png

Довольно хорошо! Для простоты мы собираемся предложить пользователям указывать, в какой социальной сети они хотели бы выполнить поиск при попытке найти контакт для сообщения, включив или не включив в .lensконце имени профиля значок или нет.

Нам нужно будет обновить состояние переменных на основе входного значения поиска, поэтому давайте вернемся к коду. Давайте обновим нашу функцию handleInputChange в файле Contacts.js, чтобы она выглядела следующим образом:
Код:
const handleInputChange = (e) => {
  setResults([]);
  setProfileName(e.target.value);
  setVariables({
    name: e.target.value.includes(".lens") ? e.target.value : `fc_fname:${e.target.value}`
  })
}
Мы проверяем текст, введенный в поле поиска, чтобы увидеть, содержит ли он .lens. Если это так, мы устанавливаем одну переменную для Lens, в противном случае мы устанавливаем ее для Farcaster.

Теперь давайте обновим нашу функцию searchForUsers, чтобы она выглядела так:
Код:
const searchForUsers = async function() {
  let res;
  if(profileName.includes(".lens")) {
    res = await fetchLensUser(variables);
  } else {
    res = await fetchFCUser(variables);
  }

  setResults(res?.data?.Wallet?.addresses || []);
}
Мы что-нибудь сделаем с результатами, а пока давайте глянем в консоль, чтобы убедиться, что все работает. Проверьте приложение в своем браузере, выполнив поиск социального профиля Lens или Farcaster. Если мы проверим вывод консоли, мы должны увидеть что-то вроде этого:
1*vCnBxfLT9lMzBKy17u81tg@2x.png

Хорошо, теперь давайте обновим нашу функцию поиска, чтобы она выглядела так:
Код:
const res = await fetchData();
  setResults(res?.data?.Wallet?.addresses || []);
}
Теперь, когда мы настраиваем результаты поиска, должен появиться наш компонент SearchResults. Давай попробуем.
1*JGLRgXcO1nKLhv8ksvbc1Q@2x.png

Выглядит не очень красиво, но это только начало. Давайте немного это очистим. Вернитесь к компоненту SearchResults и обновите его, чтобы он выглядел следующим образом:
Код:
const setContactDetails = (contact) => {

}

const SearchResults = () => {
  return (
    <div className={styles.SearchResults}>
      <h3>{profileName}</h3>
      {
        results.map(r => {
          return (
            <div key={r}>
              <button onClick={() => setContactDetails({profileName, address: r})} key={r}>{r}</button>
            </div>
          )
        })
      }
    </div>        
  )
}
Мы собираемся установить наши контакты, и нам нужно будет передать новый контакт нашему компоненту чата, чтобы мы могли отправлять сообщения через XMTP. Мы до этого доберемся, а сейчас мы просто хотим, чтобы поиск выглядел лучше. Итак, откройте файл Chat.module.css и добавьте следующее:
Код:
.SearchResults {
  margin-left: 10px;
  margin-right: 50px;
  padding: 5px;
  border: 1px solid #ccc;
}

.SearchResults button {
  text-decoration: underline;
  cursor: hover;
}
Теперь наши результаты поиска выглядят лучше:
1*O3ni0Eu5_jHItYcJAnFalg@2x.png

Процесс движется! :) Мы преобразовали социальный профиль web3 в доступные адреса кошельков благодаря API Airstack . Теперь нам нужно передать выбранный адрес кошелька в XMTP и начать чат.

Пока мы все еще работаем с файлом Contacts.js, давайте найдем способ загрузить существующие контакты и способ сохранить новые и начать/продолжить чат. В XMTP есть функция, которая позволит вам загрузить все ваши предыдущие разговоры, что очень полезно. Мы воспользуемся этим и объединим его с другим запросом Airstack, чтобы преобразовать peerAddressXMTP в социальный профиль (если он существует).

В файле Contacts.js импортируйте useEffect из React в верхней части файла, затем добавьте следующее под переменными состояния в теле основного компонента Contacts:
Код:
useEffect(() => {
  resolveContactsAndProfiles();
}, []);

const resolveSocial = async (address) => {
  const newQuery = `
  query MyQuery {
    Wallet(
      input: {identity: "${address}", blockchain: ethereum}
    ) {
      socials {
        dappName
        profileName
      }    
    }
  }
  `
  const response = await fetchQuery(newQuery)
  if(response.data.Wallet.socials && response.data.Wallet.socials.length > 0) {
    return response?.data?.Wallet?.socials[0].profileName
  }
  return "No web3 profile"
}

const resolveContactsAndProfiles = async () => {
  const results = await props.loadConversations()
  let existingContacts = [];
  for(const r of results) {
    existingContacts.push({
      profileName: await resolveSocial(r.peerAddress),
      address: r.peerAddress
    })    
  }
  setContacts(existingContacts)
}
Нам нужно будет импортировать функцию fetchQuery из Airstack SDK, поскольку мы не используем ту же функцию-перехватчик, которую используем в нашей функции поиска. Поэтому обязательно обновите импорт Airstack SDK в файле Contacts.js, чтобы он выглядел следующим образом:
Код:
import { init, useLazyQueryWithPagination, fetchQuery } from "@airstack/airstack-react";
Теперь давайте разберем, что делают эти новые функции. Функция resolveContactsAndProfiles извлекает разговоры из XMTP (нам нужно создать функцию в нашем файле Home.js, чтобы ими управлять) и возвращает результаты, включая файлы peerAddress.

Мы используем этот адрес, а также звоним в Airstack, чтобы проверить профили адреса в социальных сетях и составить список контактов. Давайте посмотрим, как мы разрешаем социальные профили для peerAddress.

В этой функции resolveSocial мы делаем запрос к API Airstack GraphQL с помощью диалога peerAddress. Если есть социальные профили, мы возвращаем первый, в противном случае мы возвращаем строку о том, что социальных профилей нет. Следует отметить, что мы возвращаем первый профиль в социальной сети из соображений простоты. Вы можете вернуть все профили в социальных сетях и создать интерфейс, позволяющий отображать их любым удобным для вас способом.

Кстати говоря, давайте обновим интерфейс нашего списка контактов. Для этого у нас уже был код-заполнитель. Под нашим компонентом SearchResults в теле основной функции у нас было отображение нашей переменной contacts. Давайте обновим, чтобы оно выглядело так:
Код:
<div>
  {
    contacts?.map((c) => {
      return (
        <div onClick={() => selectExistingContact(c)} key={c.address}>
          <h3>{c.profileName}</h3>
          <p>{c.address}</p>
        </div>
      )
    })
  }
</div>
Мы отображаем имя профиля в социальной сети (если оно есть) и расширение peerAddress. Когда мы нажимаем на контакт, мы вызываем функцию selectExistingContact. Но мы еще не написали эту функцию, поэтому давайте сделаем это сейчас. Добавьте следующую функцию ниже существующей функции setContactDetails:
Код:
const selectExistingContact = (contact) => {
  props.setSelectedContact(contact);
  props.setShowContactList(false);
}
Это установит наш выбранный контакт, как и в случае с результатами поиска. Он также закроет окно списка контактов и покажет нам чат.

Вызовы API Airstack в этом компоненте не очень эффективны. Вполне вероятно, что вам придется запрашивать одну и ту же информацию несколько раз. В качестве задачи, выходящей за рамки этого мануала, вы можете подумать о способах кэширования результатов и отправлять запросы только на новые разговоры, о которых приложение еще не знало.

Итак, мы уже знаем, что передали функцию, вызванную loadConversations из родительского компонента, в Contacts. Давайте создадим эту функцию сейчас. Откройте файл Home.js и добавьте следующую функцию:
Код:
const loadConversations = async () => {
  const conversations = await clientRef.current.conversations.list()
  return conversations
}
Нам также необходимо убедиться, что в этом файле установлена наша переменная состояния selectedContact. Вверху, рядом с другими useState хуками, добавьте:
Код:
const [selectedContact, setSelectedContact] = useState({
   profileName: "Contact Bot", address: BOT_ADDRESS
})
Нам нужно внести еще два изменения в этот файл, прежде чем мы сможем увидеть результаты нашей работы. Во-первых, давайте воспользуемся перехватчиком useEffect и послушаем изменения переменной состояния selectedContact. Мы хотим убедиться, что мы инициализируем наш диалог XMTP с этим контактом.
Код:
useEffect(() => {
  const startConvo = async() => {
    const xmtp = await Client.create(signer, { env: "production" });
    //Create or load conversation with Gm bot
    newConversation(xmtp, selectedContact.address);
    // Set the XMTP client in state for later use
    setIsOnNetwork(!!xmtp.address);
    //Set the client in the ref
    clientRef.current = xmtp;
  }


  if(selectedContact) {
    startConvo();
  }
}, [selectedContact]);
Здесь мы поместили часть того же кода, который используем в функции initXmtp, в функцию startConvo внутри хука useEffect. Мы проверяем изменения впеременной selectedContact и начинаем разговор с этим контактом, если они обнаружены.

Внутри функции initXmtp мы собираемся внести аналогичные изменения.
Код:
const initXmtp = async function () {
  const startConvo = async(contactToInit) => {
    const xmtp = await Client.create(signer, { env: "production" });
    //Create or load conversation with Gm bot
    newConversation(xmtp, contactToInit.address);
    // Set the XMTP client in state for later use
    setIsOnNetwork(!!xmtp.address);
    //Set the client in the ref
    clientRef.current = xmtp;
  }


  if(selectedContact) {
    startConvo(selectedContact);
  }  else {
    startConvo({address: BOT_ADDRESS})
  }
};
Поскольку наш новый формат для selectedContact принимает объект с именем профиля и адресом, нам необходимо внести изменения в selectedContact, которые проверяют или передают исходный адрес бота для инициализации разговора.

И, наконец, давайте удостоверимся, что мы передаем все правильные реквизиты нашему компоненту Contacts и собираемся передать компоненту новый компонент Chat(скоро вы поймете, почему). Вот обновленный компонент Contacts:
Код:
<Contacts loadConversations={loadConversations} setSelectedContact={setSelectedContact} setShowContactList={setShowContactList} />
А вот обновленный компонент Chat:
Код:
<Chat
  client={clientRef.current}
  conversation={convRef.current}
  messageHistory={messages}
  selectedContact={selectedContact}
  setShowContactList={setShowContactList}
/>
Прямо сейчас мы можем начать использовать приложение для общения, преобразуя социальные профили Web3 в одноранговые адреса в интерфейсе чата с поддержкой XMTP, но давайте внесем еще одну правку. Интерфейс чата был разработан так, чтобы всегда общаться с ботом. Давайте отобразим либо имя профиля (если применимо, либо адрес 0x). Откройте Chat.js и в верхней части основной функции обязательно передайте selectedContact следующим образом:
Код:
function Chat({ client, messageHistory, conversation, setShowContactList, selectedContact }) {
  ...
}
Теперь найдите вызванную вложенную функцию MessageList и обновите ее, чтобы она выглядела следующим образом:
Код:
const MessageList = ({ messages, selectedContact }) => {
    // Filter messages by unique id
    messages = messages.filter(
      (v, i, a) => a.findIndex((t) => t.id === v.id) === i,
    );

  const getUserName = () => {
    if(message.senderAddress === address) {
      return "You"
    } else if(selectedContact && selectedContact.profileName !== "No web3 profile") {
      return selectedContact.profileName
    } else if(selectedContact && selectedContact.address) {
      return selectedContact.address
    } else {
      return "Bot"
    }
  }

    return (
      <ul className="messageList">
        {messages.map((message, index) => (
          <li
            key={message.id}
            className="messageItem"
            title="Click to log this message to the console">
            <strong>
              {getUserName()}:
            </strong>
            <span>{message.content}</span>
            <span className="date"> ({message.sent.toLocaleTimeString()})</span>
            <span className="eyes" onClick={() => console.log(message)}>
              👀
            </span>
          </li>
        ))}
      </ul>
    );
  };
Ваше приложение должно выглядеть следующим образом: в интерфейсе чата отображается имя профиля или адрес кошелька.


1*kXXv7ty-903otHyAR957OA@2x.png


Мы можем многое улучшить, и прежде чем завершить статью, я поделюсь некоторыми идеями, чтобы вы действительно могли сделать это самостоятельно и получить невероятные впечатления. Но давайте посмотрим приложение в действии прямо сейчас!

ТЫЦ - https://miro.medium.com/v2/1*s1jzc9t8Z_bEB2CThqz2Eg.gif

Использование Airstack для разрешения адреса 0x из социального профиля web3 для использования в диалоге XMTP является мощным инструментом. Фундамент некоторых невероятных приложений можно заложить, используя только код, который мы написали до сих пор.

Давайте подведем итоги, а затем посмотрим вперед.

Следующие шаги​

В этом руководстве вы узнали, как подключить Airstack и XMTP для создания приложения чата Web3. Подача сообщений в реальном времени генерируется путем прослушивания разговоров между «пирами». Пиры определяются по адресам их кошельков.

Это не очень удобно для пользователя, поэтому мы использовали универсальный API GraphQL от Airstack для создания преобразователя социальных профилей Web3. Это позволило нам найти имена пользователей Web3, например, polluterofminds или polluterofminds.lens, чтобы начать чат на основе XMTP.

Как бы мощно это не выглядело, это только начало. Отсюда у вас есть возможность добавлять изображения профиля, сохранять списки контактов более постоянным и удобным для пользователя способом, оптимизировать инициализацию XMTP (подсказка: не следует заставлять людей подключаться несколько раз в одной сессии, как мы сделали и нашем demo) и многое другое.

Мы начали с простого примера приложения из документации XMTP и создали полнофункциональное приложение для поиска и чата с помощью Airstack.

Исходники приложения, созданного в этом мануале:

Полезные ссылки:
 


Напишите ответ...
  • Вставить:
Прикрепить файлы
Верх