User timeline

📘

full code repo https://github.com/lens-protocol/lens-api-examples

🚧

This request is protected by authentication

hint: this means it requires an x-access-token header put in the request with your authentication token.

Timeline is one of the most fundamental elements a successful social media site needs. It can be used to show the user what is happening on the social feeds of people they follow and what they are liking. The timeline queries will continue to get smarter, eventually bringing in AI and data profiling. We will continue to improve what data is returned by the API. The beauty of this is if you use the API you just inherit this functionality without having to update anything.

Presently, for the Beta release, the timeline just brings back content in date order.

What the timeline brings back

Post

If one of the profiles you follow posts it will appear on your timeline.

Comment

If one of the profiles you follow comments on a publication it will appear on your timeline.

Mirror

If one of the profiles you follow mirrors a publication it will appear in your timeline. Remember people can mirror a post or a comment.

Collected Post

If one of the profile wallet owners you follow collects a post it will appear on your timeline. Remember profiles do not collect wallets do but it is also a key action that we wanted on the timeline API.

Collected Comment

If one of the profile wallet owners you follow collects a post it will appear on your timeline. Remember profiles do not collect wallets do but it is also a key action that we wanted on the timeline API.

API Design

Below is the overview of the entire interface but we dig into specific queries below.

📘

Hot tip

If you do not know GraphQL that well remember things can be nullable if defined as so in the schema how GraphQL knows its nullable is without the ! at the end here is an example:

Not nullable:

ownedBy: EthereumAddress!

Nullable:

ownedBy: EthereumAddress

It's always worth generating the TypeScript types for the schema if your application is TypeScript here is a reference to how you would do that - https://www.apollographql.com/blog/tooling/apollo-codegen/typescript-graphql-code-generator-generate-graphql-types/

query Timeline {
  timeline(request: { profileId: "0x01", limit: 10 }) {
    items {
      __typename 
      ... on Post {
        ...PostFields
      }
      ... on Comment {
        ...CommentFields
      }
      ... on Mirror {
        ...MirrorFields
      }
    }
    pageInfo {
      prev
      next
      totalCount
    }
  }
}

fragment MediaFields on Media {
  url
  mimeType
}

fragment ProfileFields on Profile {
  id
  name
  bio
  attributes {
    displayType
    traitType
    key
    value
  }
  isFollowedByMe
  isFollowing(who: null)
  followNftAddress
  metadata
  isDefault
  handle
  picture {
    ... on NftImage {
      contractAddress
      tokenId
      uri
      verified
    }
    ... on MediaSet {
      original {
        ...MediaFields
      }
    }
  }
  coverPicture {
    ... on NftImage {
      contractAddress
      tokenId
      uri
      verified
    }
    ... on MediaSet {
      original {
        ...MediaFields
      }
    }
  }
  ownedBy
  dispatcher {
    address
  }
  stats {
    totalFollowers
    totalFollowing
    totalPosts
    totalComments
    totalMirrors
    totalPublications
    totalCollects
  }
  followModule {
    ... on FeeFollowModuleSettings {
      type
      amount {
        asset {
          name
          symbol
          decimals
          address
        }
        value
      }
      recipient
    }
    ... on ProfileFollowModuleSettings {
     type
    }
    ... on RevertFollowModuleSettings {
     type
    }
  }
}

fragment PublicationStatsFields on PublicationStats { 
  totalAmountOfMirrors
  totalAmountOfCollects
  totalAmountOfComments
}

fragment MetadataOutputFields on MetadataOutput {
  name
  description
  content
  media {
    original {
      ...MediaFields
    }
  }
  attributes {
    displayType
    traitType
    value
  }
}

fragment Erc20Fields on Erc20 {
  name
  symbol
  decimals
  address
}

fragment CollectModuleFields on CollectModule {
  __typename
    ... on FreeCollectModuleSettings {
        type
    followerOnly
    contractAddress
  }
  ... on FeeCollectModuleSettings {
    type
    amount {
      asset {
        ...Erc20Fields
      }
      value
    }
    recipient
    referralFee
  }
  ... on LimitedFeeCollectModuleSettings {
    type
    collectLimit
    amount {
      asset {
        ...Erc20Fields
      }
      value
    }
    recipient
    referralFee
  }
  ... on LimitedTimedFeeCollectModuleSettings {
    type
    collectLimit
    amount {
      asset {
        ...Erc20Fields
      }
      value
    }
    recipient
    referralFee
    endTimestamp
  }
  ... on RevertCollectModuleSettings {
    type
  }
  ... on TimedFeeCollectModuleSettings {
    type
    amount {
      asset {
        ...Erc20Fields
      }
      value
    }
    recipient
    referralFee
    endTimestamp
  }
}

