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

Часть 2: Списки команд

вступление

Добро пожаловать во вторую часть нашего руководства по использованию приложения todo, созданного с использованием конфиденциальных тем 3Box. Если вы не посещали первую часть, сначала проверьте это. Здесь мы расширяем объем нашего приложения для выполнения задач команды. Вот где действительно проявляется сила конфиденциальных потоков. Помимо списков задач, существует множество приложений для конфиденциальных обсуждений участников; личные сообщения, устойчивые к цензуре социальные сети, командный чат и многое другое. Давайте начнем с создания нашего совместного конфиденциального списка задач.

👉 Посмотрите демо здесь

🛠 Завершенная кодовая база учебника

Что вы узнаете из этого урока

Конфиденциальные обсуждения участников

  1. Пользовательский поток для присоединения к потокам конфиденциальных участников
  2. Создание ветки конфиденциальных участников
  3. Запись членов команды, открывающих пространство приложений
  4. Добавление членов команды в ветку
  5. Получение командных сообщений
  6. Добавление постов
  7. Обновление пользовательского интерфейса
  8. Тестирование пользовательского интерфейса
  9. Добавление компонента зависания профиля

Настройка и технический стек

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

  • React - интерфейсный фреймворк
  • IPFS + OrbitDB - где хранятся данные (предоставляется 3Box, поэтому нам не нужно трогать это напрямую, не нужно устанавливать отдельно)
  • MetaMask - интеграция с кошельком Web3 (требуется для облегчения подписи и шифрования данных)
  • 3Box.js - 3Box SDK, который подключает кошельки к хранилищу базы данных IPFS через 3ID.
  • Наведите курсор на профиль и Плагины редактирования профиля - компоненты React, которые мы будем использовать для ускорения разработки пользовательского интерфейса.
  • React Bootstrap - библиотека пользовательского интерфейса

1. Поток пользователей для присоединения к обсуждениям конфиденциальных участников.

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

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

Начнем с добавления жестко запрограммированного списка членов команды и модератора в состояние реакции компонента (см. Пример):

state = {
    posts: [],
    newTodo: "",
    moderatorsAddress: '0x2f4cE4f714C68A3fC871d1f543FFC24b9b3c2386',
    addresses :[
      '0xab74207ee35fBe1Fb949bdcf676899e9e72Ec530', 
      '0xFF326878D13b33591D286372E67B4AF05cD100bd', 
      '0xbaeBB7d18f8b16B0A970FDa91f1EfA626D67423E', 
      '0x5c44E8982fa3C3239C6E3C5be2cc6663c7C9387E', 
      '0xa8eE0BABE72cD9A80Ae45dD74Cd3eaE7a82fd5d1', 
      '0x5a7246af4fefe777e32399310b50bb7fe2d04f8a', 
      '0x18B14A7d061B504C75C7027738d16dcED739b2E9', 
      '0x5031f308AD02Ed86F44c586aD2B01ae55D034C7a',
      '0xB3B30f49384093eE32d26C2C8E38e6566482C6a8'
    ] // replace these with ethereum addresses you control
  }

🚨 Обратите внимание, это адреса для команды 3Box, обновите модератора, чтобы использовать ваш собственный адрес. Добавьте другой адрес, которым вы владеете, в массив членов команды, чтобы вы могли протестировать. Вы можете создать несколько учетных записей в разделе Мои учетные записи MetaMask.

Чтобы обработать логику, подробно описанную в маркированном списке, мы собираемся добавить разумный объем кода в метод componentDidMount. Начнем с добавления необходимых констант:

async componentDidMount() {
    const teamMembers = this.state.addresses.map(member => member.toLowerCase()) //prevents capitalisation errors
    const spaceName = "todo-space"
    const confidentialThreadName = "confidential-todos"
    const waitingRoomName = "waitingroom"
    let teamThread // we will set this later
    const isModerator = this.state.moderatorsAddress.toLowerCase() === this.props.accounts[0].toLowerCase()
    // Access the moderators public space
    const moderatorsSpace = await   Box.getSpace(this.state.moderatorsAddress.toUpperCase(), spaceName)
    const isTeamMember =  teamMembers.includes(this.props.accounts[0]) 
    if(!isModerator && !isTeamMember){
      this.setState({notAModOrTeam: true})
      } 
  }

