Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(conversation): cells view #18858

Draft
wants to merge 6 commits into
base: dev
Choose a base branch
from
Draft
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
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,13 @@
"@lexical/react": "0.27.1",
"@lexical/rich-text": "0.27.1",
"@mediapipe/tasks-vision": "0.10.21",
"@tanstack/react-table": "^8.21.2",
"@tanstack/react-virtual": "^3.13.2",
"@wireapp/avs": "9.10.25",
"@wireapp/avs-debugger": "0.0.7",
"@wireapp/commons": "5.4.2",
"@wireapp/core": "46.19.10",
"@wireapp/react-ui-kit": "9.40.0",
"@wireapp/core": "46.19.11",
"@wireapp/react-ui-kit": "9.40.1",
"@wireapp/store-engine-dexie": "2.1.15",
"@wireapp/telemetry": "0.3.1",
"@wireapp/webapp-events": "0.28.0",
Expand Down
1 change: 1 addition & 0 deletions server/config/client.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
CELLS_S3_BUCKET: env.CELLS_S3_BUCKET,
CELLS_S3_REGION: env.CELLS_S3_REGION,
CELLS_S3_ENDPOINT: env.CELLS_S3_ENDPOINT,
CELLS_WIRE_DOMAIN: env.CELLS_WIRE_DOMAIN,
COUNTLY_API_KEY: env.COUNTLY_API_KEY,
COUNTLY_ENABLE_LOGGING: env.COUNTLY_ENABLE_LOGGING == 'true',
COUNTLY_FORCE_REPORTING: env.COUNTLY_FORCE_REPORTING == 'true',
Expand Down Expand Up @@ -87,12 +88,12 @@
SHOW_LOADING_INFORMATION: env.FEATURE_SHOW_LOADING_INFORMATION == 'true',
USE_CORE_CRYPTO: env.FEATURE_USE_CORE_CRYPTO == 'true',
MAX_USERS_TO_PING_WITHOUT_ALERT:
(env.FEATURE_MAX_USERS_TO_PING_WITHOUT_ALERT && Number(env.FEATURE_MAX_USERS_TO_PING_WITHOUT_ALERT)) || 4,

Check warning on line 91 in server/config/client.config.ts

View workflow job for this annotation

GitHub Actions / test

No magic number: 4
DATADOG_ENVIRONMENT: env.FEATURE_DATADOG_ENVIRONMENT,
},
MAX_GROUP_PARTICIPANTS: (env.MAX_GROUP_PARTICIPANTS && Number(env.MAX_GROUP_PARTICIPANTS)) || 500,

Check warning on line 94 in server/config/client.config.ts

View workflow job for this annotation

GitHub Actions / test

No magic number: 500
MAX_VIDEO_PARTICIPANTS: (env.MAX_VIDEO_PARTICIPANTS && Number(env.MAX_VIDEO_PARTICIPANTS)) || 4,

Check warning on line 95 in server/config/client.config.ts

View workflow job for this annotation

GitHub Actions / test

No magic number: 4
NEW_PASSWORD_MINIMUM_LENGTH: (env.NEW_PASSWORD_MINIMUM_LENGTH && Number(env.NEW_PASSWORD_MINIMUM_LENGTH)) || 8,

Check warning on line 96 in server/config/client.config.ts

View workflow job for this annotation

GitHub Actions / test

No magic number: 8
URL: {
ACCOUNT_BASE: env.URL_ACCOUNT_BASE,
MOBILE_BASE: env.URL_MOBILE_BASE,
Expand Down
1 change: 1 addition & 0 deletions server/config/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ export type Env = {
CELLS_S3_BUCKET: string;
CELLS_S3_REGION: string;
CELLS_S3_ENDPOINT: string;
CELLS_WIRE_DOMAIN: string;

/** Specifies the name of the backend, e.g. Wire */
BACKEND_NAME: string;
Expand Down
4 changes: 2 additions & 2 deletions server/config/server.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,9 @@
'https://*.vimeo.com',
'https://*.youtube-nocookie.com',
],
imgSrc: ["'self'", 'blob:', 'data:', 'https://*.giphy.com'],
imgSrc: ["'self'", 'blob:', 'data:', 'https://*.giphy.com', 'https://service.zeta.pydiocells.com'],
manifestSrc: ["'self'"],
mediaSrc: ["'self'", 'blob:', 'data:'],
mediaSrc: ["'self'", 'blob:', 'data:', 'https://service.zeta.pydiocells.com'],
scriptSrc: ["'self'", "'unsafe-eval'"],
styleSrc: ["'self'", "'unsafe-inline'"],
workerSrc: ["'self'", 'blob:'],
Expand Down Expand Up @@ -116,7 +116,7 @@
TITLE: env.OPEN_GRAPH_TITLE,
},
ENABLE_DYNAMIC_HOSTNAME: env.ENABLE_DYNAMIC_HOSTNAME === 'true',
PORT_HTTP: Number(env.PORT) || 21080,

