feat: allow to recreate group through redux

pull/3281/head
Audric Ackermann 3 months ago
parent 5748fe5456
commit 0f3ab81541
No known key found for this signature in database

@ -102,8 +102,8 @@ const StyledCheckContainer = styled.div`
align-items: center;
`;
type MemberListItemProps = {
pubkey: string;
type MemberListItemProps<T extends string> = {
pubkey: T;
isSelected: boolean;
// this bool is used to make a zombie appear with less opacity than a normal member
isZombie?: boolean;
@ -112,8 +112,8 @@ type MemberListItemProps = {
withBorder?: boolean;
maxNameWidth?: string;
isAdmin?: boolean; // if true, we add a small crown on top of their avatar
onSelect?: (pubkey: string) => void;
onUnselect?: (pubkey: string) => void;
onSelect?: (pubkey: T) => void;
onUnselect?: (pubkey: T) => void;
dataTestId?: React.SessionDataTestId;
displayGroupStatus?: boolean;
groupPk?: string;
@ -125,7 +125,7 @@ const ResendContainer = ({
displayGroupStatus,
groupPk,
pubkey,
}: Pick<MemberListItemProps, 'displayGroupStatus' | 'pubkey' | 'groupPk'>) => {
}: Pick<MemberListItemProps<string>, 'displayGroupStatus' | 'pubkey' | 'groupPk'>) => {
const weAreAdmin = useWeAreAdmin(groupPk);
if (
@ -220,7 +220,7 @@ const GroupStatusContainer = ({
displayGroupStatus,
groupPk,
pubkey,
}: Pick<MemberListItemProps, 'displayGroupStatus' | 'pubkey' | 'groupPk'>) => {
}: Pick<MemberListItemProps<string>, 'displayGroupStatus' | 'pubkey' | 'groupPk'>) => {
if (
displayGroupStatus &&
groupPk &&
@ -316,7 +316,7 @@ const PromoteButton = ({ groupPk, pubkey }: { pubkey: PubkeyType; groupPk: Group
);
};
export const MemberListItem = ({
export const MemberListItem = <T extends string>({
isSelected,
pubkey,
dataTestId,
@ -332,7 +332,7 @@ export const MemberListItem = ({
withBorder,
maxNameWidth,
hideRadioButton,
}: MemberListItemProps) => {
}: MemberListItemProps<T>) => {
const memberName = useNicknameOrProfileNameOrShortenedPubkey(pubkey);
const ourName = isUsAnySogsFromCache(pubkey) ? localize('you').toString() : null;

@ -62,6 +62,13 @@ import { InvitedToGroup, NoMessageInConversation } from './SubtleNotification';
import { PubKey } from '../../session/types';
import { isUsAnySogsFromCache } from '../../session/apis/open_group_api/sogsv3/knownBlindedkeys';
import { localize } from '../../localization/localeTools';
import {
useSelectedConversationKey,
useSelectedIsPrivate,
useSelectedIsPublic,
useSelectedWeAreAdmin,
} from '../../state/selectors/selectedConversation';
import { useAreLegacyGroupsDeprecatedYet } from '../../state/selectors/releasedFeatures';
const DEFAULT_JPEG_QUALITY = 0.85;
@ -253,7 +260,7 @@ export class SessionConversation extends Component<Props, State> {
ourDisplayNameInProfile={ourDisplayNameInProfile}
selectedConversation={selectedConversation}
/>
<OutdatedLegacyGroupBanner selectedConversation={selectedConversation} />
<OutdatedLegacyGroupBanner />
</div>
{isSelectedConvoInitialLoadingInProgress ? (
<ConvoLoadingSpinner />
@ -283,6 +290,7 @@ export class SessionConversation extends Component<Props, State> {
{isDraggingFile && <SessionFileDropzone />}
</div>
<ConversationMessageRequestButtons />
<CompositionBox
@ -655,20 +663,30 @@ function OutdatedClientBanner(props: {
) : null;
}
function OutdatedLegacyGroupBanner(props: {
selectedConversation: Pick<ReduxConversationType, 'id' | 'isPrivate' | 'isPublic'>;
}) {
const { selectedConversation } = props;
function OutdatedLegacyGroupBanner() {
const dispatch = useDispatch();
const weAreAdmin = useSelectedWeAreAdmin();
const selectedConversationKey = useSelectedConversationKey();
const isPrivate = useSelectedIsPrivate();
const isPublic = useSelectedIsPublic();
const deprecatedLegacyGroups = useAreLegacyGroupsDeprecatedYet();
const isLegacyGroup =
!selectedConversation.isPrivate &&
!selectedConversation.isPublic &&
selectedConversation.id.startsWith('05');
!isPrivate && !isPublic && selectedConversationKey && selectedConversationKey.startsWith('05');
// FIXME change the date here. Remove after QA
const text = deprecatedLegacyGroups
? localize(
weAreAdmin ? 'groupLegacyBannerAdminDeprecated' : 'groupLegacyBannerMemberDeprecated'
).toString()
: localize(weAreAdmin ? 'groupLegacyBannerAdmin' : 'groupLegacyBannerMember')
.withArgs({ date: '[Date]' })
.toString();
return isLegacyGroup ? (
<NoticeBanner
text={window.i18n('groupLegacyBanner', { date: '[Date]' })} // Remove after QA
text={text}
onBannerClick={() => {
showLinkVisitWarningDialog('https://getsession.org/groups', dispatch);
}}

@ -1,16 +1,30 @@
import { useDispatch } from 'react-redux';
import type { PubkeyType } from 'libsession_util_nodejs';
import { useCallback } from 'react';
import styled from 'styled-components';
import { openRightPanel } from '../../../state/ducks/conversations';
import { useIsOutgoingRequest } from '../../../hooks/useParamSelector';
import {
use05GroupMembers,
useConversationUsername,
useIsOutgoingRequest,
} from '../../../hooks/useParamSelector';
import {
useIsMessageSelectionMode,
useSelectedConversationKey,
useSelectedIsLegacyGroup,
useSelectedWeAreAdmin,
} from '../../../state/selectors/selectedConversation';
import { Flex } from '../../basic/Flex';
import { AvatarHeader, CallButton } from './ConversationHeaderItems';
import { SelectionOverlay } from './ConversationHeaderSelectionOverlay';
import { ConversationHeaderTitle } from './ConversationHeaderTitle';
import { localize } from '../../../localization/localeTools';
import { groupInfoActions } from '../../../state/ducks/metaGroups';
import { updateConfirmModal } from '../../../state/ducks/modalDialog';
import { setLeftOverlayMode } from '../../../state/ducks/section';
import { SessionButtonColor, SessionButton } from '../../basic/SessionButton';
export const ConversationHeaderWithDetails = () => {
const isSelectionMode = useIsMessageSelectionMode();
@ -42,6 +56,7 @@ export const ConversationHeaderWithDetails = () => {
flexGrow={0}
flexShrink={0}
>
<RecreateGroupButton />
<CallButton />
<AvatarHeader
onAvatarClick={() => {
@ -57,3 +72,71 @@ export const ConversationHeaderWithDetails = () => {
</div>
);
};
const RecreateGroupContainer = styled.div`
display: flex;
justify-content: center;
align-self: center;
width: 100%;
.session-button {
padding-inline: var(--margins-3xl);
}
`;
function useShowRecreateModal() {
const dispatch = useDispatch();
return useCallback(
(name: string, members: Array<PubkeyType>) => {
dispatch(
updateConfirmModal({
title: localize('groupRecreate').toString(),
i18nMessage: { token: 'groupRecreateDescription' },
okText: localize('theContinue').toString(),
cancelText: localize('cancel').toString(),
okTheme: SessionButtonColor.Danger,
onClickOk: () => {
dispatch(setLeftOverlayMode('closed-group'));
dispatch(groupInfoActions.updateGroupCreationName({ name }));
dispatch(groupInfoActions.setSelectedGroupMembers({ membersToSet: members }));
},
onClickClose: () => {
dispatch(updateConfirmModal(null));
},
})
);
},
[dispatch]
);
}
function RecreateGroupButton() {
const isLegacyGroup = useSelectedIsLegacyGroup();
const selectedConvo = useSelectedConversationKey();
const name = useConversationUsername(selectedConvo);
const members = use05GroupMembers(selectedConvo);
const weAreAdmin = useSelectedWeAreAdmin();
const showRecreateGroupModal = useShowRecreateModal();
if (!isLegacyGroup || !weAreAdmin) {
return null;
}
return (
<RecreateGroupContainer>
<SessionButton
buttonColor={SessionButtonColor.Primary}
margin="var(--margins-sm)"
onClick={() => {
showRecreateGroupModal(name || 'Unknown group name', members);
}}
>
{localize('groupRecreate').toString()}
</SessionButton>
</RecreateGroupContainer>
);
}

@ -17,7 +17,8 @@ export const StyledSubtitleContainer = styled.div`
align-items: center;
justify-content: center;
margin: 0 auto;
min-width: 230px;
// with the "Recreate group" button (temporary) visible, at min-width we have less room available
min-width: 180px;
div:first-child {
span:last-child {

@ -6,6 +6,8 @@ import styled from 'styled-components';
import { concat, isEmpty } from 'lodash';
import useBoolean from 'react-use/lib/useBoolean';
import useUpdate from 'react-use/lib/useUpdate';
import type { PubkeyType } from 'libsession_util_nodejs';
import { MemberListItem } from '../../MemberListItem';
import { SessionButton } from '../../basic/SessionButton';
@ -35,6 +37,8 @@ import { SessionInput } from '../../inputs';
import { SessionSpinner } from '../../loading';
import { StyledLeftPaneOverlay } from './OverlayMessage';
import { hasClosedGroupV2QAButtons } from '../../../shared/env_vars';
import type { StateType } from '../../../state/reducer';
import { PubKey } from '../../../session/types';
const StyledMemberListNoContacts = styled.div`
text-align: center;
@ -115,18 +119,26 @@ export const OverlayClosedGroupV2 = () => {
const us = useOurPkStr();
const privateContactsPubkeys = useContactsToInviteToGroup();
const isCreatingGroup = useIsCreatingGroupFromUIPending();
const [groupName, setGroupName] = useState('');
const groupName = useSelector((state: StateType) => state.groups.creationGroupName) || '';
const [inviteAsAdmin, setInviteAsAdmin] = useBoolean(false);
const [groupNameError, setGroupNameError] = useState<string | undefined>();
const {
uniqueValues: selectedMemberIds,
addTo: addToSelected,
removeFrom: removeFromSelected,
} = useSet<string>([]);
const isSearch = useIsSearching();
const searchTerm = useSelector(getSearchTerm);
const searchResultContactsOnly = useSelector(getSearchResultsContactOnly);
const forceRefresh = useUpdate();
const selectedMemberIds = useSelector(
(state: StateType) => state.groups.creationMembersSelected || []
);
function addMemberToSelection(member: PubkeyType) {
dispatch(groupInfoActions.addSelectedGroupMember({ memberToAdd: member }));
}
function removeMemberFromSelection(member: PubkeyType) {
dispatch(groupInfoActions.removeSelectedGroupMember({ memberToRemove: member }));
}
function closeOverlay() {
dispatch(clearSearch());
dispatch(resetLeftOverlayMode());
@ -197,7 +209,9 @@ export const OverlayClosedGroupV2 = () => {
type="text"
placeholder={window.i18n('groupNameEnter')}
value={groupName}
onValueChanged={setGroupName}
onValueChanged={value => {
dispatch(groupInfoActions.updateGroupCreationName({ name: value }));
}}
onEnterPressed={onEnterPressed}
error={groupNameError}
maxLength={LIBSESSION_CONSTANTS.BASE_GROUP_MAX_NAME_LENGTH}
@ -221,8 +235,20 @@ export const OverlayClosedGroupV2 = () => {
}}
/>
</span>
<span style={{ display: 'flex', alignItems: 'center' }}>
Deprecated Legacy groups?{' '}
<SessionToggle
active={window.sessionFeatureFlags.forceLegacyGroupsDeprecated}
onClick={() => {
window.sessionFeatureFlags.forceLegacyGroupsDeprecated =
!window.sessionFeatureFlags.forceLegacyGroupsDeprecated;
forceRefresh();
}}
/>
</span>
</>
)}
<SessionSpinner loading={isCreatingGroup} />
<SpacerLG />
</Flex>
@ -238,18 +264,24 @@ export const OverlayClosedGroupV2 = () => {
<Localizer token="searchMatchesNoneSpecific" args={{ query: searchTerm }} />
</StyledNoResults>
) : (
contactsToRender.map((memberPubkey: string) => (
<MemberListItem
key={`member-list-${memberPubkey}`}
pubkey={memberPubkey}
isSelected={selectedMemberIds.includes(memberPubkey)}
onSelect={addToSelected}
onUnselect={removeFromSelected}
withBorder={false}
disabled={isCreatingGroup}
maxNameWidth="100%"
/>
))
contactsToRender.map((memberPubkey: string) => {
if (!PubKey.is05Pubkey(memberPubkey)) {
throw new Error('Invalid member rendered in member list');
}
return (
<MemberListItem
key={`member-list-${memberPubkey}`}
pubkey={memberPubkey}
isSelected={selectedMemberIds.includes(memberPubkey)}
onSelect={addMemberToSelection}
onUnselect={removeMemberFromSelection}
withBorder={false}
disabled={isCreatingGroup}
maxNameWidth="100%"
/>
);
})
)}
</StyledGroupMemberListContainer>

@ -8,6 +8,7 @@ import { SpacerSM } from '../../../basic/Text';
import { StyledLeftPaneOverlay } from '../OverlayMessage';
import { ActionRow, StyledActionRowContainer } from './ActionRow';
import { ContactsListWithBreaks } from './ContactsListWithBreaks';
import { groupInfoActions } from '../../../../state/ducks/metaGroups';
export const OverlayChooseAction = () => {
const dispatch = useDispatch();
@ -21,6 +22,8 @@ export const OverlayChooseAction = () => {
const openCreateGroup = useCallback(() => {
dispatch(setLeftOverlayMode('closed-group'));
dispatch(groupInfoActions.updateGroupCreationName({ name: '' }));
dispatch(groupInfoActions.setSelectedGroupMembers({ membersToSet: [] }));
}, [dispatch]);
const openJoinCommunity = useCallback(() => {

@ -8,7 +8,7 @@ import {
UserGroupsGet,
WithGroupPubkey,
} from 'libsession_util_nodejs';
import { concat, intersection, isEmpty, uniq } from 'lodash';
import { concat, intersection, isEmpty, isNil, uniq } from 'lodash';
import { from_hex } from 'libsodium-wrappers-sumo';
import { ConfigDumpData } from '../../data/configDump/configDump';
import { HexString } from '../../node/hexStrings';
@ -57,11 +57,15 @@ import { updateGroupNameModal } from './modalDialog';
export type GroupState = {
infos: Record<GroupPubkeyType, GroupInfoGet>;
members: Record<GroupPubkeyType, Array<GroupMemberGet>>;
creationFromUIPending: boolean;
memberChangesFromUIPending: boolean;
nameChangesFromUIPending: boolean;
membersInviteSending: Record<GroupPubkeyType, Array<PubkeyType>>;
membersPromoteSending: Record<GroupPubkeyType, Array<PubkeyType>>;
// those are group creation-related fields
creationFromUIPending: boolean;
creationMembersSelected: Array<PubkeyType>;
creationGroupName: string;
};
export const initialGroupState: GroupState = {
@ -72,6 +76,8 @@ export const initialGroupState: GroupState = {
nameChangesFromUIPending: false,
membersInviteSending: {},
membersPromoteSending: {},
creationMembersSelected: [],
creationGroupName: '',
};
type GroupDetailsUpdate = {
@ -1256,6 +1262,44 @@ const metaGroupSlice = createSlice({
delete state.membersInviteSending[payload.groupPk];
delete state.membersPromoteSending[payload.groupPk];
},
addSelectedGroupMember(
state: GroupState,
{ payload }: PayloadAction<{ memberToAdd: PubkeyType }>
) {
if (!state.creationMembersSelected?.length) {
state.creationMembersSelected = [payload.memberToAdd];
return state;
}
if (state.creationMembersSelected.includes(payload.memberToAdd)) {
return state;
}
const together = state.creationMembersSelected.concat(payload.memberToAdd);
state.creationMembersSelected = uniq(together);
return state;
},
setSelectedGroupMembers(
state: GroupState,
{ payload }: PayloadAction<{ membersToSet: Array<PubkeyType> }>
) {
state.creationMembersSelected = uniq(payload.membersToSet);
return state;
},
removeSelectedGroupMember(
state: GroupState,
{ payload }: PayloadAction<{ memberToRemove: PubkeyType }>
) {
const foundAt = state.creationMembersSelected?.indexOf(payload.memberToRemove);
if (state.creationMembersSelected && !isNil(foundAt) && foundAt >= 0) {
state.creationMembersSelected.splice(foundAt, 1);
}
return state;
},
updateGroupCreationName(state: GroupState, { payload }: PayloadAction<{ name: string }>) {
state.creationGroupName = payload.name;
return state;
},
},
extraReducers: builder => {
builder.addCase(initNewGroupInWrapper.fulfilled, (state, action) => {

@ -1,6 +1,8 @@
import { createSlice, type PayloadAction } from '@reduxjs/toolkit';
import { DURATION } from '../../session/constants';
export const LEGACY_GROUP_DEPRECATED_TIMESTAMP_MS = Date.now() + 10 * 1000;
// FIXME update this to the correct timestamp REMOVE AFTER QA
export const LEGACY_GROUP_DEPRECATED_TIMESTAMP_MS = Date.now() + DURATION.WEEKS * 52;
export interface ReleasedFeaturesState {
legacyGroupDeprecationTimestampRefreshAtMs: number;

@ -2,7 +2,6 @@ import { useSelector } from 'react-redux';
import { NetworkTime } from '../../util/NetworkTime';
import { LEGACY_GROUP_DEPRECATED_TIMESTAMP_MS } from '../ducks/releasedFeatures';
export const areLegacyGroupsDeprecatedYet = (): boolean => {
const theyAreDeprecated = NetworkTime.now() >= LEGACY_GROUP_DEPRECATED_TIMESTAMP_MS;

@ -258,10 +258,9 @@ export function useSelectedIsPublic() {
*/
export function useSelectedIsLegacyGroup() {
const isGroupOrCommunity = useSelectedIsGroupOrCommunity();
const isGroupV2 = useSelectedIsGroupV2();
const isPublic = useSelectedIsPublic();
const selectedConvoKey = useSelectedConversationKey();
return isGroupOrCommunity && !isGroupV2 && !isPublic;
return isGroupOrCommunity && selectedConvoKey && PubKey.is05Pubkey(selectedConvoKey);
}
export function useSelectedIsPrivate() {

@ -109,7 +109,6 @@ function isDisappearMessageV2FeatureReleasedCached(): boolean {
return !!isDisappearingMessageFeatureReleased;
}
export const ReleasedFeatures = {
checkIsUserConfigFeatureReleased,
checkIsDisappearMessageV2FeatureReleased,

Loading…
Cancel
Save