├── .github ├── scripts │ ├── notify-team.js │ └── ticket-automation.js └── workflows │ └── ticket-automation.yml ├── README.md ├── resolved └── history │ └── #26 │ └── evaluation.md └── tickets ├── #19 └── evaluation.md ├── #24 └── evaluation.md └── #27 └── evaluation.md /.github/scripts/notify-team.js: -------------------------------------------------------------------------------- 1 | const { Octokit } = require('@octokit/rest'); 2 | 3 | (async () => { 4 | const octokit = new Octokit({ auth: process.env.GITHUB_TOKEN }); 5 | const owner = process.env.REPO_OWNER; 6 | const repo = process.env.REPO_NAME; 7 | const eventType = process.env.EVENT_TYPE; 8 | const eventAction = process.env.EVENT_ACTION; 9 | const number = process.env.ISSUE_NUMBER; 10 | 11 | try { 12 | let labels = []; 13 | let commentBody = ''; 14 | 15 | // Fetch labels 16 | if (eventType === 'issues') { 17 | const { data } = await octokit.issues.get({ 18 | owner, 19 | repo, 20 | issue_number: number, 21 | }); 22 | labels = data.labels.map(label => label.name); 23 | if (eventAction === 'opened') { 24 | commentBody = `New issue #${number} created. Please review the details in the [tickets/#${number}/evaluation.md](https://github.com/${owner}/${repo}/tree/main/tickets/%23${number}/evaluation.md) file.`; 25 | } else if (eventAction === 'labeled' || eventAction === 'unlabeled') { 26 | commentBody = `Labels updated on issue #${number}. Current labels: ${labels.join(', ') || 'None'}. Please review the details in the [tickets/#${number}/evaluation.md](https://github.com/${owner}/${repo}/tree/main/tickets/%23${number}/evaluation.md) file.`; 27 | } 28 | } else if (eventType === 'discussion' && eventAction === 'created') { 29 | const { data } = await octokit.graphql(` 30 | query($owner: String!, $repo: String!, $number: Int!) { 31 | repository(owner: $owner, name: $repo) { 32 | discussion(number: $number) { 33 | labels(first: 100) { 34 | nodes { 35 | name 36 | } 37 | } 38 | } 39 | } 40 | }`, { owner, repo, number: parseInt(number) }); 41 | labels = data.repository.discussion.labels.nodes.map(label => label.name); 42 | commentBody = `New discussion #${number} created. Please review the details in the [tickets/#${number}/evaluation.md](https://github.com/${owner}/${repo}/tree/main/tickets/%23${number}/evaluation.md) file.`; 43 | } else { 44 | console.log(`Event ${eventType}.${eventAction} not supported for notifications`); 45 | return; 46 | } 47 | 48 | // Map labels to teams 49 | const labelToTeam = { 50 | 'general': [''], 51 | 'transparency': [''], 52 | 'bug': [''], 53 | 'theming': ['zen-themes'], 54 | 'userChrome': [''], 55 | 'userContent': [''], 56 | 'mods': ['zen-mods'], 57 | 'requests': ['mod-creation'], 58 | 'assistance': ['mod-creation'], 59 | 'Sine': ['sine'], 60 | 'Silkthemes': ['silkthemes'], 61 | 'Zen': ['zen'], 62 | 'needs-triage': ['triage-team'], 63 | }; 64 | 65 | let teamsToNotify = new Set(); 66 | labels.forEach(label => { 67 | if (labelToTeam[label]) { 68 | labelToTeam[label].forEach(team => { 69 | if (team) teamsToNotify.add(team); 70 | }); 71 | } 72 | }); 73 | 74 | // Default team if no teams are mapped 75 | if (teamsToNotify.size === 0) { 76 | teamsToNotify.add('triage-team'); 77 | } 78 | 79 | // Create comment with team mentions 80 | const mentions = Array.from(teamsToNotify).map(team => `@${owner}/${team}`).filter(Boolean).join(', '); 81 | const finalComment = mentions ? `${mentions}. ${commentBody}` : commentBody; 82 | 83 | // Check for recent comments to avoid duplicates 84 | if (eventType === 'issues' && (eventAction === 'labeled' || eventAction === 'unlabeled')) { 85 | const { data: comments } = await octokit.issues.listComments({ 86 | owner, 87 | repo, 88 | issue_number: number, 89 | per_page: 10, // Check recent comments 90 | }); 91 | const botUser = 'github-actions[bot]'; 92 | const recentThreshold = 60 * 1000; // 1 minute in milliseconds 93 | const now = Date.now(); 94 | const recentComment = comments.find(comment => 95 | comment.user.login === botUser && 96 | comment.body.includes('Labels updated on issue') && 97 | new Date(comment.created_at).getTime() > now - recentThreshold 98 | ); 99 | if (recentComment) { 100 | console.log(`Skipping comment for issue #${number}: Recent comment found at ${recentComment.created_at}`); 101 | return; 102 | } 103 | } 104 | 105 | // Post the comment 106 | try { 107 | if (eventType === 'issues') { 108 | await octokit.issues.createComment({ 109 | owner, 110 | repo, 111 | issue_number: number, 112 | body: finalComment, 113 | }); 114 | } else if (eventType === 'discussion') { 115 | const discussionId = await getDiscussionId(octokit, owner, repo, number); 116 | await octokit.graphql(` 117 | mutation($discussionId: ID!, $body: String!) { 118 | addDiscussionComment(input: { discussionId: $discussionId, body: $body }) { 119 | comment { 120 | id 121 | } 122 | } 123 | }`, { 124 | discussionId, 125 | body: finalComment, 126 | }); 127 | } 128 | console.log(`Notified teams: ${Array.from(teamsToNotify).join(', ')}`); 129 | } catch (commentError) { 130 | console.error(`Failed to create comment: ${commentError.message}`); 131 | console.error('This may be due to insufficient permissions for GITHUB_TOKEN. Ensure the workflow has "issues: write" and "discussions: write" permissions.'); 132 | } 133 | } catch (error) { 134 | console.error('Error:', error); 135 | process.exit(1); 136 | } 137 | })(); 138 | 139 | // Helper function to get discussion ID 140 | async function getDiscussionId(octokit, owner, repo, number) { 141 | try { 142 | const { data } = await octokit.graphql(` 143 | query($owner: String!, $repo: String!, $number: Int!) { 144 | repository(owner: $owner, name: $repo) { 145 | discussion(number: $number) { 146 | id 147 | } 148 | } 149 | }`, { owner, repo, number: parseInt(number) }); 150 | return data.repository.discussion.id; 151 | } catch (error) { 152 | console.error(`Failed to fetch discussion ID: ${error.message}`); 153 | throw error; 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /.github/scripts/ticket-automation.js: -------------------------------------------------------------------------------- 1 | const { Octokit } = require('@octokit/rest'); 2 | const fs = require('fs').promises; 3 | const path = require('path'); 4 | 5 | (async () => { 6 | const octokit = new Octokit({ auth: process.env.GITHUB_TOKEN }); 7 | const owner = process.env.REPO_OWNER; 8 | const repo = process.env.REPO_NAME; 9 | const eventType = process.env.EVENT_TYPE; 10 | const eventAction = process.env.EVENT_ACTION; 11 | const number = process.env.ISSUE_NUMBER; 12 | 13 | const REFERENCE_LABEL = 'reference'; 14 | const DEFAULT_LABEL = 'needs-triage'; 15 | const ticketsDir = path.join(process.cwd(), 'tickets'); 16 | const resolvedDir = path.join(process.cwd(), 'resolved'); 17 | const referenceDir = path.join(resolvedDir, 'reference'); 18 | const historyDir = path.join(resolvedDir, 'history'); 19 | 20 | // Ensure directories exist 21 | await Promise.all([ 22 | fs.mkdir(ticketsDir, { recursive: true }), 23 | fs.mkdir(referenceDir, { recursive: true }), 24 | fs.mkdir(historyDir, { recursive: true }) 25 | ]); 26 | 27 | const folderName = `#${number}`; 28 | const ticketFolder = path.join(ticketsDir, folderName); 29 | const referenceFolder = path.join(referenceDir, folderName); 30 | const historyFolder = path.join(historyDir, folderName); 31 | 32 | async function moveFolder(src, dest) { 33 | try { 34 | await fs.access(src); 35 | await fs.mkdir(path.dirname(dest), { recursive: true }); 36 | await fs.rename(src, dest); 37 | console.log(`Moved folder from ${src} to ${dest}`); 38 | } catch (error) { 39 | if (error.code === 'ENOENT') { 40 | console.log(`Source folder ${src} not found, skipping move.`); 41 | } else { 42 | throw error; 43 | } 44 | } 45 | } 46 | 47 | async function updateEvaluationFile(number, details, targetDir = ticketsDir) { 48 | const folderPath = path.join(targetDir, `#${number}`); 49 | await fs.mkdir(folderPath, { recursive: true }); 50 | const content = ` 51 | ### ${details.title} 52 | 53 | **Labels**: ${details.labels || 'None'}\\ 54 | **Author**: [@${details.author}](https://github.com/${details.author})\\ 55 | **Link**: ${details.html_url} 56 | `; 57 | await fs.writeFile(path.join(folderPath, 'evaluation.md'), content.trim()); 58 | console.log(`Updated evaluation.md for #${number} in ${targetDir}`); 59 | } 60 | 61 | try { 62 | if (eventType === 'issues') { 63 | let { data } = await octokit.issues.get({ owner, repo, issue_number: number }); 64 | let details = { 65 | title: data.title, 66 | labels: data.labels.map(label => label.name).join(', ') || 'None', 67 | author: data.user.login, 68 | html_url: data.html_url 69 | }; 70 | 71 | if (eventAction === 'opened') { 72 | if (data.labels.length === 0) { 73 | await octokit.issues.addLabels({ 74 | owner, 75 | repo, 76 | issue_number: number, 77 | labels: [DEFAULT_LABEL] 78 | }); 79 | console.log(`Added default label '${DEFAULT_LABEL}' to issue #${number}`); 80 | // Refetch issue data to include the new label 81 | const updatedData = await octokit.issues.get({ owner, repo, issue_number: number }); 82 | data = updatedData.data; 83 | details.labels = data.labels.map(label => label.name).join(', ') || 'None'; 84 | } 85 | await fs.rm(ticketFolder, { recursive: true, force: true }); 86 | await updateEvaluationFile(number, details); 87 | } else if (eventAction === 'edited' || eventAction === 'labeled' || eventAction === 'unlabeled') { 88 | details.labels = data.labels.map(label => label.name).join(', ') || 'None'; 89 | // Only update if the folder exists in tickets/ 90 | if (await fs.access(ticketFolder).then(() => true).catch(() => false)) { 91 | await updateEvaluationFile(number, details); 92 | } 93 | } else if (eventAction === 'closed') { 94 | const labels = data.labels.map(label => label.name); 95 | const targetDir = labels.includes(REFERENCE_LABEL) ? referenceFolder : historyFolder; 96 | await moveFolder(ticketFolder, targetDir); 97 | } else if (eventAction === 'reopened') { 98 | const possibleSrc1 = referenceFolder; 99 | const possibleSrc2 = historyFolder; 100 | if (await fs.access(possibleSrc1).then(() => true).catch(() => false)) { 101 | await moveFolder(possibleSrc1, ticketFolder); 102 | } else if (await fs.access(possibleSrc2).then(() => true).catch(() => false)) { 103 | await moveFolder(possibleSrc2, ticketFolder); 104 | } else { 105 | console.log(`No resolved folder found for #${number}, creating new ticket folder`); 106 | await updateEvaluationFile(number, details); 107 | } 108 | } 109 | } else if (eventType === 'discussion' && eventAction === 'created') { 110 | const { data } = await octokit.graphql(` 111 | query($owner: String!, $repo: String!, $number: Int!) { 112 | repository(owner: $owner, name: $repo) { 113 | discussion(number: $number) { 114 | title 115 | author { 116 | login 117 | } 118 | url 119 | labels(first: 100) { 120 | nodes { 121 | name 122 | } 123 | } 124 | } 125 | } 126 | }`, { owner, repo, number: parseInt(number) }); 127 | const details = { 128 | title: data.repository.discussion.title, 129 | labels: data.repository.discussion.labels.nodes.map(label => label.name).join(', ') || 'None', 130 | author: data.repository.discussion.author.login, 131 | html_url: data.repository.discussion.url 132 | }; 133 | await fs.rm(ticketFolder, { recursive: true, force: true }); 134 | await updateEvaluationFile(number, details); 135 | } 136 | } catch (error) { 137 | console.error('Error:', error); 138 | process.exit(1); 139 | } 140 | })(); 141 | -------------------------------------------------------------------------------- /.github/workflows/ticket-automation.yml: -------------------------------------------------------------------------------- 1 | name: Ticket Automation Bot 2 | on: 3 | issues: 4 | types: [opened, closed, reopened, labeled, unlabeled, edited] 5 | discussion: 6 | types: [created] 7 | jobs: 8 | manage-ticket-folder: 9 | runs-on: ubuntu-latest 10 | permissions: 11 | contents: write 12 | issues: write 13 | discussions: write 14 | steps: 15 | - name: Checkout Repository 16 | uses: actions/checkout@v4 17 | - name: Set up Node.js 18 | uses: actions/setup-node@v4 19 | with: 20 | node-version: '20' 21 | - name: Install Dependencies 22 | run: npm install @octokit/rest 23 | - name: Manage Ticket Folder 24 | env: 25 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 26 | REPO_OWNER: ${{ github.repository_owner }} 27 | REPO_NAME: ${{ github.event.repository.name }} 28 | EVENT_TYPE: ${{ github.event_name }} 29 | EVENT_ACTION: ${{ github.event.action }} 30 | ISSUE_NUMBER: ${{ github.event.issue.number || github.event.discussion.number }} 31 | run: node .github/scripts/ticket-automation.js 32 | - name: Commit Changes 33 | run: | 34 | git config --global user.name 'github-actions[bot]' 35 | git config --global user.email 'github-actions[bot]@users.noreply.github.com' 36 | git add tickets/ resolved/ 37 | if [ "${{ github.event_name }}" == "issues" ]; then 38 | if [ "${{ github.event.action }}" == "closed" ]; then 39 | git commit -m "Move ticket folder to resolved for closed issue #${{ github.event.issue.number }}" 40 | elif [ "${{ github.event.action }}" == "reopened" ]; then 41 | git commit -m "Move ticket folder back to tickets for reopened issue #${{ github.event.issue.number }}" 42 | elif [ "${{ github.event.action }}" == "labeled" ] || [ "${{ github.event.action }}" == "unlabeled" ] || [ "${{ github.event.action }}" == "edited" ]; then 43 | git commit -m "Update evaluation.md for issue #${{ github.event.issue.number }}" 44 | else 45 | git commit -m "Create ticket folder and evaluation.md for issue #${{ github.event.issue.number }}" 46 | fi 47 | elif [ "${{ github.event_name }}" == "discussion" ]; then 48 | git commit -m "Create ticket folder and evaluation.md for discussion #${{ github.event.discussion.number }}" 49 | fi 50 | git push 51 | env: 52 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 53 | continue-on-error: true 54 | - name: Notify Team 55 | if: github.event.action == 'opened' || github.event.action == 'created' || github.event.action == 'labeled' || github.event.action == 'unlabeled' 56 | env: 57 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 58 | REPO_OWNER: ${{ github.repository_owner }} 59 | REPO_NAME: ${{ github.event.repository.name }} 60 | EVENT_TYPE: ${{ github.event_name }} 61 | EVENT_ACTION: ${{ github.event.action }} 62 | ISSUE_NUMBER: ${{ github.event.issue.number || github.event.discussion.number }} 63 | run: node .github/scripts/notify-team.js 64 | continue-on-error: true 65 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ℹ️ Zen Help - Fast, Neat, and Helpful ❤️ 2 | This is a place where users can freely post about all their issues for Zen and quickly get reliable responses. 3 | Issues created here aren't issues with Zen but rather are people who need help with setting up stuff, creating mods, and more. 4 | 5 | # Contents 6 | 7 | ## Mods 8 | **This contains the mod related discussions and issue templates.** 9 | 10 | ### Mod Creation 11 | There are mutiple ways to create mods for Zen Browser, we reccomend you make your mods compatible with _at least_ 2 of the 3. 12 | * **Zen Mods - Native** 13 | * Documentation - How to properly create a mod 14 | * Theme Creation Ticket - Create your theme after you have followed the proper documentation 15 | * Bug Ticket - For technical bugs, issues with the mod creation code 16 | * Question Channel - Issues on With your code, Questions you have about how it works, or how to accomplish something 17 | 18 | * **Sine Mods - Externally Managed (JavaScript Compatible)** 19 | * Documentation - How to properly create a mod 20 | * Theme Creation Ticket - Create your theme after you have followed the proper documentation 21 | * Bug Ticket - For technical bugs, issues with the mod creation code 22 | * Question Channel - Issues on With your code, Questions you have about how it works, or how to accomplish something 23 | 24 | * **Silktheme Store - Externally Managed (Compadible on _Most_ Firefox Browsers)** 25 | * Documentation - How to properly create a mod 26 | * Theme Creation Ticket - Create your theme after you have followed the proper documentation 27 | * Bug Ticket - For technical bugs, issues with the mod creation code 28 | * Question Channel - Issues on With your code, Questions you have about how it works, or how to accomplish something 29 | 30 | ### [Mod Requests](https://github.com/ZenHelp/zen-help/discussions/2) 31 | Please submit any mod requests here. Ensure you read the overview before making a request. We will try to make an issue ticket quickly if your idea is plausible. 32 | 33 | ## Theme Customization 34 | Mods are cool, but themes are cooler - especially if they are _yours_, this is a proper guide on how to create a theme for Zen Browser\ 35 | 36 | `userChrome.css` & `userContent.css` are Firefox (and also Zen's) way of letting you customize your browser, using `CSS`. 37 | * **Setup**\ 38 | Follow the guide from the Zen Browser: [Live Editing Zen Theme](https://docs.zen-browser.app/guides/live-editing) 39 | 40 | Note: 41 | * If you are unable to open the browser console check if the config, `devtools.chrome.enabled` is true 42 | * `userContent.css` is the same as `userChrome.css` but for websites. Please view documentation [here](). 43 | 44 | * **Creating a Mod**\ 45 | After setup you are free to start theming, if you do not know CSS we reccomend using a tool like ChatGPT to learn basic syntax as it is not vary hard. If you have no interest in learing this, or do not have the time, please consider making a Mod Request. Your ideas are valued and we love community projects. 46 | 1. Organization\ 47 | Themes are usually organized with the following structure for Sine Mods: 48 | ``` 49 | theme_name/ 50 | ├── assets/ 51 | │ └── images/ 52 | │ ├── main.png 53 | │ └── module-image.png 54 | ├── theme_name/ 55 | | ├── theme_name.css 56 | | ├── theme-name-config.css # This is used for the prefrences.json file 57 | │ ├── CSS/ 58 | | | └── Modules/ 59 | | │ │ ├── module-one.css 60 | | │ │ └── module-two.css 61 | | | └──Pages/ 62 | │ ├── JS/ 63 | │ │ └── script.uc.js # Only compatible with Sine 64 | │ └── Assets/ 65 | │ └── icons/ 66 | │ └── icon-one.svg 67 | ├── modfiles/ 68 | │ ├── preferences.json 69 | │ └── theme.json 70 | ├── UserChrome.css 71 | └── LICENCE 72 | └── README.md 73 | 74 | ``` 75 | But when creating and testing a theme it is usually helpful to have all of your `CSS` code in your `userChrome.css` file (so trhat you can live test, because you cannot test files using CSS file imports), and then leave the assets and `JS` in there proper paths. When writing in a `userChrome.css` file it helps if you use comments `/* --- Module Name --- */` To organize your files. 76 | 2. Creation 77 | Creating a theme is not always east, so here are a few things that will help: 78 | * Make use of the inspect button\ 79 | This allows you to easily find elements and there classes and ids, good for CSS and JS 80 | * Use the Layout and Computed tabs\ 81 | These allow you to find issues on why your CSS might not be applying 82 | * Make Use of Preferences and Varibles\ 83 | Prefs and Vars can be used in the preferences.json file to allow users to have more config over a theme. For documentation on how to use them in CSS, please read [this](https://docs.zen-browser.app/themes-store/themes-marketplace-preferences). For detailed instructions on how to use Sine's prefernces, read [this](https://github.com/CosmoCreeper/Sine/wiki/Features#-powerful-new-preference-features). When using CSS varibles that are used across multiple modules (like colors or animation speeds) that you want the user to be able to toggle, crate them in the `theme-name-config.css` file in the :root, so they can be easily edited by the user and are easy to find for contributors. 84 | * **Publishing** 85 | * File Layout\ 86 | After creating your theme you can begin moving code into individual CSS files, to do this please read the following: 87 | * In your Mods userChrome.css file, add an import like `@import "theme_name/theme_name.css";` 88 | * Import all of your module files from `theme_name.css` using the same method but `@import "CSS/modules/module-one.css";` 89 | * Follow the same steps for `userContent.css` but using "Pages" instead of "Modules" 90 | * Uploading to GitHub\ 91 | Mod stores use GitHub as their way to find files, update themes (for sine), and display README information. Thus, it is required your theme is open source, and availible to the publish on Github. Here is how to do that: 92 | * Create a New Repository (this requires a GitHub account) 93 | * upload your `theme_name` folder into GitHub (Plus Button -> Upload Files) 94 | * Create a README explaining your theme, this should include an overview, images and description for each feature/module, a detailed list of prefs and varibles, and any thanks/credits for code or assets. 95 | * Create Issue Ticket to Publish Theme\ 96 | Chose where you are going to publish your theme and create an issue, we reccomend sine or silkbrush. Zen Mods is not supported at this time, due to complex userChrome loading. Follow the steps on the ticket then once you create a ticket a mod from that repo will get back to you shortly. 97 | * [sine](https://github.com/CosmoCreeper/Sine/issues/new?template=add-theme.yml) 98 | * silkthemes (currently unfinished, not ticket yet) 99 | * Share your theme in our [showcase](https://github.com/ZenHelp/zen-help/discussions/13)! 100 | --- 101 | If you need help, you can always create an issue [here](https://github.com/ZenHelp/zen-help/issues/new) and we will get right with you! 102 | -------------------------------------------------------------------------------- /resolved/history/#26/evaluation.md: -------------------------------------------------------------------------------- 1 | ### Transiton between Horizontal split view and vertical split view is not working !!!! and also the nogaps mod in nebula theme in zen browser is broken as well 2 | 3 | **Labels**: bug, mods\ 4 | **Author**: [@iamalexthomas](https://github.com/iamalexthomas)\ 5 | **Link**: https://github.com/ZenHelp/zen-help/issues/26 -------------------------------------------------------------------------------- /tickets/#19/evaluation.md: -------------------------------------------------------------------------------- 1 | ### Tabs are removed after closing zen. 2 | 3 | **Labels**: general\ 4 | **Author**: [@efithatkid](https://github.com/efithatkid)\ 5 | **Link**: https://github.com/ZenHelp/zen-help/issues/19 -------------------------------------------------------------------------------- /tickets/#24/evaluation.md: -------------------------------------------------------------------------------- 1 | ### New Tab and URL Bar History Suggestions 2 | 3 | **Labels**: general\ 4 | **Author**: [@turfandam](https://github.com/turfandam)\ 5 | **Link**: https://github.com/ZenHelp/zen-help/issues/24 -------------------------------------------------------------------------------- /tickets/#27/evaluation.md: -------------------------------------------------------------------------------- 1 | ### Load bar mod is not working properly with nebula theme in zen browser. 2 | 3 | **Labels**: bug, mods\ 4 | **Author**: [@iamalexthomas](https://github.com/iamalexthomas)\ 5 | **Link**: https://github.com/ZenHelp/zen-help/issues/27 --------------------------------------------------------------------------------