mirror of
https://github.com/linkwarden/browser-extension.git
synced 2026-06-23 04:10:26 +00:00
bug fixes and improvements
This commit is contained in:
@@ -16,15 +16,11 @@ import { Input } from './ui/Input.tsx';
|
||||
import { Button } from './ui/Button.tsx';
|
||||
import { TagInput } from './TagInput.tsx';
|
||||
import { Textarea } from './ui/Textarea.tsx';
|
||||
import {
|
||||
checkDuplicatedItem,
|
||||
getCurrentTabInfo,
|
||||
updateBadge,
|
||||
} from '../lib/utils.ts';
|
||||
import { getCurrentTabInfo, updateBadge } from '../lib/utils.ts';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useMutation, useQuery } from '@tanstack/react-query';
|
||||
import { getConfig, isConfigured } from '../lib/config.ts';
|
||||
import { postLink } from '../lib/actions/links.ts';
|
||||
import { getConfig, isConfigured as getIsConfigured } from '../lib/config.ts';
|
||||
import { checkLinkExists, postLink } from '../lib/actions/links.ts';
|
||||
import { AxiosError } from 'axios';
|
||||
import { toast } from '../../hooks/use-toast.ts';
|
||||
import { Toaster } from './ui/Toaster.tsx';
|
||||
@@ -40,18 +36,18 @@ import {
|
||||
CommandInput,
|
||||
CommandItem,
|
||||
} from './ui/Command.tsx';
|
||||
import { saveLinksInCache } from '../lib/cache.ts';
|
||||
import { Checkbox } from './ui/CheckBox.tsx';
|
||||
import { Label } from './ui/Label.tsx';
|
||||
|
||||
let configured = false;
|
||||
let duplicated = false;
|
||||
const BookmarkForm = () => {
|
||||
const [openOptions, setOpenOptions] = useState<boolean>(false);
|
||||
const [openCollections, setOpenCollections] = useState<boolean>(false);
|
||||
const [uploadImage, setUploadImage] = useState<boolean>(false);
|
||||
const [state, setState] = useState<'capturing' | 'uploading' | null>(null);
|
||||
|
||||
const [isConfigured, setIsConfigured] = useState(false);
|
||||
const [isDuplicate, setIsDuplicate] = useState(false);
|
||||
|
||||
const handleCheckedChange = (s: boolean | 'indeterminate') => {
|
||||
if (s === 'indeterminate') return;
|
||||
setUploadImage(s);
|
||||
@@ -136,32 +132,35 @@ const BookmarkForm = () => {
|
||||
});
|
||||
});
|
||||
const getConfigUse = async () => {
|
||||
configured = await isConfigured();
|
||||
duplicated = await checkDuplicatedItem();
|
||||
const config = await getConfig();
|
||||
const configured = await getIsConfigured();
|
||||
const duplicate = await checkLinkExists(config.baseUrl, config.apiKey);
|
||||
setIsDuplicate(duplicate);
|
||||
setIsConfigured(configured);
|
||||
};
|
||||
getConfigUse();
|
||||
}, [form]);
|
||||
|
||||
useEffect(() => {
|
||||
const syncBookmarks = async () => {
|
||||
try {
|
||||
const { syncBookmarks, baseUrl, defaultCollection } = await getConfig();
|
||||
form.setValue('collection', {
|
||||
name: defaultCollection,
|
||||
});
|
||||
if (!syncBookmarks) {
|
||||
return;
|
||||
}
|
||||
if (await isConfigured()) {
|
||||
await saveLinksInCache(baseUrl);
|
||||
//await syncLocalBookmarks(baseUrl);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
syncBookmarks();
|
||||
}, [form]);
|
||||
// useEffect(() => {
|
||||
// const syncBookmarks = async () => {
|
||||
// try {
|
||||
// const { syncBookmarks, baseUrl, defaultCollection } = await getConfig();
|
||||
// form.setValue('collection', {
|
||||
// name: defaultCollection,
|
||||
// });
|
||||
// if (!syncBookmarks) {
|
||||
// return;
|
||||
// }
|
||||
// if (await isConfigured()) {
|
||||
// await saveLinksInCache(baseUrl);
|
||||
// await syncLocalBookmarks(baseUrl);
|
||||
// }
|
||||
// } catch (error) {
|
||||
// console.error(error);
|
||||
// }
|
||||
// };
|
||||
// syncBookmarks();
|
||||
// }, [form]);
|
||||
|
||||
const {
|
||||
isLoading: loadingCollections,
|
||||
@@ -178,7 +177,7 @@ const BookmarkForm = () => {
|
||||
return a.pathname.localeCompare(b.pathname);
|
||||
});
|
||||
},
|
||||
enabled: configured,
|
||||
enabled: isConfigured,
|
||||
});
|
||||
|
||||
const {
|
||||
@@ -196,7 +195,7 @@ const BookmarkForm = () => {
|
||||
return a.name.localeCompare(b.name);
|
||||
});
|
||||
},
|
||||
enabled: configured,
|
||||
enabled: isConfigured,
|
||||
});
|
||||
|
||||
return (
|
||||
@@ -442,9 +441,9 @@ const BookmarkForm = () => {
|
||||
</Label>
|
||||
</div>
|
||||
)}
|
||||
{duplicated && (
|
||||
{isDuplicate && (
|
||||
<p className="text-muted text-zinc-600 dark:text-zinc-400 mt-2">
|
||||
You already have this link saved.
|
||||
You already saved this link.
|
||||
</p>
|
||||
)}
|
||||
<div className="flex justify-between items-center mt-4">
|
||||
|
||||
+17
-17
@@ -1,7 +1,7 @@
|
||||
import captureScreenshot from '../screenshot.ts';
|
||||
import { bookmarkFormValues } from '../validators/bookmarkForm.ts';
|
||||
import axios from 'axios';
|
||||
import { bookmarkMetadata } from '../cache.ts';
|
||||
// import { bookmarkMetadata } from '../cache.ts';
|
||||
import { getCurrentTabInfo } from '../utils.ts';
|
||||
|
||||
export async function postLink(
|
||||
@@ -100,18 +100,18 @@ export async function deleteLinkFetch(
|
||||
});
|
||||
}
|
||||
|
||||
export async function getLinksFetch(
|
||||
baseUrl: string,
|
||||
apiKey: string
|
||||
): Promise<{ response: bookmarkMetadata[] }> {
|
||||
const url = `${baseUrl}/api/v1/links`;
|
||||
const response = await fetch(url, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
},
|
||||
});
|
||||
return await response.json();
|
||||
}
|
||||
// export async function getLinksFetch(
|
||||
// baseUrl: string,
|
||||
// apiKey: string
|
||||
// ): Promise<{ response: bookmarkMetadata[] }> {
|
||||
// const url = `${baseUrl}/api/v1/links`;
|
||||
// const response = await fetch(url, {
|
||||
// headers: {
|
||||
// Authorization: `Bearer ${apiKey}`,
|
||||
// },
|
||||
// });
|
||||
// return await response.json();
|
||||
// }
|
||||
|
||||
export async function checkLinkExists(
|
||||
baseUrl: string,
|
||||
@@ -124,8 +124,8 @@ export async function checkLinkExists(
|
||||
}
|
||||
|
||||
const url =
|
||||
`${baseUrl}/api/v1/links?cursor=0&sort=0&searchQueryString=` +
|
||||
encodeURIComponent(`${tabInfo.url}`);
|
||||
`${baseUrl}/api/v1/search?sort=0&searchQueryString=` +
|
||||
encodeURIComponent(`url:${tabInfo.url}`);
|
||||
|
||||
const response = await fetch(url, {
|
||||
headers: {
|
||||
@@ -133,9 +133,9 @@ export async function checkLinkExists(
|
||||
},
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
const { data } = await response.json();
|
||||
|
||||
const exists = data.response.length > 0;
|
||||
const exists = !!data && data.links?.length > 0;
|
||||
|
||||
return exists;
|
||||
}
|
||||
|
||||
+96
-64
@@ -1,6 +1,10 @@
|
||||
import { getBrowser, getStorageItem, setStorageItem } from './utils.ts';
|
||||
import { bookmarkFormValues } from './validators/bookmarkForm.ts';
|
||||
import { deleteLinkFetch, getLinksFetch, postLinkFetch, updateLinkFetch } from './actions/links.ts';
|
||||
import {
|
||||
deleteLinkFetch,
|
||||
postLinkFetch,
|
||||
updateLinkFetch,
|
||||
} from './actions/links.ts';
|
||||
import BookmarkTreeNode = chrome.bookmarks.BookmarkTreeNode;
|
||||
import { getConfig } from './config.ts';
|
||||
|
||||
@@ -26,32 +30,52 @@ export async function getBookmarksMetadata(): Promise<bookmarkMetadata[]> {
|
||||
return bookmarksMetadata ? JSON.parse(bookmarksMetadata) : DEFAULTS;
|
||||
}
|
||||
|
||||
export async function saveBookmarksMetadata(bookmarksMetadata: bookmarkMetadata[]) {
|
||||
return await setStorageItem(BOOKMARKS_METADATA_KEY, JSON.stringify(bookmarksMetadata));
|
||||
export async function saveBookmarksMetadata(
|
||||
bookmarksMetadata: bookmarkMetadata[]
|
||||
) {
|
||||
return await setStorageItem(
|
||||
BOOKMARKS_METADATA_KEY,
|
||||
JSON.stringify(bookmarksMetadata)
|
||||
);
|
||||
}
|
||||
|
||||
export async function clearBookmarksMetadata() {
|
||||
return await setStorageItem(BOOKMARKS_METADATA_KEY, JSON.stringify([]));
|
||||
}
|
||||
|
||||
export async function getBookmarkMetadataById(id: number): Promise<bookmarkMetadata | undefined> {
|
||||
export async function getBookmarkMetadataById(
|
||||
id: number
|
||||
): Promise<bookmarkMetadata | undefined> {
|
||||
const bookmarksMetadata = await getBookmarksMetadata();
|
||||
return bookmarksMetadata.find((bookmarkMetadata) => bookmarkMetadata.id === id);
|
||||
return bookmarksMetadata.find(
|
||||
(bookmarkMetadata) => bookmarkMetadata.id === id
|
||||
);
|
||||
}
|
||||
|
||||
export async function getBookmarkMetadataByBookmarkId(bookmarkId: string): Promise<bookmarkMetadata | undefined> {
|
||||
export async function getBookmarkMetadataByBookmarkId(
|
||||
bookmarkId: string
|
||||
): Promise<bookmarkMetadata | undefined> {
|
||||
const bookmarksMetadata = await getBookmarksMetadata();
|
||||
return bookmarksMetadata.find((bookmarkMetadata) => bookmarkMetadata.bookmarkId === bookmarkId);
|
||||
return bookmarksMetadata.find(
|
||||
(bookmarkMetadata) => bookmarkMetadata.bookmarkId === bookmarkId
|
||||
);
|
||||
}
|
||||
|
||||
export async function getBookmarkMetadataByUrl(url: string): Promise<bookmarkMetadata | undefined> {
|
||||
export async function getBookmarkMetadataByUrl(
|
||||
url: string
|
||||
): Promise<bookmarkMetadata | undefined> {
|
||||
const bookmarksMetadata = await getBookmarksMetadata();
|
||||
return bookmarksMetadata.find((bookmarkMetadata) => bookmarkMetadata.url === url);
|
||||
return bookmarksMetadata.find(
|
||||
(bookmarkMetadata) => bookmarkMetadata.url === url
|
||||
);
|
||||
}
|
||||
|
||||
export async function saveBookmarkMetadata(bookmarkMetadata: bookmarkMetadata) {
|
||||
const bookmarksMetadata = await getBookmarksMetadata();
|
||||
const index = bookmarksMetadata.findIndex((bookmarkMetadataObject) => bookmarkMetadataObject.id === bookmarkMetadata.id);
|
||||
const index = bookmarksMetadata.findIndex(
|
||||
(bookmarkMetadataObject) =>
|
||||
bookmarkMetadataObject.id === bookmarkMetadata.id
|
||||
);
|
||||
if (index !== -1) {
|
||||
bookmarksMetadata[index] = bookmarkMetadata;
|
||||
} else {
|
||||
@@ -62,7 +86,9 @@ export async function saveBookmarkMetadata(bookmarkMetadata: bookmarkMetadata) {
|
||||
|
||||
export async function deleteBookmarkMetadata(id: string | undefined) {
|
||||
const bookmarksMetadata = await getBookmarksMetadata();
|
||||
const index = bookmarksMetadata.findIndex((bookmarkMetadata) => bookmarkMetadata.bookmarkId === id);
|
||||
const index = bookmarksMetadata.findIndex(
|
||||
(bookmarkMetadata) => bookmarkMetadata.bookmarkId === id
|
||||
);
|
||||
if (index !== -1) {
|
||||
bookmarksMetadata.splice(index, 1);
|
||||
}
|
||||
@@ -70,55 +96,57 @@ export async function deleteBookmarkMetadata(id: string | undefined) {
|
||||
}
|
||||
|
||||
// It just works, don't MOVE
|
||||
export async function saveLinksInCache(baseUrl: string) {
|
||||
try {
|
||||
const { apiKey } = await getConfig();
|
||||
const links = await getLinksFetch(baseUrl, apiKey);
|
||||
const linksResponse = links.response;
|
||||
// export async function saveLinksInCache(baseUrl: string) {
|
||||
// try {
|
||||
// const { apiKey } = await getConfig();
|
||||
// const links = await getLinksFetch(baseUrl, apiKey);
|
||||
// const linksResponse = links.response;
|
||||
|
||||
// Create a map to track which bookmarks are still present on the server
|
||||
const serverBookmarkMap = new Map<number, bookmarkMetadata>();
|
||||
linksResponse.forEach(link => serverBookmarkMap.set(link.id, link));
|
||||
// // Create a map to track which bookmarks are still present on the server
|
||||
// const serverBookmarkMap = new Map<number, bookmarkMetadata>();
|
||||
// linksResponse.forEach(link => serverBookmarkMap.set(link.id, link));
|
||||
|
||||
// Get the current bookmarks metadata from the cache
|
||||
const bookmarksMetadata = await getBookmarksMetadata();
|
||||
// // Get the current bookmarks metadata from the cache
|
||||
// const bookmarksMetadata = await getBookmarksMetadata();
|
||||
|
||||
// Update or add bookmarks based on server response
|
||||
for (let link of linksResponse) {
|
||||
const existingLinkIndex = bookmarksMetadata.findIndex((bookmarkMetadata) => bookmarkMetadata.id === link.id);
|
||||
if (existingLinkIndex !== -1) {
|
||||
// Update existing bookmark if there are changes
|
||||
link = { ...bookmarksMetadata[existingLinkIndex], ...link };
|
||||
bookmarksMetadata[existingLinkIndex] = link;
|
||||
} else {
|
||||
// Add new bookmark from the server
|
||||
const newLocalBookmark = await createBookmarkInBrowser(link);
|
||||
link.bookmarkId = newLocalBookmark.id;
|
||||
bookmarksMetadata.push(link);
|
||||
}
|
||||
}
|
||||
// // Update or add bookmarks based on server response
|
||||
// for (let link of linksResponse) {
|
||||
// const existingLinkIndex = bookmarksMetadata.findIndex((bookmarkMetadata) => bookmarkMetadata.id === link.id);
|
||||
// if (existingLinkIndex !== -1) {
|
||||
// // Update existing bookmark if there are changes
|
||||
// link = { ...bookmarksMetadata[existingLinkIndex], ...link };
|
||||
// bookmarksMetadata[existingLinkIndex] = link;
|
||||
// } else {
|
||||
// // Add new bookmark from the server
|
||||
// const newLocalBookmark = await createBookmarkInBrowser(link);
|
||||
// link.bookmarkId = newLocalBookmark.id;
|
||||
// bookmarksMetadata.push(link);
|
||||
// }
|
||||
// }
|
||||
|
||||
// Remove cached bookmarks that are no longer present on the server
|
||||
const bookmarksToRemove = bookmarksMetadata.filter((bookmarkMetadata) => !serverBookmarkMap.has(bookmarkMetadata.id));
|
||||
for (const bookmarkToRemove of bookmarksToRemove) {
|
||||
const indexToRemove = bookmarksMetadata.indexOf(bookmarkToRemove);
|
||||
if (indexToRemove !== -1) {
|
||||
bookmarksMetadata.splice(indexToRemove, 1);
|
||||
if (bookmarkToRemove.bookmarkId != null) {
|
||||
await browser.bookmarks.remove(bookmarkToRemove.bookmarkId);
|
||||
}
|
||||
}
|
||||
}
|
||||
// // Remove cached bookmarks that are no longer present on the server
|
||||
// const bookmarksToRemove = bookmarksMetadata.filter((bookmarkMetadata) => !serverBookmarkMap.has(bookmarkMetadata.id));
|
||||
// for (const bookmarkToRemove of bookmarksToRemove) {
|
||||
// const indexToRemove = bookmarksMetadata.indexOf(bookmarkToRemove);
|
||||
// if (indexToRemove !== -1) {
|
||||
// bookmarksMetadata.splice(indexToRemove, 1);
|
||||
// if (bookmarkToRemove.bookmarkId != null) {
|
||||
// await browser.bookmarks.remove(bookmarkToRemove.bookmarkId);
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// Save the updated bookmarks metadata back to the cache
|
||||
await saveBookmarksMetadata(bookmarksMetadata);
|
||||
// // Save the updated bookmarks metadata back to the cache
|
||||
// await saveBookmarksMetadata(bookmarksMetadata);
|
||||
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
// } catch (error) {
|
||||
// console.error(error);
|
||||
// }
|
||||
// }
|
||||
|
||||
export async function createBookmarkInBrowser(bookmark: bookmarkMetadata): Promise<BookmarkTreeNode> {
|
||||
export async function createBookmarkInBrowser(
|
||||
bookmark: bookmarkMetadata
|
||||
): Promise<BookmarkTreeNode> {
|
||||
const { url, name } = bookmark;
|
||||
return await browser.bookmarks.create({ url, title: name });
|
||||
}
|
||||
@@ -141,7 +169,9 @@ export async function syncLocalBookmarks(baseUrl: string) {
|
||||
const bookmarksMetadata = await getBookmarksMetadata();
|
||||
|
||||
// Create a map of cached bookmarks by URL for easy lookup
|
||||
const cachedBookmarksMap = new Map(bookmarksMetadata.map(bm => [bm.url, bm]));
|
||||
const cachedBookmarksMap = new Map(
|
||||
bookmarksMetadata.map((bm) => [bm.url, bm])
|
||||
);
|
||||
|
||||
// Prepare arrays to hold promises for new, updated, and deleted bookmarks
|
||||
const createPromises = [];
|
||||
@@ -156,7 +186,9 @@ export async function syncLocalBookmarks(baseUrl: string) {
|
||||
createPromises.push(postLinkFetch(baseUrl, localBookmark, apiKey));
|
||||
} else if (cachedBookmark.name !== localBookmark.name) {
|
||||
// Updated bookmark
|
||||
updatePromises.push(updateLinkFetch(baseUrl, cachedBookmark.id, localBookmark, apiKey));
|
||||
updatePromises.push(
|
||||
updateLinkFetch(baseUrl, cachedBookmark.id, localBookmark, apiKey)
|
||||
);
|
||||
}
|
||||
// Remove from the map to track deleted bookmarks
|
||||
cachedBookmarksMap.delete(localBookmark.url);
|
||||
@@ -168,7 +200,11 @@ export async function syncLocalBookmarks(baseUrl: string) {
|
||||
}
|
||||
|
||||
// Run all create, update, and delete operations in parallel
|
||||
await Promise.all([...createPromises, ...updatePromises, ...deletePromises]);
|
||||
await Promise.all([
|
||||
...createPromises,
|
||||
...updatePromises,
|
||||
...deletePromises,
|
||||
]);
|
||||
|
||||
// Update the cached bookmarks metadata
|
||||
await saveBookmarksMetadata(localBookmarks);
|
||||
@@ -178,7 +214,10 @@ export async function syncLocalBookmarks(baseUrl: string) {
|
||||
}
|
||||
|
||||
// Helper function to collect all bookmarks recursively
|
||||
function logBookmarks(bookmarks: BookmarkTreeNode[], accumulator: bookmarkMetadata[]) {
|
||||
function logBookmarks(
|
||||
bookmarks: BookmarkTreeNode[],
|
||||
accumulator: bookmarkMetadata[]
|
||||
) {
|
||||
for (const bookmark of bookmarks) {
|
||||
if (bookmark.url) {
|
||||
accumulator.push({
|
||||
@@ -196,10 +235,3 @@ function logBookmarks(bookmarks: BookmarkTreeNode[], accumulator: bookmarkMetada
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
+25
-36
@@ -1,9 +1,6 @@
|
||||
import { type ClassValue, clsx } from 'clsx';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
import {
|
||||
getLinksFetch,
|
||||
// checkLinkExists
|
||||
} from './actions/links.ts';
|
||||
import { checkLinkExists } from './actions/links.ts';
|
||||
import { getConfig } from './config.ts';
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
@@ -47,14 +44,6 @@ export async function getStorageItem(key: string) {
|
||||
}
|
||||
}
|
||||
|
||||
export const checkDuplicatedItem = async () => {
|
||||
const config = await getConfig();
|
||||
const currentTab = await getCurrentTabInfo();
|
||||
const { response } = await getLinksFetch(config.baseUrl, config.apiKey);
|
||||
const formatLinks = response.map((link) => link.url);
|
||||
return formatLinks.includes(currentTab.url ?? '');
|
||||
};
|
||||
|
||||
export async function setStorageItem(key: string, value: string) {
|
||||
if (getChromeStorage()) {
|
||||
return await chrome.storage.local.set({ [key]: value });
|
||||
@@ -72,28 +61,28 @@ export async function updateBadge(tabId: number | undefined) {
|
||||
if (!tabId) return;
|
||||
|
||||
// TODO: add url check endpoint for precise matching (instead of fuzzy search)
|
||||
// const browser = getBrowser();
|
||||
// const cachedConfig = await getConfig();
|
||||
// const linkExists = await checkLinkExists(
|
||||
// cachedConfig.baseUrl,
|
||||
// cachedConfig.apiKey
|
||||
// );
|
||||
// if (linkExists) {
|
||||
// if (browser.action) {
|
||||
// browser.action.setBadgeText({ tabId, text: '✓' });
|
||||
// browser.action.setBadgeBackgroundColor({ tabId, color: '#98c0ff' });
|
||||
// } else {
|
||||
// browser.browserAction.setBadgeText({ tabId, text: '✓' });
|
||||
// browser.browserAction.setBadgeBackgroundColor({
|
||||
// tabId,
|
||||
// color: '#98c0ff',
|
||||
// });
|
||||
// }
|
||||
// } else {
|
||||
// if (browser.action) {
|
||||
// browser.action.setBadgeText({ tabId, text: '' });
|
||||
// } else {
|
||||
// browser.browserAction.setBadgeText({ tabId, text: '' });
|
||||
// }
|
||||
// }
|
||||
const browser = getBrowser();
|
||||
const cachedConfig = await getConfig();
|
||||
const linkExists = await checkLinkExists(
|
||||
cachedConfig.baseUrl,
|
||||
cachedConfig.apiKey
|
||||
);
|
||||
if (linkExists) {
|
||||
if (browser.action) {
|
||||
browser.action.setBadgeText({ tabId, text: '✓' });
|
||||
browser.action.setBadgeBackgroundColor({ tabId, color: '#98c0ff' });
|
||||
} else {
|
||||
browser.browserAction.setBadgeText({ tabId, text: '✓' });
|
||||
browser.browserAction.setBadgeBackgroundColor({
|
||||
tabId,
|
||||
color: '#98c0ff',
|
||||
});
|
||||
}
|
||||
} else {
|
||||
if (browser.action) {
|
||||
browser.action.setBadgeText({ tabId, text: '' });
|
||||
} else {
|
||||
browser.browserAction.setBadgeText({ tabId, text: '' });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user