Blockstack.js - Многопользовательское наблюдение за статусом, часть I

Некоторое время я слежу за экосистемой Blockstack и даже стал участником github. Недавно команда запустила сайт для участия, который награждает людей, которые могут внести значимый вклад в команду посредством сообщений в блогах, запросов на вытягивание и т. Д. (Https://contribute.blockstack.org/)

Если вы не знаете, что такое Blockstack и чем они занимаются, я рекомендую вам заглянуть на их основной сайт https://blockstack.org/ и прочитать информационный документ. Этот пост не будет углубляться в цели или видение команды, а скорее будет посвящен техническому подходу к использованию библиотеки blockstack.js.

Программа пожертвований дала мне дополнительную мотивацию для действительно глубокого погружения в учебные пособия по blockstack.js. Особенно выделяется многослойная стойка для хранения от Кена Ляо. Я закончил обучение, но чувствовал, что он неполный и мне нужно немного больше, чтобы его можно было использовать в реальном мире.

Прежде чем вы продолжите, я настоятельно рекомендую (или требовать), чтобы вы прочитали руководство здесь https://blockstack.org/tutorials/multi-player-storage…. Иначе в этом не будет никакого смысла!

Итак, вы закончили учебник. Теперь вы можете видеть статус своего профиля и других. Если вы хотите создать новый статус, это можно сделать с помощью входных параметров. Если вы перейдете на страницу другого человека, вы потеряете права на создание. А как насчет удаления статуса?

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

Критерий №1: вы должны быть действующим пользователем, чтобы удалить статус.

Критерий №2: вам нужно отобразить кнопку удаления

Критерий № 3: отображать пользователю текст подтверждения, если удаление было намеренным.

Я решил создать компонент статуса, как показано ниже в файлеProfile.jsx

// Old
{this.state.statuses.map((status) => (
    <div className="status" key={status.id}>
      {status.text}
    </div>
    )
)}
// New
{
  this.state.statuses.map((status) => (
    <Status
      key={status.id}
      status={status}
      handleDelete={this.handleDelete}
      isLocal={this.isLocal}
    />
  ))
}

Я создал новую функцию под названием handleDelete. Подобно saveNewStatus, мне нужна была функция, которая могла бы подключаться к библиотеке blockstack.js, чтобы фильтровать и удалять предполагаемый удаленный статус и использовать функцию putFile для создания нового status.json.

handleDelete(id) {
    const statuses = this.state.statuses.filter((status) => status.id !== id)
    const options = { encrypt: false }
    putFile(statusFileName, JSON.stringify(statuses), options)
      .then(() => {
        this.setState({
          statuses
        })
      })
  }
}

Как видите, функция handleDelete относительно похожа на saveNewStatus. Функция ожидает аргумент id. С помощью этого аргумента мы можем сравнить и отфильтровать идентификатор из this.state.statuses. Если вам интересно, почему я применил этот подход в отличие от традиционного метода API DELETE, я рекомендую прочитать эту статью https://blockstack.org/tutorials/managing-data-with-gaia

Для справки - полный Profile.jsx файл

import React, { Component } from 'react';
import {
  isSignInPending,
  loadUserData,
  Person,
  getFile,
  putFile,
  lookupProfile
} from 'blockstack';
import Status from './Status.jsx';
const avatarFallbackImage = 'https://s3.amazonaws.com/onename/avatar-placeholder.png';
const statusFileName = 'statuses.json'
export default class Profile extends Component {
  constructor(props) {
   super(props);
this.state = {
     person: {
      name() {
          return 'Anonymous';
        },
      avatarUrl() {
        return avatarFallbackImage;
      },
     },
      username: "",
      newStatus: "",
      statuses: [],
      statusIndex: 0,
      isLoading: false
   };
this.handleDelete = this.handleDelete.bind(this);
    this.isLocal = this.isLocal.bind(this);
  }
componentDidMount() {
    this.fetchData()
  }
handleNewStatusChange(event) {
    this.setState({
      newStatus: event.target.value
    })
  }
handleNewStatusSubmit(event) {
    this.saveNewStatus(this.state.newStatus)
    this.setState({
      newStatus: ""
    })
  }
handleDelete(id) {
    const statuses = this.state.statuses.filter((status) => status.id !== id)
    const options = { encrypt: false }
putFile(statusFileName, JSON.stringify(statuses), options)
      .then(() => {
        this.setState({
          statuses
        })
      })
  }
saveNewStatus(statusText) {
    let statuses = this.state.statuses
let status = {
      id: this.state.statusIndex++,
      text: statusText.trim(),
      created_at: Date.now()
    }
statuses.unshift(status)
    const options = { encrypt: false }
    putFile(statusFileName, JSON.stringify(statuses), options)
      .then(() => {
        this.setState({
          statuses: statuses
        })
      })
  }
fetchData() {
    if (this.isLocal()) {
      this.setState({ isLoading: true })
      const options = { decrypt: false, zoneFileLookupURL: 'https://core.blockstack.org/v1/names/' }
      getFile(statusFileName, options)
        .then((file) => {
          var statuses = JSON.parse(file || '[]')
          this.setState({
            person: new Person(loadUserData().profile),
            username: loadUserData().username,
            statusIndex: statuses.length,
            statuses: statuses,
          })
        })
        .finally(() => {
          this.setState({ isLoading: false })
        })
    } else {
      const username = this.props.match.params.username
      this.setState({ isLoading: true })
lookupProfile(username)
        .then((profile) => {
          this.setState({
            person: new Person(profile),
            username: username
          })
        })
        .catch((error) => {
          console.log('could not resolve profile')
        })
const options = { username: username, decrypt: false, zoneFileLookupURL: 'https://core.blockstack.org/v1/names/'}
getFile(statusFileName, options)
        .then((file) => {
          var statuses = JSON.parse(file || '[]')
          this.setState({
            statusIndex: statuses.length,
            statuses: statuses
          })
        })
        .catch((error) => {
          console.log('could not fetch statuses')
        })
        .finally(() => {
          this.setState({ isLoading: false })
        })
    }
  }
isLocal() {
    return this.props.match.params.username ? false : true
  }
render() {
    const { handleSignOut } = this.props;
    const { person } = this.state;
    const { username } = this.state;
return (
      !isSignInPending() && person ?
      <div className="container">
        <div className="row">
          <div className="col-md-offset-3 col-md-6">
            <div className="col-md-12">
              <div className="avatar-section">
                <img
                  src={ person.avatarUrl() ? person.avatarUrl() : avatarFallbackImage }
                  className="img-rounded avatar"
                  id="avatar-image"
                />
                <div className="username">
                  <h1>
                    <span id="heading-name">{ person.name() ? person.name()
                      : 'Nameless Person' }</span>
                  </h1>
                  <span>{username}</span>
                  {this.isLocal() &&
                    <span>
                      &nbsp;|&nbsp;
                      <a onClick={ handleSignOut.bind(this) }>(Logout)</a>
                    </span>
                  }
                </div>
              </div>
            </div>
            {this.isLocal() &&
              <div className="new-status">
                <div className="col-md-12">
                  <textarea className="input-status"
                    value={this.state.newStatus}
                    onChange={e => this.handleNewStatusChange(e)}
                    placeholder="What's on your mind?"
                  />
                </div>
                <div className="col-md-12 text-right">
                  <button
                    className="btn btn-primary btn-lg"
                    onClick={e => this.handleNewStatusSubmit(e)}
                  >
                    Submit
                  </button>
                </div>
              </div>
            }
            <div className="col-md-12 statuses">
            {this.state.isLoading && <span>Loading...</span>}
            {
              this.state.statuses.map((status) => (
                <Status
                  key={status.id}
                  status={status}
                  handleDelete={this.handleDelete}
                  isLocal={this.isLocal}
                />
              ))
            }
            </div>
          </div>
        </div>
      </div> : null
    );
  }
}

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

