From 4f999e02ddc14257f0b4e9342717a9e7b25fb100 Mon Sep 17 00:00:00 2001 From: Louis Chemineau Date: Tue, 11 Apr 2023 18:44:26 +0200 Subject: Restructure timeline internal to allow liking from single status view Signed-off-by: Louis Chemineau --- src/components/TimelinePost.vue | 4 +- src/store/timeline.js | 260 +++++++++++++++++++++------------------ src/views/TimelineSinglePost.vue | 11 +- 3 files changed, 147 insertions(+), 128 deletions(-) (limited to 'src') diff --git a/src/components/TimelinePost.vue b/src/components/TimelinePost.vue index 5d504788..ed2a067f 100644 --- a/src/components/TimelinePost.vue +++ b/src/components/TimelinePost.vue @@ -236,7 +236,7 @@ export default { }, boost() { const params = { - post: this.item, + status: this.item, parentAnnounce: this.reblog, } if (this.isBoosted) { @@ -250,7 +250,7 @@ export default { }, like() { const params = { - post: this.item, + status: this.item, parentAnnounce: this.reblog, } if (this.isLiked) { diff --git a/src/store/timeline.js b/src/store/timeline.js index 2d0ef85d..e972c0a1 100644 --- a/src/store/timeline.js +++ b/src/store/timeline.js @@ -33,13 +33,17 @@ import logger from '../services/logger.js' const state = { /** - * @type {Object} timeline - The posts' collection + * @type {Object} List of locally known statuses */ - timeline: {}, + statuses: {}, /** - * @type {Object} timeline - The parents posts' collection + * @type {string[]} timeline - The statuses' collection */ - parentsTimeline: {}, + timeline: [], + /** + * @type {string[]} parentsTimeline - The parents statuses' collection + */ + parentsTimeline: [], /** * @type {string} type - Timeline's type: 'home', 'single-post',... */ @@ -49,6 +53,7 @@ const state = { * @property {string} params.account ??? * @property {string} params.id * @property {string} params.type ??? + * @property {string?} params.singlePost ??? */ params: {}, /** @@ -66,43 +71,63 @@ const state = { /** @type {import('vuex').MutationTree} */ const mutations = { + /** + * @param state + * @param {import ('../types/Mastodon.js').Status} status + */ + addToStatuses(state, status) { + Vue.set(state.statuses, status.id, status) + }, /** * @param state * @param {import ('../types/Mastodon.js').Status[]|import('../types/Mastodon.js').Context} data */ addToTimeline(state, data) { if (Array.isArray(data)) { - data.forEach((post) => Vue.set(state.timeline, post.id, post)) + data.forEach(status => Vue.set(state.statuses, status.id, status)) + data + .filter(status => state.timeline.indexOf(status.id) === -1) + .forEach(status => state.timeline.push(status.id)) } else { - data.descendants.forEach((post) => Vue.set(state.timeline, post.id, post)) - data.ancestors.forEach((post) => Vue.set(state.parentsTimeline, post.id, post)) + data.descendants.forEach(status => Vue.set(state.statuses, status.id, status)) + data.ancestors.forEach(status => Vue.set(state.statuses, status.id, status)) + + data.descendants + .filter(status => state.timeline.indexOf(status.id) === -1) + .forEach(status => state.timeline.push(status.id)) + data.ancestors + .filter(status => state.parentsTimeline.indexOf(status.id) === -1) + .forEach(status => state.parentsTimeline.push(status.id)) } }, /** * @param state - * @param {import ('../types/Mastodon.js').Status[]|import('../types/Mastodon.js').Context} data + * @param {import ('../types/Mastodon.js').Status[]} data */ updateInTimelines(state, data) { - data.forEach((post) => { - if (state.timeline[post.id] !== undefined) { - Vue.set(state.timeline, post.id, post) - } - - if (state.parentsTimeline[post.id] !== undefined) { - Vue.set(state.parentsTimeline, post.id, post) + data.forEach((status) => { + if (state.statuses[status.id] !== undefined) { + Vue.set(state.statuses, status.id, status) } }) }, /** * @param state - * @param {import('../types/Mastodon.js').Status} post + * @param {import('../types/Mastodon.js').Status} status */ - removePost(state, post) { - Vue.delete(state.timeline, post.id) + removeStatusf(state, status) { + const timelineIndex = state.timeline.indexOf(status.id) + if (timelineIndex !== -1) { + state.timeline.splice(timelineIndex, 1) + } + const parentsTimelineIndex = state.parentsTimeline.indexOf(status.id) + if (timelineIndex !== -1) { + state.parentsTimeline.splice(parentsTimelineIndex, 1) + } }, resetTimeline(state) { - state.timeline = {} - state.parentsTimeline = {} + state.timeline = [] + state.parentsTimeline = [] }, /** * @param state @@ -131,53 +156,41 @@ const mutations = { /** * @param state * @param {object} root0 - * @param {import('../types/Mastodon.js').Status} root0.post + * @param {import('../types/Mastodon.js').Status} root0.status */ - likePost(state, { post }) { - if (state.timeline[post.id] !== undefined) { - Vue.set(state.timeline[post.id], 'favourited', true) - } - if (post.reblog !== null && state.timeline[post.reblog.id] !== undefined) { - Vue.set(state.timeline[post.reblog.id], 'favourited', true) + likeStatus(state, { status }) { + if (state.statuses[status.id] !== undefined) { + Vue.set(state.statuses[status.id], 'favourited', true) } }, /** * @param state * @param {object} root0 - * @param {import('../types/Mastodon.js').Status} root0.post + * @param {import('../types/Mastodon.js').Status} root0.status */ - unlikePost(state, { post }) { - if (state.timeline[post.id] !== undefined) { - Vue.set(state.timeline[post.id], 'favourited', false) - } - if (post.reblog !== null && state.timeline[post.reblog.id] !== undefined) { - Vue.set(state.timeline[post.reblog.id], 'favourited', false) + unlikeStatus(state, { status }) { + if (state.statuses[status.id] !== undefined) { + Vue.set(state.statuses[status.id], 'favourited', false) } }, /** * @param state * @param {object} root0 - * @param {import('../types/Mastodon.js').Status} root0.post + * @param {import('../types/Mastodon.js').Status} root0.status */ - boostPost(state, { post }) { - if (state.timeline[post.id] !== undefined) { - Vue.set(state.timeline[post.id], 'reblogged', true) - } - if (post.reblog !== null && state.timeline[post.reblog.id] !== undefined) { - Vue.set(state.timeline[post.reblog.id], 'reblogged', true) + boostStatus(state, { status }) { + if (state.statuses[status.id] !== undefined) { + Vue.set(state.statuses[status.id], 'reblogged', true) } }, /** * @param state * @param {object} root0 - * @param {import('../types/Mastodon.js').Status} root0.post + * @param {import('../types/Mastodon.js').Status} root0.status */ - unboostPost(state, { post }) { - if (state.timeline[post.id] !== undefined) { - Vue.set(state.timeline[post.id], 'reblogged', false) - } - if (post.reblog !== null && state.timeline[post.reblog.id] !== undefined) { - Vue.set(state.timeline[post.reblog.id], 'reblogged', false) + unboostStatus(state, { status }) { + if (state.statuses[status.id] !== undefined) { + Vue.set(state.statuses[status.id], 'reblogged', false) } }, } @@ -188,23 +201,24 @@ const getters = { return state.composerDisplayStatus }, getTimeline(state) { - return Object.values(state.timeline).sort(function (a, b) { - return new Date(b.created_at).getTime() - new Date(a.created_at).getTime() - }) + return state.timeline + .map(statusId => state.statuses[statusId]) + .sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime()) }, getParentsTimeline(state) { - return Object.values(state.parentsTimeline).sort(function (a, b) { - return new Date(b.created_at).getTime() - new Date(a.created_at).getTime() - }) + return state.parentsTimeline + .map(statusId => state.statuses[statusId]) + .sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime()) + }, + getSinglePost(state) { + return state.statuses[state.params.singlePost] }, getPostFromTimeline(state) { - return (postId) => { - if (state.timeline[postId] !== undefined) { - return state.timeline[postId] - } else if (state.parentsTimeline[postId] !== undefined) { - return state.parentsTimeline[postId] + return (/** @type {string} */ statusId) => { + if (state.statuses[statusId] !== undefined) { + return state.statuses[statusId] } else { - logger.warn('Could not find post in timeline', { postId }) + logger.warn('Could not find status in timeline', { statusId }) } } }, @@ -249,108 +263,108 @@ const actions = { }, /** * @param context - * @param {import('../types/Mastodon.js').Status} post + * @param {import('../types/Mastodon.js').Status} status */ - async post(context, post) { + async post(context, status) { try { - const { data } = await axios.post(generateUrl('apps/social/api/v1/statuses'), post) + const { data } = await axios.post(generateUrl('apps/social/api/v1/statuses'), status) logger.info('Post created', data.id) } catch (error) { - showError('Failed to create a post') - logger.error('Failed to create a post', { error }) + showError('Failed to create a status') + logger.error('Failed to create a status', { error }) } }, /** * @param context - * @param {import('../types/Mastodon.js').Status} post + * @param {import('../types/Mastodon.js').Status} status */ - async postDelete(context, post) { + async postDelete(context, status) { try { - context.commit('removePost', post) - const response = await axios.delete(generateUrl(`apps/social/api/v1/post?id=${post.uri}`)) + context.commit('removeStatusf', status) + const response = await axios.delete(generateUrl(`apps/social/api/v1/post?id=${status.uri}`)) logger.info('Post deleted with token ' + response.data.result.token) } catch (error) { - context.commit('updateInTimelines', [post]) - showError('Failed to delete the post') - logger.error('Failed to delete the post', { error }) + context.commit('updateInTimelines', [status]) + showError('Failed to delete the status') + logger.error('Failed to delete the status', { error }) } }, /** * @param context * @param {object} root0 - * @param {import('../types/Mastodon.js').Status} root0.post + * @param {import('../types/Mastodon.js').Status} root0.status */ - async postLike(context, { post }) { + async postLike(context, { status }) { try { - context.commit('likePost', { post }) - const response = await axios.post(generateUrl(`apps/social/api/v1/statuses/${post.id}/favourite`)) + context.commit('likeStatus', { status }) + const response = await axios.post(generateUrl(`apps/social/api/v1/statuses/${status.id}/favourite`)) logger.info('Post liked') context.commit('updateInTimelines', [response.data]) return response } catch (error) { - context.commit('unlikePost', { post }) - showError('Failed to like post') - logger.error('Failed to like post', { error }) + context.commit('unlikeStatus', { status }) + showError('Failed to like status') + logger.error('Failed to like status', { error }) } }, /** * @param context * @param {object} root0 - * @param {import('../types/Mastodon.js').Status} root0.post + * @param {import('../types/Mastodon.js').Status} root0.status */ - async postUnlike(context, { post }) { + async postUnlike(context, { status }) { try { - // Remove post from list if we are in the 'liked' timeline + // Remove status from list if we are in the 'liked' timeline if (state.type === 'liked') { - context.commit('removePost', post) + context.commit('removeStatusf', status) } - context.commit('unlikePost', { post }) - const response = await axios.post(generateUrl(`apps/social/api/v1/statuses/${post.id}/unfavourite`)) + context.commit('unlikeStatus', { status }) + const response = await axios.post(generateUrl(`apps/social/api/v1/statuses/${status.id}/unfavourite`)) logger.info('Post unliked') context.commit('updateInTimelines', [response.data]) return response } catch (error) { - // Readd post from list if we are in the 'liked' timeline + // Readd status from list if we are in the 'liked' timeline if (state.type === 'liked') { - context.commit('addToTimeline', [post]) + context.commit('addToTimeline', [status]) } - context.commit('likePost', { post }) - showError('Failed to unlike post') - logger.error('Failed to unlike post', { error }) + context.commit('likeStatus', { status }) + showError('Failed to unlike status') + logger.error('Failed to unlike status', { error }) } }, /** * @param context * @param {object} root0 - * @param {import('../types/Mastodon.js').Status} root0.post + * @param {import('../types/Mastodon.js').Status} root0.status */ - async postBoost(context, { post }) { + async postBoost(context, { status }) { try { - context.commit('boostPost', { post }) - const response = await axios.post(generateUrl(`apps/social/api/v1/statuses/${post.id}/reblog`)) + context.commit('boostStatus', { status }) + const response = await axios.post(generateUrl(`apps/social/api/v1/statuses/${status.id}/reblog`)) logger.info('Post boosted') context.commit('updateInTimelines', [response.data]) return response } catch (error) { - context.commit('unboostPost', { post }) - showError('Failed to create a boost post') - logger.error('Failed to create a boost post', { error }) + context.commit('unboostStatus', { status }) + showError('Failed to create a boost status') + logger.error('Failed to create a boost status', { error }) } }, /** * @param context * @param {object} root0 - * @param {import('../types/Mastodon.js').Status} root0.post + * @param {import('../types/Mastodon.js').Status} root0.status */ - async postUnBoost(context, { post }) { + async postUnBoost(context, { status }) { try { - context.commit('unboostPost', { post }) - const response = await axios.post(generateUrl(`apps/social/api/v1/statuses/${post.id}/unreblog`)) + context.commit('unboostStatus', { status }) + const response = await axios.post(generateUrl(`apps/social/api/v1/statuses/${status.id}/unreblog`)) logger.info('Boost deleted') context.commit('updateInTimelines', [response.data]) return response } catch (error) { - context.commit('boostPost', { post }) + context.commit('boostStatus', { status }) showError('Failed to delete the boost') logger.error('Failed to delete the boost', { error }) } @@ -377,27 +391,27 @@ const actions = { // Compute URL to get the data let url = '' switch (state.type) { - case 'account': - url = generateUrl(`apps/social/api/v1/accounts/${state.account}/statuses`) - break - case 'tags': - url = generateUrl(`apps/social/api/v1/timelines/tag/${state.params.tag}`) - break - case 'single-post': - url = generateUrl(`apps/social/api/v1/statuses/${state.params.id}/context`) - break - case 'timeline': - url = generateUrl('apps/social/api/v1/timelines/public') - params.local = true - break - case 'federated': - url = generateUrl('apps/social/api/v1/timelines/public') - break - case 'notifications': - url = generateUrl('apps/social/api/v1/notifications') - break - default: - url = generateUrl(`apps/social/api/v1/timelines/${state.type}`) + case 'account': + url = generateUrl(`apps/social/api/v1/accounts/${state.account}/statuses`) + break + case 'tags': + url = generateUrl(`apps/social/api/v1/timelines/tag/${state.params.tag}`) + break + case 'single-post': + url = generateUrl(`apps/social/api/v1/statuses/${state.params.id}/context`) + break + case 'timeline': + url = generateUrl('apps/social/api/v1/timelines/public') + params.local = true + break + case 'federated': + url = generateUrl('apps/social/api/v1/timelines/public') + break + case 'notifications': + url = generateUrl('apps/social/api/v1/notifications') + break + default: + url = generateUrl(`apps/social/api/v1/timelines/${state.type}`) } // Get the data and add them to the timeline diff --git a/src/views/TimelineSinglePost.vue b/src/views/TimelineSinglePost.vue index ac43fbf8..f7e4b45f 100644 --- a/src/views/TimelineSinglePost.vue +++ b/src/views/TimelineSinglePost.vue @@ -7,7 +7,7 @@ :reverse-order="true" /> @@ -37,11 +37,14 @@ export default { ], data() { return { - mainPost: {}, uid: this.$route.params.account, } }, computed: { + /** @return {Status?} */ + singlePost() { + return this.$store.getters.getSinglePost + }, /** * @description Tells whether Composer shall be displayed or not * @return {boolean} @@ -80,15 +83,17 @@ export default { }, }, async beforeMount() { - this.mainPost = this.$store.getters.getPostFromTimeline(this.$route.params.id) || loadState('social', 'item') + const singlePost = this.$store.getters.getPostFromTimeline(this.$route.params.id) || loadState('social', 'item') // Fetch single post timeline + this.$store.commit('addToStatuses', singlePost) this.$store.dispatch('changeTimelineType', { type: 'single-post', params: { account: this.account, id: this.$route.params.id, type: 'single-post', + singlePost: this.$route.params.id || loadState('social', 'item').id, }, }) -- cgit v1.2.3