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);
}
})();
@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