From e0a5119ca73c5901c2c5bd3253659052d7e2bbb3 Mon Sep 17 00:00:00 2001 From: vickyyo Date: Sat, 28 Oct 2023 19:04:32 +0530 Subject: [PATCH] Highlights comments. #783 --- src/constants.js | 5 +- src/datastores/handlers/base.js | 116 ++++++++++++++++- src/datastores/handlers/electron.js | 32 ++++- src/main/index.js | 14 +- .../watch-video-comments.css | 8 ++ .../watch-video-comments.js | 108 +++++++++++++++- .../watch-video-comments.vue | 23 +++- src/renderer/helpers/api/local.js | 3 +- .../store/modules/highlighted-comments.js | 121 ++++++++++++++++-- static/locales/en-US.yaml | 1 + 10 files changed, 401 insertions(+), 30 deletions(-) diff --git a/src/constants.js b/src/constants.js index db7eb3af6bb6c..b264df064b72c 100644 --- a/src/constants.js +++ b/src/constants.js @@ -52,7 +52,10 @@ const DBActions = { }, HIGHLIGHTED_COMMENTS: { - UPSERT_COMMENT: 'db-action-highlighted-comments-upsert-highlighted-comment-by-video-id' + UPSERT_COMMENT: 'db-action-highlighted-comments-upsert-comment', + DELETE_COMMENT: 'db-action-higlighted-comments-delete-comment', + UPSERT_REPLY: 'db-action-highlighted-comments-upsert-reply', + DELETE_REPLY: 'db-action-higlighted-comments-delete-reply' } } diff --git a/src/datastores/handlers/base.js b/src/datastores/handlers/base.js index 951607297527d..0014c49f4d04f 100644 --- a/src/datastores/handlers/base.js +++ b/src/datastores/handlers/base.js @@ -178,12 +178,116 @@ class HighlightedComments { return db.highlightedComments.findAsync({}) } - static upsertHighlightedCommentByVideoId(comment, videoId) { - return db.highlightedComments.updateAsync( - { _id: videoId }, - { $push: { comments: comment } }, - { upsert: true } - ) + static async upsertHighlightedComment(videoId, comment) { + const highlightedComment = await db.highlightedComments.findOneAsync( + { _id: videoId }) + let commentIndex = -1 + if (highlightedComment !== null) { + commentIndex = highlightedComment.comments.findIndex(c => { + return JSON.parse(c.comment).commentId === JSON.parse(comment).commentId + }) + } + if (commentIndex !== -1) { + // Update the 'isCommentHighlighted' field of the found comment + return db.highlightedComments.updateAsync( + { _id: videoId }, + { $set: { [`comments.${commentIndex}.isCommentHighlighted`]: true } }, + {} + ) + } else { + // Append the comment to the 'comments' array. + return db.highlightedComments.updateAsync( + { _id: videoId }, + { $push: { comments: { comment: comment, replies: [], isCommentHighlighted: true } } }, + { upsert: true } + ) + } + } + + static async deleteHighlightedComment(videoId, comment) { + const highlightedComment = await db.highlightedComments.findOneAsync( + { _id: videoId }) + let commentIndex = -1 + if (highlightedComment !== null) { + commentIndex = highlightedComment.comments.findIndex(c => { + return JSON.parse(c.comment).commentId === JSON.parse(comment).commentId + }) + } + if (commentIndex !== -1) { + const hasReplies = highlightedComment.comments[commentIndex].replies.length > 0 + if (hasReplies) { + // Update 'isCommentHighlighted' to false if replies are non-empty + return db.highlightedComments.updateAsync( + { _id: videoId }, + { $set: { [`comments.${commentIndex}.isCommentHighlighted`]: false } }, + {} + ) + } else { + // Remove the entire entry from the 'comments' array if replies are empty + highlightedComment.comments.splice(commentIndex, 1) + db.highlightedComments.updateAsync( + { _id: videoId }, + highlightedComment, + {} + ) + } + } + } + + static async upsertHighlightedReply(videoId, comment, reply) { + const highlightedComment = await db.highlightedComments.findOneAsync({ _id: videoId }) + let commentIndex = -1 + if (highlightedComment !== null) { + commentIndex = highlightedComment.comments.findIndex(c => { + return JSON.parse(c.comment).commentId === JSON.parse(comment).commentId + }) + } + if (commentIndex === -1) { + // Append a new entry to the comments array + return db.highlightedComments.updateAsync( + { _id: videoId }, + { $push: { comments: { comment: comment, replies: [reply], isCommentHighlighted: false } } }, + { upsert: true } + ) + } else { + // Append the reply to found comment's replies entry + return db.highlightedComments.updateAsync( + { _id: videoId }, + { $push: { [`comments.${commentIndex}.replies`]: reply } }, + { upsert: true } + ) + } + } + + static async deleteHighlightedReply(videoId, comment, reply) { + const highlightedComment = await db.highlightedComments.findOneAsync({ _id: videoId }) + let commentIndex = -1 + if (highlightedComment !== null) { + commentIndex = highlightedComment.comments.findIndex(c => { + return JSON.parse(c.comment).commentId === JSON.parse(comment).commentId + }) + } + if (commentIndex !== -1) { + const moreReplies = highlightedComment.comments[commentIndex].replies.length > 1 + const isCommentHighlighted = highlightedComment.comments[commentIndex].isCommentHighlighted + if (!moreReplies && !isCommentHighlighted) { + // Remove the entire entry from the 'comments' array if there are no more + // highlighted replies and the comment is not highlighted + highlightedComment.comments.splice(commentIndex, 1) + } else { + // Remove the reply entry from the 'replies' array for the found comment. + highlightedComment.comments[commentIndex].replies = ( + highlightedComment.comments[commentIndex].replies.filter( + r => JSON.parse(r).commentId !== JSON.parse(reply).commentId + ) + ) + } + db.highlightedComments.updateAsync( + { _id: videoId }, + highlightedComment, + {} + ) + } } static persist() { diff --git a/src/datastores/handlers/electron.js b/src/datastores/handlers/electron.js index e0d78ece51067..dd8221c7a29aa 100644 --- a/src/datastores/handlers/electron.js +++ b/src/datastores/handlers/electron.js @@ -213,7 +213,7 @@ class HighlightedComments { ) } - static upsertHighlightedCommentByVideoId(comment, videoId) { + static upsertHighlightedComment(videoId, comment) { return ipcRenderer.invoke( IpcChannels.DB_HIGHLIGHTED_COMMENTS, { @@ -223,6 +223,36 @@ class HighlightedComments { ) } + static deleteHighlightedComment(videoId, comment) { + return ipcRenderer.invoke( + IpcChannels.DB_HIGHLIGHTED_COMMENTS, + { + action: DBActions.HIGHLIGHTED_COMMENTS.DELETE_COMMENT, + data: { videoId: videoId, comment: JSON.stringify(comment) } + } + ) + } + + static upsertHighlightedReply(videoId, comment, reply) { + return ipcRenderer.invoke( + IpcChannels.DB_HIGHLIGHTED_COMMENTS, + { + action: DBActions.HIGHLIGHTED_COMMENTS.UPSERT_REPLY, + data: { videoId: videoId, comment: JSON.stringify(comment), reply: JSON.stringify(reply) } + } + ) + } + + static deleteHighlightedReply(videoId, comment, reply) { + return ipcRenderer.invoke( + IpcChannels.DB_HIGHLIGHTED_COMMENTS, + { + action: DBActions.HIGHLIGHTED_COMMENTS.DELETE_REPLY, + data: { videoId: videoId, comment: JSON.stringify(comment), reply: JSON.stringify(reply) } + } + ) + } + static persist() { return ipcRenderer.invoke( IpcChannels.DB_HIGHLIGHTED_COMMENTS, diff --git a/src/main/index.js b/src/main/index.js index 57ad5ae3064e5..0a1a7fdf07770 100644 --- a/src/main/index.js +++ b/src/main/index.js @@ -1028,7 +1028,19 @@ function runApp() { return await baseHandlers.highlightedComments.find() case DBActions.HIGHLIGHTED_COMMENTS.UPSERT_COMMENT: - await baseHandlers.highlightedComments.upsertHighlightedCommentByVideoId(data.comment, data.videoId) + await baseHandlers.highlightedComments.upsertHighlightedComment(data.videoId, data.comment) + return null + + case DBActions.HIGHLIGHTED_COMMENTS.DELETE_COMMENT: + await baseHandlers.highlightedComments.deleteHighlightedComment(data.videoId, data.comment) + return null + + case DBActions.HIGHLIGHTED_COMMENTS.UPSERT_REPLY: + await baseHandlers.highlightedComments.upsertHighlightedReply(data.videoId, data.comment, data.reply) + return null + + case DBActions.HIGHLIGHTED_COMMENTS.DELETE_REPLY: + await baseHandlers.highlightedComments.deleteHighlightedReply(data.videoId, data.comment, data.reply) return null default: diff --git a/src/renderer/components/watch-video-comments/watch-video-comments.css b/src/renderer/components/watch-video-comments/watch-video-comments.css index 02ec492988960..025992f83cc8c 100644 --- a/src/renderer/components/watch-video-comments/watch-video-comments.css +++ b/src/renderer/components/watch-video-comments/watch-video-comments.css @@ -110,6 +110,14 @@ margin-block-end: 5px; } +.replyHighlighted { + font-weight: normal; + font-size: 12px; + margin-block-start: 0; + margin-inline-start: 68px; + margin-block-end: 5px; +} + .commentDate { font-weight: normal; margin-inline-start: 5px; diff --git a/src/renderer/components/watch-video-comments/watch-video-comments.js b/src/renderer/components/watch-video-comments/watch-video-comments.js index 290fcfba2a464..0a6f0de65f7f0 100644 --- a/src/renderer/components/watch-video-comments/watch-video-comments.js +++ b/src/renderer/components/watch-video-comments/watch-video-comments.js @@ -125,6 +125,21 @@ export default defineComponent({ highlightedComments: function () { return this.$store.getters.getHighlightedComments(this.id) + }, + highlightedReplyComments: function () { + return this.$store.getters.getHighlightedReplyComments(this.id) + }, + isCommentHighlighted: function () { + return (comment) => !!this.$store.getters.getHighlightedComments(this.id).find((entry) => entry.commentId === comment.commentId) + }, + isReplyHighlighted: function () { + return (comment, reply) => !!this.$store.getters.getHighlightedReplies(this.id, comment.commentId).find(entry => entry.commentId === reply.commentId) + }, + getHighlightedReplies: function () { + return (commentId) => this.$store.getters.getHighlightedReplies(this.id, commentId) + }, + isCommentOrReplyHighlighted: function () { + return (comment) => this.isCommentHighlighted(comment) || this.getHighlightedReplies(comment.commentId).length > 0 } }, mounted: function () { @@ -156,7 +171,11 @@ export default defineComponent({ getCommentData: function () { this.isLoading = true + this.commentIndexCount = -1 this.commentData = this.commentData.concat(this.highlightedComments) + this.commentData = Array.from([...this.commentData, ...this.highlightedReplyComments] + .reduce((m, o) => m.set(o.commentId, o), new Map()) + .values()) if (!process.env.IS_ELECTRON || this.backendPreference === 'invidious') { this.getCommentDataInvidious() } else { @@ -211,6 +230,11 @@ export default defineComponent({ const parsedComments = comments.contents .map(commentThread => parseLocalComment(commentThread.comment, commentThread)) + parsedComments.forEach( + comment => { + comment.commentIndex = ++this.commentIndexCount + } + ) // Append and remove duplicate comments based on commentId. this.commentData = Array.from([...this.commentData, ...parsedComments] @@ -274,6 +298,9 @@ export default defineComponent({ nextPageToken: this.nextPageToken, sortNewest: this.sortNewest }).then(({ response, commentData }) => { + commentData.forEach(function(comment) { + comment.commentIndex = ++this.commentIndexCount + }.bind(this)) this.commentData = Array.from([...this.commentData, ...commentData] .reduce((m, o) => m.set(o.commentId, o), new Map()) .values()) @@ -327,14 +354,85 @@ export default defineComponent({ }) }, - addHighlightedCommentAndReloadPage: function (comment) { - showToast(this.$t('Highlighting comment')) - this.$store.dispatch('addHighlightedComment', { videoId: this.id, comment: comment }) - window.location.reload() + toggleCommentHighlight: async function (comment) { + if (this.isCommentHighlighted(comment)) { + showToast(this.$t('Unhighlighting comment')) + await this.$store.dispatch('unhighlightComment', { videoId: this.id, comment: comment }) + const indexToRemove = this.commentData.findIndex(entry => entry.commentId === comment.commentId) + if (this.isCommentOrReplyHighlighted(comment)) { + this.commentData.splice(indexToRemove, 1) + this.commentData.unshift(comment) + } else { + // find number of highlighted comments before the comment + const highlightedOrReplyComments = this.$store.getters.getHighlightedComments(this.id).concat(this.$store.getters.getHighlightedReplyComments(this.id)) + const filteredComments = highlightedOrReplyComments.filter(c => c.commentIndex < comment.commentIndex) + const uniqueCommentCount = new Set(filteredComments.map(comment => comment.commentId)).size + const highlightedCommentCount = new Set(highlightedOrReplyComments.map(comment => comment.commentId)).size + const newIndex = comment.commentIndex + highlightedCommentCount - uniqueCommentCount + + // move comment to original index + if (indexToRemove !== -1) { + this.commentData.splice(indexToRemove, 1) + } + this.commentData.splice(newIndex, 0, comment) + } + } else { + showToast(this.$t('Highlighting comment')) + await this.$store.dispatch('highlightComment', { videoId: this.id, comment: comment }) + // move comment to top + const indexToRemove = this.commentData.findIndex(entry => entry.commentId === comment.commentId) + if (indexToRemove !== -1) { + this.commentData.splice(indexToRemove, 1) + } + this.commentData.unshift(comment) + } + }, + + toggleReplyHighlight: async function (comment, reply) { + if (this.isReplyHighlighted(comment, reply)) { + showToast(this.$t('Unhighlighting reply')) + await this.$store.dispatch('unhighlightReply', { videoId: this.id, comment: comment, reply: reply }) + if (!this.isCommentOrReplyHighlighted(comment)) { + const indexToRemove = this.commentData.findIndex(entry => entry.commentId === comment.commentId) + const highlightedOrReplyComments = this.$store.getters.getHighlightedComments(this.id).concat(this.$store.getters.getHighlightedReplyComments(this.id)) + const filteredComments = highlightedOrReplyComments.filter(c => c.commentIndex < comment.commentIndex) + const uniqueCommentCount = new Set(filteredComments.map(comment => comment.commentId)).size + const highlightedCommentCount = new Set(highlightedOrReplyComments.map(comment => comment.commentId)).size + const newIndex = comment.commentIndex + highlightedCommentCount - uniqueCommentCount + + // move comment to original index + if (indexToRemove !== -1) { + this.commentData.splice(indexToRemove, 1) + } + this.commentData.splice(newIndex, 0, comment) + } + const replyIndex = comment.replies.findIndex(entry => entry.commentId === reply.commentId) + if (replyIndex !== -1) { + comment.replies.splice(replyIndex, 1) + comment.replies.splice(replyIndex, 0, reply) + } + } else { + showToast(this.$t('Highlighting reply')) + await this.$store.dispatch('highlightReply', { videoId: this.id, comment: comment, reply: reply }) + // move reply to top + const replyIndex = comment.replies.findIndex(entry => entry.commentId === reply.commentId) + if (replyIndex !== -1) { + comment.replies.splice(replyIndex, 1) + } + comment.replies.unshift(reply) + + // move comment to top + const indexToRemove = this.commentData.findIndex(entry => entry.commentId === comment.commentId) + if (indexToRemove !== -1) { + this.commentData.splice(indexToRemove, 1) + } + this.commentData.unshift(comment) + } }, ...mapActions([ - 'addHighlightedComment' + 'highlightComment', + 'unhighlightComment' ]) } }) diff --git a/src/renderer/components/watch-video-comments/watch-video-comments.vue b/src/renderer/components/watch-video-comments/watch-video-comments.vue index 68039dd3ce3a2..ae7f4256e3c20 100644 --- a/src/renderer/components/watch-video-comments/watch-video-comments.vue +++ b/src/renderer/components/watch-video-comments/watch-video-comments.vue @@ -79,7 +79,7 @@

