add favorite/unfavorite feature
This commit is contained in:
		
							parent
							
								
									3a17f7ff7b
								
							
						
					
					
						commit
						1b7a01f1ee
					
				
					 24 changed files with 291 additions and 108 deletions
				
			
		
							
								
								
									
										1
									
								
								.gitignore
									
										
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.gitignore
									
										
									
									
										vendored
									
									
								
							| 
						 | 
				
			
			@ -5,3 +5,4 @@ yarn.lock
 | 
			
		|||
templates/.*
 | 
			
		||||
assets/*.css
 | 
			
		||||
/mastodon
 | 
			
		||||
mastodon.log
 | 
			
		||||
| 
						 | 
				
			
			@ -60,13 +60,10 @@ async function runMastodon () {
 | 
			
		|||
    await exec(cmd, {cwd: mastodonDir})
 | 
			
		||||
  }
 | 
			
		||||
  const promise = spawn('foreman', ['start'], {cwd: mastodonDir})
 | 
			
		||||
  const log = fs.createWriteStream('mastodon.log', {flags: 'a'})
 | 
			
		||||
  childProc = promise.childProcess
 | 
			
		||||
  childProc.stdout.on('data', function (data) {
 | 
			
		||||
    console.log(data.toString('utf8').replace(/\n$/, ''))
 | 
			
		||||
  })
 | 
			
		||||
  childProc.stderr.on('data', function (data) {
 | 
			
		||||
    console.error(data.toString('utf8').replace(/\n$/, ''))
 | 
			
		||||
  })
 | 
			
		||||
  childProc.stdout.pipe(log)
 | 
			
		||||
  childProc.stderr.pipe(log)
 | 
			
		||||
 | 
			
		||||
  await waitForMastodonToStart()
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -3,20 +3,28 @@ import { store } from '../_store/store'
 | 
			
		|||
import { database } from '../_database/database'
 | 
			
		||||
import { toast } from '../_utils/toast'
 | 
			
		||||
 | 
			
		||||
export async function setFavorited(statusId, favorited) {
 | 
			
		||||
export async function setFavorited (statusId, favorited) {
 | 
			
		||||
  if (!store.get('online')) {
 | 
			
		||||
    toast.say('You cannot favorite or unfavorite while offline.')
 | 
			
		||||
    return
 | 
			
		||||
  }
 | 
			
		||||
  let instanceName = store.get('currentInstance')
 | 
			
		||||
  let accessToken = store.get('accessToken')
 | 
			
		||||
  try {
 | 
			
		||||
    let status = await (favorited
 | 
			
		||||
    let result = await (favorited
 | 
			
		||||
        ? favoriteStatus(instanceName, accessToken, statusId)
 | 
			
		||||
        : unfavoriteStatus(instanceName, accessToken, statusId))
 | 
			
		||||
    await database.insertStatus(instanceName, status)
 | 
			
		||||
    if (result.error) {
 | 
			
		||||
      throw new Error(result.error)
 | 
			
		||||
    }
 | 
			
		||||
    await database.setStatusFavorited(instanceName, statusId, favorited)
 | 
			
		||||
    let statusModifications = store.get('statusModifications')
 | 
			
		||||
    let currentStatusModifications = statusModifications[instanceName] =
 | 
			
		||||
      (statusModifications[instanceName] || {favorites: {}, reblogs: {}})
 | 
			
		||||
    currentStatusModifications.favorites[statusId] = favorited
 | 
			
		||||
    store.set({statusModifications: statusModifications})
 | 
			
		||||
  } catch (e) {
 | 
			
		||||
    toast.say('Failed to favorite/unfavorite. Please try again.')
 | 
			
		||||
    console.error(e)
 | 
			
		||||
    toast.say('Failed to favorite or unfavorite. ' + (e.message || ''))
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,18 +1,14 @@
 | 
			
		|||
import { getWithTimeout, paramsString } from '../_utils/ajax'
 | 
			
		||||
import { basename } from './utils'
 | 
			
		||||
import { auth, basename } from './utils'
 | 
			
		||||
 | 
			
		||||
export async function getBlockedAccounts (instanceName, accessToken, limit = 80) {
 | 
			
		||||
  let url = `${basename(instanceName)}/api/v1/blocks`
 | 
			
		||||
  url += '?' + paramsString({ limit })
 | 
			
		||||
  return getWithTimeout(url, {
 | 
			
		||||
    'Authorization': `Bearer ${accessToken}`
 | 
			
		||||
  })
 | 
			
		||||
  return getWithTimeout(url, auth(accessToken))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function getMutedAccounts (instanceName, accessToken, limit = 80) {
 | 
			
		||||
  let url = `${basename(instanceName)}/api/v1/mutes`
 | 
			
		||||
  url += '?' + paramsString({ limit })
 | 
			
		||||
  return getWithTimeout(url, {
 | 
			
		||||
    'Authorization': `Bearer ${accessToken}`
 | 
			
		||||
  })
 | 
			
		||||
  return getWithTimeout(url, auth(accessToken))
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,16 +1,12 @@
 | 
			
		|||
import { post } from '../_utils/ajax'
 | 
			
		||||
import { basename } from './utils'
 | 
			
		||||
import { basename, auth } from './utils'
 | 
			
		||||
 | 
			
		||||
export async function favoriteStatus(instanceName, accessToken, statusId) {
 | 
			
		||||
export async function favoriteStatus (instanceName, accessToken, statusId) {
 | 
			
		||||
  let url = `${basename(instanceName)}/api/v1/statuses/${statusId}/favourite`
 | 
			
		||||
  return post(url, null, {
 | 
			
		||||
    'Authorization': `Bearer ${accessToken}`
 | 
			
		||||
  })
 | 
			
		||||
  return post(url, null, auth(accessToken))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function unfavoriteStatus(instanceName, accessToken, statusId) {
 | 
			
		||||
export async function unfavoriteStatus (instanceName, accessToken, statusId) {
 | 
			
		||||
  let url = `${basename(instanceName)}/api/v1/statuses/${statusId}/unfavourite`
 | 
			
		||||
  return post(url, null, {
 | 
			
		||||
    'Authorization': `Bearer ${accessToken}`
 | 
			
		||||
  })
 | 
			
		||||
  return post(url, null, auth(accessToken))
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,9 +1,7 @@
 | 
			
		|||
import { getWithTimeout } from '../_utils/ajax'
 | 
			
		||||
import { basename } from './utils'
 | 
			
		||||
import { auth, basename } from './utils'
 | 
			
		||||
 | 
			
		||||
export function getLists (instanceName, accessToken) {
 | 
			
		||||
  let url = `${basename(instanceName)}/api/v1/lists`
 | 
			
		||||
  return getWithTimeout(url, {
 | 
			
		||||
    'Authorization': `Bearer ${accessToken}`
 | 
			
		||||
  })
 | 
			
		||||
  return getWithTimeout(url, auth(accessToken))
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,5 +1,5 @@
 | 
			
		|||
import { getWithTimeout, paramsString } from '../_utils/ajax'
 | 
			
		||||
import { basename } from './utils'
 | 
			
		||||
import { auth, basename } from './utils'
 | 
			
		||||
 | 
			
		||||
export async function getPinnedStatuses (instanceName, accessToken, accountId) {
 | 
			
		||||
  let url = `${basename(instanceName)}/api/v1/accounts/${accountId}/statuses`
 | 
			
		||||
| 
						 | 
				
			
			@ -7,7 +7,5 @@ export async function getPinnedStatuses (instanceName, accessToken, accountId) {
 | 
			
		|||
    limit: 40,
 | 
			
		||||
    pinned: true
 | 
			
		||||
  })
 | 
			
		||||
  return getWithTimeout(url, {
 | 
			
		||||
    'Authorization': `Bearer ${accessToken}`
 | 
			
		||||
  })
 | 
			
		||||
  return getWithTimeout(url, auth(accessToken))
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,18 +1,14 @@
 | 
			
		|||
import { getWithTimeout, paramsString } from '../_utils/ajax'
 | 
			
		||||
import { basename } from './utils'
 | 
			
		||||
import { auth, basename } from './utils'
 | 
			
		||||
 | 
			
		||||
export async function getReblogs (instanceName, accessToken, statusId, limit = 80) {
 | 
			
		||||
  let url = `${basename(instanceName)}/api/v1/statuses/${statusId}/reblogged_by`
 | 
			
		||||
  url += '?' + paramsString({ limit })
 | 
			
		||||
  return getWithTimeout(url, {
 | 
			
		||||
    'Authorization': `Bearer ${accessToken}`
 | 
			
		||||
  })
 | 
			
		||||
  return getWithTimeout(url, auth(accessToken))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function getFavorites (instanceName, accessToken, statusId, limit = 80) {
 | 
			
		||||
  let url = `${basename(instanceName)}/api/v1/statuses/${statusId}/favourited_by`
 | 
			
		||||
  url += '?' + paramsString({ limit })
 | 
			
		||||
  return getWithTimeout(url, {
 | 
			
		||||
    'Authorization': `Bearer ${accessToken}`
 | 
			
		||||
  })
 | 
			
		||||
  return getWithTimeout(url, auth(accessToken))
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,12 +1,10 @@
 | 
			
		|||
import { getWithTimeout, paramsString } from '../_utils/ajax'
 | 
			
		||||
import { basename } from './utils'
 | 
			
		||||
import { auth, basename } from './utils'
 | 
			
		||||
 | 
			
		||||
export function search (instanceName, accessToken, query) {
 | 
			
		||||
  let url = `${basename(instanceName)}/api/v1/search?` + paramsString({
 | 
			
		||||
    q: query,
 | 
			
		||||
    resolve: true
 | 
			
		||||
  })
 | 
			
		||||
  return getWithTimeout(url, {
 | 
			
		||||
    'Authorization': `Bearer ${accessToken}`
 | 
			
		||||
  })
 | 
			
		||||
  return getWithTimeout(url, auth(accessToken))
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,5 +1,5 @@
 | 
			
		|||
import { getWithTimeout, paramsString } from '../_utils/ajax'
 | 
			
		||||
import { basename } from './utils'
 | 
			
		||||
import { auth, basename } from './utils'
 | 
			
		||||
 | 
			
		||||
function getTimelineUrlPath (timeline) {
 | 
			
		||||
  switch (timeline) {
 | 
			
		||||
| 
						 | 
				
			
			@ -57,14 +57,12 @@ export function getTimeline (instanceName, accessToken, timeline, maxId, since)
 | 
			
		|||
    // special case - this is a list of descendents and ancestors
 | 
			
		||||
    let statusUrl = `${basename(instanceName)}/api/v1/statuses/${timeline.split('/').slice(-1)[0]}}`
 | 
			
		||||
    return Promise.all([
 | 
			
		||||
      getWithTimeout(url, {'Authorization': `Bearer ${accessToken}`}),
 | 
			
		||||
      getWithTimeout(statusUrl, {'Authorization': `Bearer ${accessToken}`})
 | 
			
		||||
      getWithTimeout(url, auth(accessToken)),
 | 
			
		||||
      getWithTimeout(statusUrl, auth(accessToken))
 | 
			
		||||
    ]).then(res => {
 | 
			
		||||
      return [].concat(res[0].ancestors).concat([res[1]]).concat(res[0].descendants)
 | 
			
		||||
    })
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return getWithTimeout(url, {
 | 
			
		||||
    'Authorization': `Bearer ${accessToken}`
 | 
			
		||||
  })
 | 
			
		||||
  return getWithTimeout(url, auth(accessToken))
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,25 +1,19 @@
 | 
			
		|||
import { getWithTimeout, paramsString } from '../_utils/ajax'
 | 
			
		||||
import { basename } from './utils'
 | 
			
		||||
import { auth, basename } from './utils'
 | 
			
		||||
 | 
			
		||||
export function getVerifyCredentials (instanceName, accessToken) {
 | 
			
		||||
  let url = `${basename(instanceName)}/api/v1/accounts/verify_credentials`
 | 
			
		||||
  return getWithTimeout(url, {
 | 
			
		||||
    'Authorization': `Bearer ${accessToken}`
 | 
			
		||||
  })
 | 
			
		||||
  return getWithTimeout(url, auth(accessToken))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function getAccount (instanceName, accessToken, accountId) {
 | 
			
		||||
  let url = `${basename(instanceName)}/api/v1/accounts/${accountId}`
 | 
			
		||||
  return getWithTimeout(url, {
 | 
			
		||||
    'Authorization': `Bearer ${accessToken}`
 | 
			
		||||
  })
 | 
			
		||||
  return getWithTimeout(url, auth(accessToken))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function getRelationship (instanceName, accessToken, accountId) {
 | 
			
		||||
  let url = `${basename(instanceName)}/api/v1/accounts/relationships`
 | 
			
		||||
  url += '?' + paramsString({id: accountId})
 | 
			
		||||
  let res = await getWithTimeout(url, {
 | 
			
		||||
    'Authorization': `Bearer ${accessToken}`
 | 
			
		||||
  })
 | 
			
		||||
  let res = await getWithTimeout(url, auth(accessToken))
 | 
			
		||||
  return res[0]
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -13,3 +13,9 @@ export function basename (instanceName) {
 | 
			
		|||
  }
 | 
			
		||||
  return `https://${instanceName}`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function auth (accessToken) {
 | 
			
		||||
  return {
 | 
			
		||||
    'Authorization': `Bearer ${accessToken}`
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -2,8 +2,10 @@
 | 
			
		|||
  <button type="button"
 | 
			
		||||
          aria-label="{{label}}"
 | 
			
		||||
          aria-pressed="{{!!pressed}}"
 | 
			
		||||
          class="icon-button {{pressed ? 'pressed' : ''}} {{big ? 'big-icon' : ''}}"
 | 
			
		||||
          class="{{computedClass}}"
 | 
			
		||||
          disabled="{{disabled}}"
 | 
			
		||||
          delegate-click-key="{{delegateKey}}"
 | 
			
		||||
          delegate-keydown-key="{{delegateKey}}"
 | 
			
		||||
          on:click
 | 
			
		||||
  >
 | 
			
		||||
    <svg>
 | 
			
		||||
| 
						 | 
				
			
			@ -13,8 +15,10 @@
 | 
			
		|||
{{else}}
 | 
			
		||||
  <button type="button"
 | 
			
		||||
          aria-label="{{label}}"
 | 
			
		||||
          class="icon-button {{big ? 'big-icon' : ''}}"
 | 
			
		||||
          class="{{computedClass}}"
 | 
			
		||||
          disabled="{{disabled}}"
 | 
			
		||||
          delegate-click-key="{{delegateKey}}"
 | 
			
		||||
          delegate-keydown-key="{{delegateKey}}"
 | 
			
		||||
          on:click
 | 
			
		||||
  >
 | 
			
		||||
    <svg>
 | 
			
		||||
| 
						 | 
				
			
			@ -44,7 +48,7 @@
 | 
			
		|||
    fill: var(--action-button-fill-color-hover);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  button.icon-button:active svg {
 | 
			
		||||
  button.icon-button.not-pressable:active svg {
 | 
			
		||||
    fill: var(--action-button-fill-color-active);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -60,3 +64,19 @@
 | 
			
		|||
    fill: var(--action-button-fill-color-pressed-active);
 | 
			
		||||
  }
 | 
			
		||||
</style>
 | 
			
		||||
<script>
 | 
			
		||||
  import identity from 'lodash/identity'
 | 
			
		||||
 | 
			
		||||
  export default {
 | 
			
		||||
    computed: {
 | 
			
		||||
      computedClass: (pressable, pressed, big) => {
 | 
			
		||||
        return [
 | 
			
		||||
          'icon-button',
 | 
			
		||||
          !pressable && 'not-pressable',
 | 
			
		||||
          pressed && 'pressed',
 | 
			
		||||
          big && 'big-icon',
 | 
			
		||||
        ].filter(identity).join(' ')
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
</script>
 | 
			
		||||
| 
						 | 
				
			
			@ -28,7 +28,7 @@
 | 
			
		|||
  {{#if isStatusInOwnThread}}
 | 
			
		||||
  <StatusDetails status="{{originalStatus}}" />
 | 
			
		||||
  {{/if}}
 | 
			
		||||
  <StatusToolbar :status :isStatusInOwnThread />
 | 
			
		||||
  <StatusToolbar status="{{originalStatus}}" :isStatusInOwnThread :timelineType :timelineValue />
 | 
			
		||||
</article>
 | 
			
		||||
 | 
			
		||||
<style>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -15,6 +15,8 @@
 | 
			
		|||
    pressable="true"
 | 
			
		||||
    pressed="{{favorited}}"
 | 
			
		||||
    href="#fa-star"
 | 
			
		||||
    delegateKey="{{favoriteKey}}"
 | 
			
		||||
    ref:favoriteNode
 | 
			
		||||
    />
 | 
			
		||||
  <IconButton
 | 
			
		||||
    label="Show more actions"
 | 
			
		||||
| 
						 | 
				
			
			@ -34,12 +36,33 @@
 | 
			
		|||
<script>
 | 
			
		||||
  import IconButton from '../IconButton.html'
 | 
			
		||||
  import { store } from '../../_store/store'
 | 
			
		||||
  import { registerDelegate, unregisterDelegate } from '../../_utils/delegate'
 | 
			
		||||
  import { setFavorited } from '../../_actions/favorite'
 | 
			
		||||
 | 
			
		||||
  export default {
 | 
			
		||||
    oncreate() {
 | 
			
		||||
      this.onFavoriteClick = this.onFavoriteClick.bind(this)
 | 
			
		||||
 | 
			
		||||
      let favoriteKey = this.get('favoriteKey')
 | 
			
		||||
      registerDelegate('click', favoriteKey, this.onFavoriteClick)
 | 
			
		||||
      registerDelegate('keydown', favoriteKey, this.onFavoriteClick)
 | 
			
		||||
    },
 | 
			
		||||
    ondestroy() {
 | 
			
		||||
      let favoriteKey = this.get('favoriteKey')
 | 
			
		||||
      unregisterDelegate('click', favoriteKey, this.onFavoriteClick)
 | 
			
		||||
      unregisterDelegate('keydown', favoriteKey, this.onFavoriteClick)
 | 
			
		||||
    },
 | 
			
		||||
    components: {
 | 
			
		||||
      IconButton
 | 
			
		||||
    },
 | 
			
		||||
    store: () => store,
 | 
			
		||||
    methods: {
 | 
			
		||||
      onFavoriteClick() {
 | 
			
		||||
        let statusId = this.get('statusId')
 | 
			
		||||
        let favorited = this.get('favorited')
 | 
			
		||||
        /* no await */ setFavorited(statusId, !favorited)
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    computed: {
 | 
			
		||||
      visibility: (status) => status.visibility,
 | 
			
		||||
      boostLabel: (visibility) => {
 | 
			
		||||
| 
						 | 
				
			
			@ -70,7 +93,9 @@
 | 
			
		|||
          return $currentStatusModifications.favorites[status.id]
 | 
			
		||||
        }
 | 
			
		||||
        return status.favourited
 | 
			
		||||
      }
 | 
			
		||||
      },
 | 
			
		||||
      statusId: (status) => status.id,
 | 
			
		||||
      favoriteKey: (statusId, timelineType, timelineValue) => `fav-${timelineType}-${timelineValue}-${statusId}`
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
</script>
 | 
			
		||||
