Skip to content

Instantly share code, notes, and snippets.

@lincolnthalles
Last active February 16, 2026 17:04
Show Gist options
  • Select an option

  • Save lincolnthalles/957ee1b48634d4c41f5054526b17b5b0 to your computer and use it in GitHub Desktop.

Select an option

Save lincolnthalles/957ee1b48634d4c41f5054526b17b5b0 to your computer and use it in GitHub Desktop.
Add "Random Memo" button to Memos v0.21+ sidebar
/*
Select the appropriate code based on the Memos version you are currently running.
*/
/*
For Memos v0.22.2-v0.24.4.
*/
const RANDOM_MEMO_SETTINGS = {
// Kinds of memos to cache: PUBLIC = visible to everyone, PROTECTED = logged in users, PRIVATE = only the creator
memoKinds: ["PUBLIC", "PROTECTED", "PRIVATE"],
// Username of the memo creator to filter the memos
memoCreatorUsername: "",
// Button text
buttonText: "Random",
// Button tooltip
buttonTooltip: "Show a random memo",
};
async function getRandomMemoUid(settings) {
const memoKinds = settings.memoKinds || ["PUBLIC", "PROTECTED", "PRIVATE"];
const filters = [
`row_status=="NORMAL"`,
`visibilities==${JSON.stringify(memoKinds)}`,
"random==true",
];
if (settings.memoCreatorUsername) {
filters.push(`creator=="users/${settings.memoCreatorUsername}"`);
}
const apiEndpoint =
`${window.location.origin}/api/v1/memos?pageSize=1&filter=${encodeURIComponent(filters.join("&&"))}`;
return await fetch(apiEndpoint)
.then((response) => response.json())
.then((json) => {
if (json.memos.length === 0) {
return null;
}
return json.memos[0].uid;
})
.catch((error) => {
throw new Error(`Error fetching memos: ${error}`);
});
}
async function insertRandomMemoButton(settings) {
if (document.getElementById("header-random") !== null) {
return;
}
let insertionTries = 0;
while (
document.getElementById("header-setting") === null &&
insertionTries < 20
) {
await new Promise((r) => setTimeout(r, 100));
insertionTries++;
}
const headerSetting = document.getElementById("header-setting");
if (headerSetting === null) {
return;
}
headerSetting.insertAdjacentHTML(
"afterend",
`<a id="header-random"
class="px-2 py-2 rounded-2xl border flex flex-row items-center text-lg text-gray-800 dark:text-gray-400 hover:bg-white hover:border-gray-200 dark:hover:border-zinc-700 dark:hover:bg-zinc-800 w-full px-4 border-transparent"
href="/" title="${settings.buttonTooltip}">
<div aria-label="Random">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-dices w-6 h-auto opacity-70 shrink-0"><rect width="12" height="12" x="2" y="10" rx="2" ry="2"/><path d="m17.92 14 3.5-3.5a2.24 2.24 0 0 0 0-3l-5-4.92a2.24 2.24 0 0 0-3 0L10 6"/><path d="M6 18h.01"/><path d="M10 14h.01"/><path d="M15 6h.01"/><path d="M18 9h.01"/></svg>
</div>
<span class="ml-3 truncate">${settings.buttonText}</span>
</a>`,
);
const headerRandom = document.getElementById("header-random");
if (headerRandom === null) {
return;
}
const randomMemoUid = (await getRandomMemoUid(settings)) || "";
headerRandom.href = `/m/${randomMemoUid}`;
headerRandom.onclick = async () => {
window.location.href = `/m/${randomMemoUid}`;
};
headerRandom.onauxclick = async () => {
window.open(`/m/${randomMemoUid}`, "_blank");
};
}
(async () => {
try {
await insertRandomMemoButton(RANDOM_MEMO_SETTINGS);
} catch (e) {
console.error(e);
}
})();
/*
For Memos v0.25.0 and above.
Due to the limitations of the current Memos API, this script will fetch
ALL memos from the server and cache their IDs to reduce further requests.
This may create a lot of network traffic. If this is an issue,
tweak the settings below to narrow down what is fetched.
*/
const RANDOM_MEMO_SETTINGS = {
// Amount of memos to cache. 0 = all
cacheAmount: 0,
// Time in minutes to expire the UUIDs cache
cacheTimeMinutes: 60,
// Amount of memos to fetch per request. Internal default is 10, limit is 1000.
// Use a smaller page size to process memos in smaller batches, lowering the server and browser load.
// Note that lower values will increase the amount of network requests.
pageSize: 1000,
// Memo state. E.g. "NORMAL", or "ARCHIVED".
state: "",
// Kinds of memos to cache: PUBLIC = visible to everyone, PROTECTED = logged in users, PRIVATE = only the creator.
// E.g. ["PUBLIC", "PROTECTED", "PRIVATE"]
visibility: [],
// Tags to filter memos by. E.g. ["tag1", "tag2"]
tags: [],
// User ID of the memo creator. 0=everyone
creatorID: 0,
// Order by display_time, create_time, update_time or name. Asc or desc. E.g. "update_time desc".
// This filter may make sense if you are restricting the randomness to a limited amount of memos.
orderBy: "",
// Button text
buttonText: "Random",
// Button tooltip
buttonTooltip: "Show a random memo",
// SVG Icon
icon: `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-dices w-6 h-auto opacity-70 shrink-0"><rect width="12" height="12" x="2" y="10" rx="2" ry="2"/><path d="m17.92 14 3.5-3.5a2.24 2.24 0 0 0 0-3l-5-4.92a2.24 2.24 0 0 0-3 0L10 6"/><path d="M6 18h.01"/><path d="M10 14h.01"/><path d="M15 6h.01"/><path d="M18 9h.01"/></svg>`,
// Reference element ID. May be set to "header-memos" or "header-explore" to place the button above in the list.
refElementId: "header-attachments",
// Generated element ID
genElementId: "header-random",
// Local storage key to store the memo uuids
cacheKey: "randomMemoCache",
// Local storage key to store the last update timestamp
cacheKeyLastUpdate: "randomMemoCacheLastUpdate",
};
function getRandomMemo(settings) {
const cachedUuids = localStorage.getItem(settings.cacheKey);
if (!cachedUuids) {
console.error("[random memo button]: found no cached UUIDs");
return "";
}
const cache = JSON.parse(cachedUuids);
const randomIndex = Math.floor(Math.random() * cache.length);
return cache[randomIndex];
}
async function fetchMemos(settings, pageToken = null, collected = 0) {
const query = [];
if (pageToken) {
query.push(`pageToken=${pageToken}`);
}
if (settings.pageSize) {
query.push(`pageSize=${settings.pageSize}`);
}
if (settings.state) {
query.push(`state=${settings.state}`);
}
if (settings.orderBy) {
query.push(`orderBy=${settings.orderBy}`);
}
const filters = [];
if (settings.visibility.length) {
filters.push(`visibility in ${JSON.stringify(settings.visibility)}`);
}
if (settings.creatorID) {
filters.push(`creator_id==${settings.creatorID}`);
}
if (settings.tags.length) {
filters.push(`tag in ${JSON.stringify(settings.tags)}`);
}
if (filters.length) {
query.push(`filter=${encodeURIComponent(filters.join("&&"))}`);
}
const apiEndpoint = `${window.location.origin}/api/v1/memos?${query.join("&")}`;
const memoUuids = [];
await fetch(apiEndpoint)
.then((response) => response.json())
.then(async (json) => {
if (!json?.memos?.length) {
return null;
}
for (let i = 0; i < json.memos.length; i++) {
if (settings.cacheAmount > 0 && i >= settings.cacheAmount) break;
memoUuids.push(json.memos[i].name);
}
if (json.nextPageToken) {
const totalCollected = collected + memoUuids.length;
if (settings.cacheAmount > 0 && totalCollected >= settings.cacheAmount) return;
await fetchMemos(settings, json.nextPageToken, totalCollected).then((nextPage) => {
const remainingLimit = settings.cacheAmount - totalCollected;
if (settings.cacheAmount > 0 && nextPage.length > remainingLimit) {
nextPage = nextPage.slice(0, remainingLimit);
}
memoUuids.push(...nextPage);
});
}
})
.catch((error) => {
throw new Error(`[random memo button]: error fetching memos: ${error}`);
});
return memoUuids;
}
async function cacheMemosUuids(settings) {
const lastUpdate = localStorage.getItem(settings.cacheKeyLastUpdate);
const cacheTimeMillis = (settings.cacheTimeMinutes || 60) * 60 * 1000;
const mustUpdate = lastUpdate === null || Date.now() - lastUpdate > cacheTimeMillis;
if (localStorage.getItem(settings.cacheKey) !== null && !mustUpdate) {
return;
}
return await fetchMemos(settings)
.then((memoUuids) => {
localStorage.setItem(settings.cacheKey, JSON.stringify(memoUuids));
localStorage.setItem(settings.cacheKeyLastUpdate, Date.now());
})
.catch((error) => {
throw new Error(`[random memo button]: error fetching memos: ${error}`);
});
}
async function insertRandomMemoButton(settings) {
if (document.getElementById(settings.genElementId) !== null) {
return;
}
let refElement = null;
for (let i = 0; i < 20; i++) {
refElement = document.getElementById(settings.refElementId);
if (refElement) break;
await new Promise((r) => setTimeout(r, 50));
}
if (!refElement) {
return;
}
const genElement = refElement.cloneNode(true);
genElement.id = settings.genElementId;
genElement.title = settings.buttonTooltip;
genElement.removeAttribute("href");
genElement.addEventListener("mouseenter", () => {
genElement.style.cursor = "pointer";
});
genElement.addEventListener("mouseleave", () => {
genElement.style.cursor = "";
});
genElement.removeAttribute("data-discover");
const div = genElement.querySelector("div");
if (div) {
div.removeAttribute("data-state");
div.removeAttribute("data-slot");
}
const svg = genElement.querySelector("svg");
if (svg) {
svg.innerHTML = settings.icon;
}
const span = genElement.querySelector("span");
if (span) {
span.innerHTML = settings.buttonText;
}
if (refElement.parentNode) {
refElement.parentNode.insertBefore(genElement, refElement.nextSibling);
}
const setHref = () => {
const randomMemo = getRandomMemo(settings);
if (!randomMemo) return;
genElement.href = `/${randomMemo}`;
};
genElement.onclick = setHref;
genElement.onauxclick = setHref;
}
(async () => {
try {
await insertRandomMemoButton(RANDOM_MEMO_SETTINGS);
await cacheMemosUuids(RANDOM_MEMO_SETTINGS);
} catch (e) {
console.error(e);
}
})();
@lincolnthalles
Copy link
Copy Markdown
Author

