Hacking Graphite is a series of tutorials that harnesses the power of decentralization, open source code, and creativity. Come hack with Graphite and learn JavaScript, Blockstack, and decentralization in the process.

In the first tutorial in the series, we forked the Graphite repository, updated Graphite Sheets, and created our own personal API that could be controlled by simply updating cells in a spreadsheet. Check it out here.

For this tutorial, we’re going to explore IPFS. IPFS is a p2p storage solution that allows permanent storage of files. If you’re familiar with Git version control, IPFS employs a similar concept—if you change a file, the file is not changed but a new one is created. This is great if you want to keep a permanent history of changes. For more information on IPFS, you can read this introduction by ConsenSys, or you can get your info straight from the source.

Graphite uses Blockstack’s SDKs and APIs to provide users storage in either a free, default storage bucket or the storage hub of their own choosing. There is no p2p storage involved and no current integration with IPFS. Let’s change that!

Before we begin, sign up for a Graphite account or log in here:

Graphite
Graphite allows you to create, communicate, and share without giving up your privacy.

Once you’ve logged in, to to Contact, add a new Contact, search for graphite.id and add that user as a contact. Once you’ve done so, you can create a Graphite Document and share it with graphite.id. It’d be awesome to get questions this way and to have you all share the types of hacks you’re interested in going forward.

Anyway, let’s get started!

We’re going to skip the setup portion of the tutorial because it will be the same for each lesson. You can refer to the last tutorial for instructions on cloning the Graphite repository, installing dependencies, and getting started. Once you’re ready to go, open the graphite folder in the text editor of your choice and get going.

Note: If you are using the same code from the previous tutorial, you’ll want to start fresh with a new clone of the Graphite repository. A lot has changed in just a couple weeks!

How and When

There are a lot of options for triggering an event that saves a Graphite document to IPFS, but we’re going to go with the simplest option for this tutorial. But first, let’s walk through what happens when you create and edit a Graphite Document.

The creation of a document updates an index file that stores just enough basic information about the document to refer to it later. To see this, open src -> components -> helpers. Find the documents.js file and take a look at lines 50–71. That function is what is called when you click the plus button to create a new document. It’s really creating/updating two files.

First, it creates an object that will be pushed into the overall documents index array. You can see that object being pushed into the value state using the JavaScript spread operator on line 66. Second, another object is created to actually create the document we are asking the system to create. You’ll notice this object has a few more properties than the one used for the index. That’s the object that will be used when we save the new single document, and that single document is what is referred, by its id, in the document index.

All of this object creation and saving is utilizing the user’s selected storage location or the default storage option available when a Graphite user first creates their account. It’s most definitely not using IPFS, but it is important to understand how a document is created, because next we need to understand how a document is updated.

Take a look in that same helpers folder at the singleDoc.js file. This is the file that includes the functions to fetch the document when you click on its link from the main documents collection page. It’s also the file that’s used to save updates to the document and the documents collection index file. If you look at lines 421–466, you’ll see the handleAutoAdd function. This function is triggered every three seconds after a change is made to the document. It is cancelled if another change is made within that three-second window (we don’t want to be trying to save as a user is typing away).

We could use this function to trigger an auto-save to IPFS. However, in this tutorial, we’re not going to save to IPFS after every change. We’re going to let the user click a button and push the file to IPFS when they’re ready. But this handleAutoAdd function will come into play. So, stick around.

This finally leads us to the where part of this section of the tutorial. Where are we going to place the action that will allow a user to store their Graphite document on IPFS? It seems like it makes the most sense to put it in the drop-down menu of the document header.

From your terminal, start Graphite with npm run start. And we’ll look at that document header dropdown. Once Graphite is running, open a document or create a new one. Inside the document, take a look at the header:

None of the menu items quite fit what we’re looking for. What if we create a new one that says “Export”? Let’s do that, then we can nest a Post to IPFS button in that Export dropdown.