fragment PostFields on Post {
  id
  profile {
    ...ProfileFields
  }
  stats {
    ...PublicationStatsFields
  }
  metadata {
    ...MetadataOutputFields
  }
  createdAt
  collectModule {
    ...CollectModuleFields
  }
  referenceModule {
    ... on FollowOnlyReferenceModuleSettings {
      type
    }
  }
  appId
  collectedBy {
    ...WalletFields
  }
  hidden
  reaction(request: null)
  mirrors(by: null)
  hasCollectedByMe
}

fragment MirrorBaseFields on Mirror {
  id
  profile {
    ...ProfileFields
  }
  stats {
    ...PublicationStatsFields
  }
  metadata {
    ...MetadataOutputFields
  }
  createdAt
  collectModule {
    ...CollectModuleFields
  }
  referenceModule {
    ... on FollowOnlyReferenceModuleSettings {
      type
    }
  }
  appId
  hidden
  reaction(request: null)
  hasCollectedByMe
}

fragment MirrorFields on Mirror {
  ...MirrorBaseFields
  mirrorOf {
   ... on Post {
      ...PostFields          
   }
   ... on Comment {
      ...CommentFields          
   }
  }
}

fragment CommentBaseFields on Comment {
  id
  profile {
    ...ProfileFields
  }
  stats {
    ...PublicationStatsFields
  }
  metadata {
    ...MetadataOutputFields
  }
  createdAt
  collectModule {
    ...CollectModuleFields
  }
  referenceModule {
    ... on FollowOnlyReferenceModuleSettings {
      type
    }
  }
  appId
  collectedBy {
    ...WalletFields
  }
  hidden
  reaction(request: null)
  mirrors(by: null)
  hasCollectedByMe
}

fragment CommentFields on Comment {
  ...CommentBaseFields
  mainPost {
    ... on Post {
      ...PostFields
    }
    ... on Mirror {
      ...MirrorBaseFields
      mirrorOf {
        ... on Post {
           ...PostFields          
        }
        ... on Comment {
           ...CommentMirrorOfFields        
        }
      }
    }
  }
}

fragment CommentMirrorOfFields on Comment {
  ...CommentBaseFields
  mainPost {
    ... on Post {
      ...PostFields
    }
    ... on Mirror {
       ...MirrorBaseFields
    }
  }
}

fragment WalletFields on Wallet {
   address,
   defaultProfile {
    ...ProfileFields
   }
}
{
  "data": {
    "timeline": {
      "items": [
        {
          "__typename": "Post",
          "id": "0x01-0x01",
          "profile": {
            "id": "0x01",
            "name": null,
            "bio": null,
            "attributes": [
              {
                "displayType": null,
                "traitType": null,
                "key": "custom_field",
                "value": "yes this is custom"
              }
            ],
            "isFollowedByMe": false,
                        "isFollowing": false,
            "followNftAddress": null,
            "metadata": "ipfs://QmSfyMcnh1wnJHrAWCBjZHapTS859oNSsuDFiAPPdAHgHP",
            "isDefault": false,
            "handle": "hey",
            "picture": null,
            "coverPicture": null,
            "ownedBy": "0xD020E01C0c90Ab005A01482d34B808874345FD82",
            "dispatcher": null,
            "stats": {
              "totalFollowers": 2,
              "totalFollowing": 1,
              "totalPosts": 1,
              "totalComments": 0,
              "totalMirrors": 0,
              "totalPublications": 1,
              "totalCollects": 0
            },
            "followModule": null
          },
          "stats": {
            "totalAmountOfMirrors": 0,
            "totalAmountOfCollects": 0,
            "totalAmountOfComments": 0
          },
          "metadata": {
            "name": "",
            "description": "",
            "content": "Hello",
            "media": null,
            "attributes": []
          },
          "createdAt": "2022-02-10T09:50:46.000Z",
          "collectModule": {
            "__typename": "FreeCollectModuleSettings",
            "type": "FreeCollectModule"
          },
          "referenceModule": null,
          "appId": null,
          "collectedBy": null,
          "hidden": false,
          "reaction": null,
          "mirrors": [],
                    "hasCollectedByMe": false
        }
      ],
      "pageInfo": {
        "prev": "{\"entityIdentifier\":\"0x01-0x01\",\"timestamp\":1644486646,\"cursorDirection\":\"BEFORE\"}",
        "next": "{\"entityIdentifier\":\"0x01-0x01\",\"timestamp\":1644486646,\"cursorDirection\":\"AFTER\"}",
        "totalCount": 1
      }
    }
  }
}
type Query {
   timeline(request: TimelineRequest!): PaginatedTimelineResult!
}