Помимо установки имен пространства и двух потоков, которые мы будем использовать, нам нужно знать, является ли текущий this.props.accounts[0] модератором или членом команды.

Нам также необходимо получить доступ к общедоступному пространству модераторов, так как именно здесь мы сохраняем адреса потоков. Поскольку для доступа к нему мы используем 3Box.js, нам нужно импортировать библиотеку в верхней части файла:

import Box from '3box'

2. Создание ветки конфиденциальных участников.

Внутри componentDidMount мы можем добавить следующий блок кода (см. Пример):

if (!moderatorsSpace[confidentialThreadName] && isModerator) {
      // if theconfidentialThreadname is not saved in the mods space
      // it means it theconfidentialThreadhas has not been created. 
      // create the confidentialconfidentialThreadand save the  
      // address in the moderators public space
      const confidentialThread = await this.props.space.createConfidentialThread(confidentialThreadName)
      await this.props.space.public.set(confidentialThreadName, confidentialThread.address)
      const waitingRoom = await this.props.space.joinThread(waitingRoomName)
      await this.props.space.public.set(waitingRoomName, waitingRoom.address)
      this.setState({teamThread : confidentialThread})
      console.log("confidential thread & waiting room thread made")
    }

Оператор if означает, что к нему можно будет получить доступ только в том случае, если конфиденциальный поток не был создан и сохранен в общедоступном пространстве модераторов. Мы также используем space.joinThread для создания общедоступного потока и присоединения к нему, в котором хранится информация о ходе работы членов команды при открытии пространства (необходимо добавить в поток). Адрес этой общедоступной цепочки, waitRoom, сохраняется в общедоступном пространстве модератора, так что члены команды могут получить к нему доступ. Мы сохраняем конфиденциальный поток для состояния реакции, поэтому модератор может сразу добавить задачу.

Чтобы это работало, нам нужно передать пространство в качестве свойства компоненту Команда в App.js:

<Route path="/team">
    <Team 
      accounts={this.state.accounts}
      space={this.state.space} />
  </Route>

Зайдя в браузер, вы должны увидеть журнал консоли "confidential thread and waiting room thread made".

Мы также можем добавить некоторую логику для случая, когда член команды входит в систему раньше модератора (см. Пример):

if(!moderatorsSpace[confidentialThreadName] && !isModerator && isTeamMember){
      this.setState({moderatorLoginToCreate: true})
  }

3. Запись членов команды, открывающих пространство приложений.

Теперь давайте обработаем поток для пользователей, когда конфиденциальный поток был создан. Начиная с членов команды при первом посещении. Здесь первое, что им нужно сделать, это открыть то же пространство, в котором находится конфиденциальный поток, и сохранить это обновление в общедоступном потоке ожидания (см. Пример).

// when the confidential thread has been created
if (moderatorsSpace[confidentialThreadName]) {
	
	if(isTeamMember && !isModerator){
	// check for the space opening status of the team member in 
        // the waitingRoom public thread    
	const waitingRoom = await this.props.space.joinThreadByAddress(moderatorsSpace[waitingRoomName])
	 const rawPosts = await waitingRoom.getPosts()
	
	  // where we handle the logic of adding team members to the 
	  // waiting room and confidential thread
			
    }
	// Add the moderator login here 
 }

Поскольку нам нужно хранить данные в потоке waitRoom в строковой форме, мы будем создавать объект и JSON преобразовывать его в строку для хранения в потоке. Этот объект будет записывать адреса членов команды, которые находятся в комнате ожидания (открытое пространство, но не добавлены в поток) и добавлены (после того, как они были добавлены модератором). Например:

