diff --git a/install/data/defaults.json b/install/data/defaults.json index 3b92872280..52b7683954 100644 --- a/install/data/defaults.json +++ b/install/data/defaults.json @@ -69,6 +69,7 @@ "gdpr_enabled": 1, "allowProfileImageUploads": 1, "teaserPost": "last-reply", + "showPostPreviewsOnHover": 1, "allowPrivateGroups": 1, "unreadCutoff": 2, "bookmarkThreshold": 5, diff --git a/public/language/en-GB/admin/settings/post.json b/public/language/en-GB/admin/settings/post.json index 00baa56fc1..ab8245738c 100644 --- a/public/language/en-GB/admin/settings/post.json +++ b/public/language/en-GB/admin/settings/post.json @@ -40,6 +40,7 @@ "teaser.last-post": "Last – Show the latest post, including the original post, if no replies", "teaser.last-reply": "Last – Show the latest reply, or a \"No replies\" placeholder if no replies", "teaser.first": "First", + "showPostPreviewsOnHover": "Show a preview of posts when mouse overed", "unread": "Unread Settings", "unread.cutoff": "Unread cutoff days", "unread.min-track-last": "Minimum posts in topic before tracking last read", diff --git a/public/src/client/topic.js b/public/src/client/topic.js index 6fe0310dd6..6661b43d65 100644 --- a/public/src/client/topic.js +++ b/public/src/client/topic.js @@ -12,10 +12,11 @@ define('forum/topic', [ 'components', 'storage', 'hooks', + 'api', ], function ( infinitescroll, threadTools, postTools, events, posts, navigator, sort, - components, storage, hooks + components, storage, hooks, api ) { const Topic = {}; let currentUrl = ''; @@ -55,6 +56,7 @@ define('forum/topic', [ addParentHandler(); addDropupHandler(); addRepliesHandler(); + addPostsPreviewHandler(); handleBookmark(tid); @@ -172,6 +174,59 @@ define('forum/topic', [ }); } + function addPostsPreviewHandler() { + if (!ajaxify.data.showPostPreviewsOnHover) { + return; + } + let timeoutId = 0; + $('[component="topic"]').on('mouseenter', '[component="post"] a, [component="topic/event"] a', async function () { + const link = $(this); + + async function renderPost(pid) { + const postData = await socket.emit('posts.getPostSummaryByPid', { pid: pid }); + if (postData) { + const tooltip = await app.parseAndTranslate('partials/topic/post-preview', { post: postData }); + tooltip.hide().find('.timeago').timeago(); + tooltip.appendTo($('body')).fadeIn(300); + const postContent = link.parents('[component="topic"]').find('[component="post/content"]').first(); + const postRect = postContent.offset(); + const postWidth = postContent.width(); + const linkRect = link.offset(); + tooltip.css({ + top: linkRect.top + 30, + left: postRect.left, + width: postWidth, + }); + } + } + + const href = link.attr('href'); + const pathname = utils.urlToLocation(href).pathname; + $('#post-tooltip').remove(); + const postMatch = pathname && pathname.match(/\/post\/([\d]+)/); + const topicMatch = pathname && pathname.match(/\/topic\/([\d]+)/); + if (postMatch) { + const pid = postMatch[1]; + if (parseInt(link.parents('[component="post"]').attr('data-pid'), 10) === parseInt(pid, 10)) { + return; // dont render self post + } + + timeoutId = setTimeout(async () => { + renderPost(pid); + }, 300); + } else if (topicMatch) { + timeoutId = setTimeout(async () => { + const tid = topicMatch[1]; + const topicData = await api.get('/topics/' + tid, {}); + renderPost(topicData.mainPid); + }, 300); + } + }).on('mouseleave', '[component="post"] a, [component="topic/event"] a', function () { + clearTimeout(timeoutId); + $('#post-tooltip').remove(); + }); + } + function updateTopicTitle() { const span = components.get('navbar/title').find('span'); if ($(window).scrollTop() > 50 && span.hasClass('hidden')) { diff --git a/src/controllers/topics.js b/src/controllers/topics.js index fa7a9b6d68..291acf78cf 100644 --- a/src/controllers/topics.js +++ b/src/controllers/topics.js @@ -96,6 +96,7 @@ topicsController.get = async function getTopic(req, res, next) { topicData.updateUrlWithPostIndex = settings.updateUrlWithPostIndex; topicData.allowMultipleBadges = meta.config.allowMultipleBadges === 1; topicData.privateUploads = meta.config.privateUploads === 1; + topicData.showPostPreviewsOnHover = meta.config.showPostPreviewsOnHover === 1; topicData.rssFeedUrl = `${relative_path}/topic/${topicData.tid}.rss`; if (req.loggedIn) { topicData.rssFeedUrl += `?uid=${req.uid}&token=${rssToken}`; diff --git a/src/socket.io/posts.js b/src/socket.io/posts.js index a4ab025cc4..9fc33293cf 100644 --- a/src/socket.io/posts.js +++ b/src/socket.io/posts.js @@ -98,6 +98,22 @@ SocketPosts.getPostSummaryByIndex = async function (socket, data) { return postsData[0]; }; +SocketPosts.getPostSummaryByPid = async function (socket, data) { + if (!data || !data.pid) { + throw new Error('[[error:invalid-data]]'); + } + const { pid } = data; + const tid = await posts.getPostField(pid, 'tid'); + const topicPrivileges = await privileges.topics.get(tid, socket.uid); + if (!topicPrivileges['topics:read']) { + throw new Error('[[error:no-privileges]]'); + } + + const postsData = await posts.getPostSummaryByPids([pid], socket.uid, { stripTags: false }); + posts.modifyPostByPrivilege(postsData[0], topicPrivileges); + return postsData[0]; +}; + SocketPosts.getPost = async function (socket, pid) { sockets.warnDeprecated(socket, 'GET /api/v3/posts/:pid'); return await api.posts.get(socket, { pid }); diff --git a/src/views/admin/settings/post.tpl b/src/views/admin/settings/post.tpl index 8e7029e906..6c018ec974 100644 --- a/src/views/admin/settings/post.tpl +++ b/src/views/admin/settings/post.tpl @@ -193,6 +193,12 @@ +
+ +
diff --git a/src/views/partials/topic/post-preview.tpl b/src/views/partials/topic/post-preview.tpl new file mode 100644 index 0000000000..666313112c --- /dev/null +++ b/src/views/partials/topic/post-preview.tpl @@ -0,0 +1,13 @@ +
+
+ + + + +
+
{post.content}
+