Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 49 additions & 11 deletions app/components/FileManager.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,36 @@ export default function FileManager() {
});
};

// Handle URL changes from browser back/forward buttons
useEffect(() => {
if (isLoadingStorages || storages.length === 0 || !isInitialized) {
return;
}

const urlParam = getUrlFromSearchParams();

if (!urlParam) {
// No URL in params - reset to root if we have a storage selected
if (selectedStorageId) {
const storage = storages.find((s) => s.id === selectedStorageId);
if (storage) {
setCurrentPath("/");
removeUrlFromStorage();
}
}
return;
}

// URL changed - update state to match
const matchingStorage = storages.find((s) => urlParam === s.url || urlParam.startsWith(s.url));

if (matchingStorage) {
setSelectedStorageId(matchingStorage.id);
setCurrentPath(urlParam === matchingStorage.url ? "/" : urlParam);
saveUrlToStorage(urlParam);
}
}, [searchParams, storages, isLoadingStorages, isInitialized, selectedStorageId]);

useEffect(() => {
if (isLoadingStorages || storages.length === 0 || isInitialized) {
return;
Expand Down Expand Up @@ -187,19 +217,28 @@ export default function FileManager() {
};
}, [contextMenuState]);

const updateUrl = (url: string | null) => {
const updateUrl = (url: string | null, addToHistory: boolean = true) => {
if (!url || url === "/") {
removeUrlFromStorage();
if (typeof window !== "undefined" && window.location.search) {
router.replace("/", { scroll: false });
if (addToHistory) {
router.push("/", { scroll: false });
} else {
router.replace("/", { scroll: false });
}
}
return;
}

const params = new URLSearchParams();
params.set("url", safeEncodeUrl(url));
saveUrlToStorage(url);
router.replace(`/?${params.toString()}`, { scroll: false });

if (addToHistory) {
router.push(`/?${params.toString()}`, { scroll: false });
} else {
router.replace(`/?${params.toString()}`, { scroll: false });
}
};

const containerUrlToBrowse = selectedStorageId
Expand Down Expand Up @@ -273,7 +312,7 @@ export default function FileManager() {

if (fileToRename && currentPath === fileToRename.url) {
setCurrentPath(newUrl);
updateUrl(newUrl);
updateUrl(newUrl, false);
}
// Trigger refresh to update file list immediately
setRefreshKey((prev) => prev + 1);
Expand Down Expand Up @@ -600,11 +639,11 @@ export default function FileManager() {
setSelectedStorageId(file.id);
setCurrentPath("/");
setSelectedFileIds([]);
updateUrl(file.url);
updateUrl(file.url, true);
} else if (selectedStorageId) {
setCurrentPath(file.url);
setSelectedFileIds([]);
updateUrl(file.url);
updateUrl(file.url, true);
}
} else {
window.open(file.url, "_blank");
Expand Down Expand Up @@ -725,15 +764,15 @@ export default function FileManager() {
setSelectedStorageId(null);
setCurrentPath("/");
setSelectedFileIds([]);
updateUrl(null);
updateUrl(null, true);
} else {
const selectedStorage = storages.find((s) => s.id === selectedStorageId);
if (selectedStorage && path === selectedStorage.url) {
setCurrentPath("/");
updateUrl(selectedStorage.url);
updateUrl(selectedStorage.url, true);
} else {
setCurrentPath(path);
updateUrl(path);
updateUrl(path, true);
}
setSelectedFileIds([]);
}
Expand Down Expand Up @@ -907,8 +946,7 @@ export default function FileManager() {
resourceUrl={sharedResourceUrl}
resourceName={sharedResourceName}
onOpenInApp={(url) => {
// Navigate to the resource URL in the file manager
updateUrl(url);
updateUrl(url, true);
}}
/>
{isDragActive && (
Expand Down
24 changes: 23 additions & 1 deletion app/components/ShareDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import Modal from "./shared/Modal";
import Button from "./shared/Button";
import UrlCombobox, { ComboboxOption } from "./shared/UrlCombobox";
import { FileItemData } from "./FileItem";
import { fetchUserContacts, Contact } from "../lib/helpers/contactUtils";
import { fetchUserContacts, Contact, addContactToProfile } from "../lib/helpers/contactUtils";
import { fetchAndParseProfile, extractNameAndEmail } from "../lib/helpers/profileUtils";
import { getResourceAccessList } from "../lib/helpers/acpUtils";
import { UserIcon, MagnifyingGlassIcon, LockClosedIcon, XMarkIcon, CheckCircleIcon } from "@heroicons/react/24/outline";
Expand Down Expand Up @@ -165,6 +165,28 @@ export default function ShareDialog({
const webIds = peopleChips.map((chip) => chip.webId);
await onShare(webIds, selectedAccessLevel);

// After successful sharing, add new WebIDs to contacts if they're not already there
const existingContactWebIds = new Set(contacts.map(c => c.webId));
const newWebIds = webIds.filter(webId => !existingContactWebIds.has(webId));

for (const webId of newWebIds) {
try {
await addContactToProfile(webId);
} catch (error) {
console.warn(`Failed to add ${webId} to contacts:`, error);
}
}

// Refresh contacts list if we added any new ones
if (newWebIds.length > 0) {
try {
const updatedContacts = await fetchUserContacts();
setContacts(updatedContacts);
} catch (error) {
console.warn("Failed to refresh contacts list:", error);
}
}

// Refresh access list after sharing
if (file) {
const resourceUrl = file.type === "folder" && !file.url.endsWith("/") ? file.url + "/" : file.url;
Expand Down
69 changes: 68 additions & 1 deletion app/lib/helpers/contactUtils.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,16 @@
import { NamedNode } from "n3";
import { fetchAndParseProfile, extractNameAndEmail, getCachedProfile } from "./profileUtils";
import { getSession } from "./sessionUtils";
import { getSession, getAuthenticatedSession } from "./sessionUtils";
import {
getSolidDataset,
getThing,
createThing,
addUrl,
getUrlAll,
setThing,
saveSolidDatasetAt,
UrlString
} from "@inrupt/solid-client";

export interface Contact {
webId: string;
Expand Down Expand Up @@ -75,3 +85,60 @@ export async function fetchUserContacts(): Promise<Contact[]> {
return [];
}
}

/**
* Adds a contact (WebID) to the user's profile using foaf:knows relationship
* @param contactWebId - The WebID of the contact to add
* @returns Promise that resolves when the contact is added
*/
export async function addContactToProfile(contactWebId: string): Promise<void> {
const session = getSession();

if (!session.info.isLoggedIn || !session.info.webId) {
throw new Error("User is not logged in");
}

const userWebId = session.info.webId;
const { fetch } = getAuthenticatedSession();

const profileUrl = userWebId.split('#')[0] as UrlString;

try {
// Fetch the user's profile dataset
let dataset = await getSolidDataset(profileUrl, { fetch });

// Get the main subject (usually WebID or WebID#me)
const mainSubject = userWebId as UrlString;
let thing = getThing(dataset, mainSubject);

// If thing doesn't exist, try with #me fragment
if (!thing) {
const meSubject = `${profileUrl}#me` as UrlString;
thing = getThing(dataset, meSubject);
}

// If still doesn't exist, create a new thing
if (!thing) {
thing = createThing({ url: mainSubject });
}

// Check if the contact is already in the knows list
const existingKnows = getUrlAll(thing, FOAF_KNOWS);
if (existingKnows.includes(contactWebId)) {
// Contact already exists, no need to add
return;
}

// Add the foaf:knows relationship
thing = addUrl(thing, FOAF_KNOWS, contactWebId as UrlString);

// Update the dataset
const updatedDataset = setThing(dataset, thing);

// Save the updated dataset
await saveSolidDatasetAt(profileUrl, updatedDataset, { fetch });
} catch (error) {
console.error("Failed to add contact to profile:", error);
throw error;
}
}