Open orangemn6 opened 3 years ago
@TysonMN how would I go about doing this?
I don't know. It depends on your website.
it is built with hugo
Never heard of it. Sorry.
@orangemn6 Add two elements to your page, each with a unique ID, e.g. "dark-mode" and "light-mode". Both elements have the Utterances script as a child with different values for the theme attribute. On dark/light mode switch you hide one element (display: none;) and show the other (display: block;).
<div id="light-mode">
<script src="https://utteranc.es/client.js"
repo="$REPO"
issue-term="pathname"
theme="github-light"
crossorigin="anonymous"
async>
</script>
</div>
<div id="dark-mode">
<script src="https://utteranc.es/client.js"
repo="$REPO"
issue-term="pathname"
theme="github-dark"
crossorigin="anonymous"
async>
</script>
</div>
Here you can find the relevant code of my blog. https://github.com/brennerm/brennerm.github.io/blob/gh-pages/static/js/application.js#L64 https://github.com/brennerm/brennerm.github.io/blob/gh-pages/_layouts/post.html#L25
Be aware that this will result in the Utterances script being loaded twice, but as it happens asynchronously I don't really care too much.
@brennerm I like that idea! I am trying to make an if statement now
@brennerm for me the script is loaded twice on top of each other? is this supposed to happen?
That is the behavior on my blog. I don't see a benefit in trying to optimize this.
@orangemn6 Do you mean on my blog?
@brennerm yes
Mmh that's strange. Had another look at it and switched to a CSS based logic. Below you can find my rules.
html[data-theme="dark"] #utterances-light {
display: none;
}
html[data-theme="dark"] #utterances-dark {
display: block;
}
html #utterances-light,
html[data-theme="light"] #utterances-light {
display: block;
}
html #utterances-dark,
html[data-theme="light"] #utterances-dark {
display: none;
}
Please let me know if that works for you.
@brennerm, are you saying that those CSS rules allow your website to switch between light and dark modes while only querying GitHub for data once?
Nope, just switched to CSS rules for hiding one div and displaying the other instead of using Javascript.
@orangemn6 I have gone with a slightly different approach.
https://git.sr.ht/~tristan957/tristan.partin.io/tree/master/layouts/_default/single.html#L32
My logic is above. My website is https://tristan.partin.io. I think the CSS-based solution is cool, but users will very rarely switch site themes, so every time a user visits your site, you'll have an extra request going for no reason. When you can just re-request utterances with a different theme if and only if the user switches themes.
Have any one successfully done this on Gatsby (React). Would you mind sharing your code? (Currently there is only 1 mode on utterances)
header.mdx (inject to header of the page):
export const ModeToggle = ({ ...props }) => {
const [colorMode, setColorMode] = useColorMode()
const utterance = document.getElementById("utterance")
return (
<Button
className="mode-toggle"
aria-label="Toggle mode"
variant="icon"
onClick={() => setColorMode(colorMode === "default" ? "dark" : "default")}
// Set dark and light mode
ml={[2, 4]}
hoverColor="primary"
focusColor="text"
{...props}
>
<Icon name={colorMode === "default" ? "sun" : "moon"} size="5" />
</Button>
)
}
comment.js:
import React, { Component } from 'react';
// import { useColorMode } from '@reflexjs/gatsby-theme-core';
export default class Comments extends Component {
constructor(props) {
super(props);
this.commentBox = React.createRef();
}
componentDidMount() {
const scriptEl = document.createElement('script');
scriptEl.async = true;
scriptEl.src = 'https://utteranc.es/client.js';
scriptEl.setAttribute('src', 'https://utteranc.es/client.js');
scriptEl.setAttribute('crossorigin', 'anonymous');
scriptEl.setAttribute('repo', 'loctran016/loctran016.github.io');
scriptEl.setAttribute('issue-term', 'title');
scriptEl.setAttribute('id', 'utterance');
scriptEl.setAttribute('theme', 'github-light');
this.commentBox.current.appendChild(scriptEl);
}
render() {
return (
<div id="comments" className="comment-box-wrapper container pt-9">
<h1 className="my-0">
Comments
</h1>
<hr className="my-0" />
<div ref={this.commentBox} className="comment-box" />
</div>
);
}
}
scriptEl.setAttribute('theme', 'github-light');
make that dependent on your color mode and try again. Also you are setting src
twice.
scriptEl.setAttribute('theme', 'github-light');
make that dependent on your color mode and try again. Also you are setting
src
twice.
I've done this. My code is here: comment.js and header.mdx but my site still require a reload to change the utturances theme
Class-based components have a method to determine whether they should re-render. I forget what it is called. You need to implement that method and say if the color changes, re-render. I have moved on to hook-based components, but that is what I remember from React pre-hooks!
shouldComponentUpdate()
is what you are looking for.
I am really not sure how using hooks in class-based components works though since your useColorMode()
looks like a hook.
I am really not sure how using hooks in class-based components works though since your
useColorMode()
looks like a hook. Well, here is my code for dark/light mode button (if it's dark mode, the id of button will be 'dark', in light mode is 'light')const [colorMode, setColorMode] = useColorMode() id={colorMode === "default" ? "light" : "dark"}
and then set the theme like this:
const utturancesTheme = typeof(document.getElementById("dark")) != 'undefined' && document.getElementById("dark") != null ? 'icy-dark' : 'github-light'; scriptEl.setAttribute('theme', utturancesTheme);
Well, the
shouldComponentUpdate()
doesn't work, it just add another utterances (for ex: if I click on the ToggleButton once, there will be 2 utturances comment box on the page). Anyway, thanks for your help But actually, reload will make it render and people don't usually change the light/dark mode too many times
You need to unmount the first utterances instance. That's what I do
You need to unmount the first utterances instance. That's what I do
I've tried. Using .remove() doesn't help.
What's the suggested approach to add a loading spinner while the theme changes? After injecting the script
tag, it overwrites itself and inserts the utterance iframe. I want to show a loading spinner instead of the iframe as soon as the theme is changed and inject another script
tag to load the new theme.
Thanks
EDIT: doesn't address spinner but uses postMessage to change theme
I came across the following in this repo in src/theme.ts:
addEventListener('message', event => {
if (event.origin === origin && event.data.type === 'set-theme') {
link.href = `/stylesheets/themes/${event.data.theme}/utterances.css`;
}
});
So one way to do is to avoid injecting the script
again but send a postMessage
instead. Note: if you have dev tools open and you are disabling cache while they're open in your settings, then there will be a delay when you switch your comments section's theme. If anyone else is interested, you can find my convoluted solution (using React) below. The TLDR is this though:
iframe.contentWindow.postMessage({ type: "set-theme", theme: themeName }, "*")
Also, you can probably make the solution less convoluted by copying the useEffect
in useScript
and just not depending on attributes
in order to re-run the effect. You want to change the theme via postMessage
not via re-injection of script
tag.
Comments.js
import React, { useRef } from "react"
import useScript from "~utils/useScript"
const attributes = {
// repo: "justin-calleja/choices",
repo: "justin-calleja/justincalleja.com",
"issue-term": "pathname",
label: "comment",
theme: "github-dark",
crossorigin: "anonymous",
}
const Comments = ({ themeName }) => {
const initThemeNameRef = useRef(themeName)
// avoid re-rendering the script tag by using the ref value for the
// theme name rather than the prop. Use postMessage to change the
// theme instead of removing the iframe and injecting the utterance
// script again.
attributes.theme = initThemeNameRef.current
const commentsRef = useRef(null)
useScript("https://utteranc.es/client.js", attributes, commentsRef)
const prevThemeNameRef = useRef(themeName)
// if themeName prop has changed:
if (prevThemeNameRef.current !== themeName) {
prevThemeNameRef.current = themeName
if (commentsRef.current) {
const iframe = commentsRef.current.getElementsByTagName("iframe")[0]
if (iframe) {
console.log("about to postMessage with themeName:", themeName)
iframe.contentWindow.postMessage(
{ type: "set-theme", theme: themeName },
"*",
)
}
}
}
return <div className="comments" ref={commentsRef}></div>
}
export default Comments
useScript.js
// @ts-check
import { useEffect } from "react"
/**
*
* @param {string} url
* @param {{ [key: string]: string }} attributes
* @param {MutableRefObject<HTMLElement | null>} parentRef
*/
const useScript = (url, attributes = {}, parentRef) => {
useEffect(() => {
const script = document.createElement("script")
script.src = url
script.async = true
if (attributes) {
for (let [key, value] of Object.entries(attributes)) {
script.setAttribute(key, value)
}
}
const parentEl =
parentRef && parentRef.current ? parentRef.current : document.body
parentEl.appendChild(script)
return () => {
// NOTE: script might have been removed e.g. when using utteranc
// script (for comments), once you inject the script it overwrites
// itself to include an iframe in its place. i.e. the original script
// tag is no longer in the html.
try {
parentEl.removeChild(script)
} catch (err) {
// The node to be removed is not a child of this node
if (err.message.includes("is not a child of this node")) {
parentEl.innerHTML = ""
}
}
}
}, [url, attributes, parentRef])
}
export default useScript
If you are using hugo and want utterances theme consistent with your website, this may help:
<section class="article discussion">
<script>
function loadComment() {
let theme = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches ? 'github-dark' : 'github-light';
let s = document.createElement('script');
s.src = 'https://utteranc.es/client.js';
s.setAttribute('repo', '{{- .Site.Params.comments.utterancesRepo -}}');
s.setAttribute('issue-term', '{{- .Site.Params.comments.utterancesIssueTerm | default "pathname" -}}');
s.setAttribute('theme', theme);
s.setAttribute('crossorigin', 'anonymous');
s.setAttribute('async', '');
document.querySelector('section.article.discussion').innerHTML = '';
document.querySelector('section.article.discussion').appendChild(s);
}
loadComment();
if (window.matchMedia)
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => {loadComment();});
</script>
</section>
I'm using Jekyll, in my blog, I have a method that is called each time the theme is changed. Just as @justin-calleja says, posting a message to the utterances iframe should generally work to switch themes:
const utterancesTheme = newTheme === themes.DARK ? "github-dark" : "github-light";
document.getElementsByClassName("utterances-frame")[0].contentWindow.postMessage({ type: "set-theme", theme: utterancesTheme }, "*")
My approach to load the correct theme on the first time is to declare two Utterances with different themes, then get the current theme from a variable in localStorage
, finally delete the utterances theme we don't want:
<div id = "uttComments">
<script id="uttDark"
src="https://utteranc.es/client.js"
repo="Dialvive/dialvive.github.io"
issue-term="pathname"
label="✨💬✨"
theme="github-dark"
crossorigin="anonymous"
async>
</script>
<script id="uttLight"
src="https://utteranc.es/client.js"
repo="Dialvive/dialvive.github.io"
issue-term="pathname"
label="✨💬✨"
theme="github-light"
crossorigin="anonymous"
async>
</script>
</div>
<script>
const savedTheme = localStorage.getItem("theme");
var uttLight = document.getElementById("uttLight");
var uttDark = document.getElementById("uttDark");
if(savedTheme === themes.DARK){
uttLight.parentNode.removeChild(uttLight);
} else {
uttDark.parentNode.removeChild(uttDark);
}
</script>
Almost always the unnecessary Utterances is deleted before it is loaded, therefore it is better than hiding it completely loaded.
Combining loctran016's approach with some of qianbinbin's code and adapting to dark-mode-toggle:
function getUtterancesTheme(mode) {
return mode === 'dark' ? 'github-dark' : 'github-light';
}
// Defer loading utterances until everything else is loaded.
// This way more important page content is prioritized.
window.addEventListener('load', () => {
let initialTheme = 'github-light';
const toggle = document.getElementsByTagName('dark-mode-toggle');
if (toggle.length !== 1) {
console.warn('Expected one dark-mode-toggle, but got: ', toggle);
} else {
initialTheme = getUtterancesTheme(toggle[0].mode);
}
// Add script that loads utterance
const commentsContainer = document.getElementById('comments');
if (commentsContainer) {
const s = document.createElement('script');
s.src = 'https://utteranc.es/client.js';
s.setAttribute('repo', REPO);
s.setAttribute('issue-term', 'pathname');
s.setAttribute('theme', initialTheme);
s.setAttribute('crossorigin', 'anonymous');
s.setAttribute('async', '');
commentsContainer.appendChild(s);
}
});
document.addEventListener('colorschemechange', (e) => {
const mode = e.detail.colorScheme;
const theme = getUtterancesTheme(mode);
for (const frame of document.getElementsByClassName('utterances-frame')) {
frame.contentWindow.postMessage({ type: 'set-theme', theme: theme }, '*');
}
});
This script expects to find a <div id="comments"></div>
in the page, and will place utterances inside this div.
By far the best way to do it is to use what's already built in (although same as previous comment, but it's clearer since it's barebones)
let iframe = document.querySelector("iframe");
let msg = {
type: "set-theme",
theme: 'your-theme'
};
iframe.contentWindow.postMessage(msg, "https://utteranc.es");
Yes, sending the set-theme
message is clear. However, this means that the theme will switch after load and this change will be visible to the user. To me that seems sloppy so instead my solution initializes it on the component.
Also, when do you send the message to have it switch? I imagine there is a way to know when utterances is ready to receive messages, but I'm not sure what it is. I don't think that messages sent via postMessage
get queued waiting for a listener, since that would be asking for memory leaks (but it would allow for much cleaner solutions to this if it did).
Yes, sending the
set-theme
message is clear. However, this means that the theme will switch after load and this change will be visible to the user. To me that seems sloppy so instead my solution initializes it on the component.Also, when do you send the message to have it switch? I imagine there is a way to know when utterances is ready to receive messages, but I'm not sure what it is. I don't think that messages sent via
postMessage
get queued waiting for a listener, since that would be asking for memory leaks (but it would allow for much cleaner solutions to this if it did).
What I posted wasn't meant to be sent when you load it to change it (that's sloppy as you said). For that, you can simply load the script with the correct theme already specified, as you did. (Besides, I had no need to re-post a solution for that which you already provided)
It was meant for changing the theme after it's already loaded (like the website theme switches). This is in-line with the title of the issue feature request (as people already know they can specify a different theme when it loads, that's the easy part).
So it was just a stripped down version of the answer to the issue's title, for anyone that wanted to see it easily.
Of course, it has been stated quite a few times here already. I just posted it because it's easy to see (perhaps compared to some other answers, due to formatting. Plus, I had to figure this solution out for myself, so I wanted to share it at the same time)
Your answer is plenty useful, thanks!
so my website has a light and dark theme. would there be a way to do what the title says?