{
	waitingRoom: [
             '0xa8eE0BABE72cD9A80Ae45dD74Cd3eaE7a82fd5d1',
             '0x5031f308AD02Ed86F44c586aD2B01ae55D034C7a'],
	added: ['0xB3B30f49384093eE32d26C2C8E38e6566482C6a8']
}

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

Теперь после rawPosts добавим следующий код для случая, когда в ветке комнаты ожидания нет сообщений (см. Пример):

if(rawPosts.length < 1){
  await waitingRoom.post(JSON.stringify({
        waitingRoom: [this.props.accounts[0]], 
        added: []}))
  this.setState({moderatorWillAdd: true})
	console.log("first team member added")
}

Когда пользователь открыл пространство, мы можем добавить их в массив waitRoom. Мы также обновляем состояние реакции, чтобы мы могли указать пользователю, что он должен вернуться после того, как его модератор вошел в систему.

Вен добавит следующий оператор if для обработки потока waitRoom, когда он содержит сообщения (см. Пример).

if(rawPosts.length > 0) {
      const mostRecent = JSON.parse(rawPosts[rawPosts.length -1].message)
      // check user has been added to the confidential thread
      if(mostRecent.added.includes(this.props.accounts[0])){
        const confidentialThread = await this.props.space.joinThreadByAddress(moderatorsSpace[confidentialThreadName])
        console.log("joinedconfidentialThreadas team")
        this.setState({
             teamThread: confidentialThread, 
             teamMembersAdded: mostRecent.added
             })
             // TODO get posts
	}
		    if(mostRecent.waitingRoom.includes(this.props.accounts[0])){
          this.setState({moderatorWillAdd: true})
	 return
      } else {
        // add user to waiting room in prep for adding to thread
        console.log("adding user to waiting room")
        mostRecent.waitingRoom.push(this.props.accounts[0])
        await waitingRoom.post(JSON.stringify(mostRecent))
        this.setState({
              moderatorWillAdd: true})
	return 
      }
    }

Здесь мы анализируем самый последний пост, чтобы прочитать его. Затем, проверяя, присутствует ли текущий пользователь в добавленном массиве (то есть он был добавлен модератором), мы присоединяемся к конфиденциальному потоку с помощью joinThreadByAddress.

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

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

4. Добавление участников команды в цепочку.

Последняя часть, которую нам нужно добавить в оператор moderatorsSpace[confidentialThreadName] if, - это поток, в котором модератор повторно посещает приложение, чтобы добавить членов команды из списка ожидания в поток задач (см. Пример).

// Add the moderator login here 
if(isModerator){
        // moderator joins the confidential thread and adds users in 
        // waiting room. Then move these users to the added array
        const confidentialThread= await this.props.space.joinThreadByAddress(moderatorsSpace[confidentialThreadName])
        console.log("moderator joinedconfidentialThreads")
        const waitingRoom = await this.props.space.joinThreadByAddress(moderatorsSpace[waitingRoomName])
        const rawPosts = await waitingRoom.getPosts()
       
        if(rawPosts.length > 0) {
        const mostRecent = JSON.parse(rawPosts[rawPosts.length -1 ].message)
        mostRecent.waitingRoom.map(async address => {
            await confidentialThread.addMember(address)
        });
	// Once team members have been added to the waiting room,
	// move them to the added array
        const newWaitingRoom = {
          waitingRoom: [], 
          added: mostRecent.added.concat(mostRecent.waitingRoom)}
          const inConfidentialThread = await confidentialThread.listMembers()
          await waitingRoom.deletePost(rawPosts[rawPosts.length -1].postId)
          await waitingRoom.post(JSON.stringify(newWaitingRoom))
	  console.log("updated waiting room arr", newWaitingRoom)
        }
      this.setState({teamThread : confidentialThread})
      // TODO get posts
  }

