Merge pull request #55 from Bilb/group-migrate

chore: recreate legacy groups + disable UI actions
pull/3281/head
Audric Ackermann 3 months ago committed by GitHub
commit d035c685c1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -481,6 +481,11 @@
"learnMore": "Learn More",
"leave": "Leave",
"leaving": "Leaving...",
"legacyGroupAfterDeprecationAdmin": "This group is now read-only. Recreate this group to keep chatting.",
"legacyGroupAfterDeprecationMember": "This group is now read-only. Ask the group admin to recreate this group to keep chatting.",
"legacyGroupBeforeDeprecationAdmin": "Groups have been upgraded! Recreate this group for improved reliability. This group will become read-only on {date}.",
"legacyGroupBeforeDeprecationMember": "Groups have been upgraded! Ask the group admin to recreate this group for improved reliability. This group will become read-only on {date}.",
"legacyGroupChatHistory": "Chat history will not be transferred to the new group. You can still view all chat history in your old group.",
"legacyGroupMemberNew": "<b>{name}</b> joined the group.",
"legacyGroupMemberNewMultiple": "<b>{name}</b> and <b>{count} others</b> joined the group.",
"legacyGroupMemberNewYouMultiple": "<b>You</b> and <b>{count} others</b> joined the group.",
@ -724,6 +729,7 @@
"recoveryPasswordRestoreDescription": "Enter your recovery password to load your account. If you haven't saved it, you can find it in your app settings.",
"recoveryPasswordView": "View Password",
"recoveryPasswordWarningSendDescription": "This is your recovery password. If you send it to someone they'll have full access to your account.",
"recreateGroup": "Recreate Group",
"redo": "Redo",
"remove": "Remove",
"removePasswordFail": "Failed to remove password",
@ -773,6 +779,7 @@
"shareAccountIdDescription": "Invite your friend to chat with you on {app_name} by sharing your Account ID with them.",
"shareAccountIdDescriptionCopied": "Share with your friends wherever you usually speak with them — then move the conversation here.",
"shareExtensionDatabaseError": "There is an issue opening the database. Please restart the app and try again.",
"shareExtensionNoAccountError": "Oops! Looks like you don't have a {app_name} account yet.<br/><br/>You'll need to create one in the {app_name} app before you can share.",
"shareToSession": "Share to {app_name}",
"show": "Show",
"showAll": "Show All",
@ -814,4 +821,3 @@
"yes": "Yes",
"you": "You"
}

@ -41,6 +41,7 @@ window.sessionFeatureFlags = {
useOnionRequests: true,
useTestNet: isTestNet() || isTestIntegration(),
useClosedGroupV2: true, // TODO DO NOT MERGE Remove after QA
forceLegacyGroupsDeprecated: false, // TODO DO NOT MERGE Remove after QA
useClosedGroupV2QAButtons: true, // TODO DO NOT MERGE Remove after QA
replaceLocalizedStringsWithKeys: false,
debug: {

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

@ -40,6 +40,7 @@ import { Storage } from '../util/storage';
import { UserGroupsWrapperActions } from '../webworker/workers/browser/libsession_worker_interface';
import { NoticeBanner } from './NoticeBanner';
import { Flex } from './basic/Flex';
import { initialReleasedFeaturesState } from '../state/ducks/releasedFeatures';
function makeLookup<T>(items: Array<T>, key: string): { [key: string]: T } {
// Yep, we can't index into item without knowing what it is. True. But we want to.
@ -88,6 +89,7 @@ async function createSessionInboxStore() {
settings: getSettingsInitialState(),
groups: initialGroupState,
userGroups: { userGroups },
releasedFeatures: initialReleasedFeaturesState,
};
return createStore(initialState);

@ -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,32 @@ 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 ? 'legacyGroupAfterDeprecationAdmin' : 'legacyGroupAfterDeprecationMember'
).toString()
: localize(
weAreAdmin ? 'legacyGroupBeforeDeprecationAdmin' : 'legacyGroupBeforeDeprecationMember'
)
.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('recreateGroup').toString(),
i18nMessage: { token: 'legacyGroupChatHistory' },
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 || localize('groupUnknown').toString(), members);
}}
>
{localize('recreateGroup').toString()}
</SessionButton>
</RecreateGroupContainer>
);
}