The HTML

First, we need to create the menu item titled “Export.” In your Graphite code, find the src/components/documents/Menu.js file. You’ll see each item has a class name of “topmenu” and items below it have the “submenu” class name. We need to add a new “topmenu” item. Let’s add it right after the “Share” menu item:<li className="topmenu">
 <a>Share</a>
 <ul className="submenu">
  ...
 </ul>
</li><li className="topmenu">
 <a>Export</a>
</li>

The next step is creating the dropdown from that menu item with the Post to IPFS button. To do that, we just need to add a nest unordered list like you see with the other dropdown items. Don’t forget to give that unordered list the class name of “submenu.” The list item here is the Post to IPFS button, which in this case is an anchor element.<li className="topmenu">
 <a>Export</a>
 <ul className="submenu">
   <li><a>Post to IPFS</a></li>
 </ul>
</li>

Save that and take a look at your document now:

That button doesn’t do anything yet, but it will. Don’t you worry, it will. Let’s prepare it for the glory days by giving it an onClick that points to the eventual function we will use.<li><a onClick={this.props.postToIPFS}>Post to IPFS</a></li>

You might be asking why we are calling this.props. The reason will become evident in a minute, but the quick answer is all of the SingleDoc.js functions are passed as props from the parent element. And in this case, we are then passing the necessary props to the Menu.js component.

That’s it for the HTML for now. We will circle back and add a modal that will display the current link as well as all past links.

The JavaScript

All of Graphite’s documents components went through a significant refactor a few months ago. So, unlike the Sheets components which each house their own functions, Graphite documents components reference functions from helper files. Those functions are bound in the App.js file which acts as the parent component for all other Graphite components (except Sheets at the moment). Think of it as state management without Redux.

In the React world, there are many (MANY) ways of achieving similar goals. The way Graphite creates parent/child relationships in the components may be different than how you’d like to do it. That’s fine. It’s also outside the scope of this tutorial. So, for the sake of this lesson, just know that App.js houses all the references to functions in the helper files and it passes those functions to the appropriate children components as props.

Let’s go ahead and create the function we will use, postToIPFS, in the helpers folder and within the singleDoc.js file. You can put this function anywhere in that file, but I like to just tack them on to the end.export function postToIPFS() {
 console.log("It's working!")
}

The console.log statement will just confirm that our function is wired up properly. You’ll notice the function is specifically declared as an export function. That’s because we are exporting this helpers file to App.js and then passing the function to the appropriate component (SingleDoc.js).

With that saved, go into your src folder and into the components folder, and find the App.js file. This file is massive. Remember, in Graphite’s case, all of the functions and other components are housed within this main parent. At line 124 add a line break and add postToIPFS. It should look like this when you’re done:...
postToIPFS
} from ./helpers/singleDoc;

Now, let’s bind that function. Binding functions is only necessary if you are not using ES6. That’s outside the scope of this tutorial, so we’re going to bind!

Graphite has not yet upgraded to the newest version of React in which componentWillMount is deprecated. So, for the time being, that’s where these functions are bound. If you find lines 674–710, you’ll see all of the functions used by the SingleDoc.js component. Let’s add out new one to the bottom that list like this:this.postToIPFS = postToIPFS.bind(this);

And finally, the last part of making this function sync up with our SingleDoc.js component, we need to pass the function as a prop to that component, then we’ll pass it from that component to Menu.js. Graphite uses BrowserRouter from react-router-dom so you’ll find the SingleDoc.js component starting at line 1131 and wrapped in a <Route>element. You can see all the existing functions passed to the SingleDoc.js component:componentDidMountData={this.componentDidMountData}
handleAutoAdd={this.handleAutoAdd}
handleChange={this.handleChange}
print={this.print}
sharePublicly={this.sharePublicly}
handleTitleChange={this.handleTitleChange}
handleBack={this.handleBack}
sharedInfoSingleDocRTC={this.sharedInfoSingleDocRTC}
sharedInfoSingleDocStatic={this.sharedInfoSingleDocStatic}
handleStealthy={this.handleStealthy}
postToMedium={this.postToMedium}
shareToTeam={this.shareToTeam}
initialDocLoad={this.initialDocLoad}
getYjsConnectionStatus={this.getYjsConnectionStatus}
toggleReadOnly={this.toggleReadOnly}
stopSharing={this.stopSharing}
stealthyChat={this.stealthyChat}