You will see the paging result behavior repeated a lot in the API, this is to allow you to fetch a certain amount and then page it for the most optimal request speed. Every time something is wrapped in a paging result you will always get returned a pageInfo which holds the cursors for the previous and next alongside the total count which exists in the database. These cursors are just pointers for the server to get to the next result and do not need to be understood by your client or server. If you ever want to then page to the next result you can pass these previous and next cursor in the request cursor property.

Request

Let's look into the request a little more:

profiled - required

profileId is required - because a wallet can have many profiles the logged-in user must species which profileId they wish to get back on the timeline.

sources

Publications Metadata standards highlight that you can pass in an AppId into the metadata and allow you to tag the content with a source. You can use "sources" in the request to only bring back data relevant to that source. This means UI can have different timelines depending on their sources.

Response

Timeline responses with union Publication = Post | Comment | Mirror however below we will provide ways to distinguish between them and how to link the various types together.

Post

  • will be __typename - Post
  • collectedBy will be null

Comment

  • will be __typename - Comment
  • collectedBy will be null

Mirror

  • will be __typename - Mirror
  • you can look get back to the publication mirrored on the field mirrorOf which is a type of Post or Comment

Collected Post

  • will be __typename - Post
  • collectedBy will be populated by a Wallet

Collected Comment

  • will be __typename - Comment
  • collectedBy will be populated by a Wallet

📘

Collecting...

Because we want to push the social graph for the original author of the publication if someone collects a mirror it will mark it as if they have collected the main publication from the timeline point of view.

Full code example

import { apolloClient } from './apollo-client';
// this is showing you how you use it with react for example
// if your using node or something else you can import using
// @apollo/client/core!
import { gql } from '@apollo/client'

