由于自己总是在某一话题下聊天,每次有新的回复时,总是需要来回切换,浏览帖子时就很不友好,不能一边浏览一边回复帖子么!!!
于是它就诞生了,目前支持设置显示单一话题的回复,但需要你手动动一下脚本
let fixedTopic_id = ‘’;这个值,就是他,将他修改为你想要看到最新回复的话题id,即可实现查看单一帖子的回复内容,不修改的话,即为你最新回复的贴子内容
由于没有监听器,所以现在是手动刷新,带有刷新按钮,有需求的话,后续可能会有…
或者你看完内容,关闭后,再次打开,会刷新,或者你回复一下,也会刷新,但是有点慢,介意的自己优化啦
有没有bug呢,应该是有的,第一次写脚本,不太会
另外,支持ctrl+enter发送
好了,上代码,上图
// ==UserScript==
// @name 最近回复
// @namespace http://tampermonkey.net/
// @version 0.1
// @description Display and quickly reply to the most recent reply to your recent post on Linux.do forum
// @author unique
// @match https://linux.do/*
// @grant none
// ==/UserScript==
(function () {
'use strict';
//若需要固定话题聊天,只需要将下面的值进行替换对应的话题id,如//let fixedTopic_id = 1,不填入该值则为你最近回复的话题
let fixedTopic_id = '';
// 添加拖动功能
function addDraggableFeature(element) {
let pos1 = 0, pos2 = 0, pos3 = 0, pos4 = 0;
const dragMouseDown = function (e) {
// 检查事件的目标是否是输入框
if (e.target.tagName.toUpperCase() === 'INPUT' || e.target.tagName.toUpperCase() === 'TEXTAREA') {
return; // 如果是,则不执行拖动逻辑
}
e = e || window.event;
e.preventDefault();
pos3 = e.clientX;
pos4 = e.clientY;
document.onmouseup = closeDragElement;
document.onmousemove = elementDrag;
};
const elementDrag = function (e) {
e = e || window.event;
e.preventDefault();
pos1 = pos3 - e.clientX;
pos2 = pos4 - e.clientY;
pos3 = e.clientX;
pos4 = e.clientY;
element.style.top = (element.offsetTop - pos2) + "px";
element.style.left = (element.offsetLeft - pos1) + "px";
// 为了避免与拖动冲突,在此移除bottom和right样式
element.style.bottom = '';
element.style.right = '';
};
const closeDragElement = function () {
document.onmouseup = null;
document.onmousemove = null;
};
element.onmousedown = dragMouseDown;
}
const getUsernameFromAvatarUrl = url => {
const regex = /https:\/\/cdn\.linux\.do\/user_avatar\/linux\.do\/(.+?)\/96\/\d+_2\.png/;
const match = url.match(regex);
return match && match.length > 1 ? match[1] : null;
};
const getCsrfToken = () => {
const csrfTokenMeta = document.querySelector('meta[name="csrf-token"]');
return csrfTokenMeta ? csrfTokenMeta.getAttribute('content') : null;
};
const fetchMostRecentReply = async () => {
try {
const imgElement = document.querySelector('.avatar');
const avatarUrl = imgElement.getAttribute('src');
const username = getUsernameFromAvatarUrl(avatarUrl);
const response = await fetch(`https://linux.do/user_actions.json?offset=0&username=${username}&filter=5`);
if (!response.ok) throw new Error('Failed to fetch recent reply.');
const jsonData = await response.json();
if (jsonData.length === 0) {
console.error('No recent actions found');
return null;
}
const recentPost = jsonData.user_actions[0];
let topicId = '';
if (fixedTopic_id === '') {
topicId = recentPost.topic_id;
} else {
topicId = fixedTopic_id;
}
const postTopicResponse = await fetch(`https://linux.do/t/topic/${topicId}.json`);
const postTopicJsonData = await postTopicResponse.json();
const postNumber = postTopicJsonData.last_read_post_number;
const postResponse = await fetch(`https://linux.do/t/topic/${topicId}/${postNumber}.json`);
const postJsonData = await postResponse.json();
const posts = postJsonData.post_stream.posts;
const lastTenPosts = posts.map(post => ({
username: post.username,
avatar_template: post.avatar_template,
cooked: post.cooked,
date: post.created_at,
title: postJsonData.title
}));
return {postId: recentPost.post_id, topicId: topicId, mostRecentReply: lastTenPosts};
} catch (error) {
console.error('Error fetching recent reply:', error);
return null;
}
};
const createAndAppendElement = (tag, attributes, textContent, parent) => {
const element = document.createElement(tag);
if (attributes) {
Object.keys(attributes).forEach(key => {
element.setAttribute(key, attributes[key]);
});
}
if (textContent) {
element.textContent = textContent;
} else {
element.innerHTML = attributes && attributes.innerHTML ? attributes.innerHTML : '';
}
if (parent) {
parent.appendChild(element);
}
return element;
};
const sendNewPost = async (content, topicId) => {
const url = 'https://linux.do/posts';
const csrfToken = getCsrfToken();
if (!csrfToken) return;
const headers = {
'authority': 'linux.do',
'Accept': 'application/json, text/plain, */*',
'Accept-Encoding': 'gzip, deflate, br',
'Accept-Language': 'en-US,en;q=0.9',
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
'Origin': 'https://linux.do',
'Referer': 'https://linux.do/',
'Sec-Fetch-Dest': 'empty',
'Sec-Fetch-Mode': 'cors',
'Sec-Fetch-Site': 'same-origin',
'X-Requested-With': 'XMLHttpRequest',
'X-CSRF-Token': csrfToken
};
const formData = new URLSearchParams({
'raw': content,
'unlist_topic': 'false',
'category': '2',
'topic_id': topicId,
'is_warning': 'false',
'archetype': 'regular',
'typing_duration_msecs': '4800',
'composer_open_duration_msecs': '11073',
'featured_link': '',
'shared_draft': 'false',
'draft_key': `topic_${topicId}`,
'nested_post': 'true'
});
try {
const response = await fetch(url, {
method: 'POST',
headers: headers,
body: formData,
credentials: 'include'
});
if (!response.ok) throw new Error('Failed to send new post.');
} catch (error) {
console.error('Error sending new post:', error);
}
};
let isReplyBoxNotEmpty = false;
let copyContent = '';
const updatePopupContent = async (popup) => {
const recentReply = await fetchMostRecentReply();
if (!recentReply) return;
popup.innerHTML = '';
// 创建总标题元素
const titleElement = createAndAppendElement('div', {
style: 'font-size: 18px; font-weight: bold; margin-bottom: 10px;display: flex; align-items: center;'
}, null, popup);
// 创建 SVG 图标元素
const svgIcon = document.createElement('img');
svgIcon.src = 'data:image/svg+xml;base64,' + btoa('<?xml version="1.0" ?><!DOCTYPE svg PUBLIC \'-//W3C//DTD SVG 1.1//EN\' \'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd\'><svg id="Capa_1" style="enable-background:new 0 0 60 60; font-weight: bold;" version="1.1" viewBox="0 0 60 60" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M54.07,1H15.93C12.66,1,10,3.66,10,6.93V15H5.93C2.66,15,0,17.66,0,20.929V42.07C0,45.34,2.66,48,5.93,48H12v10c0,0.413,0.254,0.784,0.64,0.933C12.757,58.978,12.879,59,13,59c0.276,0,0.547-0.115,0.74-0.327L23.442,48H44.07c3.27,0,5.93-2.66,5.93-5.929V34h4.07c3.27,0,5.93-2.66,5.93-5.93V6.93C60,3.66,57.34,1,54.07,1z M48,42.071C48,44.237,46.237,46,44.07,46H23c-0.282,0-0.551,0.119-0.74,0.327L14,55.414V47c0-0.552-0.447-1-1-1H5.93C3.763,46,2,44.237,2,42.07V20.929C2,18.763,3.763,17,5.93,17H11h33.07c2.167,0,3.93,1.763,3.93,3.93V33V42.071z M58,28.07c0,2.167-1.763,3.93-3.93,3.93H50V20.93c0-3.27-2.66-5.93-5.93-5.93H12V6.93C12,4.763,13.763,3,15.93,3H54.07C56.237,3,58,4.763,58,6.93V28.07z"/></svg>');
// 设置 SVG 图标的宽度和高度
svgIcon.style.width = '24px';
svgIcon.style.height = '24px';
svgIcon.style.marginRight = '10px'; // 添加右边距
svgIcon.style.fill = '#007bff'; // 设置颜色为蓝色
// 将 SVG 图标添加到标题元素中
titleElement.appendChild(svgIcon);
// 创建标题文本
const titleText = document.createTextNode(recentReply.mostRecentReply[0].title.substring(0, 20));
// 将标题文本添加到标题元素中
titleElement.appendChild(titleText);
const createCard = (reply) => {
const cardHtml = `
<div style="display: flex; align-items: start; padding: 12px; background-color: #f0f0f0; border-radius: 12px; margin-bottom: 12px; max-width: 380px;">
<img src="${reply.avatar_template.replace("{size}", "144")}" alt="" width="45" height="45" class="avatar" loading="lazy" style="border-radius: 50%; margin-right: 10px; object-fit: cover;">
<div style="flex-grow: 1; overflow: hidden;">
<div style="display: flex; justify-content: space-between; align-items: center;">
<div style="color: #333; font-weight: bold; margin-bottom: 4px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">${reply.username}</div>
<div style="color: #777; font-size: 12px;">${formatDate(reply.date)}</div>
</div>
<div style="color: #555; word-wrap: break-word;font-size: 16px; font-family: 'Microsoft YaHei';">${reply.cooked}</div>
</div>
</div>
`;
function formatDate(dateString) {
const date = new Date(dateString);
const hours = ('0' + date.getHours()).slice(-2);
const minutes = ('0' + date.getMinutes()).slice(-2);
return `${hours}:${minutes}`;
}
const card = createAndAppendElement('div', {innerHTML: cardHtml}, null, popup);
// Check for images in the reply and add click event to open them in a new tab
const images = card.querySelectorAll('img');
images.forEach(image => {
image.style.cursor = 'pointer';
image.addEventListener('click', (event) => {
event.preventDefault();
window.open(image.src, '_blank');
image.style.maxWidth === '100%' ? image.style.maxWidth = '' : image.style.maxWidth = '100%';
});
});
return card;
};
recentReply.mostRecentReply.forEach(reply => {
createCard(reply);
});
const previousRefreshButton = document.getElementById('quick-reply-refresh');
if (previousRefreshButton) {
previousRefreshButton.parentNode.removeChild(previousRefreshButton);
}
const inputContainer = createAndAppendElement('div', {style: 'display: flex; align-items: center; margin-top: 10px; background-color: #f0f0f0; padding: 8px; border-radius: 10px;'}, null, popup);
const replyBox = createAndAppendElement('textarea', {
id: 'quick-reply-box',
style: 'flex: 1; padding: 10px; background-color: #fff; border: 1px solid #ddd; border-radius: 18px; resize: none; font-size: 14px; line-height: 1.5; outline: none; margin-right: 10px; box-sizing: border-box; height: 40px;margin-left: 6px'
}, null, inputContainer);
const sendButton = createAndAppendElement('button', {
id: 'quick-reply-send',
style: 'flex-shrink: 0; background-color: #007bff; color: white; border: none; border-radius: 18px; cursor: pointer; font-size: 14px; line-height: 1; outline: none; padding: 10px 16px;'
}, 'Send', inputContainer);
replyBox.addEventListener('keydown', (event) => {
if (event.ctrlKey && event.key === 'Enter') {
event.preventDefault();
sendButton.click();
}
});
sendButton.addEventListener('click', async () => {
const content = replyBox.value;
if (content !== null && content.trim() !== '') {
await sendNewPost(content.trim(), recentReply.topicId, recentReply.postId);
sendButton.textContent = 'Success!';
setTimeout(() => {
sendButton.textContent = 'Send';
}, 2000);
replyBox.value = '';
await updatePopupContent(popup);
} else {
alert('Please enter a valid reply!');
}
});
const refreshButton = createAndAppendElement('button', {
id: 'quick-reply-refresh',
style: 'flex-shrink: 0; background-color: transparent; border: none; cursor: pointer; outline: none; padding: 0;margin-left: 6px'
}, null, inputContainer);
refreshButton.innerHTML = `
<svg id="Layer_1" style="enable-background:new 0 0 150 128;" version="1.1" viewBox="0 0 128 128" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="24" height="24">
<g fill="#0000FF">
<path d="M96.1,103.6c-10.4,8.4-23.5,12.4-36.8,11.1c-10.5-1-20.3-5.1-28.2-11.8H44v-8H18v26h8v-11.9c9.1,7.7,20.4,12.5,32.6,13.6 c1.9,0.2,3.7,0.3,5.5,0.3c13.5,0,26.5-4.6,37-13.2c19.1-15.4,26.6-40.5,19.1-63.9l-7.6,2.4C119,68.6,112.6,90.3,96.1,103.6z"/>
<path d="M103,19.7c-21.2-18.7-53.5-20-76.1-1.6C7.9,33.5,0.4,58.4,7.7,81.7l7.6-2.4C9,59.2,15.5,37.6,31.9,24.4 C51.6,8.4,79.7,9.6,98,26H85v8h26V8h-8V19.7z"/>
</g>
</svg>
`;
replyBox.addEventListener('input', () => {
isReplyBoxNotEmpty = replyBox.value.trim() !== '';
copyContent = replyBox.value;
});
refreshButton.addEventListener('click', async () => {
await updatePopupContent(popup);
replyBox.value = copyContent;
});
};
const init = async () => {
try {
const popup = createAndAppendElement('div', {
id: 'quick-reply-popup',
style: 'display: none; position: fixed; bottom: 10px; right: 10px; z-index: 9999; width: 400px; max-height: 500px; overflow-y: auto; padding: 10px; background-color: #fff; border: 1px solid #ccc; box-shadow: 0 2px 4px rgba(0,0,0,0.1);'
}, null, document.body);
await updatePopupContent(popup);
const buttonWrapper = createAndAppendElement('div', {
style: 'position: fixed; bottom: 10px; right: 10px; z-index: 9999; cursor: move;' // Add cursor: move;
}, null, document.body);
const openButton = createAndAppendElement('button', {
id: 'quick-reply-open',
style: 'padding: 5px 10px; background-color: #007bff; color: white; border: none; border-radius: 3px; cursor: pointer;'
}, '最近回复', buttonWrapper);
addDraggableFeature(buttonWrapper);
addDraggableFeature(popup);
openButton.addEventListener('click', async () => {
const popup = document.getElementById('quick-reply-popup');
if (popup.style.display === 'none') {
popup.style.display = 'block';
openButton.textContent = '关闭回复';
} else {
popup.style.display = 'none';
openButton.textContent = '最近回复';
}
await updatePopupContent(popup);
});
} catch (error) {
console.error('Error initializing script:', error);
}
};
init();
})();
在此也特别鸣谢一下,在第一版脚本完成时,
@Reno 佬给出的体验,以及其他热佬在我测试时给的帮助