From 00ccf3577755ef454c437a1eea889ca32d2cec74 Mon Sep 17 00:00:00 2001 From: Nolan Lawson Date: Sat, 24 Feb 2018 18:20:33 -0800 Subject: [PATCH] add reblogging/unreblogging --- routes/_actions/favorite.js | 4 +- routes/_actions/reblog.js | 27 +++++++ routes/_api/reblog.js | 12 +++ routes/_components/status/StatusToolbar.html | 42 ++++++---- routes/_database/timelines.js | 8 ++ routes/_utils/ajax.js | 2 +- ...nfavorite.js => 30-favorite-unfavorite.js} | 4 +- tests/spec/31-reblog-unreblog.js | 78 +++++++++++++++++++ tests/utils.js | 8 ++ 9 files changed, 167 insertions(+), 18 deletions(-) create mode 100644 routes/_actions/reblog.js create mode 100644 routes/_api/reblog.js rename tests/spec/{12-favorite-unfavorite.js => 30-favorite-unfavorite.js} (96%) create mode 100644 tests/spec/31-reblog-unreblog.js diff --git a/routes/_actions/favorite.js b/routes/_actions/favorite.js index 60959e3b..c809159e 100644 --- a/routes/_actions/favorite.js +++ b/routes/_actions/favorite.js @@ -5,7 +5,7 @@ import { toast } from '../_utils/toast' export async function setFavorited (statusId, favorited) { if (!store.get('online')) { - toast.say('You cannot favorite or unfavorite while offline.') + toast.say(`You cannot ${favorited ? 'favorite' : 'unfavorite'} while offline.`) return } let instanceName = store.get('currentInstance') @@ -22,6 +22,6 @@ export async function setFavorited (statusId, favorited) { store.set({statusModifications: statusModifications}) } catch (e) { console.error(e) - toast.say('Failed to favorite or unfavorite. ' + (e.message || '')) + toast.say(`Failed to ${favorited ? 'favorite' : 'unfavorite'}. ` + (e.message || '')) } } diff --git a/routes/_actions/reblog.js b/routes/_actions/reblog.js new file mode 100644 index 00000000..0069dac6 --- /dev/null +++ b/routes/_actions/reblog.js @@ -0,0 +1,27 @@ +import { store } from '../_store/store' +import { database } from '../_database/database' +import { toast } from '../_utils/toast' +import { reblogStatus, unreblogStatus } from '../_api/reblog' + +export async function setReblogged (statusId, reblogged) { + if (!store.get('online')) { + toast.say(`You cannot ${reblogged ? 'boost' : 'unboost'} while offline.`) + return + } + let instanceName = store.get('currentInstance') + let accessToken = store.get('accessToken') + try { + await (reblogged + ? reblogStatus(instanceName, accessToken, statusId) + : unreblogStatus(instanceName, accessToken, statusId)) + await database.setStatusReblogged(instanceName, statusId, reblogged) + let statusModifications = store.get('statusModifications') + let currentStatusModifications = statusModifications[instanceName] = + (statusModifications[instanceName] || {favorites: {}, reblogs: {}}) + currentStatusModifications.reblogs[statusId] = reblogged + store.set({statusModifications: statusModifications}) + } catch (e) { + console.error(e) + toast.say(`Failed to ${reblogged ? 'boost' : 'unboost'}. ` + (e.message || '')) + } +} diff --git a/routes/_api/reblog.js b/routes/_api/reblog.js new file mode 100644 index 00000000..31dcac6e --- /dev/null +++ b/routes/_api/reblog.js @@ -0,0 +1,12 @@ +import { post } from '../_utils/ajax' +import { basename, auth } from './utils' + +export async function reblogStatus (instanceName, accessToken, statusId) { + let url = `${basename(instanceName)}/api/v1/statuses/${statusId}/reblog` + return post(url, null, auth(accessToken)) +} + +export async function unreblogStatus (instanceName, accessToken, statusId) { + let url = `${basename(instanceName)}/api/v1/statuses/${statusId}/unreblog` + return post(url, null, auth(accessToken)) +} diff --git a/routes/_components/status/StatusToolbar.html b/routes/_components/status/StatusToolbar.html index 436fb233..bbda2199 100644 --- a/routes/_components/status/StatusToolbar.html +++ b/routes/_components/status/StatusToolbar.html @@ -4,11 +4,13 @@ href="#fa-reply" /> status.visibility, - boostLabel: (visibility) => { + reblogLabel: (visibility) => { switch (visibility) { case 'private': return 'Cannot be boosted because this is followers-only' @@ -73,7 +82,7 @@ return 'Boost' } }, - boostIcon: (visibility) => { + reblogIcon: (visibility) => { switch (visibility) { case 'private': return '#fa-lock' @@ -83,9 +92,15 @@ return '#fa-retweet' } }, - boostDisabled: (visibility) => { + reblogDisabled: (visibility) => { return visibility === 'private' || visibility === 'direct' }, + reblogged: (status, $currentStatusModifications) => { + if ($currentStatusModifications && status.id in $currentStatusModifications.reblogs) { + return $currentStatusModifications.reblogs[status.id] + } + return status.reblogged + }, favorited: (status, $currentStatusModifications) => { if ($currentStatusModifications && status.id in $currentStatusModifications.favorites) { return $currentStatusModifications.favorites[status.id] @@ -93,7 +108,8 @@ return status.favourited }, statusId: (status) => status.id, - favoriteKey: (statusId, timelineType, timelineValue) => `fav-${timelineType}-${timelineValue}-${statusId}` + favoriteKey: (statusId, timelineType, timelineValue) => `fav-${timelineType}-${timelineValue}-${statusId}`, + reblogKey: (statusId, timelineType, timelineValue) => `reblog-${timelineType}-${timelineValue}-${statusId}`, } } \ No newline at end of file diff --git a/routes/_database/timelines.js b/routes/_database/timelines.js index f4294c94..12bd4766 100644 --- a/routes/_database/timelines.js +++ b/routes/_database/timelines.js @@ -416,3 +416,11 @@ export async function setStatusFavorited (instanceName, statusId, favorited) { status.favourites_count = (status.favourites_count || 0) + delta }) } + +export async function setStatusReblogged (instanceName, statusId, reblogged) { + return updateStatus(instanceName, statusId, status => { + let delta = (reblogged ? 1 : 0) - (status.reblogged ? 1 : 0) + status.reblogged = reblogged + status.reblogs_count = (status.reblogs_count || 0) + delta + }) +} diff --git a/routes/_utils/ajax.js b/routes/_utils/ajax.js index 162f5867..533430f2 100644 --- a/routes/_utils/ajax.js +++ b/routes/_utils/ajax.js @@ -7,7 +7,7 @@ function fetchWithTimeout (url, options) { }) } -async function throwErrorIfInvalidResponse(response) { +async function throwErrorIfInvalidResponse (response) { let json = await response.json() if (response.status >= 200 && response.status < 300) { return json diff --git a/tests/spec/12-favorite-unfavorite.js b/tests/spec/30-favorite-unfavorite.js similarity index 96% rename from tests/spec/12-favorite-unfavorite.js rename to tests/spec/30-favorite-unfavorite.js index 0fae5b30..b52f5375 100644 --- a/tests/spec/12-favorite-unfavorite.js +++ b/tests/spec/30-favorite-unfavorite.js @@ -5,7 +5,7 @@ import { } from '../utils' import { foobarRole } from '../roles' -fixture`12-favorite-unfavorite.js` +fixture`30-favorite-unfavorite.js` .page`http://localhost:4002` test('favorites a status', async t => { @@ -54,7 +54,7 @@ test('unfavorites a status', async t => { .expect(getNthFavorited(1)).eql('true') }) -test('Keeps the correct count', async t => { +test('Keeps the correct favorites count', async t => { await t.useRole(foobarRole) .hover(getNthStatus(4)) .click(getNthFavoriteButton(4)) diff --git a/tests/spec/31-reblog-unreblog.js b/tests/spec/31-reblog-unreblog.js new file mode 100644 index 00000000..3dee8be3 --- /dev/null +++ b/tests/spec/31-reblog-unreblog.js @@ -0,0 +1,78 @@ +import { + getNthReblogButton, getNthReblogged, getNthStatus, getReblogsCount, getUrl, homeNavButton, + notificationsNavButton, + scrollToBottomOfTimeline, scrollToTopOfTimeline +} from '../utils' +import { foobarRole } from '../roles' + +fixture`31-reblog-unreblog.js` + .page`http://localhost:4002` + +test('reblogs a status', async t => { + await t.useRole(foobarRole) + .hover(getNthStatus(0)) + .expect(getNthReblogged(0)).eql('false') + .click(getNthReblogButton(0)) + .expect(getNthReblogged(0)).eql('true') + + // scroll down and back up to force an unrender + await scrollToBottomOfTimeline(t) + await scrollToTopOfTimeline(t) + await t + .hover(getNthStatus(0)) + .expect(getNthReblogged(0)).eql('true') + .click(notificationsNavButton) + .click(homeNavButton) + .expect(getNthReblogged(0)).eql('true') + .click(notificationsNavButton) + .expect(getUrl()).contains('/notifications') + .click(homeNavButton) + .expect(getUrl()).eql('http://localhost:4002/') + .expect(getNthReblogged(0)).eql('true') + .click(getNthReblogButton(0)) + .expect(getNthReblogged(0)).eql('false') +}) + +test('unreblogs a status', async t => { + await t.useRole(foobarRole) + .hover(getNthStatus(4)) + .expect(getNthReblogged(4)).eql('false') + .click(getNthReblogButton(4)) + .expect(getNthReblogged(4)).eql('true') + .click(getNthReblogButton(4)) + .expect(getNthReblogged(4)).eql('false') + + // scroll down and back up to force an unrender + await scrollToBottomOfTimeline(t) + await scrollToTopOfTimeline(t) + await t + .hover(getNthStatus(4)) + .expect(getNthReblogged(4)).eql('false') + .click(notificationsNavButton) + .click(homeNavButton) + .expect(getNthReblogged(4)).eql('false') + .click(notificationsNavButton) + .navigateTo('/') + .expect(getNthReblogged(4)).eql('false') + .click(getNthReblogButton(4)) + .expect(getNthReblogged(4)).eql('true') +}) + +test('Keeps the correct reblogs count', async t => { + await t.useRole(foobarRole) + .hover(getNthStatus(4)) + .expect(getNthReblogged(4)).eql('true') + .click(getNthStatus(4)) + .expect(getUrl()).contains('/status') + .expect(getNthReblogged(0)).eql('true') + .expect(getReblogsCount()).eql(2) + .click(homeNavButton) + .expect(getUrl()).eql('http://localhost:4002/') + .hover(getNthStatus(4)) + .click(getNthReblogButton(4)) + .expect(getNthReblogged(4)).eql('false') + .click(getNthStatus(4)) + .expect(getUrl()).contains('/status') + .expect(getNthReblogged(0)).eql('false') + .expect(getReblogsCount()).eql(1) +}) diff --git a/tests/utils.js b/tests/utils.js index bdfc7040..a573cd7f 100644 --- a/tests/utils.js +++ b/tests/utils.js @@ -51,6 +51,14 @@ export function getFavoritesCount () { return favoritesCountElement.innerCount } +export function getNthReblogButton (n) { + return getNthStatus(n).find('.status-toolbar button:nth-child(2)') +} + +export function getNthReblogged (n) { + return getNthReblogButton(n).getAttribute('aria-pressed') +} + export function getReblogsCount () { return reblogsCountElement.innerCount }