CurateBot Devlog 7: Curating Tweets
Tags: devjournal, javascript, vue, serverless
Published: 2021-01-07 09:18:11

With tweets loaded, I now need to display them one-by-one to the user, so the user can decide what to do with each of them. The code can be seen [here](https://github.com/meseta/curatebot/tree/a737858b7db4c3170aeb4f597c1eeb47c148e117), most of the changes are in the new [`Curate.vue` file](https://github.com/meseta/curatebot/blob/a737858b7db4c3170aeb4f597c1eeb47c148e117/web/src/views/Curate.vue)

# The Curate view
The curate <template> isn't that different from the load <template> - it gains a couple of buttons, so I won't go into too much detail here ([take a look at the code](https://github.com/meseta/curatebot/blob/a737858b7db4c3170aeb4f597c1eeb47c148e117/web/src/views/Curate.vue))

# Pre-fetch tweets
The more interesting thing is how it loads the tweets. Since I expect a user to go through the tweets relatively quickly, I want to pre-load the next tweet so that the user doesn't have to wait for a load. So, as soon as the component is mounted, I have some code that runs on mount, which loads 2 tweets (`.limit(2)`):

```ts
  mounted() {
    firestore.collection('users').doc(this.uid).get()
    .then(doc => {
      this.count = doc?.get("newCount") || 0;
    })
    this.currentLoading = true;
    this.nextLoading = true;
    return firestore.collection('users').doc(this.uid).collection('tweets')
    .where('queued', '==', false).orderBy('added').limit(2).get()
    .then(query => {
      if (query.size) {
        this.currentId = query.docs[0].id;
        this.currentTweet = query.docs[0].get("tweet");
        this.currentDocRef = query.docs[0].ref;
        if (query.size > 1) {
          this.nextId = query.docs[1].id;
          this.nextTweet = query.docs[1].get("tweet");
          this.nextDocRef = query.docs[1].ref;
        }
      }
      this.currentLoading = false;
      this.nextLoading = false;
    })
    .catch(err => {
      console.error(err);
      this.showError("Something went wrong, could not load tweets");
    })
  }
```

This then sets the `currentTweet` and `nextTweet` variables accordingly. Then, when the `currentTweet` is dealt with, it's cleared, the `nextTweet` takes it's place, and then a new request issued to fetch the next tweet after that:

```ts

  advance(): any { // eslint-disable-line @typescript-eslint/no-explicit-any
    if (this.nextLoading) {
      // if we're already loading, that means the next one will appear shortly,
      // so just clear display and wait
      this.currentId = "";
      this.currentTweet = "";
      this.currentDocRef = null;
      return
    }
    
    this.currentId = this.nextId;
    this.currentTweet = this.nextTweet;
    this.currentDocRef = this.nextDocRef;
    this.nextId = ""
    this.nextTweet = ""
    this.nextDocRef = null
    if (this.currentDocRef) {
      this.nextLoading = true;
      return firestore.collection('users').doc(this.uid).collection('tweets')
      .where('queued', '==', false).orderBy('added').limit(2).get()
      .then(query => {
        this.nextLoading = false;
        if (query.size > 1) {
          this.nextId = query.docs[1].id;
          this.nextTweet = query.docs[1].get("tweet");
          this.nextDocRef = query.docs[1].ref;
          if(!this.currentDocRef) { // in case current tweet was already dealt with while a document was being fetched
            return this.advance()
          }
        }
      })
      .catch(err => {
        console.error(err)
        this.showError("Something went wrong, could not load next tweet");
      })
    }
  }
```

I actually improve this mechanism by using `.beginAfter()` method rather than `.limit(2)`, but this appears in a later commit.

# Editing
Allowing editing the tweet is relatively straightfoward - I already opted to display the tweet using a textarea, so it was a case of enabling the textarea, and then providing a function to write the results into the Firestore database once completed editing.

```ts
   saveAction() {
    if (this.currentDocRef && this.editing && this.currentTweet) {
      this.currentLoading = true;
      this.currentDocRef.update({
        tweet: this.currentTweet
      })
      .then(() => {
        this.currentLoading = false;
        this.editing = false;
      })
      .catch(err => {
        console.error(err)
        this.showError("Something went wrong, could not edit tweet");
      })
    }
  }
```

# Enqueue and Delete
Enqueueing a tweet is simply a case of updating it's property to `queued: true`, and then running the above advance function. Conversely, Deleting a tweet actually deletes it from the database using a `delete()` method.

The result:
![curate](https://dev-to-uploads.s3.amazonaws.com/i/3eb9o0ttrz0ha222xbkm.gif)
 
I can actually make this go faster, as it synchronously waits for the delete/enqueue action to complete before switching. However, I think it is going fast enough, and lets users get feedback of any failures when they happen.

# Swipe Controls
Since I want to be able to use this on my phone, I enabled swiping left/right to delete/enqueue. Vuetify has these out of the box. The visual feedback of swipe isn't very good (I should add transitions later), but it's very easy to add, the <v-textarea> simply gains a `v-touch` directive, to trigger the already existing `deleteAction()` and `enqueueAction()`, the `true` argument causes those functions to show an alert to give users some better feedback. I am considering replacing this with a transition animation in future.

```vue
      <v-textarea
        v-model="displayTweet"
        outlined
        label="Tweet"
        counter
        :readonly="!editing"
        :disabled="currentLoading"
        :loading="loading"
        v-touch="{
          left: () => deleteAction(true),
          right: () => enqueueAction(true),
        }"
      >
```