Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -627,12 +627,14 @@ export const Conversation = ({
userRepository={repositories.user}
cellsRepository={repositories.cells}
conversationRepository={conversationRepository}
isSharedDriveSearchAndFiltersEnabled={isSharedDriveSearchAndFiltersEnabled}
isSearchViewOpen={isSharedDriveSearchAndFiltersEnabled && isSharedDriveSearchViewOpen}
onOpenSearchView={() => {
if (isSharedDriveSearchAndFiltersEnabled) {
setIsSharedDriveSearchViewOpen(true);
}
}}
onCloseSearchView={() => setIsSharedDriveSearchViewOpen(false)}
/>
)}
</ConversationTabPanel>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ interface CellsTableProps {
conversationQualifiedId: QualifiedId;
conversationName: string;
onRefresh: () => void;
onCloseSearchView?: () => void;
}

export const CellsTable = ({
Expand All @@ -49,6 +50,7 @@ export const CellsTable = ({
conversationQualifiedId,
conversationName,
onRefresh,
onCloseSearchView,
}: CellsTableProps) => {
const table = useReactTable({
data: nodes,
Expand All @@ -57,6 +59,7 @@ export const CellsTable = ({
conversationQualifiedId,
conversationName,
onRefresh,
onCloseSearchView,
}),
getCoreRowModel: getCoreRowModel(),
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,15 +38,17 @@ export const getCellsTableColumns = ({
conversationQualifiedId,
conversationName,
onRefresh,
onCloseSearchView,
}: {
cellsRepository: CellsRepository;
conversationQualifiedId: QualifiedId;
conversationName: string;
onRefresh: () => void;
onCloseSearchView?: () => void;
}) => [
columnHelper.accessor('name', {
header: t('cells.tableRow.name'),
cell: info => <CellsTableNameColumn node={info.row.original} conversationQualifiedId={conversationQualifiedId} />,
cell: info => <CellsTableNameColumn node={info.row.original} onCloseSearchView={onCloseSearchView} />,
}),
columnHelper.accessor('owner', {
header: t('cells.tableRow.owner'),
Expand Down Expand Up @@ -84,6 +86,7 @@ export const getCellsTableColumns = ({
conversationQualifiedId={conversationQualifiedId}
conversationName={conversationName}
onRefresh={onRefresh}
onCloseSearchView={onCloseSearchView}
/>
);
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,6 @@
*
*/

import {QualifiedId} from '@wireapp/api-client/lib/user/';

import {FolderIcon, PlayIcon} from '@wireapp/react-ui-kit';

import {FileTypeIcon} from 'Components/Conversation/common/FileTypeIcon/FileTypeIcon';
Expand All @@ -39,18 +37,18 @@ import {useCellsFilePreviewModal} from '../../common/CellsFilePreviewModalContex

interface CellsTableNameColumnProps {
node: CellNode;
conversationQualifiedId: QualifiedId;
onCloseSearchView?: () => void;
}

export const CellsTableNameColumn = ({node, conversationQualifiedId}: CellsTableNameColumnProps) => {
export const CellsTableNameColumn = ({node, onCloseSearchView}: CellsTableNameColumnProps) => {
return (
<>
<span css={mobileNameStyles}>{node.name}</span>
<div css={wrapperStyles}>
{node.type === CellNodeType.FILE ? (
<FileNameColumn file={node} />
) : (
<FolderNameColumn name={node.name} conversationQualifiedId={conversationQualifiedId} />
<FolderNameColumn name={node.name} path={node.path} onCloseSearchView={onCloseSearchView} />
)}
</div>
</>
Expand Down Expand Up @@ -91,14 +89,22 @@ const FileNameColumn = ({file}: {file: CellFile}) => {
);
};

const FolderNameColumn = ({name, conversationQualifiedId}: {name: string; conversationQualifiedId: QualifiedId}) => {
const FolderNameColumn = ({
name,
path,
onCloseSearchView,
}: {
name: string;
path: string;
onCloseSearchView?: () => void;
}) => {
return (
<>
<FolderIcon width={24} height={24} />
<button
type="button"
css={desktopNameStyles}
onClick={event => openFolder({conversationQualifiedId, name, event})}
onClick={event => openFolder({path, event, onBeforeNavigate: onCloseSearchView})}
>
{name}
</button>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ interface CellsTableRowOptionsProps {
conversationQualifiedId: QualifiedId;
conversationName: string;
onRefresh: () => void;
onCloseSearchView?: () => void;
}

export const CellsTableRowOptions = ({
Expand All @@ -66,6 +67,7 @@ export const CellsTableRowOptions = ({
conversationQualifiedId,
conversationName,
onRefresh,
onCloseSearchView,
}: CellsTableRowOptionsProps) => {
return (
<DropdownMenu>
Expand All @@ -81,6 +83,7 @@ export const CellsTableRowOptions = ({
conversationQualifiedId={conversationQualifiedId}
conversationName={conversationName}
onRefresh={onRefresh}
onCloseSearchView={onCloseSearchView}
/>
</DropdownMenu>
);
Expand All @@ -92,6 +95,7 @@ const CellsTableRowOptionsContent = ({
conversationQualifiedId,
conversationName,
onRefresh,
onCloseSearchView,
}: CellsTableRowOptionsProps) => {
const {fireAndForgetInvoker} = useApplicationContext();
const {handleOpenFile} = useCellsFilePreviewModal();
Expand Down Expand Up @@ -175,7 +179,7 @@ const CellsTableRowOptionsContent = ({
<DropdownMenu.Item
onClick={() =>
node.type === CellNodeType.FOLDER
? openFolder({conversationQualifiedId, name: node.name})
? openFolder({path: node.path, onBeforeNavigate: onCloseSearchView})
: handleOpenFile(node)
}
>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,10 @@ interface ConversationCellsProps {
userRepository: UserRepository;
activeConversation: Conversation;
conversationRepository: ConversationRepository;
isSharedDriveSearchAndFiltersEnabled: boolean;
isSearchViewOpen: boolean;
onOpenSearchView: () => void;
onCloseSearchView: () => void;
}

export const ConversationCells = memo(
Expand All @@ -68,8 +70,10 @@ export const ConversationCells = memo(
userRepository,
activeConversation,
conversationRepository,
isSharedDriveSearchAndFiltersEnabled,
isSearchViewOpen,
onOpenSearchView,
onCloseSearchView,
}: ConversationCellsProps) => {
const {fireAndForgetInvoker} = useApplicationContext();
const {cellsState: initialCellState, name} = useKoSubscribableChildren(activeConversation, ['cellsState', 'name']);
Expand All @@ -92,7 +96,9 @@ export const ConversationCells = memo(
const {refresh, setOffset} = useGetAllCellsNodes({
cellsRepository,
conversationQualifiedId,
enabled: isCellsStateReady,
//Without this, the browse hook's hashchange handler would compete with
// (and flap against) search results.
enabled: isCellsStateReady && !isSearchViewOpen,
fireAndForgetInvoker,
userRepository,
});
Expand All @@ -112,6 +118,7 @@ export const ConversationCells = memo(
cellsRepository,
conversationQualifiedId,
enabled: isCellsStateReady && isSearchViewOpen,
allowSearchWhenDisabled: !isSharedDriveSearchAndFiltersEnabled,
fireAndForgetInvoker,
userRepository,
filters: filterState,
Expand Down Expand Up @@ -164,6 +171,8 @@ export const ConversationCells = memo(
});
}, [loadMoreOffset, loadMoreSearchResults]);

const handleSearchViewClosure = isSearchViewOpen ? onCloseSearchView : undefined;

useOnPresignedUrlExpired({conversationId, refreshCallback: handleRefresh});

const isLoading = nodesStatus === 'loading';
Expand Down Expand Up @@ -208,6 +217,9 @@ export const ConversationCells = memo(
conversationQualifiedId={conversationQualifiedId}
conversationName={name}
onRefresh={handleRefresh}
// opening a folder must close search view and open the browse view
// with that folder (and breadcrumbs)
onCloseSearchView={handleSearchViewClosure}
/>
)}
{isCellsStatePending && !isRefreshing && (
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/*
* Wire
* Copyright (C) 2026 Wire Swiss GmbH
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see http://www.gnu.org/licenses/.
*
*/

import {openFolder} from './openFolder';

const CONV_ID = 'abc-123';
const DOMAIN = 'staging.zinfra.io';

function currentHash(): string {
return window.location.hash.replace('#', '');
}

describe('openFolder', () => {
beforeEach(() => {
window.location.hash = '';
});

describe('absolute path (id@domain)', () => {
it('navigates to the folder when path has one level', () => {
openFolder({path: `${CONV_ID}@${DOMAIN}/Test`});

expect(currentHash()).toBe(`/conversation/${CONV_ID}/${DOMAIN}/files/Test`);
});

it('navigates to the correct nested folder', () => {
openFolder({path: `${CONV_ID}@${DOMAIN}/Test/2025/deep`});

expect(currentHash()).toBe(`/conversation/${CONV_ID}/${DOMAIN}/files/Test/2025/deep`);
});

it('encodes path segments with special characters', () => {
openFolder({path: `${CONV_ID}@${DOMAIN}/My Folder/Sub Folder`});

expect(currentHash()).toBe(`/conversation/${CONV_ID}/${DOMAIN}/files/My%20Folder/Sub%20Folder`);
});

it('calls onBeforeNavigate before changing the URL', () => {
const hashBeforeNavigate: string[] = [];
const onBeforeNavigate = () => hashBeforeNavigate.push(currentHash());

openFolder({path: `${CONV_ID}@${DOMAIN}/Test`, onBeforeNavigate});

expect(hashBeforeNavigate).toEqual(['']);
expect(currentHash()).toContain('/files/Test');
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -20,29 +20,29 @@
import {MouseEvent as ReactMouseEvent} from 'react';

import {QualifiedId} from '@wireapp/api-client/lib/user/';
import {parseQualifiedId} from '@wireapp/core/lib/util/qualifiedIdUtil';

import {generateConversationUrl} from 'src/script/router/routeGenerator';
import {createNavigate} from 'src/script/router/routerBindings';

import {getCellsFilesPath} from '../getCellsFilesPath/getCellsFilesPath';

interface OpenFolderParams {
conversationQualifiedId: QualifiedId;
name: string;
path: string;
event?: ReactMouseEvent<Element, MouseEvent>;
// Called before navigating so the search view can close before the browse view re-enables.
onBeforeNavigate?: () => void;
conversationQualifiedId?: QualifiedId;
}

export const openFolder = ({conversationQualifiedId, name, event}: OpenFolderParams) => {
const currentPath = getCellsFilesPath();
const pathSegments = currentPath ? currentPath.split('/') : [];
const encodedSegments = [...pathSegments, name].map(segment => encodeURIComponent(segment));
const newPath = encodedSegments.join('/');

createNavigate(
generateConversationUrl({
id: conversationQualifiedId.id,
domain: conversationQualifiedId.domain,
filePath: `files/${newPath}`,
}),
)(event);
export const openFolder = ({path, event, onBeforeNavigate, conversationQualifiedId}: OpenFolderParams) => {
const stripped = path.startsWith('/') ? path.slice(1) : path;
const [firstSegment, ...rest] = stripped.split('/');

const isAbsolute = firstSegment.includes('@');
const {id, domain} = isAbsolute ? parseQualifiedId(firstSegment) : (conversationQualifiedId ?? {id: '', domain: ''});
const filePathParts = isAbsolute ? rest : stripped.split('/').filter(Boolean);

const filePath = `files/${filePathParts.map(encodeURIComponent).join('/')}`;

onBeforeNavigate?.();
createNavigate(generateConversationUrl({id, domain, filePath}))(event);
};
Loading
Loading