Теперь, когда мы завершили этот поток, мы можем проверить в консоли, что он работает правильно.

  • Когда модератор заходит в первый раз - confidential thread and waiting room thread made
  • Первый член команды входит в систему - first team member added вошел в систему и moderatorWillAdd: true в состоянии реакции
  • Модератор возвращается после того, как член команды входит в систему - moderator joinedconfidentialThreads, updated waiting room array с обновленным объектом комнаты ожидания
  • Тот же член команды возвращается - joinedconfidentialThreadas team и teamThread: confidentialThread, teamMembersAdded: mostRecent.added в состоянии реакции.

5. Получение командных постов

Теперь у нас есть логика для добавления участников в конфиденциальный поток, и следующее - добавить функциональность для получения сообщений. К счастью, мы можем повторно использовать часть логики, которую использовали в личных конфиденциальных беседах. Мы можем добавить методы getPosts и parsePosts в компонент Команда (см. Пример):

async getPosts() {
    const rawPosts = await this.state.teamThread.getPosts()
    const posts = this.parsePosts(rawPosts)
    this.setState({ posts })
  }
parsePosts = (postArr) => {
    return postArr.map((rawPost) => {
      let post = JSON.parse(rawPost.message)
      post.id = rawPost.postId
      return post
    })
  }

Мы вызываем функцию getPosts сразу после того, как член команды присоединился к конфиденциальной беседе, мы также добавим сюда оператор return. (См. Пример):

console.log("joinedconfidentialThreadas team")
this.setState({ teamThread: confidentialThread, teamMembersAdded: mostRecent.added })
// TODO get posts
this.getPosts()
return

И после присоединения модератора добавьте строчку:

// TODO get posts
  this.getPosts()

После этого вы должны увидеть, что posts: [] сохранено в состоянии реакции для компонента группы. Мы еще не добавили ни одной публикации, так что это ожидаемо.

6. Добавление постов

Чтобы добавить сообщения, давайте добавим модальный компонент к функции рендеринга:

<ModalComponent
    buttonText={"Add a ToDo"}
    ModalHeader={"Add a Todo"}
    ModalBodyText={"One more thing on the list."}
    submitFunc={this.onSubmit} >
    <Container>
      <Form>
        <Form.Group controlId="formBasicEmail">
          <Form.Label>New Item</Form.Label>
          <Form.Control
            type="text"
            value={this.state.newTodo}
            onChange={(e) => (this.setState({ newTodo: e.target.value }))}
          />
        </Form.Group>
      </Form>
    </Container>
  </ModalComponent>

Этот компонент работает аналогично тому, который мы использовали для личных задач. Нам также нужно добавить функцию onSubmit:

onSubmit = async () => {
    if (this.state.newTodo) {
      const orderNumber = this.state.posts.length > 0 ? (this.state.posts[this.state.posts.length - 1].order + 1) : (1)
      const post = JSON.stringify({
        text: this.state.newTodo,
        completed: false,
        show: true,
        order: orderNumber,
        postedBy: this.props.accounts[0]
      })
      await this.state.teamThread.post(post)
      this.setState({ newTodo: null })
      this.getPosts()
    }
  }

Давайте также добавим компонент пользовательского интерфейса для рендеринга задач.

{this.state.posts &&
    <TODO
      posts={this.state.posts}
      deletePost={this.deletePost}
      toggleDone={this.toggleDone}
      accounts={this.props.accounts} />
  }

Наконец, давайте добавим методы deletePost и toggleDone:

toggleDone = async (todo) => {
    const post = JSON.stringify({
      text: todo.text,
      completed: !todo.completed,
      show: true,
      order: todo.order,
      postedBy: todo.postedBy
    });
    await this.state.teamThread.post(post);
    await this.state.teamThread.deletePost(todo.id)
    this.getPosts()
  }
  
  deletePost = async (postId) => {
    await this.state.teamThread.deletePost(postId)
    await this.getPosts()
  }

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

7. Обновление пользовательского интерфейса.