Для простоты я решил показать исходный код ниже с некоторыми пунктами.

import React, { Component } from 'react';
class Status extends Component {
  constructor(props) {
    super(props);
this.state = {
      showDeleteConfirmation: false
    }
this.onDeleteClick = this.onDeleteClick.bind(this);
    this.toggleDeleteConfirmation = this.toggleDeleteConfirmation.bind(this);
  }
onDeleteClick() {
    const { status } = this.props;
    this.props.handleDelete(status.id);
  }
toggleDeleteConfirmation() {
    this.setState({ showDeleteConfirmation: !this.state.showDeleteConfirmation })
  }
render() {
    const { status, isLocal } = this.props;
if(!isLocal()) {
      return (
        <div className="status">
          <div className="status-text">
            {status.text}
          </div>
        </div>
      )
    }
return (
      <div className="status">
        <div className="status-text">
          {status.text}
        </div>
<div className="status-button-options">
          <button
            className="btn btn-danger status-delete"
            onClick={this.toggleDeleteConfirmation}
            disabled={this.state.showDeleteConfirmation}
          >
              Delete
          </button>
        </div>
        {
          this.state.showDeleteConfirmation &&
          <div className="status-delete-confirmation">
            <h4>ARE YOU SURE?</h4>
            <button className="btn btn-danger status-delete-yes" onClick={this.onDeleteClick}>
              YES
            </button>
            <button className="btn btn-info status-delete-no" onClick={this.toggleDeleteConfirmation}>
              NO
            </button>
          </div>
        }
      </div>
    )
  }
}
export default Status;
  1. свойства статуса передаются через родителя через Profile.jsx
  2. isLocal функция передается через родителя через Profile.jsx. Если isLocal() равно false, отображать текст состояния БЕЗ возможности удаления. (Это означает, что вы просматриваете чужой профиль)
  3. showDeleteConfirmation - это внутреннее состояние компонента Status, определяющее, показывать ли текст подтверждения или нет. Если это правда, показать текст подтверждения.
  4. У кнопки удаления есть onClick обработчик, который вызывает this.toggleDeleteConfirmation. Эта функция отвечает за отображение текста подтверждения при изменении состояния.
  5. Текст подтверждения будет отображаться, как показано ниже.

3а. Кнопка "Удалить" будет отключена через this.state.showDeleteConfirmation

3b. Щелчок NO вызовет this.toggleDeleteConfirmation и сбросит состояние. Таким образом, скрывая кнопки подтверждения.

3b. При нажатии ДА вызовется this.onDeleteClick. Внутри этой функции он вызывает родительскую функцию профиля handleDelete через props this.props.handleDelete(status.id), который также включает идентификатор статуса. Таким образом, статус будет удален, и statuses.json отобразит новые результаты.

4. При просмотре пользователя вам не будет показана опция «Удалить».

Пример: http: // localhost: 8080 / kkomaz.id

Незначительные надстройки CSS

Я добавил небольшое поле между кнопками, чтобы сделать интерфейс более чистым, который вы можете скопировать и вставить в конец файла style.css.

.status-delete{margin-top: 10px}
.status-delete-confirmation {margin-top: 10px}
.status-delete-yes{margin-right: 5px}

Заключение

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

До внесения изменений у нас была возможность создавать статус и просматривать статусы других профилей. Теперь у нас есть возможность УДАЛИТЬ. Дальше должна быть возможность ИЗМЕНИТЬ статус. Будьте на связи!

Ссылка на Github:

Https://github.com/kkomaz/publik