Overview
ClipSync provides real-time clipboard synchronization across all devices in a session. Content sent from any device instantly appears on all other connected devices using Supabase’s realtime database subscriptions.
Real-Time Synchronization
How It Works
ClipSync uses Supabase’s PostgreSQL realtime capabilities to instantly sync clipboard updates:
useEffect(() => {
if (!sessionCode) return;
const channel = supabase
.channel("clipboard")
.on("postgres_changes", {
event: "*",
schema: "public",
table: "clipboard"
}, (payload) => {
if (payload.new.session_code === sessionCode && payload.eventType === "INSERT") {
setHistory((prev) => [payload.new, ...prev]);
setClipboard("");
}
if (payload.eventType === "DELETE") {
if (deleteOne) {
setHistory([]);
} else {
setHistory((prev) => prev.filter((item) => item.id !== payload.old.id));
}
}
})
.subscribe();
return () => {
supabase.removeChannel(channel);
};
}, [sessionCode, isOffline]);
The realtime subscription only listens for updates matching your current session code, ensuring privacy between sessions.
Sending Content to Clipboard
Text Content
To send text content:
- Type or paste content into the textarea
- Optionally mark content as sensitive
- Click “Send to Clipboard”
const updateClipboard = async () => {
// if fileURL exists, then it is a file so text can be empty
if (!clipboard && !fileUrl) return toast.error("Please enter some text to update clipboard");
if (clipboard.length > 15000) return toast.error("Clipboard content is too long. Please keep it under 15000 characters.");
let firstTime = false;
if (!sessionCode) {
await createSession(setSessionCode);
firstTime = true;
}
const code = localStorage.getItem("sessionCode");
await supabase.from("clipboard").insert([{
session_code: code,
content: clipboard,
fileUrl: fileUrl ? fileUrl.url : null,
file: fileUrl ? fileUrl : null,
sensitive: isSensitive,
}]);
if (history.length == 0 && firstTime) {
// Manually fetch latest history to update UI immediately
const { data, error: fetchError } = await supabase
.from("clipboard")
.select("*")
.eq("session_code", code)
.order("created_at", { ascending: false });
if (!fetchError) {
setHistory(data);
}
}
setFileUrl(null);
setClipboard("");
sessionStorage.removeItem("clipboard");
setIsSensitive(false);
toast.success("Clipboard updated successfully!");
};
Paste from System Clipboard
ClipSync can read directly from your system clipboard:
const addClipboardText = () => {
navigator.clipboard.readText().then((text) => {
if (text.trim()) {
setClipboard(text);
toast.success("Clipboard text pasted successfully!");
} else {
alert("Clipboard is empty or contains unsupported data.");
}
}).catch(() => {
alert("An error occurred while reading clipboard");
});
};
Your browser may prompt for clipboard access permission the first time you use this feature.
Character Limits
ClipSync enforces a 15,000 character limit per clipboard entry to ensure performance and database efficiency.
From App.jsx:170:
if (clipboard.length > 15000)
return toast.error("Clipboard content is too long. Please keep it under 15000 characters.");
Content exceeding 15,000 characters will be rejected. Consider splitting large content into multiple entries or using file sharing for longer text.
Copying from History
Each clipboard entry in the history can be copied back to your system clipboard with one click:
const copyToClipboard = (content) => {
navigator.clipboard.writeText(content);
toast.success("Text copied to clipboard!");
};
The copy button is available for each history item:
<button
aria-label="Copy Content"
className="text-blue-500 active:text-blue-700 active:scale-95"
onClick={() => copyToClipboard(item.content)}>
<Copy size={19} />
</button>
Sensitive Content Masking
ClipSync includes a sensitive content feature to protect private information when sharing your screen or device.
How It Works
- Check the “Sensitive” checkbox before sending content
- The content is stored normally in the database
- When displayed in the history, it’s replaced with asterisks
Implementation
From utils/index.js:1-11:
export const convertLinksToAnchor = (text, item) => {
if(item.sensitive) {
// replace it with ******
return "**********************";
}
const urlRegex = /(https?:\/\/[^\s]+|www\.[^\s]+)/g;
return text.replace(urlRegex, (url) => {
let hyperlink = url.startsWith("www.") ? `https://${url}` : url;
return `<a href="${hyperlink}" target="_blank" rel="noopener noreferrer" class="text-blue-500 underline">${url}</a>`;
});
};
UI checkbox from App.jsx:531-536:
<div className="flex absolute items-center gap-1 w-fit right-3 bottom-3 z-10">
<label htmlFor="is-sensitive">Sensitive</label>
<input
checked={isSensitive}
onChange={(e) => setIsSensitive(e.target.checked)}
type="checkbox" id="is-sensitive" className="ml-2" />
</div>
Use sensitive mode for passwords, API keys, personal information, or any content you want to keep private from onlookers.
Automatic Link Detection
Clipboard content is automatically scanned for URLs, which are converted to clickable links:
<p
onClick={() => toggleExpand(item.id)}
className={`text-sm flex-1 word-wrap link-wrap cursor-pointer truncate text-wrap w-fit`}
dangerouslySetInnerHTML={{
__html: expandedId === item.id
? convertLinksToAnchor(item.content, item)
: item.content.length > 180
? convertLinksToAnchor(item.content.substring(0, 180), item) + "..."
: convertLinksToAnchor(item.content.substring(0, 180), item)
}}
></p>
Clipboard History
Viewing History
All clipboard entries are stored and displayed in chronological order (newest first):
const fetchClipboardHistory = async () => {
if (!sessionCode) return;
let { data, error } = await supabase
.from("clipboard")
.select("*")
.eq("session_code", sessionCode);
if (!error) setClipboard(data || []);
};
Searching History
ClipSync includes a search feature to filter clipboard history:
const handleSearch = (e) => {
setSearchKeyword(e.target.value);
if (e.target.value.trim() === "") {
setSearchResults(history);
return;
}
const results = history.filter((item) =>
item.content.toLowerCase().includes(e.target.value.toLowerCase()));
setSearchResults(results);
}
Editing History Items
You can edit previously sent content:
const handleEdit = async (id) => {
setDeleteOne(true);
// fetch item from database
const content = history.find((item) => item.id === id).content;
setClipboard(content);
// delete item from database
await supabase.from("clipboard").delete().eq("id", id);
// set file if exists
setFileUrl(history.find((item) => item.id === id).file);
// delete item from history
const newHistory = history.filter((item) => item.id !== id);
setHistory(newHistory);
setDeleteOne(false);
toast.success("Clipboard content added to editor!");
}
Editing removes the original entry and loads it back into the editor. You’ll need to send it again after making changes.
Clearing History
Delete all clipboard entries from your session:
const deleteAll = async () => {
const response = confirm("Are you sure you want to clear clipboards?");
if (!response) return;
if (history.length == 0) return toast.error("No items in your clipboard history");
// delete all items from database with session code if file exists delete from storage
history.forEach(async (item) => {
if (item.file) {
await supabase.storage.from("clipboard").remove([item.file.name]);
}
});
const { error } = await supabase.from("clipboard").delete().eq("session_code", sessionCode);
if (error) {
toast.error("An error occurred while deleting clipboard history");
return;
}
setHistory([]);
toast.success("Clipboard history deleted successfully!");
};
Deleting history is permanent and affects all devices in the session. Associated files are also removed from storage.