话题的聊天室脚本

由于自己总是在某一话题下聊天,每次有新的回复时,总是需要来回切换,浏览帖子时就很不友好,不能一边浏览一边回复帖子么!!!

于是它就诞生了,目前支持设置显示单一话题的回复,但需要你手动动一下脚本

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 佬给出的体验,以及其他热佬在我测试时给的帮助

27 个赞

为佬友打call :heart_eyes_cat:

7 个赞

不错不错:heart_eyes:

11 个赞

7 个赞

好,很好,非常好!

8 个赞

nice

7 个赞

我来发个自己改的样式但是fixed的旧代码 :rofl:

圆角样式
// ==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';

    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];
            const topicId = recentPost.topic_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 }));

            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, postId) => {
        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 createCard = (reply) => {
            const cardHtml = `
                <div style="display: flex; align-items: start; padding: 15px; background-color: #f0f0f0; border-radius: 25px; margin-bottom: 20px; width: 290px;">
                    <img src="${reply.avatar_template.replace("{size}", "144")}" alt="" width="40" height="40" class="avatar" loading="lazy" style="border-radius: 50%; margin-right: 10px; object-fit: cover;">
                    <div style="flex-grow: 1; overflow: hidden;">
                        <div style="color: #333; font-weight: bold; font-size: 12px; margin-bottom: 4px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">${reply.username}</div>
                        <div style="color: #555; word-wrap: break-word;font-size: 14px; font-family: 'Microsoft YaHei';">${reply.cooked}</div>
                    </div>
                </div>
            `;
            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%';
                });
            });

            const outerDiv = createAndAppendElement('div', { style: 'width: 100%; display: flex; justify-content: center;' }, null, popup);
            outerDiv.appendChild(card);

            return outerDiv;
        };

        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: 280px; right: 10px; z-index: 9999; width: 340px; max-height: 480px; overflow-y: auto; padding: 12px; background-color: #fff; border-radius: 25px; border: 1px solid #ccc; box-shadow: 0 2px 4px rgba(0,0,0,0.1);'
            }, null, document.body);

            await updatePopupContent(popup);

            const openButton = createAndAppendElement('button', {
                id: 'quick-reply-open',
                style: 'position: fixed; bottom: 250px; right: 282px; z-index: 9999; padding: 5px 10px; font-size: 16px; background-color: #007bff; color: white; border: none; border-radius: 10px; cursor: pointer;'
            }, '最近回复', document.body);

            openButton.addEventListener('click', async () => {
                const popup = document.getElementById('quick-reply-popup');
                if (popup.style.display === 'none') {
                    popup.style.display = 'block';
                    openButton.textContent = '关闭回复';
                    await updatePopupContent(popup);
                } else {
                    popup.style.display = 'none';
                    openButton.textContent = '最近回复';
                }
            });

        } catch (error) {
            console.error('Error initializing script:', error);
        }
    };

    init();

})();
5 个赞

我来改一下,把你的样式搞进去

11 个赞

厉害厉害

6 个赞

好,得支持下

4 个赞

@Reno佬的提供的圆角样式

圆角样式
// ==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       Your name
// @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: 15px; background-color: #f0f0f0; border-radius: 25px; margin-bottom: 20px; width: 300px;">
                    <img src="${reply.avatar_template.replace("{size}", "144")}" alt="" width="40" height="40" 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; font-size: 12px; 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();

})();

10 个赞

看出来了,都是后花园

4 个赞

啊哈哈哈哈,是的

8 个赞

支持一波

3 个赞

你们?

2 个赞

始皇是不是要贴下服务器负载,,兜不住了么? :crazy_face:

4 个赞

优秀a

3 个赞

好好好

3 个赞

感谢付出~
彻底将主题改造成聊天室了 :v: :v: :v:

3 个赞

坐等一个油猴脚本

2 个赞