Skip to content

Instantly share code, notes, and snippets.

@lincolnthalles
Last active April 22, 2026 07:39
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);
}
})();
@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