Check warning on line 119 in server/config/server.config.ts

View workflow job for this annotation

GitHub Actions / test

No magic number: 21080
ROBOTS: {
ALLOW: readFile(ROBOTS_ALLOW_FILE, 'User-agent: *\r\nDisallow: /'),
ALLOWED_HOSTS: ['app.wire.com'],
Expand Down
37 changes: 37 additions & 0 deletions src/i18n/en-US.json
Original file line number Diff line number Diff line change
Expand Up @@ -355,6 +355,43 @@
"callingRestrictedConferenceCallTeamMemberModalTitle": "Feature unavailable",
"cameraStatusOff": "off",
"cameraStatusOn": "on",
"cellsGlobalView.heading": "All Files",
"cellsGlobalView.emptySearchResultsHeading": "No matching files found",
"cellsGlobalView.emptySearchResultsDescription": "Try adjusting your search or check for typos.",
"cellsGlobalView.errorHeading": "Something went wrong",
"cellsGlobalView.errorDescription": "We couldn't load your files. Please try again later.",
"cellsGlobalView.noFilesHeading": "There are no files yet",
"cellsGlobalView.noFilesDescription": "You'll find all files shared across your conversations here.",
"cellsGlobalView.refreshButton": "Refresh list",
"cellsGlobalView.searchFailed": "Something went wrong, please try again later.",
"cellsGlobalView.searchPlaceholder": "Search files",
"cellsGlobalView.searchCloseButton": "Close",
"cellsGlobalView.deleteModalHeading": "Delete file",
"cellsGlobalView.deleteModalDescription": "This will permanently delete the file {name} for all participants.",
"cellsGlobalView.deleteModalError": "Something went wrong, please try again later and refresh the list.",
"cellsGlobalView.optionsLabel": "More options",
"cellsGlobalView.optionDelete": "Delete",
"cellsGlobalView.optionOpen": "Open",
"cellsGlobalView.optionShare": "Share",
"cellsGlobalView.optionDownload": "Download",
"cellsGlobalView.tableRowName": "Name",
"cellsGlobalView.tableRowConversationName": "Conversation Name",
"cellsGlobalView.tableRowOwner": "Owner",
"cellsGlobalView.tableRowSize": "Size",
"cellsGlobalView.tableRowCreated": "Created",
"cellsGlobalView.tableRowActions": "Actions",
"cellsGlobalView.shareFileModalHeading": "Share file via link",
"cellsGlobalView.shareFileModalEnablePublicLink": "Enable public link",
"cellsGlobalView.shareFileModalDisablePublicLink": "Disable public link",
"cellsGlobalView.shareFileModalEnablePublicLinkDescription": "Your file will be uploaded and shared via a public link. Only those with the link can view it—ensure you trust your recipients.",
"cellsGlobalView.shareFileModalDisablePublicLinkDescription": "Disable public link",
"cellsGlobalView.shareFileModalCopyLink": "Copy link",
"cellsGlobalView.shareFileModalLinkCopied": "Link copied",
"cellsGlobalView.shareFileModalErrorLoadingLink": "Something went wrong, please try again later.",
"cellsGlobalView.shareFileModalGeneratedPublicLink": "Generated public link",
"cellsGlobalView.imageFullScreenModalCloseButton": "Close",
"cellsSidebar.heading": "Files",
"cellsSidebar.title": "All",
"chooseHandle.handlePlaceholder": "Username",
"chooseHandle.headline": "Set username",
"chooseHandle.subhead": "Your username helps people find you.",
Expand Down
31 changes: 30 additions & 1 deletion src/script/cells/CellsRepository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,12 @@ export class CellsRepository {
const uuid = createUuid();
const versionId = createUuid();

await this.apiClient.api.cells.uploadFileDraft({path, file, uuid, versionId});
await this.apiClient.api.cells.uploadFileDraft({
path,
file,
uuid,
versionId,
});

return {
uuid,
Expand All @@ -44,4 +49,28 @@ export class CellsRepository {
async deleteFileDraft({uuid, versionId}: {uuid: string; versionId: string}) {
return this.apiClient.api.cells.deleteFileDraft({uuid, versionId});
}

async deleteFile({uuid}: {uuid: string}) {
return this.apiClient.api.cells.deleteFile({uuid});
}

async getAllFiles({path}: {path: string}) {
return this.apiClient.api.cells.getAllFiles({path: path || this.basePath});
}

async getPublicLink({uuid, label}: {uuid: string; label: string}) {
return this.apiClient.api.cells.getFilePublicLink({
uuid,
label,
alreadyShared: false,
});
}

async deletePublicLink({uuid}: {uuid: string}) {
return this.apiClient.api.cells.deleteFilePublicLink({uuid});
}

async searchFiles({query}: {query: string}) {
return this.apiClient.api.cells.searchFiles({phrase: query});
}
}
174 changes: 100 additions & 74 deletions src/script/components/Conversation/Conversation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@

import {UIEvent, useCallback, useEffect, useState} from 'react';

import cx from 'classnames';
import {container} from 'tsyringe';

import {useMatchMedia} from '@wireapp/react-ui-kit';
Expand All @@ -42,7 +43,10 @@ import {getLogger} from 'Util/Logger';
import {safeMailOpen, safeWindowOpen} from 'Util/SanitizationUtil';
import {formatBytes} from 'Util/util';

import {ConversationCells} from './ConversationCells/ConversationCells';
import {ConversationFileDropzone} from './ConversationFileDropzone/ConversationFileDropzone';
import {ConversationMessagesWrapper} from './ConversationMessagesWrapper/ConversationMessagesWrapper';
import {ConversationTabs} from './ConversationTabBar/ConversationTabBar';
import {useReadReceiptSender} from './hooks/useReadReceipt';
import {ReadOnlyConversationMessage} from './ReadOnlyConversationMessage';
import {useFilesUploadDropzone} from './useFilesUploadDropzone/useFilesUploadDropzone';
Expand Down Expand Up @@ -123,6 +127,8 @@ export const Conversation = ({
// To be changed when design chooses a breakpoint, the conditional can be integrated to the ui-kit directly
const smBreakpoint = useMatchMedia('max-width: 640px');

const [activeTabIndex, setActiveTabIndex] = useState(0);

const {addReadReceiptToBatch} = useReadReceiptSender(repositories.message);

useEffect(() => {
Expand Down Expand Up @@ -462,13 +468,13 @@ export const Conversation = ({
[addReadReceiptToBatch, repositories.conversation, repositories.integration, updateConversationLastRead],
);

const isCellsEnabled = Config.getConfig().FEATURE.ENABLE_CELLS;

const {getRootProps, getInputProps, open, isDragAccept} = useFilesUploadDropzone({
isTeam: inTeam,
cellsRepository: repositories.cells,
});

const isCellsEnabled = true;

return (
<ConversationFileDropzone
isDragAccept={isDragAccept}
Expand All @@ -490,80 +496,100 @@ export const Conversation = ({
openRightSidebar={openRightSidebar}
isRightSidebarOpen={isRightSidebarOpen}
isReadOnlyConversation={isReadOnlyConversation || isSelfUserRemoved}
withBottomDivider={!isCellsEnabled}
/>

{activeCalls.map(call => {
const {conversation} = call;
const callingViewModel = mainViewModel.calling;
const callingRepository = callingViewModel.callingRepository;

if (!smBreakpoint) {
return null;
}

return (
<CallingCell
key={conversation.id}
classifiedDomains={classifiedDomains}
call={call}
callActions={callingViewModel.callActions}
callingRepository={callingRepository}
propertiesRepository={repositories.properties}
/>
);
})}

<MessagesList
conversation={activeConversation}
selfUser={selfUser}
conversationRepository={conversationRepository}
assetRepository={repositories.asset}
messageRepository={repositories.message}
messageActions={mainViewModel.actions}
invitePeople={clickOnInvitePeople}
cancelConnectionRequest={clickOnCancelRequest}
showUserDetails={showUserDetails}
showMessageDetails={showMessageDetails}
showMessageReactions={showMessageReactions}
showParticipants={showParticipants}
showImageDetails={showDetail}
resetSession={onSessionResetClick}
onClickMessage={handleClickOnMessage}
onLoading={loading => setIsConversationLoaded(!loading)}
getVisibleCallback={getInViewportCallback}
isMsgElementsFocusable={isMsgElementsFocusable}
setMsgElementsFocusable={setMsgElementsFocusable}
isRightSidebarOpen={isRightSidebarOpen}
updateConversationLastRead={updateConversationLastRead}
/>

{isConversationLoaded &&
!isSelfUserRemoved &&
(isReadOnlyConversation ? (
<ReadOnlyConversationMessage reloadApp={reloadApp} conversation={activeConversation} />
) : (
<InputBar
key={activeConversation?.id}
conversation={activeConversation}
conversationRepository={repositories.conversation}
eventRepository={repositories.event}
messageRepository={repositories.message}
openGiphy={openGiphy}
propertiesRepository={repositories.properties}
searchRepository={repositories.search}
storageRepository={repositories.storage}
teamState={teamState}
selfUser={selfUser}
onShiftTab={() => setMsgElementsFocusable(false)}
uploadDroppedFiles={uploadDroppedFiles}
uploadImages={uploadImages}
uploadFiles={isCellsEnabled ? () => open() : uploadFiles}
/>
))}

<div className="conversation-loading">
<div className="icon-spinner spin accent-text"></div>
</div>
{isCellsEnabled && (
<>
<ConversationTabs activeTabIndex={activeTabIndex} onIndexChange={setActiveTabIndex} />
<div
id="tabpanel-files"
role="tabpanel"
aria-labelledby="tab-files"
tabIndex={0}
className={cx('conversation-tabpanel conversation-tabpanel--files', {
'conversation-tabpanel--hidden': activeTabIndex === 0,
})}
>
{activeTabIndex === 1 && <ConversationCells conversationId={activeConversation.id} />}
</div>
</>
)}

<ConversationMessagesWrapper isCellsEnabled={isCellsEnabled} isPanelHidden={activeTabIndex === 1}>
{activeCalls.map(call => {
const {conversation} = call;
const callingViewModel = mainViewModel.calling;
const callingRepository = callingViewModel.callingRepository;

if (!smBreakpoint) {
return null;
}

return (
<CallingCell
key={conversation.id}
classifiedDomains={classifiedDomains}
call={call}
callActions={callingViewModel.callActions}
callingRepository={callingRepository}
propertiesRepository={repositories.properties}
/>
);
})}

<MessagesList
conversation={activeConversation}
selfUser={selfUser}
conversationRepository={conversationRepository}
assetRepository={repositories.asset}
messageRepository={repositories.message}
messageActions={mainViewModel.actions}
invitePeople={clickOnInvitePeople}
cancelConnectionRequest={clickOnCancelRequest}
showUserDetails={showUserDetails}
showMessageDetails={showMessageDetails}
showMessageReactions={showMessageReactions}
showParticipants={showParticipants}
showImageDetails={showDetail}
resetSession={onSessionResetClick}
onClickMessage={handleClickOnMessage}
onLoading={loading => setIsConversationLoaded(!loading)}
getVisibleCallback={getInViewportCallback}
isMsgElementsFocusable={isMsgElementsFocusable}
setMsgElementsFocusable={setMsgElementsFocusable}
isRightSidebarOpen={isRightSidebarOpen}
updateConversationLastRead={updateConversationLastRead}
/>

{isConversationLoaded &&
!isSelfUserRemoved &&
(isReadOnlyConversation ? (
<ReadOnlyConversationMessage reloadApp={reloadApp} conversation={activeConversation} />
) : (
<InputBar
key={activeConversation?.id}
conversation={activeConversation}
conversationRepository={repositories.conversation}
eventRepository={repositories.event}
messageRepository={repositories.message}
openGiphy={openGiphy}
propertiesRepository={repositories.properties}
searchRepository={repositories.search}
storageRepository={repositories.storage}
teamState={teamState}
selfUser={selfUser}
onShiftTab={() => setMsgElementsFocusable(false)}
uploadDroppedFiles={uploadDroppedFiles}
uploadImages={uploadImages}
uploadFiles={isCellsEnabled ? () => open() : uploadFiles}
/>
))}

<div className="conversation-loading">
<div className="icon-spinner spin accent-text"></div>
</div>
</ConversationMessagesWrapper>
</>
)}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/*
* Wire
* Copyright (C) 2025 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 {CSSObject} from '@emotion/react';

export const wrapperStyles: CSSObject = {
display: 'flex',
justifyContent: 'space-between',
alignItems: 'flex-start',
flexDirection: 'column',
marginBottom: '20px',
width: '100%',
};

export const headingStyles: CSSObject = {
color: 'var(--main-color)',
fontWeight: 'var(--font-weight-semibold)',
fontSize: 'var(--font-size-medium)',
marginBottom: '8px',
};

export const searchWrapperStyles: CSSObject = {
display: 'flex',
alignItems: 'center',
gap: '8px',
};

export const contentStyles: CSSObject = {
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
gap: '8px',
width: '100%',
};
Loading
Loading