lincolnthalles commented Mar 28, 2024

This user script is meant for usememos/memos.

How to add this script

Click Settings -> System and paste the code in the Additional script field, click Save and reload the page.

image

Edit history

  • Moved code from this comment to this gist.
  • Updated code to handle v0.21+ API (name field was renamed to uid).
  • Refactored all user-relevant settings into a single object.
  • Tweaked button style when the sidebar is collapsed (there's still a minor icon alignment issue, though).
  • Added specific code to handle v0.25+. Caching is now required as the internal random filter was removed. Added a lot of filtering options. The random button is now replicated on the fly from the attachments, replacing the icon and action, preventing any styling issues.

@rheicide
Copy link
Copy Markdown

@lincolnthalles Thanks for this! I notice that random search has been implemented for the API in v0.21 (usememos/memos@bb10bb2); maybe this script can be updated to use that?

@lincolnthalles
Copy link
Copy Markdown
Author

@lincolnthalles Thanks for this! I notice that random search has been implemented for the API in v0.21 (usememos/memos@bb10bb2); maybe this script can be updated to use that?

I've just added random_memo_v0.21+.js.
While this works and has less code, it has the drawback of doing one API query every time the index is reloaded.

The earlier code does a big API query once and caches all memo IDs for the specified amount of time.

@rheicide
Copy link
Copy Markdown

Thank you for adding that, I appreciate it. The extra API queries are a trade-off, but it's nice to have another option.

I just tried it and there were no errors; however, it appeared to always show the latest memo when clicking the Random button. There's probably some mistake in the API's implementation.

@lincolnthalles
Copy link
Copy Markdown
Author

Thank you for adding that, I appreciate it. The extra API queries are a trade-off, but it's nice to have another option.

I just tried it and there were no errors; however, it appeared to always show the latest memo when clicking the Random button. There's probably some mistake in the API's implementation.

The random API is only working on the latest v0.21.0. If you have a preliminary v0.21.0 built over a week ago, it won't work until you upgrade the server. Memos uses a rolling release model, so there can be different v0.21.0 in the wild.

You can test the API at the demo website.

@rheicide
Copy link
Copy Markdown

Right, the demo website's API works—thanks for pointing that out. Now I'm not sure what's wrong with my instance (with MariaDB as storage) because I just double-checked and it was already using the latest stable Docker image updated just 2 days ago. I also tried switching to the 0.21 tag and the latest one (which actually is identical to stable, same hash) but still got the same behavior.

Anyway, for now I'll be using your original version and I'll try the v0.21+ one later when there's a new Docker image. Sorry for wasting your time on this issue!

@mtcsol
Copy link
Copy Markdown

mtcsol commented Jun 21, 2024

After upgrading to version v0.22.2, clicking the button does not respond.

@lincolnthalles
Copy link
Copy Markdown
Author

lincolnthalles commented Jun 21, 2024

After upgrading to version v0.22.2, clicking the button does not respond.

API was renamed from v2 to v1. Fixed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment