From 5748fe545673d7ecd37fb3996fb0cbdf5ed4a73a Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Mon, 3 Feb 2025 16:31:13 +1100 Subject: [PATCH 1/4] chore: disable a bunch of UI once legacy groups are deprecated --- preload.js | 1 + ts/components/SessionInboxView.tsx | 2 + .../header/ConversationHeaderItems.tsx | 4 +- .../header/ConversationHeaderTitle.tsx | 6 +++ .../message-item/GenericReadableMessage.tsx | 7 ++- .../message/reactions/Reaction.tsx | 9 +++- ts/components/leftpane/ActionsPanel.tsx | 3 ++ .../menu/ConversationListItemContextMenu.tsx | 18 +++++++- .../DeleteGroupMenuItem.tsx | 24 ++++++++++ .../useRefreshReleasedFeaturesTimestamp.ts | 46 +++++++++++++++++++ ts/session/apis/snode_api/swarmPolling.ts | 15 ++++-- ts/state/ducks/releasedFeatures.tsx | 25 ++++++++++ ts/state/reducer.ts | 3 ++ ts/state/selectors/index.ts | 2 + ts/state/selectors/releasedFeatures.ts | 21 +++++++++ ts/state/selectors/selectedConversation.ts | 22 ++++++--- ts/util/releaseFeature.ts | 1 + ts/window.d.ts | 1 + 18 files changed, 194 insertions(+), 16 deletions(-) create mode 100644 ts/hooks/useRefreshReleasedFeaturesTimestamp.ts create mode 100644 ts/state/ducks/releasedFeatures.tsx create mode 100644 ts/state/selectors/releasedFeatures.ts diff --git a/preload.js b/preload.js index 199e72a82..c1d294df8 100644 --- a/preload.js +++ b/preload.js @@ -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: { diff --git a/ts/components/SessionInboxView.tsx b/ts/components/SessionInboxView.tsx index bff9301b2..c1e7c5c4d 100644 --- a/ts/components/SessionInboxView.tsx +++ b/ts/components/SessionInboxView.tsx @@ -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(items: Array, 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); diff --git a/ts/components/conversation/header/ConversationHeaderItems.tsx b/ts/components/conversation/header/ConversationHeaderItems.tsx index 39d05e8f7..c92d94eae 100644 --- a/ts/components/conversation/header/ConversationHeaderItems.tsx +++ b/ts/components/conversation/header/ConversationHeaderItems.tsx @@ -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 ( { - if (onAvatarClick) { + if (onAvatarClick && !isDisabledLegacyGroupDeprecated) { onAvatarClick(pubkey); } }} diff --git a/ts/components/conversation/header/ConversationHeaderTitle.tsx b/ts/components/conversation/header/ConversationHeaderTitle.tsx index 4da8d91eb..e1d64ce88 100644 --- a/ts/components/conversation/header/ConversationHeaderTitle.tsx +++ b/ts/components/conversation/header/ConversationHeaderTitle.tsx @@ -18,6 +18,7 @@ import { useSelectedSubscriberCount, } from '../../../state/selectors/selectedConversation'; import { ConversationHeaderSubtitle } from './ConversationHeaderSubtitle'; +import { useSelectedDisableLegacyGroupDeprecatedActions } from '../../../hooks/useRefreshReleasedFeaturesTimestamp'; export type SubtitleStrings = Record & { 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; diff --git a/ts/components/conversation/message/message-item/GenericReadableMessage.tsx b/ts/components/conversation/message/message-item/GenericReadableMessage.tsx index cca3dbf56..f53ea150d 100644 --- a/ts/components/conversation/message/message-item/GenericReadableMessage.tsx +++ b/ts/components/conversation/message/message-item/GenericReadableMessage.tsx @@ -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) => { + 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(() => { diff --git a/ts/components/conversation/message/reactions/Reaction.tsx b/ts/components/conversation/message/reactions/Reaction.tsx index a82de029d..d1c266e7d 100644 --- a/ts/components/conversation/message/reactions/Reaction.tsx +++ b/ts/components/conversation/message/reactions/Reaction.tsx @@ -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(''); } diff --git a/ts/components/leftpane/ActionsPanel.tsx b/ts/components/leftpane/ActionsPanel.tsx index ec1842774..d2f3c0339 100644 --- a/ts/components/leftpane/ActionsPanel.tsx +++ b/ts/components/leftpane/ActionsPanel.tsx @@ -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; diff --git a/ts/components/menu/ConversationListItemContextMenu.tsx b/ts/components/menu/ConversationListItemContextMenu.tsx index 83aeb1964..ffe476a78 100644 --- a/ts/components/menu/ConversationListItemContextMenu.tsx +++ b/ts/components/menu/ConversationListItemContextMenu.tsx @@ -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 ( + + + + + + ); + } + return ( diff --git a/ts/components/menu/items/LeaveAndDeleteGroup/DeleteGroupMenuItem.tsx b/ts/components/menu/items/LeaveAndDeleteGroup/DeleteGroupMenuItem.tsx index 9fdc5aaa9..415b14e56 100644 --- a/ts/components/menu/items/LeaveAndDeleteGroup/DeleteGroupMenuItem.tsx +++ b/ts/components/menu/items/LeaveAndDeleteGroup/DeleteGroupMenuItem.tsx @@ -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 = () => { ); }; + +export const DeleteDeprecatedLegacyGroupMenuItem = () => { + const convoId = useConvoIdFromContext(); + const username = useConversationUsername(convoId) || convoId; + + const shortCircuitDeleteDeprecatedGroup = useDisableLegacyGroupDeprecatedActions(convoId); + + if (!shortCircuitDeleteDeprecatedGroup) { + return null; + } + + const token = 'groupDelete'; + + return ( + { + void showDeleteGroupByConvoId(convoId, username); + }} + > + + + ); +}; diff --git a/ts/hooks/useRefreshReleasedFeaturesTimestamp.ts b/ts/hooks/useRefreshReleasedFeaturesTimestamp.ts new file mode 100644 index 000000000..8ffc21135 --- /dev/null +++ b/ts/hooks/useRefreshReleasedFeaturesTimestamp.ts @@ -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); +} diff --git a/ts/session/apis/snode_api/swarmPolling.ts b/ts/session/apis/snode_api/swarmPolling.ts index 0d06fbb0e..b483ba9c6 100644 --- a/ts/session/apis/snode_api/swarmPolling.ts +++ b/ts/session/apis/snode_api/swarmPolling.ts @@ -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 diff --git a/ts/state/ducks/releasedFeatures.tsx b/ts/state/ducks/releasedFeatures.tsx new file mode 100644 index 000000000..e66ec1bc0 --- /dev/null +++ b/ts/state/ducks/releasedFeatures.tsx @@ -0,0 +1,25 @@ +import { createSlice, type PayloadAction } from '@reduxjs/toolkit'; + +export const LEGACY_GROUP_DEPRECATED_TIMESTAMP_MS = Date.now() + 10 * 1000; + +export interface ReleasedFeaturesState { + legacyGroupDeprecationTimestampRefreshAtMs: number; +} + +export const initialReleasedFeaturesState = { + legacyGroupDeprecationTimestampRefreshAtMs: Date.now(), +}; + +const releasedFeaturesSlice = createSlice({ + name: 'releasedFeatures', + initialState: initialReleasedFeaturesState, + reducers: { + updateLegacyGroupDeprecationTimestampUpdatedAt: (state, action: PayloadAction) => { + state.legacyGroupDeprecationTimestampRefreshAtMs = action.payload; + }, + }, +}); + +const { actions, reducer } = releasedFeaturesSlice; +export const { updateLegacyGroupDeprecationTimestampUpdatedAt } = actions; +export const releasedFeaturesReducer = reducer; diff --git a/ts/state/reducer.ts b/ts/state/reducer.ts index e20833166..9b6620679 100644 --- a/ts/state/reducer.ts +++ b/ts/state/reducer.ts @@ -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 diff --git a/ts/state/selectors/index.ts b/ts/state/selectors/index.ts index f64511879..9e90331db 100644 --- a/ts/state/selectors/index.ts +++ b/ts/state/selectors/index.ts @@ -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'; diff --git a/ts/state/selectors/releasedFeatures.ts b/ts/state/selectors/releasedFeatures.ts new file mode 100644 index 000000000..d21b3c7d7 --- /dev/null +++ b/ts/state/selectors/releasedFeatures.ts @@ -0,0 +1,21 @@ +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); +} diff --git a/ts/state/selectors/selectedConversation.ts b/ts/state/selectors/selectedConversation.ts index 7d790ca1b..8e794fc62 100644 --- a/ts/state/selectors/selectedConversation.ts +++ b/ts/state/selectors/selectedConversation.ts @@ -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 ); } diff --git a/ts/util/releaseFeature.ts b/ts/util/releaseFeature.ts index 76be91603..7f24e48c9 100644 --- a/ts/util/releaseFeature.ts +++ b/ts/util/releaseFeature.ts @@ -109,6 +109,7 @@ function isDisappearMessageV2FeatureReleasedCached(): boolean { return !!isDisappearingMessageFeatureReleased; } + export const ReleasedFeatures = { checkIsUserConfigFeatureReleased, checkIsDisappearMessageV2FeatureReleased, diff --git a/ts/window.d.ts b/ts/window.d.ts index 11c98e329..cb91d9317 100644 --- a/ts/window.d.ts +++ b/ts/window.d.ts @@ -103,6 +103,7 @@ declare global { useTestNet: boolean; useClosedGroupV2: boolean; useClosedGroupV2QAButtons: boolean; + forceLegacyGroupsDeprecated: boolean; replaceLocalizedStringsWithKeys: boolean; debug: { debugLogging: boolean; From 0f3ab81541ef5022290f829cd3f4c9d3510bd8e1 Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Tue, 4 Feb 2025 15:30:06 +1100 Subject: [PATCH 2/4] feat: allow to recreate group through redux --- ts/components/MemberListItem.tsx | 16 ++-- .../conversation/SessionConversation.tsx | 36 ++++++-- .../header/ConversationHeader.tsx | 85 ++++++++++++++++++- .../header/ConversationHeaderSubtitle.tsx | 3 +- .../leftpane/overlay/OverlayClosedGroup.tsx | 70 ++++++++++----- .../choose-action/OverlayChooseAction.tsx | 3 + ts/state/ducks/metaGroups.ts | 48 ++++++++++- ts/state/ducks/releasedFeatures.tsx | 4 +- ts/state/selectors/releasedFeatures.ts | 1 - ts/state/selectors/selectedConversation.ts | 5 +- ts/util/releaseFeature.ts | 1 - 11 files changed, 226 insertions(+), 46 deletions(-) diff --git a/ts/components/MemberListItem.tsx b/ts/components/MemberListItem.tsx index 6e6f7bc1f..972fbe19f 100644 --- a/ts/components/MemberListItem.tsx +++ b/ts/components/MemberListItem.tsx @@ -102,8 +102,8 @@ const StyledCheckContainer = styled.div` align-items: center; `; -type MemberListItemProps = { - pubkey: string; +type MemberListItemProps = { + 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) => { +}: Pick, 'displayGroupStatus' | 'pubkey' | 'groupPk'>) => { const weAreAdmin = useWeAreAdmin(groupPk); if ( @@ -220,7 +220,7 @@ const GroupStatusContainer = ({ displayGroupStatus, groupPk, pubkey, -}: Pick) => { +}: Pick, 'displayGroupStatus' | 'pubkey' | 'groupPk'>) => { if ( displayGroupStatus && groupPk && @@ -316,7 +316,7 @@ const PromoteButton = ({ groupPk, pubkey }: { pubkey: PubkeyType; groupPk: Group ); }; -export const MemberListItem = ({ +export const MemberListItem = ({ isSelected, pubkey, dataTestId, @@ -332,7 +332,7 @@ export const MemberListItem = ({ withBorder, maxNameWidth, hideRadioButton, -}: MemberListItemProps) => { +}: MemberListItemProps) => { const memberName = useNicknameOrProfileNameOrShortenedPubkey(pubkey); const ourName = isUsAnySogsFromCache(pubkey) ? localize('you').toString() : null; diff --git a/ts/components/conversation/SessionConversation.tsx b/ts/components/conversation/SessionConversation.tsx index 1b192f259..eecbcd9d2 100644 --- a/ts/components/conversation/SessionConversation.tsx +++ b/ts/components/conversation/SessionConversation.tsx @@ -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 { ourDisplayNameInProfile={ourDisplayNameInProfile} selectedConversation={selectedConversation} /> - + {isSelectedConvoInitialLoadingInProgress ? ( @@ -283,6 +290,7 @@ export class SessionConversation extends Component { {isDraggingFile && } + ; -}) { - 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 ? ( { showLinkVisitWarningDialog('https://getsession.org/groups', dispatch); }} diff --git a/ts/components/conversation/header/ConversationHeader.tsx b/ts/components/conversation/header/ConversationHeader.tsx index 7c1fd1ca0..ec90a9a61 100644 --- a/ts/components/conversation/header/ConversationHeader.tsx +++ b/ts/components/conversation/header/ConversationHeader.tsx @@ -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} > + { @@ -57,3 +72,71 @@ export const ConversationHeaderWithDetails = () => { ); }; + +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) => { + 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 ( + + { + showRecreateGroupModal(name || 'Unknown group name', members); + }} + > + {localize('groupRecreate').toString()} + + + ); +} diff --git a/ts/components/conversation/header/ConversationHeaderSubtitle.tsx b/ts/components/conversation/header/ConversationHeaderSubtitle.tsx index 742ce17d1..e8845e3e9 100644 --- a/ts/components/conversation/header/ConversationHeaderSubtitle.tsx +++ b/ts/components/conversation/header/ConversationHeaderSubtitle.tsx @@ -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 { diff --git a/ts/components/leftpane/overlay/OverlayClosedGroup.tsx b/ts/components/leftpane/overlay/OverlayClosedGroup.tsx index 7ee5e8bbf..b507166d2 100644 --- a/ts/components/leftpane/overlay/OverlayClosedGroup.tsx +++ b/ts/components/leftpane/overlay/OverlayClosedGroup.tsx @@ -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(); - const { - uniqueValues: selectedMemberIds, - addTo: addToSelected, - removeFrom: removeFromSelected, - } = useSet([]); 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 = () => { }} /> + + Deprecated Legacy groups?{' '} + { + window.sessionFeatureFlags.forceLegacyGroupsDeprecated = + !window.sessionFeatureFlags.forceLegacyGroupsDeprecated; + forceRefresh(); + }} + /> + )} + @@ -238,18 +264,24 @@ export const OverlayClosedGroupV2 = () => { ) : ( - contactsToRender.map((memberPubkey: string) => ( - - )) + contactsToRender.map((memberPubkey: string) => { + if (!PubKey.is05Pubkey(memberPubkey)) { + throw new Error('Invalid member rendered in member list'); + } + + return ( + + ); + }) )} diff --git a/ts/components/leftpane/overlay/choose-action/OverlayChooseAction.tsx b/ts/components/leftpane/overlay/choose-action/OverlayChooseAction.tsx index 00984c8fe..6b6badd30 100644 --- a/ts/components/leftpane/overlay/choose-action/OverlayChooseAction.tsx +++ b/ts/components/leftpane/overlay/choose-action/OverlayChooseAction.tsx @@ -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(() => { diff --git a/ts/state/ducks/metaGroups.ts b/ts/state/ducks/metaGroups.ts index 17767c083..135254ad3 100644 --- a/ts/state/ducks/metaGroups.ts +++ b/ts/state/ducks/metaGroups.ts @@ -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; members: Record>; - creationFromUIPending: boolean; memberChangesFromUIPending: boolean; nameChangesFromUIPending: boolean; membersInviteSending: Record>; membersPromoteSending: Record>; + + // those are group creation-related fields + creationFromUIPending: boolean; + creationMembersSelected: Array; + 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 }> + ) { + 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) => { diff --git a/ts/state/ducks/releasedFeatures.tsx b/ts/state/ducks/releasedFeatures.tsx index e66ec1bc0..346ec31cb 100644 --- a/ts/state/ducks/releasedFeatures.tsx +++ b/ts/state/ducks/releasedFeatures.tsx @@ -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; diff --git a/ts/state/selectors/releasedFeatures.ts b/ts/state/selectors/releasedFeatures.ts index d21b3c7d7..0f6865d63 100644 --- a/ts/state/selectors/releasedFeatures.ts +++ b/ts/state/selectors/releasedFeatures.ts @@ -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; diff --git a/ts/state/selectors/selectedConversation.ts b/ts/state/selectors/selectedConversation.ts index 8e794fc62..f8f0b1749 100644 --- a/ts/state/selectors/selectedConversation.ts +++ b/ts/state/selectors/selectedConversation.ts @@ -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() { diff --git a/ts/util/releaseFeature.ts b/ts/util/releaseFeature.ts index 7f24e48c9..76be91603 100644 --- a/ts/util/releaseFeature.ts +++ b/ts/util/releaseFeature.ts @@ -109,7 +109,6 @@ function isDisappearMessageV2FeatureReleasedCached(): boolean { return !!isDisappearingMessageFeatureReleased; } - export const ReleasedFeatures = { checkIsUserConfigFeatureReleased, checkIsDisappearMessageV2FeatureReleased, From 821a304fa331c21395de30b600461196ffc3448c Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Tue, 4 Feb 2025 16:37:37 +1100 Subject: [PATCH 3/4] chore: add strings for recreating group, but will be overriden --- _locales/en/messages.json | 8 +++++++- ts/components/conversation/SessionConversation.tsx | 6 ++++-- ts/components/conversation/header/ConversationHeader.tsx | 6 +++--- 3 files changed, 14 insertions(+), 6 deletions(-) diff --git a/_locales/en/messages.json b/_locales/en/messages.json index fcc19bfd7..6d128540a 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -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": "{name} joined the group.", "legacyGroupMemberNewMultiple": "{name} and {count} others joined the group.", "legacyGroupMemberNewYouMultiple": "You and {count} others 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.

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" } - diff --git a/ts/components/conversation/SessionConversation.tsx b/ts/components/conversation/SessionConversation.tsx index eecbcd9d2..5063e703f 100644 --- a/ts/components/conversation/SessionConversation.tsx +++ b/ts/components/conversation/SessionConversation.tsx @@ -678,9 +678,11 @@ function OutdatedLegacyGroupBanner() { // FIXME change the date here. Remove after QA const text = deprecatedLegacyGroups ? localize( - weAreAdmin ? 'groupLegacyBannerAdminDeprecated' : 'groupLegacyBannerMemberDeprecated' + weAreAdmin ? 'legacyGroupAfterDeprecationAdmin' : 'legacyGroupAfterDeprecationMember' ).toString() - : localize(weAreAdmin ? 'groupLegacyBannerAdmin' : 'groupLegacyBannerMember') + : localize( + weAreAdmin ? 'legacyGroupBeforeDeprecationAdmin' : 'legacyGroupBeforeDeprecationMember' + ) .withArgs({ date: '[Date]' }) .toString(); diff --git a/ts/components/conversation/header/ConversationHeader.tsx b/ts/components/conversation/header/ConversationHeader.tsx index ec90a9a61..d8f5a3eae 100644 --- a/ts/components/conversation/header/ConversationHeader.tsx +++ b/ts/components/conversation/header/ConversationHeader.tsx @@ -91,8 +91,8 @@ function useShowRecreateModal() { (name: string, members: Array) => { dispatch( updateConfirmModal({ - title: localize('groupRecreate').toString(), - i18nMessage: { token: 'groupRecreateDescription' }, + title: localize('recreateGroup').toString(), + i18nMessage: { token: 'legacyGroupChatHistory' }, okText: localize('theContinue').toString(), cancelText: localize('cancel').toString(), okTheme: SessionButtonColor.Danger, @@ -135,7 +135,7 @@ function RecreateGroupButton() { showRecreateGroupModal(name || 'Unknown group name', members); }} > - {localize('groupRecreate').toString()} + {localize('recreateGroup').toString()} ); From 3f1adff28891f772dd14d73ed89e4ad499f2a37b Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Wed, 5 Feb 2025 13:28:58 +1100 Subject: [PATCH 4/4] chore: address PR reviews --- ts/components/conversation/header/ConversationHeader.tsx | 2 +- .../message/message-content/MessageContentWithStatus.tsx | 5 +++++ ts/components/leftpane/overlay/OverlayClosedGroup.tsx | 8 +++++--- ts/state/ducks/metaGroups.ts | 3 +-- 4 files changed, 12 insertions(+), 6 deletions(-) diff --git a/ts/components/conversation/header/ConversationHeader.tsx b/ts/components/conversation/header/ConversationHeader.tsx index d8f5a3eae..fe4433d39 100644 --- a/ts/components/conversation/header/ConversationHeader.tsx +++ b/ts/components/conversation/header/ConversationHeader.tsx @@ -132,7 +132,7 @@ function RecreateGroupButton() { buttonColor={SessionButtonColor.Primary} margin="var(--margins-sm)" onClick={() => { - showRecreateGroupModal(name || 'Unknown group name', members); + showRecreateGroupModal(name || localize('groupUnknown').toString(), members); }} > {localize('recreateGroup').toString()} diff --git a/ts/components/conversation/message/message-content/MessageContentWithStatus.tsx b/ts/components/conversation/message/message-content/MessageContentWithStatus.tsx index 482b85dd1..39c82fd5d 100644 --- a/ts/components/conversation/message/message-content/MessageContentWithStatus.tsx +++ b/ts/components/conversation/message/message-content/MessageContentWithStatus.tsx @@ -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) => { @@ -75,6 +77,9 @@ export const MessageContentWithStatuses = (props: Props) => { ); const onDoubleClickReplyToMessage = (e: MouseEvent) => { + if (legacyGroupActionsDisabled) { + return; + } const currentSelection = window.getSelection(); const currentSelectionString = currentSelection?.toString() || undefined; diff --git a/ts/components/leftpane/overlay/OverlayClosedGroup.tsx b/ts/components/leftpane/overlay/OverlayClosedGroup.tsx index b507166d2..f0f8081ce 100644 --- a/ts/components/leftpane/overlay/OverlayClosedGroup.tsx +++ b/ts/components/leftpane/overlay/OverlayClosedGroup.tsx @@ -144,6 +144,10 @@ export const OverlayClosedGroupV2 = () => { dispatch(resetLeftOverlayMode()); } + function onValueChanged(value: string) { + dispatch(groupInfoActions.updateGroupCreationName({ name: value })); + } + async function onEnterPressed() { setGroupNameError(undefined); if (isCreatingGroup) { @@ -209,9 +213,7 @@ export const OverlayClosedGroupV2 = () => { type="text" placeholder={window.i18n('groupNameEnter')} value={groupName} - onValueChanged={value => { - dispatch(groupInfoActions.updateGroupCreationName({ name: value })); - }} + onValueChanged={onValueChanged} onEnterPressed={onEnterPressed} error={groupNameError} maxLength={LIBSESSION_CONSTANTS.BASE_GROUP_MAX_NAME_LENGTH} diff --git a/ts/state/ducks/metaGroups.ts b/ts/state/ducks/metaGroups.ts index 135254ad3..95151b9ee 100644 --- a/ts/state/ducks/metaGroups.ts +++ b/ts/state/ducks/metaGroups.ts @@ -1273,8 +1273,7 @@ const metaGroupSlice = createSlice({ if (state.creationMembersSelected.includes(payload.memberToAdd)) { return state; } - const together = state.creationMembersSelected.concat(payload.memberToAdd); - state.creationMembersSelected = uniq(together); + state.creationMembersSelected.push(payload.memberToAdd); return state; },