@ -12,19 +12,21 @@ import {
} from '../../../state/selectors/selectedConversation';
import { Avatar, AvatarSize } from '../../avatar/Avatar';
import { SessionIconButton } from '../../icon';
import { useDisableLegacyGroupDeprecatedActions } from '../../../hooks/useRefreshReleasedFeaturesTimestamp';
export const AvatarHeader = (props: {
pubkey: string;
onAvatarClick?: (pubkey: string) => void;
}) => {
const { pubkey, onAvatarClick } = props;
const isDisabledLegacyGroupDeprecated = useDisableLegacyGroupDeprecatedActions(pubkey);
return (
<span className="module-conversation-header__avatar">
<Avatar
size={AvatarSize.S}
onAvatarClick={() => {
if (onAvatarClick) {
if (onAvatarClick && !isDisabledLegacyGroupDeprecated) {
onAvatarClick(pubkey);
}
}}

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

@ -18,6 +18,7 @@ import {
useSelectedSubscriberCount,
} from '../../../state/selectors/selectedConversation';
import { ConversationHeaderSubtitle } from './ConversationHeaderSubtitle';
import { useSelectedDisableLegacyGroupDeprecatedActions } from '../../../hooks/useRefreshReleasedFeaturesTimestamp';
export type SubtitleStrings = Record<string, string> & {
notifications?: string;
@ -63,6 +64,8 @@ export const ConversationHeaderTitle = (props: ConversationHeaderTitleProps) =>
const isGroup = useSelectedIsGroupOrCommunity();
const selectedMembersCount = useSelectedMembersCount();
const isDisabledLegacyGroupDeprecated = useSelectedDisableLegacyGroupDeprecatedActions();
const expirationMode = useSelectedConversationDisappearingMode();
const disappearingMessageSubtitle = useDisappearingMessageSettingText({
convoId,
@ -97,6 +100,9 @@ export const ConversationHeaderTitle = (props: ConversationHeaderTitleProps) =>
}, [i18n, isGroup, isKickedFromGroup, isPublic, selectedMembersCount, subscriberCount]);
const handleRightPanelToggle = () => {
if (isDisabledLegacyGroupDeprecated) {
return;
}
if (isRightPanelOn) {
dispatch(closeRightPanel());
return;

@ -19,6 +19,7 @@ import { MessageContextMenu } from './MessageContextMenu';
import { MessageReactions } from './MessageReactions';
import { MessageStatus } from './MessageStatus';
import { useIsMessageSelectionMode } from '../../../../state/selectors/selectedConversation';
import { useSelectedDisableLegacyGroupDeprecatedActions } from '../../../../hooks/useRefreshReleasedFeaturesTimestamp';
export type MessageContentWithStatusSelectorProps = { isGroup: boolean } & Pick<
MessageRenderingProps,
@ -62,6 +63,7 @@ export const MessageContentWithStatuses = (props: Props) => {
const hideAvatar = useHideAvatarInMsgList(props.messageId);
const multiSelectMode = useIsMessageSelectionMode();
const legacyGroupActionsDisabled = useSelectedDisableLegacyGroupDeprecatedActions();
const onClickOnMessageOuterContainer = useCallback(
(event: MouseEvent<HTMLDivElement>) => {
@ -75,6 +77,9 @@ export const MessageContentWithStatuses = (props: Props) => {
);
const onDoubleClickReplyToMessage = (e: MouseEvent<HTMLDivElement>) => {
if (legacyGroupActionsDisabled) {
return;
}
const currentSelection = window.getSelection();
const currentSelectionString = currentSelection?.toString() || undefined;

@ -13,6 +13,7 @@ import { getGenericReadableMessageSelectorProps } from '../../../../state/select
import { MessageContentWithStatuses } from '../message-content/MessageContentWithStatus';
import { StyledMessageReactionsContainer } from '../message-content/MessageReactions';
import { useIsMessageSelectionMode } from '../../../../state/selectors/selectedConversation';
import { useSelectedDisableLegacyGroupDeprecatedActions } from '../../../../hooks/useRefreshReleasedFeaturesTimestamp';
export type GenericReadableMessageSelectorProps = Pick<
MessageRenderingProps,
@ -65,6 +66,7 @@ export const GenericReadableMessage = (props: Props) => {
const { ctxMenuID, messageId } = props;
const [enableReactions, setEnableReactions] = useState(true);
const legacyGroupIsDeprecated = useSelectedDisableLegacyGroupDeprecatedActions();
const msgProps = useSelector((state: StateType) =>
getGenericReadableMessageSelectorProps(state, props.messageId)
@ -83,6 +85,9 @@ export const GenericReadableMessage = (props: Props) => {
const handleContextMenu = useCallback(
(e: MouseEvent<HTMLElement>) => {
if (legacyGroupIsDeprecated) {
return;
}
// this is quite dirty but considering that we want the context menu of the message to show on click on the attachment
// and the context menu save attachment item to save the right attachment I did not find a better way for now.
@ -108,7 +113,7 @@ export const GenericReadableMessage = (props: Props) => {
}
setIsRightClicked(enableContextMenu);
},
[ctxMenuID, multiSelectMode, msgProps?.isKickedFromGroup]
[ctxMenuID, multiSelectMode, msgProps?.isKickedFromGroup, legacyGroupIsDeprecated]
);
useEffect(() => {

@ -11,6 +11,7 @@ import { abbreviateNumber } from '../../../../util/abbreviateNumber';
import { nativeEmojiData } from '../../../../util/emoji';
import { popupXDefault, popupYDefault } from '../message-content/MessageReactions';
import { POPUP_WIDTH, ReactionPopup, TipPosition } from './ReactionPopup';
import { useSelectedDisableLegacyGroupDeprecatedActions } from '../../../../hooks/useRefreshReleasedFeaturesTimestamp';
const StyledReaction = styled.button<{
selected: boolean;
@ -79,6 +80,8 @@ export const Reaction = (props: ReactionProps) => {
} = props;
const rightOverlayMode = useRightOverlayMode();
const areDeprecatedLegacyGroupDisabled = useSelectedDisableLegacyGroupDeprecatedActions();
const legacyGroupDeprecated = useSelectedDisableLegacyGroupDeprecatedActions();
const isMessageSelection = useIsMessageSelectionMode();
const reactionsMap = (reactions && Object.fromEntries(reactions)) || {};
const senders = reactionsMap[emoji]?.senders || [];
@ -106,7 +109,8 @@ export const Reaction = (props: ReactionProps) => {
const handleReactionClick = () => {
if (!isMessageSelection) {
if (onClick) {
// Note: disable emoji clicks if the legacy group is deprecated (group is readonly)
if (onClick && !legacyGroupDeprecated) {
onClick(emoji);
}
}
@ -174,6 +178,9 @@ export const Reaction = (props: ReactionProps) => {
senders={reactionsMap[popupReaction]?.senders}
tooltipPosition={tooltipPosition}
onClick={() => {
if (areDeprecatedLegacyGroupDisabled) {
return;
}
if (handlePopupReaction) {
handlePopupReaction('');
}

@ -49,6 +49,7 @@ import { getIsModalVisible } from '../../state/selectors/modal';
import { ReleasedFeatures } from '../../util/releaseFeature';
import { MessageQueue } from '../../session/sending';
import { useRefreshReleasedFeaturesTimestamp } from '../../hooks/useRefreshReleasedFeaturesTimestamp';
const Section = (props: { type: SectionType }) => {
const ourNumber = useSelector(getOurNumber);
@ -297,6 +298,8 @@ export const ActionsPanel = () => {
void triggerAvatarReUploadIfNeeded();
}, DURATION.DAYS * 1);
useRefreshReleasedFeaturesTimestamp();
if (!ourPrimaryConversation) {
window?.log?.warn('ActionsPanel: ourPrimaryConversation is not set');
return null;

@ -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,23 +119,35 @@ 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());
}
function onValueChanged(value: string) {
dispatch(groupInfoActions.updateGroupCreationName({ name: value }));
}
async function onEnterPressed() {
setGroupNameError(undefined);
if (isCreatingGroup) {
@ -197,7 +213,7 @@ export const OverlayClosedGroupV2 = () => {
type="text"
placeholder={window.i18n('groupNameEnter')}
value={groupName}
onValueChanged={setGroupName}
onValueChanged={onValueChanged}
onEnterPressed={onEnterPressed}
error={groupNameError}
maxLength={LIBSESSION_CONSTANTS.BASE_GROUP_MAX_NAME_LENGTH}
@ -221,8 +237,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 +266,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(() => {

@ -33,7 +33,11 @@ import { ItemWithDataTestId } from './items/MenuItemWithDataTestId';
import { getMenuAnimation } from './MenuAnimation';
import { LeaveCommunityMenuItem } from './items/LeaveCommunity/LeaveCommunityMenuItem';
import { LeaveGroupMenuItem } from './items/LeaveAndDeleteGroup/LeaveGroupMenuItem';
import { DeleteGroupMenuItem } from './items/LeaveAndDeleteGroup/DeleteGroupMenuItem';
import {
DeleteDeprecatedLegacyGroupMenuItem,
DeleteGroupMenuItem,
} from './items/LeaveAndDeleteGroup/DeleteGroupMenuItem';
import { useDisableLegacyGroupDeprecatedActions } from '../../hooks/useRefreshReleasedFeaturesTimestamp';
export type PropsContextConversationItem = {
triggerId: string;
@ -45,10 +49,22 @@ const ConversationListItemContextMenu = (props: PropsContextConversationItem) =>
const convoIdFromContext = useConvoIdFromContext();
const disabledLegacyGroup = useDisableLegacyGroupDeprecatedActions(convoIdFromContext);
if (isSearching) {
return null;
}
if (disabledLegacyGroup) {
return (
<SessionContextMenuContainer>
<Menu id={triggerId} animation={getMenuAnimation()}>
<DeleteDeprecatedLegacyGroupMenuItem />
</Menu>
</SessionContextMenuContainer>
);
}
return (
<SessionContextMenuContainer>
<Menu id={triggerId} animation={getMenuAnimation()}>

@ -12,6 +12,7 @@ import { useIsMessageRequestOverlayShown } from '../../../../state/selectors/sec
import { ItemWithDataTestId } from '../MenuItemWithDataTestId';
import { showDeleteGroupItem } from './guard';
import { Localizer } from '../../../basic/Localizer';
import { useDisableLegacyGroupDeprecatedActions } from '../../../../hooks/useRefreshReleasedFeaturesTimestamp';
export const DeleteGroupMenuItem = () => {
const convoId = useConvoIdFromContext();
@ -46,3 +47,26 @@ export const DeleteGroupMenuItem = () => {
</ItemWithDataTestId>
);
};
export const DeleteDeprecatedLegacyGroupMenuItem = () => {
const convoId = useConvoIdFromContext();
const username = useConversationUsername(convoId) || convoId;
const shortCircuitDeleteDeprecatedGroup = useDisableLegacyGroupDeprecatedActions(convoId);
if (!shortCircuitDeleteDeprecatedGroup) {
return null;
}
const token = 'groupDelete';
return (
<ItemWithDataTestId
onClick={() => {
void showDeleteGroupByConvoId(convoId, username);
}}
>
<Localizer token={token} />
</ItemWithDataTestId>
);
};

@ -0,0 +1,46 @@
import useInterval from 'react-use/lib/useInterval';
import { useDispatch, useSelector } from 'react-redux';
import { DURATION } from '../session/constants';
import { updateLegacyGroupDeprecationTimestampUpdatedAt } from '../state/ducks/releasedFeatures';
import { NetworkTime } from '../util/NetworkTime';
import { PubKey } from '../session/types';
import { areLegacyGroupsDeprecatedYet } from '../state/selectors/releasedFeatures';
import { useSelectedConversationKey } from '../state/selectors/selectedConversation';
import type { StateType } from '../state/reducer';
import { ConversationTypeEnum } from '../models/types';
export function useRefreshReleasedFeaturesTimestamp() {
const dispatch = useDispatch();
useInterval(() => {
const nowFromNetwork = NetworkTime.now();
dispatch(updateLegacyGroupDeprecationTimestampUpdatedAt(nowFromNetwork));
}, 1 * DURATION.SECONDS);
}
export function getDisableLegacyGroupDeprecatedActions(state: StateType, convoId?: string) {
if (!convoId || !PubKey.is05Pubkey(convoId)) {
return false;
}
const selectedConvoIsGroup =
state.conversations.conversationLookup[convoId]?.type === ConversationTypeEnum.GROUP;
if (!selectedConvoIsGroup) {
return false;
}
const legacyGroupDeprecated = areLegacyGroupsDeprecatedYet();
// here we have
// - a valid convoId
// - that starts with 05
// - that is a group (i.e. a legacy group)
// - and legacy group deprecation date has been hit
return legacyGroupDeprecated;
}
export function useDisableLegacyGroupDeprecatedActions(convoId?: string) {
return useSelector((state: StateType) => getDisableLegacyGroupDeprecatedActions(state, convoId));
}
export function useSelectedDisableLegacyGroupDeprecatedActions() {
const convoId = useSelectedConversationKey();
return useDisableLegacyGroupDeprecatedActions(convoId);
}

@ -58,6 +58,7 @@ import {
} from './types';
import { ConversationTypeEnum } from '../../../models/types';
import { Snode } from '../../../data/types';
import { areLegacyGroupsDeprecatedYetOutsideRedux } from '../../../state/selectors/releasedFeatures';
const minMsgCountShouldRetry = 95;
/**
@ -296,11 +297,15 @@ export class SwarmPolling {
.filter(m => !allGroupsInWrapper.some(w => w.pubkeyHex === m.pubkey.key))
.map(entryToKey);
const allLegacyGroupsTracked = legacyGroups
.filter(m => this.shouldPollByTimeout(m)) // should we poll from it depending on this group activity?
.filter(m => allGroupsLegacyInWrapper.some(w => w.pubkeyHex === m.pubkey.key)) // we don't poll from legacy groups which are not in the user group wrapper
.map(m => m.pubkey.key) // extract the pubkey
.map(m => [m, ConversationTypeEnum.GROUP] as PollForLegacy); //
const legacyGroupDeprecatedDisabled = areLegacyGroupsDeprecatedYetOutsideRedux();
const allLegacyGroupsTracked = legacyGroupDeprecatedDisabled
? []
: legacyGroups
.filter(m => this.shouldPollByTimeout(m)) // should we poll from it depending on this group activity?
.filter(m => allGroupsLegacyInWrapper.some(w => w.pubkeyHex === m.pubkey.key)) // we don't poll from legacy groups which are not in the user group wrapper
.map(m => m.pubkey.key) // extract the pubkey
.map(m => [m, ConversationTypeEnum.GROUP] as PollForLegacy); //
toPollDetails = concat(toPollDetails, allLegacyGroupsTracked);
const allGroupsTracked = groups

@ -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,43 @@ 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;
}
state.creationMembersSelected.push(payload.memberToAdd);
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) => {

@ -0,0 +1,27 @@
import { createSlice, type PayloadAction } from '@reduxjs/toolkit';
import { DURATION } from '../../session/constants';
// 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;
}
export const initialReleasedFeaturesState = {
legacyGroupDeprecationTimestampRefreshAtMs: Date.now(),
};
const releasedFeaturesSlice = createSlice({
name: 'releasedFeatures',
initialState: initialReleasedFeaturesState,
reducers: {
updateLegacyGroupDeprecationTimestampUpdatedAt: (state, action: PayloadAction<number>) => {
state.legacyGroupDeprecationTimestampRefreshAtMs = action.payload;
},
},
});
const { actions, reducer } = releasedFeaturesSlice;
export const { updateLegacyGroupDeprecationTimestampUpdatedAt } = actions;
export const releasedFeaturesReducer = reducer;

@ -21,6 +21,7 @@ import {
} from './ducks/stagedAttachments';
import { userConfigReducer as userConfig, UserConfigState } from './ducks/userConfig';
import { userGroupReducer, UserGroupState } from './ducks/userGroups';
import { releasedFeaturesReducer, ReleasedFeaturesState } from './ducks/releasedFeatures';
export type StateType = {
search: SearchStateType;
@ -39,6 +40,7 @@ export type StateType = {
settings: SettingsState;
groups: GroupState;
userGroups: UserGroupState;
releasedFeatures: ReleasedFeaturesState;
};
const reducers = {
@ -58,6 +60,7 @@ const reducers = {
settings: settingsReducer,
groups: groupReducer,
userGroups: userGroupReducer,
releasedFeatures: releasedFeaturesReducer,
};
// Making this work would require that our reducer signature supported AnyAction, not

@ -9,6 +9,7 @@ import * as StagedAttachmentSelectors from './stagedAttachments';
import * as ThemeSelectors from './theme';
import * as UserSelectors from './user';
import * as UserConfigSelectors from './userConfig';
import * as ReleasedFeaturesSelectors from './releasedFeatures';
export {
CallSelectors,
@ -22,6 +23,7 @@ export {
ThemeSelectors,
UserConfigSelectors,
UserSelectors,
ReleasedFeaturesSelectors,
};
export * from './messages';

@ -0,0 +1,20 @@
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;
return window.sessionFeatureFlags.forceLegacyGroupsDeprecated || theyAreDeprecated;
};
export function areLegacyGroupsDeprecatedYetOutsideRedux() {
if (!window.inboxStore) {
return false;
}
return areLegacyGroupsDeprecatedYet();
}
export function useAreLegacyGroupsDeprecatedYet() {
return useSelector(areLegacyGroupsDeprecatedYet);
}

@ -18,6 +18,7 @@ import {
import { getLibMembersPubkeys, useLibGroupName } from './groups';
import { getCanWrite, getModerators, getSubscriberCount } from './sogsRoomInfo';
import { getLibGroupDestroyed, getLibGroupKicked, useLibGroupDestroyed } from './userGroups';
import { getDisableLegacyGroupDeprecatedActions } from '../../hooks/useRefreshReleasedFeaturesTimestamp';
const getIsSelectedPrivate = (state: StateType): boolean => {
return Boolean(getSelectedConversation(state)?.isPrivate) || false;
@ -74,13 +75,20 @@ export function getSelectedCanWrite(state: StateType) {
const isBlindedAndDisabledMsgRequests = getSelectedBlindedDisabledMsgRequests(state); // true if isPrivate, blinded and explicitly disabled msgreq
return !(
isBlocked ||
isKickedFromGroup ||
isSelectedGroupKicked ||
isSelectedGroupDestroyed ||
readOnlySogs ||
isBlindedAndDisabledMsgRequests
const disabledLegacyGroupWrite = getDisableLegacyGroupDeprecatedActions(
state,
selectedConvoPubkey
);
return (
!(
isBlocked ||
isKickedFromGroup ||
isSelectedGroupKicked ||
isSelectedGroupDestroyed ||
readOnlySogs ||
isBlindedAndDisabledMsgRequests
) && !disabledLegacyGroupWrite
);
}
@ -250,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() {

1
ts/window.d.ts vendored

@ -103,6 +103,7 @@ declare global {
useTestNet: boolean;
useClosedGroupV2: boolean;
useClosedGroupV2QAButtons: boolean;
forceLegacyGroupsDeprecated: boolean;
replaceLocalizedStringsWithKeys: boolean;
debug: {
debugLogging: boolean;

Loading…
Cancel
Save