feat: refacto GroupUpdateName to be functional component

pull/3052/head
Audric Ackermann 2 years ago
parent 8be63b12b3
commit bd6f5e6e0d

@ -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<Props, State> {
private readonly convo: ConversationModel;
oldAvatarPath: string | null;
fireInputEvent: () => Promise<void>;
}) {
if (!isPublic) {
return null;
}
constructor(props: Props) {
super(props);
return (
<div className="avatar-center">
<div className="avatar-center-inner">
<Avatar
forcedAvatarPath={newAvatarObjecturl || oldAvatarPath}
size={AvatarSize.XL}
pubkey={conversationId}
/>
<div className="image-upload-section" role="button" onClick={fireInputEvent} />
</div>
</div>
);
}
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<string | null>(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 (
<SessionWrapperModal
title={titleText}
onClose={() => this.closeDialog()}
additionalClassName="update-group-dialog"
>
{this.state.errorDisplayed ? (
<>
<SpacerMD />
<p className={errorMessageClasses}>{errorMsg}</p>
<SpacerMD />
</>
) : null}
{this.renderAvatar()}
<SpacerMD />
{isAdmin ? (
<input
type="text"
className="profile-name-input"
value={this.state.groupName}
placeholder={window.i18n('groupNamePlaceholder')}
onChange={this.onGroupNameChanged}
tabIndex={0}
required={true}
aria-required={true}
autoFocus={true}
data-testid="group-name-input"
/>
) : null}
<div className="session-modal__button-group">
<SessionButton
text={okText}
onClick={this.onClickOK}
buttonType={SessionButtonType.Simple}
/>
<SessionButton
text={cancelText}
buttonColor={SessionButtonColor.Danger}
buttonType={SessionButtonType.Simple}
onClick={this.closeDialog}
/>
</div>
</SessionWrapperModal>
);
}
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 (
<div className="avatar-center">
<div className="avatar-center-inner">
<Avatar
forcedAvatarPath={newAvatarObjecturl || oldAvatarPath}
size={AvatarSize.XL}
pubkey={pubkey}
/>
<div className="image-upload-section" role="button" onClick={this.fireInputEvent} />
</div>
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 (
<SessionWrapperModal
title={titleText}
onClose={() => closeDialog()}
additionalClassName="update-group-dialog"
>
{errorMsg ? (
<>
<SpacerMD />
<p className={errorMessageClasses}>{errorMsg}</p>
<SpacerMD />
</>
) : null}
<GroupAvatar
conversationId={conversationId}
fireInputEvent={fireInputEvent}
isPublic={isCommunity}
newAvatarObjecturl={newAvatarObjecturl}
oldAvatarPath={oldAvatarPath}
/>
<SpacerMD />
{isAdmin ? (
<input
type="text"
className="profile-name-input"
value={newGroupName}
placeholder={window.i18n('groupNamePlaceholder')}
onChange={e => setNewGroupName(e.target.value)}
tabIndex={0}
required={true}
aria-required={true}
autoFocus={true}
data-testid="group-name-input"
/>
) : null}
<SessionSpinner loading={isNameChangePending} />
<div className="session-modal__button-group">
<SessionButton
text={okText}
onClick={onClickOK}
buttonType={SessionButtonType.Simple}
disabled={isNameChangePending}
/>
<SessionButton
text={cancelText}
buttonColor={SessionButtonColor.Danger}
buttonType={SessionButtonType.Simple}
onClick={closeDialog}
/>
</div>
);
}
private async fireInputEvent() {
const scaledObjectUrl = await pickFileForAvatar();
if (scaledObjectUrl) {
this.setState({ newAvatarObjecturl: scaledObjectUrl });
}
}
</SessionWrapperModal>
);
}

@ -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);

@ -201,9 +201,6 @@ Storage.onready(async () => {
if (newVersion) {
window.log.info(`New version detected: ${currentVersion}; previous: ${lastVersion}`);
await Data.cleanupOrphanedAttachments();
await deleteAllLogs();
}

@ -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!');
}

@ -56,7 +56,7 @@ export type GroupDiff = PropsForGroupUpdateType;
async function initiateClosedGroupUpdate(
groupId: string,
groupName: string,
members: Array<string>
members: Array<string> | 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();
}

@ -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 => {

@ -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<string> {
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).

@ -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<string | null> {
if (window.sessionFeatureFlags.integrationTestEnv) {
if (isTestIntegration()) {
window.log.info(
'shorting pickFileForAvatar as it does not work in playwright/notsending the filechooser event'
);

1
ts/window.d.ts vendored

@ -30,7 +30,6 @@ declare global {
useOnionRequests: boolean;
useTestNet: boolean;
useClosedGroupV2: boolean;
integrationTestEnv: boolean;
debug: {
debugLogging: boolean;
debugLibsessionDumps: boolean;

Loading…
Cancel
Save