From bd6f5e6e0d060bddea2755ebfdf1d284c5019ba2 Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Fri, 15 Mar 2024 11:45:21 +1100 Subject: [PATCH] feat: refacto GroupUpdateName to be functional component --- .../dialog/UpdateGroupNameDialog.tsx | 339 ++++++++---------- ts/components/leftpane/ActionsPanel.tsx | 1 + ts/mains/main_renderer.tsx | 3 - ts/node/sql.ts | 2 +- ts/session/group/closed-group.ts | 13 +- ts/state/ducks/metaGroups.ts | 2 +- ts/state/selectors/groups.ts | 8 + ts/types/attachments/VisualAttachment.ts | 3 +- ts/window.d.ts | 1 - 9 files changed, 169 insertions(+), 203 deletions(-) diff --git a/ts/components/dialog/UpdateGroupNameDialog.tsx b/ts/components/dialog/UpdateGroupNameDialog.tsx index 2a047899d..83cf07c1e 100644 --- a/ts/components/dialog/UpdateGroupNameDialog.tsx +++ b/ts/components/dialog/UpdateGroupNameDialog.tsx @@ -1,240 +1,199 @@ /* eslint-disable @typescript-eslint/no-misused-promises */ -import autoBind from 'auto-bind'; import classNames from 'classnames'; -import React from 'react'; +import React, { useState } from 'react'; -import { clone } from 'lodash'; -import { ConversationModel } from '../../models/conversation'; +import { useDispatch } from 'react-redux'; +import useKey from 'react-use/lib/useKey'; +import { useIsClosedGroup, useIsPublic } from '../../hooks/useParamSelector'; import { ConvoHub } from '../../session/conversations'; import { ClosedGroup } from '../../session/group/closed-group'; import { initiateOpenGroupUpdate } from '../../session/group/open-group'; import { PubKey } from '../../session/types'; import { groupInfoActions } from '../../state/ducks/metaGroups'; import { updateGroupNameModal } from '../../state/ducks/modalDialog'; -import { getLibGroupNameOutsideRedux } from '../../state/selectors/groups'; +import { useGroupNameChangeFromUIPending } from '../../state/selectors/groups'; import { pickFileForAvatar } from '../../types/attachments/VisualAttachment'; import { SessionWrapperModal } from '../SessionWrapperModal'; import { Avatar, AvatarSize } from '../avatar/Avatar'; import { SessionButton, SessionButtonColor, SessionButtonType } from '../basic/SessionButton'; +import { SessionSpinner } from '../basic/SessionSpinner'; import { SpacerMD } from '../basic/Text'; -type Props = { +function GroupAvatar({ + isPublic, + conversationId, + fireInputEvent, + newAvatarObjecturl, + oldAvatarPath, +}: { + isPublic: boolean; conversationId: string; -}; - -interface State { - groupName: string | undefined; - originalGroupName: string; - errorDisplayed: boolean; - errorMessage: string; - oldAvatarPath: string | null; newAvatarObjecturl: string | null; -} - -// TODO break those last class bases components into functional ones (search for `extends React`) -export class UpdateGroupNameDialog extends React.Component { - private readonly convo: ConversationModel; + oldAvatarPath: string | null; + fireInputEvent: () => Promise; +}) { + if (!isPublic) { + return null; + } - constructor(props: Props) { - super(props); + return ( +
+
+ +
+
+
+ ); +} - autoBind(this); - this.convo = ConvoHub.use().get(props.conversationId); +export function UpdateGroupNameDialog(props: { conversationId: string }) { + const dispatch = useDispatch(); + const { conversationId } = props; + const [errorMsg, setErrorMsg] = useState(''); + const [newAvatarObjecturl, setNewAvatarObjecturl] = useState(null); + const isCommunity = useIsPublic(conversationId); + const isClosedGroup = useIsClosedGroup(conversationId); + const convo = ConvoHub.use().get(conversationId); + const isNameChangePending = useGroupNameChangeFromUIPending(); + + if (!convo) { + throw new Error('UpdateGroupNameDialog corresponding convo not found'); + } - const libGroupName = getLibGroupNameOutsideRedux(props.conversationId); - const groupNameFromConvo = this.convo.getRealSessionUsername(); - const groupName = libGroupName || groupNameFromConvo; + const oldAvatarPath = convo?.getAvatarPath() || null; + const originalGroupName = convo?.getRealSessionUsername(); + const [newGroupName, setNewGroupName] = useState(originalGroupName); - this.state = { - groupName: clone(groupName), - originalGroupName: clone(groupName) || '', - errorDisplayed: false, - errorMessage: 'placeholder', - oldAvatarPath: this.convo.getAvatarPath(), - newAvatarObjecturl: null, - }; + function closeDialog() { + dispatch(updateGroupNameModal(null)); } - public componentDidMount() { - window.addEventListener('keyup', this.onKeyUp); + function onShowError(msg: string) { + if (errorMsg === msg) { + return; + } + setErrorMsg(msg); } - public componentWillUnmount() { - window.removeEventListener('keyup', this.onKeyUp); + async function fireInputEvent() { + const scaledObjectUrl = await pickFileForAvatar(); + if (scaledObjectUrl) { + setNewAvatarObjecturl(scaledObjectUrl); + } } - public onClickOK() { - const { groupName, newAvatarObjecturl, oldAvatarPath } = this.state; - const trimmedGroupName = groupName?.trim(); + function onClickOK() { + if (isNameChangePending) { + return; + } + const trimmedGroupName = newGroupName?.trim(); if (!trimmedGroupName) { - this.onShowError(window.i18n('emptyGroupNameError')); + onShowError(window.i18n('emptyGroupNameError')); return; } + onShowError(''); - if (trimmedGroupName !== this.state.originalGroupName || newAvatarObjecturl !== oldAvatarPath) { - if (this.convo.isPublic()) { - void initiateOpenGroupUpdate(this.convo.id, trimmedGroupName, { + if (trimmedGroupName !== originalGroupName || newAvatarObjecturl !== oldAvatarPath) { + if (isCommunity) { + void initiateOpenGroupUpdate(conversationId, trimmedGroupName, { objectUrl: newAvatarObjecturl, }); - this.closeDialog(); + closeDialog(); } else { - const groupPk = this.convo.id; - if (PubKey.is03Pubkey(groupPk)) { - const groupv2Action = groupInfoActions.currentDeviceGroupNameChange({ - groupPk, + if (PubKey.is03Pubkey(conversationId)) { + const updateNameAction = groupInfoActions.currentDeviceGroupNameChange({ + groupPk: conversationId, newName: trimmedGroupName, }); - window.inboxStore.dispatch(groupv2Action as any); + dispatch(updateNameAction as any); return; // keeping the dialog open until the async thunk is done } - const members = this.convo.getGroupMembers() || []; - void ClosedGroup.initiateClosedGroupUpdate(this.convo.id, trimmedGroupName, members); - this.closeDialog(); + void ClosedGroup.initiateClosedGroupUpdate(conversationId, trimmedGroupName, null); + closeDialog(); } } } - public render() { - const okText = window.i18n('ok'); - const cancelText = window.i18n('cancel'); - const titleText = window.i18n('updateGroupDialogTitle', [ - this.convo.getRealSessionUsername() || window.i18n('unknown'), - ]); - - const errorMsg = this.state.errorMessage; - const errorMessageClasses = classNames( - 'error-message', - this.state.errorDisplayed ? 'error-shown' : 'error-faded' - ); - - const isAdmin = !this.convo.isPublic(); - - return ( - this.closeDialog()} - additionalClassName="update-group-dialog" - > - {this.state.errorDisplayed ? ( - <> - -

{errorMsg}

- - - ) : null} - - {this.renderAvatar()} - - - {isAdmin ? ( - - ) : null} - -
- - -
-
- ); - } - - private onShowError(msg: string) { - if (this.state.errorDisplayed) { - return; - } + useKey('Escape', closeDialog); + useKey('Esc', closeDialog); + useKey('Enter', onClickOK); - this.setState({ - errorDisplayed: true, - errorMessage: msg, - }); - - setTimeout(() => { - this.setState({ - errorDisplayed: false, - }); - }, 3000); - } - - private onKeyUp(event: any) { - switch (event.key) { - case 'Enter': - this.onClickOK(); - break; - case 'Esc': - case 'Escape': - this.closeDialog(); - break; - default: - } - } - - private closeDialog() { - window.removeEventListener('keyup', this.onKeyUp); - - window.inboxStore?.dispatch(updateGroupNameModal(null)); + if (!isClosedGroup && !isCommunity) { + throw new Error('groupNameUpdate dialog only works for communities and closed groups'); } - private onGroupNameChanged(event: any) { - const groupName = event.target.value; - this.setState(state => { - return { - ...state, - groupName, - }; - }); - } - - private renderAvatar() { - const isPublic = this.convo.isPublic(); - const pubkey = this.convo.id; - - const { newAvatarObjecturl, oldAvatarPath } = this.state; - - if (!isPublic) { - return undefined; - } - - return ( -
-
- -
-
+ const okText = window.i18n('ok'); + const cancelText = window.i18n('cancel'); + const titleText = window.i18n('updateGroupDialogTitle', [ + originalGroupName || window.i18n('unknown'), + ]); + + const errorMessageClasses = classNames('error-message', errorMsg ? 'error-shown' : 'error-faded'); + + const isAdmin = !isCommunity; + // return null; + + return ( + closeDialog()} + additionalClassName="update-group-dialog" + > + {errorMsg ? ( + <> + +

{errorMsg}

+ + + ) : null} + + + + + {isAdmin ? ( + setNewGroupName(e.target.value)} + tabIndex={0} + required={true} + aria-required={true} + autoFocus={true} + data-testid="group-name-input" + /> + ) : null} + + + +
+ +
- ); - } - - private async fireInputEvent() { - const scaledObjectUrl = await pickFileForAvatar(); - if (scaledObjectUrl) { - this.setState({ newAvatarObjecturl: scaledObjectUrl }); - } - } +
+ ); } diff --git a/ts/components/leftpane/ActionsPanel.tsx b/ts/components/leftpane/ActionsPanel.tsx index 5565fafee..9ea9c8d51 100644 --- a/ts/components/leftpane/ActionsPanel.tsx +++ b/ts/components/leftpane/ActionsPanel.tsx @@ -208,6 +208,7 @@ const doAppStartUp = async () => { void getSwarmPollingInstance().start(); void loadDefaultRooms(); void SnodePool.getFreshSwarmFor(UserUtils.getOurPubKeyStrFromCache()); // refresh our swarm on start to speed up the first message fetching event + void Data.cleanupOrphanedAttachments(); // TODOLATER make this a job of the JobRunner debounce(triggerAvatarReUploadIfNeeded, 200); diff --git a/ts/mains/main_renderer.tsx b/ts/mains/main_renderer.tsx index 6149b240e..1215857a1 100644 --- a/ts/mains/main_renderer.tsx +++ b/ts/mains/main_renderer.tsx @@ -201,9 +201,6 @@ Storage.onready(async () => { if (newVersion) { window.log.info(`New version detected: ${currentVersion}; previous: ${lastVersion}`); - - await Data.cleanupOrphanedAttachments(); - await deleteAllLogs(); } diff --git a/ts/node/sql.ts b/ts/node/sql.ts index 65adc4bdf..0941e38e1 100644 --- a/ts/node/sql.ts +++ b/ts/node/sql.ts @@ -230,7 +230,7 @@ async function initializeSql({ return true; } -function removeDB(configDir = null) { +function removeDB(configDir: string | null = null) { if (isInstanceInitialized()) { throw new Error('removeDB: Cannot erase database when it is open!'); } diff --git a/ts/session/group/closed-group.ts b/ts/session/group/closed-group.ts index f92ae9209..551ee4ae3 100644 --- a/ts/session/group/closed-group.ts +++ b/ts/session/group/closed-group.ts @@ -56,7 +56,7 @@ export type GroupDiff = PropsForGroupUpdateType; async function initiateClosedGroupUpdate( groupId: string, groupName: string, - members: Array + members: Array | null ) { const isGroupV2 = PubKey.is03Pubkey(groupId); if (isGroupV2) { @@ -76,14 +76,16 @@ async function initiateClosedGroupUpdate( throw new Error(`Groups cannot be deleteAfterRead`); } + const updatedMembers = members === null ? convo.getGroupMembers() : members; + // do not give an admins field here. We don't want to be able to update admins and // updateOrCreateClosedGroup() will update them if given the choice. const groupDetails: GroupInfo = { id: groupId, name: groupName, - members, + members: updatedMembers, // remove from the zombies list the zombies not which are not in the group anymore - zombies: convo.getGroupZombies()?.filter(z => members.includes(z)), + zombies: convo.getGroupZombies()?.filter(z => updatedMembers.includes(z)), activeAt: Date.now(), expirationType, expireTimer, @@ -102,7 +104,7 @@ async function initiateClosedGroupUpdate( const updateObj: GroupInfo = { id: groupId, name: groupName, - members, + members: updatedMembers, admins: convo.getGroupAdmins(), expireTimer: convo.get('expireTimer'), }; @@ -142,8 +144,7 @@ async function initiateClosedGroupUpdate( diff: leavingOnlyDiff, ...sharedDetails, }); - const stillMembers = members; - await sendRemovedMembers(convo, diff.kicked, stillMembers, dbMessageLeaving.id as string); + await sendRemovedMembers(convo, diff.kicked, updatedMembers, dbMessageLeaving.id as string); } await convo.commit(); } diff --git a/ts/state/ducks/metaGroups.ts b/ts/state/ducks/metaGroups.ts index 5beecedaf..fcca693b5 100644 --- a/ts/state/ducks/metaGroups.ts +++ b/ts/state/ducks/metaGroups.ts @@ -1430,7 +1430,7 @@ const metaGroupSlice = createSlice({ window.log.debug(`groupMembers after currentDeviceGroupNameChange: ${stringify(members)}`); }); builder.addCase(currentDeviceGroupNameChange.rejected, (state, action) => { - window.log.error('a currentDeviceGroupNameChange was rejected', action.error); + window.log.error(`a ${currentDeviceGroupNameChange.name} was rejected`, action.error); state.nameChangesFromUIPending = false; }); builder.addCase(currentDeviceGroupNameChange.pending, state => { diff --git a/ts/state/selectors/groups.ts b/ts/state/selectors/groups.ts index b94f5af49..049361a83 100644 --- a/ts/state/selectors/groups.ts +++ b/ts/state/selectors/groups.ts @@ -38,6 +38,10 @@ function getIsMemberGroupChangePendingFromUI(state: StateType): boolean { return getLibGroupsState(state).memberChangesFromUIPending; } +function getGroupNameChangeFromUIPending(state: StateType): boolean { + return getLibGroupsState(state).nameChangesFromUIPending; +} + export function getLibAdminsPubkeys(state: StateType, convo?: string): Array { const members = getMembersOfGroup(state, convo); return members.filter(m => m.promoted).map(m => m.pubkeyHex); @@ -148,6 +152,10 @@ export function useMemberGroupChangePending() { return useSelector(getIsMemberGroupChangePendingFromUI); } +export function useGroupNameChangeFromUIPending() { + return useSelector(getGroupNameChangeFromUIPending); +} + /** * The selectors above are all deriving data from libsession. * There is also some data that we only need in memory, not part of libsession (and so unsaved). diff --git a/ts/types/attachments/VisualAttachment.ts b/ts/types/attachments/VisualAttachment.ts index dea63f707..a17f3422f 100644 --- a/ts/types/attachments/VisualAttachment.ts +++ b/ts/types/attachments/VisualAttachment.ts @@ -10,6 +10,7 @@ import { getDecryptedMediaUrl, } from '../../session/crypto/DecryptedAttachmentsManager'; import { ToastUtils } from '../../session/utils'; +import { isTestIntegration } from '../../shared/env_vars'; import { GoogleChrome } from '../../util'; import { autoScaleForAvatar, autoScaleForThumbnail } from '../../util/attachmentsUtil'; import { isAudio } from '../MIME'; @@ -205,7 +206,7 @@ export async function autoScaleAvatarBlob(file: File) { * Shows the system file picker for images, scale the image down for avatar/opengroup measurements and return the blob objectURL on success */ export async function pickFileForAvatar(): Promise { - if (window.sessionFeatureFlags.integrationTestEnv) { + if (isTestIntegration()) { window.log.info( 'shorting pickFileForAvatar as it does not work in playwright/notsending the filechooser event' ); diff --git a/ts/window.d.ts b/ts/window.d.ts index de2c41d44..6d1034143 100644 --- a/ts/window.d.ts +++ b/ts/window.d.ts @@ -30,7 +30,6 @@ declare global { useOnionRequests: boolean; useTestNet: boolean; useClosedGroupV2: boolean; - integrationTestEnv: boolean; debug: { debugLogging: boolean; debugLibsessionDumps: boolean;