| 
						 | 
				
			
			@ -390,13 +390,29 @@ export async function getNotificationIdsForStatus (instanceName, statusId) {
 | 
			
		|||
}
 | 
			
		||||
 | 
			
		||||
//
 | 
			
		||||
// insert statuses
 | 
			
		||||
// update statuses
 | 
			
		||||
//
 | 
			
		||||
 | 
			
		||||
export async function insertStatus(instanceName, status) {
 | 
			
		||||
async function updateStatus (instanceName, statusId, updateFunc) {
 | 
			
		||||
  const db = await getDatabase(instanceName)
 | 
			
		||||
  cacheStatus(statusesCache, status)
 | 
			
		||||
  if (hasInCache(statusesCache, instanceName, statusId)) {
 | 
			
		||||
    let status = getInCache(statusesCache, instanceName, statusId)
 | 
			
		||||
    updateFunc(status)
 | 
			
		||||
    cacheStatus(status, instanceName)
 | 
			
		||||
  }
 | 
			
		||||
  return dbPromise(db, STATUSES_STORE, 'readwrite', (statusesStore) => {
 | 
			
		||||
    putStatus(statusesStore, status)
 | 
			
		||||
    statusesStore.get(statusId).onsuccess = e => {
 | 
			
		||||
      let status = e.target.result
 | 
			
		||||
      updateFunc(status)
 | 
			
		||||
      putStatus(statusesStore, status)
 | 
			
		||||
    }
 | 
			
		||||
  })
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function setStatusFavorited (instanceName, statusId, favorited) {
 | 
			
		||||
  return updateStatus(instanceName, statusId, status => {
 | 
			
		||||
    let delta = (favorited ? 1 : 0) - (status.favourited ? 1 : 0)
 | 
			
		||||
    status.favourited = favorited
 | 
			
		||||
    status.favourites_count = (status.favourites_count || 0) + delta
 | 
			
		||||
  })
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -97,8 +97,8 @@ export function instanceComputations (store) {
 | 
			
		|||
  )
 | 
			
		||||
 | 
			
		||||
  store.compute('currentStatusModifications',
 | 
			
		||||
    ['statusModifications', 'instanceName'],
 | 
			
		||||
    (statusModifications, instanceName) => {
 | 
			
		||||
      return statusModifications[instanceName]
 | 
			
		||||
    ['statusModifications', 'currentInstance'],
 | 
			
		||||
    (statusModifications, currentInstance) => {
 | 
			
		||||
      return statusModifications[currentInstance]
 | 
			
		||||
    })
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -9,14 +9,21 @@ function fetchWithTimeout (url, options) {
 | 
			
		|||
 | 
			
		||||
async function _post (url, body, headers, timeout) {
 | 
			
		||||
  let fetchFunc = timeout ? fetchWithTimeout : fetch
 | 
			
		||||
  return (await fetchFunc(url, {
 | 
			
		||||
    method: 'POST',
 | 
			
		||||
    headers: Object.assign(headers, {
 | 
			
		||||
  let opts = {
 | 
			
		||||
    method: 'POST'
 | 
			
		||||
  }
 | 
			
		||||
  if (body) {
 | 
			
		||||
    opts.headers = Object.assign(headers, {
 | 
			
		||||
      'Accept': 'application/json',
 | 
			
		||||
      'Content-Type': 'application/json'
 | 
			
		||||
    }),
 | 
			
		||||
    body: JSON.stringify(body)
 | 
			
		||||
  })).json()
 | 
			
		||||
    })
 | 
			
		||||
    opts.body = JSON.stringify(body)
 | 
			
		||||
  } else {
 | 
			
		||||
    opts.headers = Object.assign(headers, {
 | 
			
		||||
      'Accept': 'application/json'
 | 
			
		||||
    })
 | 
			
		||||
  }
 | 
			
		||||
  return (await fetchFunc(url, opts)).json()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function _get (url, headers, timeout) {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -15,7 +15,7 @@
 | 
			
		|||
    <form class="add-new-instance" on:submit='onSubmit(event)' aria-labelledby="add-an-instance-h1">
 | 
			
		||||
 | 
			
		||||
      {{#if $logInToInstanceError && $logInToInstanceErrorForText === $instanceNameInSearch}}
 | 
			
		||||
      <div class="form-error" role="alert">
 | 
			
		||||
      <div class="form-error form-error-user-error" role="alert">
 | 
			
		||||
        Error: {{$logInToInstanceError}}
 | 
			
		||||
      </div>
 | 
			
		||||
      {{/if}}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,11 +1,9 @@
 | 
			
		|||
import { Selector as $ } from 'testcafe'
 | 
			
		||||
import { addInstanceButton, getUrl, instanceInput, settingsButton } from '../utils'
 | 
			
		||||
import { addInstanceButton, formError, getUrl, instanceInput, settingsButton } from '../utils'
 | 
			
		||||
 | 
			
		||||
fixture`02-login-spec.js`
 | 
			
		||||
  .page`http://localhost:4002`
 | 
			
		||||
 | 
			
		||||
const formError = $('.form-error')
 | 
			
		||||
 | 
			
		||||
function manualLogin (t, username, password) {
 | 
			
		||||
  return t.click($('a').withText('log in to an instance'))
 | 
			
		||||
    .expect(getUrl()).contains('/settings/instances/add')
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,21 +1,20 @@
 | 
			
		|||
import { Selector as $ } from 'testcafe'
 | 
			
		||||
import { getUrl, validateTimeline } from '../utils'
 | 
			
		||||
import { getFirstVisibleStatus, getUrl, validateTimeline } from '../utils'
 | 
			
		||||
import { homeTimeline, notifications, localTimeline, favorites } from '../fixtures'
 | 
			
		||||
import { foobarRole } from '../roles'
 | 
			
		||||
 | 
			
		||||
fixture`03-basic-timeline-spec.js`
 | 
			
		||||
  .page`http://localhost:4002`
 | 
			
		||||
 | 
			
		||||
const firstArticle = $('.virtual-list-item[aria-hidden=false] .status-article')
 | 
			
		||||
 | 
			
		||||
test('Shows the home timeline', async t => {
 | 
			
		||||
  await t.useRole(foobarRole)
 | 
			
		||||
    .expect(firstArticle.hasAttribute('aria-setsize')).ok()
 | 
			
		||||
    .expect(firstArticle.getAttribute('aria-posinset')).eql('0')
 | 
			
		||||
    .expect(getFirstVisibleStatus().exists).ok()
 | 
			
		||||
    .expect(getFirstVisibleStatus().hasAttribute('aria-setsize')).ok()
 | 
			
		||||
    .expect(getFirstVisibleStatus().getAttribute('aria-posinset')).eql('0')
 | 
			
		||||
 | 
			
		||||
  await validateTimeline(t, homeTimeline)
 | 
			
		||||
 | 
			
		||||
  await t.expect(firstArticle.getAttribute('aria-setsize')).eql('49')
 | 
			
		||||
  await t.expect(getFirstVisibleStatus().getAttribute('aria-setsize')).eql('49')
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
test('Shows notifications', async t => {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,5 +1,5 @@
 | 
			
		|||
import { Selector as $ } from 'testcafe'
 | 
			
		||||
import { getNthStatus, getUrl } from '../utils'
 | 
			
		||||
import { getFavoritesCount, getNthStatus, getReblogsCount, getUrl } from '../utils'
 | 
			
		||||
import { foobarRole } from '../roles'
 | 
			
		||||
 | 
			
		||||
fixture`11-reblog-favorites-count.js`
 | 
			
		||||
| 
						 | 
				
			
			@ -9,7 +9,7 @@ test('shows favorites', async t => {
 | 
			
		|||
  await t.useRole(foobarRole)
 | 
			
		||||
    .click(getNthStatus(0))
 | 
			
		||||
    .expect(getUrl()).contains('/statuses/99549266679020981')
 | 
			
		||||
    .expect($('.status-favs-reblogs').nth(0).getAttribute('aria-label')).eql('Favorited 2 times')
 | 
			
		||||
    .expect(getFavoritesCount()).eql(2)
 | 
			
		||||
    .expect($('.icon-button[aria-label="Favorite"]').getAttribute('aria-pressed')).eql('true')
 | 
			
		||||
    .click($('.status-favs-reblogs').nth(1))
 | 
			
		||||
    .expect(getUrl()).contains('/statuses/99549266679020981/favorites')
 | 
			
		||||
| 
						 | 
				
			
			@ -23,7 +23,7 @@ test('shows boosts', async t => {
 | 
			
		|||
  await t.useRole(foobarRole)
 | 
			
		||||
    .click(getNthStatus(0))
 | 
			
		||||
    .expect(getUrl()).contains('/statuses/99549266679020981')
 | 
			
		||||
    .expect($('.status-favs-reblogs').nth(1).getAttribute('aria-label')).eql('Boosted 1 time')
 | 
			
		||||
    .expect(getReblogsCount()).eql(1)
 | 
			
		||||
    .expect($('.icon-button[aria-label="Boost"]').getAttribute('aria-pressed')).eql('false')
 | 
			
		||||
    .click($('.status-favs-reblogs').nth(0))
 | 
			
		||||
    .expect(getUrl()).contains('/statuses/99549266679020981/reblogs')
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										75
									
								
								tests/spec/12-favorite-unfavorite.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										75
									
								
								tests/spec/12-favorite-unfavorite.js
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,75 @@
 | 
			
		|||
import {
 | 
			
		||||
  getFavoritesCount,
 | 
			
		||||
  getNthFavoriteButton, getNthFavorited, getNthStatus, getUrl, homeNavButton, notificationsNavButton,
 | 
			
		||||
  scrollToBottomOfTimeline, scrollToTopOfTimeline
 | 
			
		||||
} from '../utils'
 | 
			
		||||
import { foobarRole } from '../roles'
 | 
			
		||||
 | 
			
		||||
fixture`12-favorite-unfavorite.js`
 | 
			
		||||
  .page`http://localhost:4002`
 | 
			
		||||
 | 
			
		||||
test('favorites a status', async t => {
 | 
			
		||||
  await t.useRole(foobarRole)
 | 
			
		||||
    .hover(getNthStatus(4))
 | 
			
		||||
    .expect(getNthFavorited(4)).eql('false')
 | 
			
		||||
    .click(getNthFavoriteButton(4))
 | 
			
		||||
    .expect(getNthFavorited(4)).eql('true')
 | 
			
		||||
 | 
			
		||||
  // scroll down and back up to force an unrender
 | 
			
		||||
  await scrollToBottomOfTimeline(t)
 | 
			
		||||
  await scrollToTopOfTimeline(t)
 | 
			
		||||
  await t
 | 
			
		||||
    .hover(getNthStatus(4))
 | 
			
		||||
    .expect(getNthFavorited(4)).eql('true')
 | 
			
		||||
    .click(notificationsNavButton)
 | 
			
		||||
    .click(homeNavButton)
 | 
			
		||||
    .expect(getNthFavorited(4)).eql('true')
 | 
			
		||||
    .click(notificationsNavButton)
 | 
			
		||||
    .expect(getUrl()).contains('/notifications')
 | 
			
		||||
    .click(homeNavButton)
 | 
			
		||||
    .expect(getUrl()).eql('http://localhost:4002/')
 | 
			
		||||
    .expect(getNthFavorited(4)).eql('true')
 | 
			
		||||
    .click(getNthFavoriteButton(4))
 | 
			
		||||
    .expect(getNthFavorited(4)).eql('false')
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
test('unfavorites a status', async t => {
 | 
			
		||||
  await t.useRole(foobarRole)
 | 
			
		||||
    .expect(getNthFavorited(1)).eql('true')
 | 
			
		||||
    .click(getNthFavoriteButton(1))
 | 
			
		||||
    .expect(getNthFavorited(1)).eql('false')
 | 
			
		||||
 | 
			
		||||
  // scroll down and back up to force an unrender
 | 
			
		||||
  await scrollToBottomOfTimeline(t)
 | 
			
		||||
  await scrollToTopOfTimeline(t)
 | 
			
		||||
  await t
 | 
			
		||||
    .expect(getNthFavorited(1)).eql('false')
 | 
			
		||||
    .click(notificationsNavButton)
 | 
			
		||||
    .click(homeNavButton)
 | 
			
		||||
    .expect(getNthFavorited(1)).eql('false')
 | 
			
		||||
    .click(notificationsNavButton)
 | 
			
		||||
    .navigateTo('/')
 | 
			
		||||
    .expect(getNthFavorited(1)).eql('false')
 | 
			
		||||
    .click(getNthFavoriteButton(1))
 | 
			
		||||
    .expect(getNthFavorited(1)).eql('true')
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
test('Keeps the correct count', async t => {
 | 
			
		||||
  await t.useRole(foobarRole)
 | 
			
		||||
    .hover(getNthStatus(4))
 | 
			
		||||
    .click(getNthFavoriteButton(4))
 | 
			
		||||
    .expect(getNthFavorited(4)).eql('true')
 | 
			
		||||
    .click(getNthStatus(4))
 | 
			
		||||
    .expect(getUrl()).contains('/status')
 | 
			
		||||
    .expect(getNthFavorited(0)).eql('true')
 | 
			
		||||
    .expect(getFavoritesCount()).eql(2)
 | 
			
		||||
    .click(homeNavButton)
 | 
			
		||||
    .expect(getUrl()).eql('http://localhost:4002/')
 | 
			
		||||
    .hover(getNthStatus(4))
 | 
			
		||||
    .click(getNthFavoriteButton(4))
 | 
			
		||||
    .expect(getNthFavorited(4)).eql('false')
 | 
			
		||||
    .click(getNthStatus(4))
 | 
			
		||||
    .expect(getUrl()).contains('/status')
 | 
			
		||||
    .expect(getNthFavorited(0)).eql('false')
 | 
			
		||||
    .expect(getFavoritesCount()).eql(1)
 | 
			
		||||
})
 | 
			
		||||
| 
						 | 
				
			
			@ -1,10 +1,23 @@
 | 
			
		|||
import { ClientFunction as exec, Selector as $ } from 'testcafe'
 | 
			
		||||
 | 
			
		||||
const SCROLL_INTERVAL = 3
 | 
			
		||||
 | 
			
		||||
export const settingsButton = $('nav a[aria-label=Settings]')
 | 
			
		||||
export const instanceInput = $('#instanceInput')
 | 
			
		||||
export const addInstanceButton = $('.add-new-instance button')
 | 
			
		||||
export const modalDialogContents = $('.modal-dialog-contents')
 | 
			
		||||
export const closeDialogButton = $('.close-dialog-button')
 | 
			
		||||
export const notificationsNavButton = $('nav a[href="/notifications"]')
 | 
			
		||||
export const homeNavButton = $('nav a[href="/"]')
 | 
			
		||||
export const formError = $('.form-error-user-error')
 | 
			
		||||
 | 
			
		||||
export const favoritesCountElement = $('.status-favs-reblogs:nth-child(3)').addCustomDOMProperties({
 | 
			
		||||
  innerCount: el => parseInt(el.innerText, 10)
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
export const reblogsCountElement = $('.status-favs-reblogs:nth-child(2)').addCustomDOMProperties({
 | 
			
		||||
  innerCount: el => parseInt(el.innerText, 10)
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
export const getUrl = exec(() => window.location.href)
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -15,11 +28,31 @@ export const getActiveElementClass = exec(() =>
 | 
			
		|||
export const goBack = exec(() => window.history.back())
 | 
			
		||||
 | 
			
		||||
export function getNthStatus (n) {
 | 
			
		||||
  return $(`[aria-hidden="false"] > article[aria-posinset="${n}"]`)
 | 
			
		||||
  return $(`div[aria-hidden="false"] > article[aria-posinset="${n}"]`)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function getLastVisibleStatus () {
 | 
			
		||||
  return $(`[aria-hidden="false"] > article[aria-posinset]`).nth(-1)
 | 
			
		||||
  return $(`div[aria-hidden="false"] > article[aria-posinset]`).nth(-1)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function getFirstVisibleStatus () {
 | 
			
		||||
  return $(`div[aria-hidden="false"] > article[aria-posinset]`).nth(0)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function getNthFavoriteButton (n) {
 | 
			
		||||
  return getNthStatus(n).find('.status-toolbar button:nth-child(3)')
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function getNthFavorited (n) {
 | 
			
		||||
  return getNthFavoriteButton(n).getAttribute('aria-pressed')
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function getFavoritesCount () {
 | 
			
		||||
  return favoritesCountElement.innerCount
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function getReblogsCount () {
 | 
			
		||||
  return reblogsCountElement.innerCount
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function validateTimeline (t, timeline) {
 | 
			
		||||
| 
						 | 
				
			
			@ -47,23 +80,47 @@ export async function validateTimeline (t, timeline) {
 | 
			
		|||
    }
 | 
			
		||||
 | 
			
		||||
    // hovering forces TestCafé to scroll to that element: https://git.io/vABV2
 | 
			
		||||
    if (i % 3 === 2) { // only scroll every nth element
 | 
			
		||||
    if (i % SCROLL_INTERVAL === (SCROLL_INTERVAL - 1)) { // only scroll every nth element
 | 
			
		||||
      await t.hover(getNthStatus(i))
 | 
			
		||||
        .expect($('.loading-footer').exist).notOk()
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function scrollToBottomOfTimeline (t) {
 | 
			
		||||
  let lastSize = null
 | 
			
		||||
export async function scrollTimelineUp (t) {
 | 
			
		||||
  let oldFirstItem = await getFirstVisibleStatus().getAttribute('aria-posinset')
 | 
			
		||||
  await t.hover(getFirstVisibleStatus())
 | 
			
		||||
  let newFirstItem
 | 
			
		||||
  while (true) {
 | 
			
		||||
    await t.hover(getLastVisibleStatus())
 | 
			
		||||
      .expect($('.loading-footer').exist).notOk()
 | 
			
		||||
    let newSize = await getLastVisibleStatus().getAttribute('aria-setsize')
 | 
			
		||||
    if (newSize === lastSize) {
 | 
			
		||||
    newFirstItem = await getFirstVisibleStatus().getAttribute('aria-posinset')
 | 
			
		||||
    if (newFirstItem === '0' || newFirstItem !== oldFirstItem) {
 | 
			
		||||
      break
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function scrollToTopOfTimeline (t) {
 | 
			
		||||
  let i = await getFirstVisibleStatus().getAttribute('aria-posinset')
 | 
			
		||||
  while (true) {
 | 
			
		||||
    await t.hover(getNthStatus(i))
 | 
			
		||||
      .expect($('.loading-footer').exist).notOk()
 | 
			
		||||
    i -= SCROLL_INTERVAL
 | 
			
		||||
    if (i <= 0) {
 | 
			
		||||
      break
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function scrollToBottomOfTimeline (t) {
 | 
			
		||||
  let i = 0
 | 
			
		||||
  while (true) {
 | 
			
		||||
    await t.hover(getNthStatus(i))
 | 
			
		||||
      .expect($('.loading-footer').exist).notOk()
 | 
			
		||||
    let size = await getNthStatus(i).getAttribute('aria-setsize')
 | 
			
		||||
    i += SCROLL_INTERVAL
 | 
			
		||||
    if (i >= size - 1) {
 | 
			
		||||
      break
 | 
			
		||||
    }
 | 
			
		||||
    lastSize = newSize
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		
		Reference in a new issue