const GET_TIMELINE = `
  query($request: TimelineRequest!) {
    timeline(request: $request) {
      items {
        __typename 
        ... on Post {
          ...PostFields
        }
        ... on Comment {
          ...CommentFields
        }
        ... on Mirror {
          ...MirrorFields
        }
      }
      pageInfo {
        prev
        next
        totalCount
      }
    }
  }

  fragment MediaFields on Media {
    url
    width
    height
    mimeType
  }

  fragment ProfileFields on Profile {
    id
    name
    bio
    attributes {
      displayType
      traitType
      key
      value
    }
        isFollowedByMe
    isFollowing(who: null)
        followNftAddress
    metadata
    isDefault
    handle
    picture {
      ... on NftImage {
        contractAddress
        tokenId
        uri
        verified
      }
      ... on MediaSet {
        original {
          ...MediaFields
        }
        small {
          ...MediaFields
        }
        medium {
          ...MediaFields
        }
      }
    }
    coverPicture {
      ... on NftImage {
        contractAddress
        tokenId
        uri
        verified
      }
      ... on MediaSet {
        original {
          ...MediaFields
        }
        small {
         ...MediaFields
        }
        medium {
          ...MediaFields
        }
      }
    }
    ownedBy
    dispatcher {
      address
    }
    stats {
      totalFollowers
      totalFollowing
      totalPosts
      totalComments
      totalMirrors
      totalPublications
      totalCollects
    }
    followModule {
      ... on FeeFollowModuleSettings {
        type
        amount {
          asset {
            name
            symbol
            decimals
            address
          }
          value
        }
        recipient
      }
      ... on ProfileFollowModuleSettings {
       type
      }
      ... on RevertFollowModuleSettings {
       type
      }
    }
  }

  fragment PublicationStatsFields on PublicationStats { 
    totalAmountOfMirrors
    totalAmountOfCollects
    totalAmountOfComments
  }

  fragment MetadataOutputFields on MetadataOutput {
    name
    description
    content
    media {
      original {
        ...MediaFields
      }
      small {
        ...MediaFields
      }
      medium {
        ...MediaFields
      }
    }
    attributes {
      displayType
      traitType
      value
    }
  }

  fragment Erc20Fields on Erc20 {
    name
    symbol
    decimals
    address
  }

  fragment CollectModuleFields on CollectModule {
    __typename
    ... on EmptyCollectModuleSettings {
      type
    }
    ... on FeeCollectModuleSettings {
      type
      amount {
        asset {
          ...Erc20Fields
        }
        value
      }
      recipient
      referralFee
    }
    ... on LimitedFeeCollectModuleSettings {
      type
      collectLimit
      amount {
        asset {
          ...Erc20Fields
        }
        value
      }
      recipient
      referralFee
    }
    ... on LimitedTimedFeeCollectModuleSettings {
      type
      collectLimit
      amount {
        asset {
          ...Erc20Fields
        }
        value
      }
      recipient
      referralFee
      endTimestamp
    }
    ... on RevertCollectModuleSettings {
      type
    }
    ... on TimedFeeCollectModuleSettings {
      type
      amount {
        asset {
          ...Erc20Fields
        }
        value
      }
      recipient
      referralFee
      endTimestamp
    }
  }

  fragment PostFields on Post {
    id
    profile {
      ...ProfileFields
    }
    stats {
      ...PublicationStatsFields
    }
    metadata {
      ...MetadataOutputFields
    }
    createdAt
    collectModule {
      ...CollectModuleFields
    }
    referenceModule {
      ... on FollowOnlyReferenceModuleSettings {
        type
      }
    }
    appId
    collectedBy {
      ...WalletFields
    }
        hidden
        reaction(request: null)
        mirrors(by: null)
    hasCollectedByMe
  }

  fragment MirrorBaseFields on Mirror {
    id
    profile {
      ...ProfileFields
    }
    stats {
      ...PublicationStatsFields
    }
    metadata {
      ...MetadataOutputFields
    }
    createdAt
    collectModule {
      ...CollectModuleFields
    }
    referenceModule {
      ... on FollowOnlyReferenceModuleSettings {
        type
      }
    }
    appId
    hidden
    reaction(request: null)
    hasCollectedByMe
  }

  fragment MirrorFields on Mirror {
    ...MirrorBaseFields
    mirrorOf {
     ... on Post {
        ...PostFields          
     }
     ... on Comment {
        ...CommentFields          
     }
    }
  }

  fragment CommentBaseFields on Comment {
    id
    profile {
      ...ProfileFields
    }
    stats {
      ...PublicationStatsFields
    }
    metadata {
      ...MetadataOutputFields
    }
    createdAt
    collectModule {
      ...CollectModuleFields
    }
    referenceModule {
      ... on FollowOnlyReferenceModuleSettings {
        type
      }
    }
    appId
    collectedBy {
      ...WalletFields
    }
    hidden
        reaction(request: null)
        mirrors(by: null)
    hasCollectedByMe
  }

  fragment CommentFields on Comment {
    ...CommentBaseFields
    mainPost {
      ... on Post {
        ...PostFields
      }
      ... on Mirror {
        ...MirrorBaseFields
        mirrorOf {
          ... on Post {
             ...PostFields          
          }
          ... on Comment {
             ...CommentMirrorOfFields        
          }
        }
      }
    }
  }

  fragment CommentMirrorOfFields on Comment {
    ...CommentBaseFields
    mainPost {
      ... on Post {
        ...PostFields
      }
      ... on Mirror {
         ...MirrorBaseFields
      }
    }
  }

fragment WalletFields on Wallet {
   address,
   defaultProfile {
    ...ProfileFields
   }
  }
`

export const getTimeline = (profileId) => {
   return apolloClient.query({
    query: gql(GET_TIMELINE),
    variables: {
      request: {
        profileId,
        limit: 10
      }
    },
  })
}
// this is showing you how you use it with react for example
// if your using node or something else you can import using
// @apollo/client/core!
import { ApolloClient, InMemoryCache, HttpLink, ApolloLink } from '@apollo/client'

const httpLink = new HttpLink({ uri: 'https://api-mumbai.lens.dev/' });

// example how you can pass in the x-access-token into requests using `ApolloLink`
const authLink = new ApolloLink((operation, forward) => {
  // Retrieve the authorization token from local storage.
  // if your using node etc you have to handle your auth different
  const token = localStorage.getItem('auth_token');

  // Use the setContext method to set the HTTP headers.
  operation.setContext({
    headers: {
      'x-access-token': token ? `Bearer ${token}` : ''
    }
  });

  // Call the next link in the middleware chain.
  return forward(operation);
});

export const apolloClient = new ApolloClient({
  link: authLink.concat(httpLink),
  cache: new InMemoryCache(),
})

Did this page help you?