Теперь, когда основные функции работают, мы должны обновить пользовательский интерфейс, чтобы передать поток пользователю. Используя состояние реакции, которое мы создали ранее в componentDidMount, мы можем создать константу isActiveMember, чтобы указать, когда пользователь вошел в систему и активен. Мы также добавим краткие сообщения, чтобы указать пользователю его статус, когда он не является активным участником (см. Пример).

render() {
   const isActiveMember =  !this.state.notAModOrTeam && !this.state.moderatorWillAdd && !this.state.moderatorLoginToCreate && this.state.teamThread
    return (
      <div>
        <h2>Team TODOs</h2>
        {/* {TODO Section} */}
        {this.state.notAModOrTeam && (
              <h1>You are not part of this team</h1>)}
        {this.state.moderatorLoginToCreate && (
              <h1>Ask your moderator to login to start</h1>)}
        {this.state.moderatorWillAdd && (
               <h1>Wait for your moderator to log in and add you</h1>)}
        {this.state.posts && isActiveMember && (
          <TODO
            posts={this.state.posts}
            deletePost={this.deletePost}
            toggleDone={this.toggleDone}
            accounts={this.props.accounts}
          />
        )}
        {isActiveMember && (
          <>
            <ModalComponent
              buttonText={"Add a ToDo"}
              ModalHeader={"Add a Todo"}
              ModalBodyText={"One more thing on the list."}
              submitFunc={this.onSubmit}
            >
              <Container>
                <Form>
                  <Form.Group controlId="formBasicEmail">
                    <Form.Label>New Item</Form.Label>
                    <Form.Control
                      type="text"
                      value={this.state.newTodo}
                      onChange={(e) =>
                        this.setState({ newTodo: e.target.value })
                      }
                    />
                  </Form.Group>
                </Form>
              </Container>
            </ModalComponent>
          </>
        )}
      </div>
    )
  }

8. Тестирование пользовательского интерфейса.

Наш интерфейс почти готов, теперь мы можем проверить, работает ли он должным образом. Попробуйте следующее:

  • Пользователи, которые не являются ни модераторами, ни членами команды - пользовательский интерфейс отобразит «Вы не являетесь частью этой команды».
  • Член команды входит в систему раньше модератора - пользовательский интерфейс отобразит «Попросите вашего модератора войти в систему, чтобы начать».
  • Модератор, входящий в систему впервые - «конфиденциальный поток и поток из комнаты ожидания созданы» будет зарегистрирован в консоли. Должна быть возможность публиковать, проверять выполненные и удалять задачи.
  • Член команды после того, как модератор создал ветку:
  • Первый вход - «Подождите, пока ваш модератор войдет в систему и добавит вас» будет отображаться в пользовательском интерфейсе.
  • Возвращаясь к входу в систему после того, как модератор снова вошел в систему - вы должны иметь возможность публиковать, удалять и проверять выполненные задачи (которые они создали сами).

Если ваше приложение работает, как указано выше, поздравляю, вы выполнили основные функции этого руководства.

9. Добавление компонента зависания профиля.

И последнее, что мы собираемся сделать в рамках этого руководства, - это использовать компонент наведения курсора на профиль, чтобы показать изображение профиля пользователя и имя 3Box. Если вы еще не установили изображение профиля и имя, вы можете сделать это в 3Box Hub (редактирование профилей 3Box также может быть закодировано для работы в любом приложении).

Сначала используйте npm для установки компонента:

npm i profile-hover

Мы импортируем его в начало нашего компонента TODO.js:

import ProfileHover from 'profile-hover'

Наведение профиля - это самый минимальный вариант добавления плагина 3Box, поэтому все, что нам нужно сделать, это добавить следующее внутри функции рендеринга в строке 36:

{item.postedBy && <ProfileHover address={item.postedBy} showName={true} />}

Теперь вы увидите рендеринг профиля 3Box каждого пользователя в пользовательском интерфейсе.

На этом мы завершаем эту серию обучающих программ, состоящую из двух частей.

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