All we’re going to do is add our new function beneath those in the same fashion:...
stopSharing={this.stopSharing}
stealthyChat={this.stealthyChat}
postToIPFS={this.postToIPFS}

Save it, then find the src/components/documents/SingleDoc.js file. In that file, find the Menu component. It should be at line 155. Just like we did in App.js we are going to pass the postToIPFS function through to Menu.js right at the bottom of the other props.<Menu
 downloadDoc={this.props.downloadDoc}
 handleaddItem={this.props.handleaddItem}
 formatSpacing={this.props.formatSpacing}
 print={this.props.print}
 graphitePro={graphitePro}
 teamDoc={teamDoc}
 userRole={userRole}
 mediumConnected={mediumConnected}
 postToIPFS={this.props.postToIPFS}
/>

That’s it. Now, go to your single document, open the menu in the document header, and click Post to IPFS. Open your console and if all went well, you should see It's working!.

You might not believe it, but the hard part is done! We’re going to start into the IPFS section of this tutorial next.

IPFS

IPFS has a JavaScript implementation which makes using it in a React app like Graphite super simple. But, it’s worth noting that js-ipfs is still in alpha, so it’s subject to issues. You should always use alpha software at your own risk.

Here’s the link to the IPFS JavaScript repository if you want to explore further after this tutorial. To start using it in Graphite, we need to return to the command line. In your terminal window, run the following command (kill Graphite if it’s still running):

npm install ipfs --save

Once that’s done installing, go ahead and start Graphite back up with npm run start, then open your text editor again. In the src folder, then in the components folder, find the helpers folder. And again, find the singleDoc.js helper file. This is where we are going to import the js-ipfs library we just installed. At the top of the file, below the import statements add:const IPFS = require('ipfs')
const node = new IPFS()

We are also going to add those same to lines to the top of the src/components/documents/SingleDoc.js file below the import statements. The reason we’re adding them in two places is because we’ll use one set to initiate the node and another set to post and read files.

With those two lines, we have enabled all the power of IPFS we will need.