{{ $t("Highlighted comment") }} @@ -122,9 +122,10 @@ > {{ comment.time }} @@ -211,6 +212,12 @@ > +

+ {{ $t("Highlighted reply") }} +

- + {{ reply.time }}

diff --git a/src/renderer/helpers/api/local.js b/src/renderer/helpers/api/local.js index 476c80c8d5e4f..6fa72923a3dd2 100644 --- a/src/renderer/helpers/api/local.js +++ b/src/renderer/helpers/api/local.js @@ -893,7 +893,8 @@ export function parseLocalComment(comment, commentThread = undefined) { replyToken, showReplies: false, replies: [], - commentId: comment.comment_id + commentId: comment.comment_id, + commentIndex: 0 } } diff --git a/src/renderer/store/modules/highlighted-comments.js b/src/renderer/store/modules/highlighted-comments.js index 49edecca66781..168a744f3c28c 100644 --- a/src/renderer/store/modules/highlighted-comments.js +++ b/src/renderer/store/modules/highlighted-comments.js @@ -1,14 +1,28 @@ import { DBHighlightedCommentHandlers } from '../../../datastores/handlers/index' const state = { - videoHighlightedComments: {} + videoHighlightedComments: {}, + videoHighlightedReplyComments: {}, + videoHighlightedReplies: {} } const getters = { getHighlightedComments: (state) => (videoId) => ( videoId in state.videoHighlightedComments ? state.videoHighlightedComments[videoId] - : []) + : [] + ), + getHighlightedReplies: (state) => (videoId, commentId) => ( + (videoId in state.videoHighlightedReplies && + commentId in state.videoHighlightedReplies[videoId]) + ? state.videoHighlightedReplies[videoId][commentId] + : [] + ), + getHighlightedReplyComments: (state) => (videoId) => ( + videoId in state.videoHighlightedReplyComments + ? state.videoHighlightedReplyComments[videoId] + : [] + ), } const actions = { @@ -20,15 +34,38 @@ const actions = { console.error(errMessage) } }, - async addHighlightedComment({ commit }, payload) { + async highlightComment({ commit }, payload) { try { - await DBHighlightedCommentHandlers.upsertHighlightedCommentByVideoId(payload.comment, payload.videoId) - commit('upsertHighlightedCommentToVideo', payload.comment, payload.videoId) + await DBHighlightedCommentHandlers.upsertHighlightedComment(payload.videoId, payload.comment) + commit('upsertHighlightedComment', payload) + } catch (errMessage) { + console.error(errMessage) + } + }, + async unhighlightComment({ commit }, payload) { + try { + await DBHighlightedCommentHandlers.deleteHighlightedComment(payload.videoId, payload.comment) + commit('deleteHighlightedComment', payload) + } catch (errMessage) { + console.error(errMessage) + } + }, + async highlightReply({ commit }, payload) { + try { + await DBHighlightedCommentHandlers.upsertHighlightedReply(payload.videoId, payload.comment, payload.reply) + commit('upsertHighlightedReply', payload) + } catch (errMessage) { + console.error(errMessage) + } + }, + async unhighlightReply({ commit }, payload) { + try { + await DBHighlightedCommentHandlers.deleteHighlightedReply(payload.videoId, payload.comment, payload.reply) + commit('deleteHighlightedReply', payload) } catch (errMessage) { console.error(errMessage) } }, - async grabAllHighlightedComments({ commit, dispatch, state }) { try { const payload = await DBHighlightedCommentHandlers.find() @@ -45,10 +82,10 @@ const mutations = { state.videoHighlightedComments[videoId] = [] } }, - upsertHighlightedCommentToVideo(state, comment, videoId) { + upsertHighlightedComment(state, { videoId, comment }) { if (videoId in state.videoHighlightedComments) { const comments = state.videoHighlightedComments[videoId] - const existingCommentIndex = comments.findIndex(c => c.text === comment.text) + const existingCommentIndex = comments.findIndex(c => c.commentId === comment.commentId) if (existingCommentIndex === -1) { state.videoHighlightedComments[videoId].push(comment) } @@ -56,7 +93,48 @@ const mutations = { state.videoHighlightedComments[videoId] = [comment] } }, - + deleteHighlightedComment(state, { videoId, comment }) { + if (videoId in state.videoHighlightedComments) { + state.videoHighlightedComments[videoId] = state.videoHighlightedComments[videoId].filter(entry => entry.commentId !== comment.commentId) + } + }, + upsertHighlightedReply(state, { comment, videoId, reply }) { + if (videoId in state.videoHighlightedReplyComments) { + const comments = state.videoHighlightedReplyComments[videoId] + if (!comments.find(c => c.commentId === comment.commentId)) { + state.videoHighlightedReplyComments[videoId].push(comment) + } + } else { + state.videoHighlightedReplyComments[videoId] = [comment] + } + if (videoId in state.videoHighlightedReplies) { + if (comment.commentId in state.videoHighlightedReplies[videoId]) { + if (!state.videoHighlightedReplies[videoId][comment.commentId].find(entry => entry.commentId === reply.commentId)) { + state.videoHighlightedReplies[videoId][comment.commentId].push(reply) + } else { + state.videoHighlightedReplies[videoId][comment.commentId] = [reply] + } + } else { + state.videoHighlightedReplies[videoId][comment.commentId] = [reply] + } + } else { + state.videoHighlightedReplies[videoId] = {} + state.videoHighlightedReplies[videoId][comment.commentId] = [reply] + } + }, + deleteHighlightedReply(state, { videoId, comment, reply }) { + if (videoId in state.videoHighlightedReplies && + comment.commentId in state.videoHighlightedReplies[videoId]) { + state.videoHighlightedReplies[videoId][comment.commentId] = state.videoHighlightedReplies[videoId][comment.commentId].filter( + entry => entry.commentId !== reply.commentId + ) + if (!state.videoHighlightedReplies[videoId][comment.commentId].length) { + if (videoId in state.videoHighlightedReplyComments) { + state.videoHighlightedReplyComments[videoId] = state.videoHighlightedReplyComments[videoId].filter(entry => entry.commentId !== comment.commentId) + } + } + } + }, setAllHighlightedComments(state, payload) { if (Array.isArray(payload) && payload.length > 0) { payload.forEach(entry => { @@ -65,7 +143,30 @@ const mutations = { } state.videoHighlightedComments[entry._id] = ( state.videoHighlightedComments[entry._id].concat( - entry.comments.map(x => JSON.parse(x)))) + entry.comments + .filter(x => x.isCommentHighlighted) + .map(x => JSON.parse(x.comment)))) + + if (!(entry._id in state.videoHighlightedReplyComments)) { + state.videoHighlightedReplyComments[entry._id] = [] + } + state.videoHighlightedReplyComments[entry._id] = ( + state.videoHighlightedReplyComments[entry._id].concat( + entry.comments + .filter(x => x.replies.length > 0) + .map(x => JSON.parse(x.comment)) + ) + ) + if (!(entry._id in state.videoHighlightedReplies)) { + state.videoHighlightedReplies[entry._id] = {} + } + entry.comments.forEach(commentEntry => { + if (commentEntry.replies.length > 0) { + state.videoHighlightedReplies[entry._id][JSON.parse(commentEntry.comment).commentId] = ( + commentEntry.replies.map(x => JSON.parse(x)) + ) + } + }) }) } } diff --git a/static/locales/en-US.yaml b/static/locales/en-US.yaml index e20b6762e6210..a5304a8e5f801 100644 --- a/static/locales/en-US.yaml +++ b/static/locales/en-US.yaml @@ -815,6 +815,7 @@ Comments: Member: Member Subscribed: Subscribed Hearted: Hearted + Highlighted: Highlighted Up Next: Up Next #Tooltips