Now, in the src/components/documents/SingleDoc.js file, right at the end of the componentDidMount function, initialize the IPFS node like this:node.on('ready', async () => {
 const version = await node.version(
 console.log('Version:', version.version)
});

Let’s talk about what this is doing. We put this callback in the componentDidMount function because we only want it to fire after the page has loaded. When it fires, it’s initializing the IPFS node and console logging the node version. That’s it. But it’s so important for the next step.

In the src/components/helpers/singleDoc.js file, you can now scroll down to the postToIPFS function you created earlier. Inside that function, let’s add a simple test:export function postToIPFS() {
 node.files.add({
   path: 'helloworld.txt',
   content: Buffer.from('Hello, World')
 }).then((res) => {
   console.log(res[0].hash)
 })
}

Let’s talk about what’s happening here. The first line, node.files.add, is telling it to add some files. That is a callback in and of itself that says when the node is ready, add a file with the title (or path) of helloworld.txt and the content of Hello, World. Then, console log the resulting hash. IPFS is a content-addressed file system, which means the hash you receive will specifically correspond to the content of the file. If you post the same thing again, you’ll get the same hash. However, if you change the content, you’ll get a different hash.

Go ahead and save that, start Graphite from the terminal again if you need to, open up your Graphite documents, open the console, click the menu button in the document header, and click Post to IPFS. If all goes well, you should see the resulting hash in the console:QmTev1ZgJkHgFYiCX7MgELEDJuMygPNGcinqBa2RmfnGFu

The hash will be exactly the same as what you see above because the content is exactly the same. In fact, hundreds of other people have seen this exact same hash. That’s the cool thing about content-addressed file storage.

Now, how can you even use this? Well, if you’re running a local IPFS node, you can use that hash to access your file. However, we’re not going to go into how to run an IPFS node. Instead, we’re going to use a public gateway to access IPFS files.

If you navigate to the following link, you’ll see my test file. You can replace my hash (the Qm string) with your own to see your file.

https://ipfs.io/ipfs/QmTev1ZgJkHgFYiCX7MgELEDJuMygPNGcinqBa2RmfnGFu

Pretty cool, right! That’s a permanent representation of the content you just created.

But wait, we just hard-coded some text into this. We want to actually store our Graphite document, right? Let’s do that!

Back in src/components/helpers/singleDoc.js you can update the postToIPFS function to dynamically create the right title/file path and post the right content. Here’s how:node.files.add({
   path: this.state.title + '.txt',
   content: Buffer.from(JSON.stringify(this.state.content))
 }).then((res) => {
   console.log(res[0].hash)
 })

It looks almost the exact same as before except we’re injecting state in place of the hard-coded sections. Instead of a path of helloworld.txt we are not using the document’s title, as represented by this.state.title. And for the content, we are still passing in a string, but instead of Hello, World, we’re stringifying the content of the Graphite document, which is stored in this.state.contentas JSON.

Go ahead and save that, then hit the Post to IPFS button again. You should get a different hash in the console, and if you pop that into the public IPFS gateway link from before, you should see the content of your document (hint: make sure you’ve typed something into your document first).

Here’s my link in all its lorem ipsum-y goodness:

https://ipfs.io/ipfs/QmTUP7DXHdjqM1f3vA2v4BH8jv5CJWyTTBbkpmKYbpgCwA

Very cool!

And to prove that the hash will actually change if the content changes, let’s go ahead and make a change to the body of your Graphite Doc. Change the text to anything you’d like, just make sure it’s different from the last time you posted to IPFS.

Once you’ve changed the content, go ahead and hit that Post to IPFS button again and grab the hash that shows up in the console. In my case, I bolded and italicized all the lorem ipsum content, and it did, in fact, give me a different hash:QmYnxhDsVH72sMAUTDxKCmmtrfPwTfWkuLGxwFow6gpwSK

You can see that my hash is clearly different than the first time I stored the file and the content, of course, is different as well. Here’s my file on the public IPFS gateway:

https://ipfs.io/ipfs/QmYnxhDsVH72sMAUTDxKCmmtrfPwTfWkuLGxwFow6gpwSK

Replace the hash with the one you got in your console, and you should see your updated content.

Conclusion

There is so much more we could do with this, but the tutorial is pretty long as is. Let’s recap and then I’ll leave you with some ideas on how to use this enhancement to Graphite.

First, we created a way to post new content from Graphite to IPFS. We then installed the JavaScript implementation of IPFS into Graphite and wired up a callback that would initialize an IPFS node that ran in your browser. Finally, we wrote a function that fired off an IPFS callback that stored the content of your Graphite Doc in the distributed web.

The potential for improvements built on top of this tutorial are almost limitless, but here are some ideas:

  • Save the hash each time the file is updated to the default Graphite Storage
  • Create a version control system that keeps all the hashes and allows the user to access past versions (this is probably coming in the production Graphite soon!)
  • Auto-save to IPFS (remember the handleAutoAdd function I mentioned way back toward the beginning of this post?
  • Encrypt the content before sending it off to IPFS
  • Load the content directly from IPFS rather than Graphite’s default storage
  • Etc
  • Etc
  • Etc

Happy Hacking!