diff --git a/ts/components/conversation/message/message-content/MessageContextMenu.tsx b/ts/components/conversation/message/message-content/MessageContextMenu.tsx index 97d966a81a..d647fc1d97 100644 --- a/ts/components/conversation/message/message-content/MessageContextMenu.tsx +++ b/ts/components/conversation/message/message-content/MessageContextMenu.tsx @@ -31,11 +31,14 @@ import { WithMessageId } from '../../../../session/types/with'; import { DeleteItem } from '../../../menu/items/DeleteMessage/DeleteMessageMenuItem'; import { RetryItem } from '../../../menu/items/RetrySend/RetrySendMenuItem'; import { useBanUserCb } from '../../../menuAndSettingsHooks/useBanUser'; -import { useUnbanUserCb } from '../../../menuAndSettingsHooks/useUnbanUser'; import { tr } from '../../../../localization/localeTools'; import { sectionActions } from '../../../../state/ducks/section'; import { useRemoveSenderFromCommunityAdmin } from '../../../menuAndSettingsHooks/useRemoveSenderFromCommunityAdmin'; import { useAddSenderAsCommunityAdmin } from '../../../menuAndSettingsHooks/useAddSenderAsCommunityAdmin'; +import { + useAddUserPermissions, + useClearUserPermissions, +} from '../../../menuAndSettingsHooks/useAddUserPermissions'; import { showContextMenu } from '../../../../util/contextMenu'; import { clampNumber } from '../../../../util/maths'; import { PopoverTriggerPosition } from '../../../SessionTooltip'; @@ -128,8 +131,27 @@ const CommunityAdminActionItems = ({ messageId }: WithMessageId) => { const sender = useMessageSender(messageId); const isSenderAdmin = useMessageSenderIsAdmin(messageId); - const banUserCb = useBanUserCb(convoId, sender); - const unbanUserCb = useUnbanUserCb(convoId, sender); + const sharedBanUnbanProps = { + conversationId: convoId, + pubkey: sender, + }; + + const banUserCb = useBanUserCb({ + banType: 'ban', + ...sharedBanUnbanProps, + }); + const unbanUserCb = useBanUserCb({ + banType: 'unban', + ...sharedBanUnbanProps, + }); + const serverBanUser = useBanUserCb({ + banType: 'server-ban', + ...sharedBanUnbanProps, + }); + const serverUnbanUser = useBanUserCb({ + banType: 'server-unban', + ...sharedBanUnbanProps, + }); const removeSenderFromCommunityAdminCb = useRemoveSenderFromCommunityAdmin({ conversationId: convoId, @@ -141,6 +163,14 @@ const CommunityAdminActionItems = ({ messageId }: WithMessageId) => { senderId: sender, }); + const addUploadPermissionCb = useAddUserPermissions(sender, convoId, ['upload']); + const clearUploadPermissionCb = useClearUserPermissions(sender, convoId, ['upload']); + + // Fixed to `true` as not currently exposed by the backend/tracked in session-desktop + const isRoomUploadRestricted = true; + const canSenderUpload = true; + const canSenderNotUpload = true; + // Note: add/removeSenderFromCommunityAdminCb can be null if we are a moderator only, see below if (!convoId || !sender || !banUserCb || !unbanUserCb) { return null; @@ -182,6 +212,29 @@ const CommunityAdminActionItems = ({ messageId }: WithMessageId) => { {tr('adminPromoteToAdmin')} ) : null} + + {serverBanUser ? ( + {tr('serverBanUserDev')} + ) : null} + {serverUnbanUser ? ( + + {tr('serverUnbanUserDev')} + + ) : null} + {!isSenderAdmin && isRoomUploadRestricted && ( + <> + {canSenderUpload && addUploadPermissionCb ? ( + + {tr('addUploadPermissionDev')} + + ) : null} + {canSenderNotUpload && clearUploadPermissionCb ? ( + + {tr('clearUploadPermissionDev')} + + ) : null} + + )} ); }; diff --git a/ts/components/conversation/right-panel/overlay/message-info/components/MessageInfo.tsx b/ts/components/conversation/right-panel/overlay/message-info/components/MessageInfo.tsx index 79e45ef65a..aa26e39f0b 100644 --- a/ts/components/conversation/right-panel/overlay/message-info/components/MessageInfo.tsx +++ b/ts/components/conversation/right-panel/overlay/message-info/components/MessageInfo.tsx @@ -103,31 +103,32 @@ const DebugMessageInfo = ({ messageId }: { messageId: string }) => { if (!isDevProd()) { return null; } - // Note: the strings here are hardcoded because we do not share them with other platforms through crowdin return ( <> - {convoId ? : null} - {messageHash ? : null} - {serverId ? : null} - {timestamp ? : null} + {convoId ? : null} + {messageHash ? : null} + {serverId ? : null} + {timestamp ? : null} {serverTimestamp ? ( - + + ) : null} + {expirationType ? ( + ) : null} - {expirationType ? : null} {expirationDurationMs ? ( ) : null} {expirationTimestamp ? ( ) : null} {message ? ( - + ) : null} ); diff --git a/ts/components/dialog/BanOrUnbanUserDialog.tsx b/ts/components/dialog/BanOrUnbanUserDialog.tsx index a1276e362c..deb66bfc60 100644 --- a/ts/components/dialog/BanOrUnbanUserDialog.tsx +++ b/ts/components/dialog/BanOrUnbanUserDialog.tsx @@ -12,18 +12,27 @@ import { import { ConvoHub } from '../../session/conversations/ConversationController'; import { PubKey } from '../../session/types'; import { ToastUtils } from '../../session/utils'; -import { BanType, updateBanOrUnbanUserModal } from '../../state/ducks/modalDialog'; +import { + BanType, + isBan, + isServerBanUnban, + updateBanOrUnbanUserModal, +} from '../../state/ducks/modalDialog'; import { SessionButton, SessionButtonColor, SessionButtonType } from '../basic/SessionButton'; import { SessionSpinner } from '../loading'; import { ModalBasicHeader, ModalActionsContainer, SessionWrapperModal, + WrapperModalWidth, } from '../SessionWrapperModal'; import { tr } from '../../localization/localeTools'; import { SimpleSessionInput } from '../inputs/SessionInput'; import { ModalDescription } from './shared/ModalDescriptionContainer'; import { ModalFlexContainer } from './shared/ModalFlexContainer'; +import { SessionToggle } from '../basic/SessionToggle'; +import { getFeatureFlag } from '../../state/ducks/types/releasedFeaturesReduxTypes'; +import { Flex } from '../basic/Flex'; async function banOrUnBanUserCall( convo: ConversationModel, @@ -41,21 +50,47 @@ async function banOrUnBanUserCall( try { // this is a v2 opengroup const roomInfos = convo.toOpenGroupV2(); - const isChangeApplied = - banType === 'ban' - ? await sogsV3BanUser(pubkey, roomInfos, deleteAll) - : await sogsV3UnbanUser(pubkey, roomInfos); + const isChangeApplied = isBan(banType) + ? await sogsV3BanUser({ + userToBan: pubkey, + roomInfos, + deleteAllMessages: deleteAll, + banType, + }) + : await sogsV3UnbanUser({ + userToUnban: pubkey, + roomInfos, + banType, + }); if (!isChangeApplied) { window?.log?.warn(`failed to ${banType} user: ${isChangeApplied}`); - // eslint-disable-next-line no-unused-expressions - banType === 'ban' ? ToastUtils.pushUserBanFailure() : ToastUtils.pushUserUnbanSuccess(); + switch (banType) { + case 'ban': + ToastUtils.pushUserBanFailure(); + break; + case 'unban': + ToastUtils.pushUserUnbanFailure(); + break; + case 'server-ban': + ToastUtils.pushGlobalUserBanFailure(); + break; + case 'server-unban': + ToastUtils.pushGlobalUserUnbanFailure(); + break; + default: + throw new Error('Unknown banType'); + } return false; } window?.log?.info(`${pubkey.key} user ${banType}ned successfully...`); - // eslint-disable-next-line no-unused-expressions - banType === 'ban' ? ToastUtils.pushUserBanSuccess() : ToastUtils.pushUserUnbanSuccess(); + + if (isBan(banType)) { + ToastUtils.pushUserBanSuccess(); + } else { + ToastUtils.pushUserUnbanSuccess(); + } return true; } catch (e) { window?.log?.error(`Got error while ${banType}ning user:`, e); @@ -70,7 +105,6 @@ export const BanOrUnBanUserDialog = (props: { pubkey?: string; }) => { const { conversationId, banType, pubkey } = props; - const isBan = banType === 'ban'; const dispatch = getAppDispatch(); const convo = ConvoHub.use().get(conversationId); const inputRef = useRef(null); @@ -78,24 +112,29 @@ export const BanOrUnBanUserDialog = (props: { useFocusMount(inputRef, true); const [inputBoxValue, setInputBoxValue] = useState(''); const [inProgress, setInProgress] = useState(false); + const [localBanType, setLocalBanType] = useState(banType); - const displayName = useConversationUsernameWithFallback(true, pubkey); + const isServerWide = isServerBanUnban(localBanType); + const isBanAction = isBan(localBanType); + const displayName = useConversationUsernameWithFallback(true, pubkey); const hasPubkeyOnLoad = pubkey?.length; const inputTextToDisplay = !!pubkey && displayName ? `${displayName} ${PubKey.shorten(pubkey)}` : undefined; /** - * Ban or Unban a user from an open group - * @param deleteAll Delete all messages for that user in the group (only works with ban) + * Ban or Unban a user from a community + * @param deleteAll Delete all messages for that user in the community (only works with ban) */ const banOrUnBanUser = async (deleteAll: boolean) => { const castedPubkey = pubkey?.length ? pubkey : inputBoxValue; - window?.log?.info(`asked to ${banType} user: ${castedPubkey}, banAndDeleteAll:${deleteAll}`); + window?.log?.info( + `asked to ${localBanType} user: ${castedPubkey}, banAndDeleteAll:${deleteAll}` + ); setInProgress(true); - const isBanned = await banOrUnBanUserCall(convo, castedPubkey, banType, deleteAll); + const isBanned = await banOrUnBanUserCall(convo, castedPubkey, localBanType, deleteAll); if (isBanned) { // clear input box setInputBoxValue(''); @@ -107,7 +146,9 @@ export const BanOrUnBanUserDialog = (props: { setInProgress(false); }; - const title = isBan ? tr('banUser') : tr('banUnbanUser'); + const serverHost = new window.URL(convo.toOpenGroupV2().serverUrl).host; + const serverWideSuffix = isServerWide ? ` @ ${serverHost}` : ''; + const title = `${isBanAction ? tr('banUser') : tr('banUnbanUser')}${serverWideSuffix}`; /** * Starts procedure for banning/unbanning user and all their messages using dialog @@ -119,25 +160,41 @@ export const BanOrUnBanUserDialog = (props: { const onClose = () => { dispatch(updateBanOrUnbanUserModal(null)); }; - useKey('Escape', onClose); - const buttonText = isBan ? tr('banUser') : tr('banUnbanUser'); + let buttonText = ''; + + if (isServerWide) { + if (isBanAction) { + buttonText = tr('serverBanUserDev'); + } else { + buttonText = tr('serverUnbanUserDev'); + } + } else if (isBanAction) { + buttonText = tr('banUser'); + } else { + buttonText = tr('banUnbanUser'); + } return ( } onClose={onClose} buttonChildren={ - + {/* * Note: we can only ban-and-delete-all when we have a pubkey on load currently. @@ -146,12 +203,12 @@ export const BanOrUnBanUserDialog = (props: { * When hasPubkeyOnLoad is true, the dialog was shown from a right click on the messages list, * so we do have the blindedId already in this case. */} - {isBan && hasPubkeyOnLoad ? ( + {isBanAction && hasPubkeyOnLoad ? ( @@ -169,7 +226,7 @@ export const BanOrUnBanUserDialog = (props: { {}} - inputDataTestId={isBan ? 'ban-user-input' : 'unban-user-input'} + inputDataTestId={isBanAction ? 'ban-user-input' : 'unban-user-input'} allowEscapeKeyPassthrough={true} /> + {getFeatureFlag('useDevCommunityActions') ? ( + +

Server-wide:

+ { + const withoutServer = localBanType.replace('server-', '') as BanType; + const withServer = `server-${withoutServer}` as BanType; + setLocalBanType(isServerWide ? withoutServer : withServer); + }} + /> +
+ ) : null}
diff --git a/ts/components/dialog/ModalContainer.tsx b/ts/components/dialog/ModalContainer.tsx index fecb438441..44a7ebb103 100644 --- a/ts/components/dialog/ModalContainer.tsx +++ b/ts/components/dialog/ModalContainer.tsx @@ -1,4 +1,5 @@ import { + useUpdateCommunityPermissionsModalState, useConfirmModal, useInviteContactModal, useAddModeratorsModal, @@ -43,6 +44,7 @@ import { UpdateConversationDetailsDialog } from './UpdateConversationDetailsDial import { UserProfileModal } from './UserProfileModal'; import { OpenUrlModal } from './OpenUrlModal'; import { BlockOrUnblockDialog } from './blockOrUnblock/BlockOrUnblockDialog'; +import { UpdateCommunityPermissionsDialog } from './UpdateCommunityPermissionsDialog'; import { DebugMenuModal } from './debug/DebugMenuModal'; import { ConversationSettingsDialog } from './conversationSettings/conversationSettingsDialog'; import { SessionConfirm } from './SessionConfirm'; @@ -57,6 +59,7 @@ export const ModalContainer = () => { const addModeratorsModalState = useAddModeratorsModal(); const removeModeratorsModalState = useRemoveModeratorsModal(); const updateGroupMembersModalState = useUpdateGroupMembersModal(); + const updateCommunityPermissionsModalState = useUpdateCommunityPermissionsModalState(); const updateConversationDetailsModalState = useUpdateConversationDetailsModal(); const userProfileModalState = useUserProfileModal(); const changeNicknameModal = useChangeNickNameDialog(); @@ -100,6 +103,9 @@ export const ModalContainer = () => { {updateGroupMembersModalState && ( )} + {updateCommunityPermissionsModalState && ( + + )} {updateConversationDetailsModalState && ( )} diff --git a/ts/components/dialog/UpdateCommunityPermissionsDialog.tsx b/ts/components/dialog/UpdateCommunityPermissionsDialog.tsx new file mode 100644 index 0000000000..512ad629ac --- /dev/null +++ b/ts/components/dialog/UpdateCommunityPermissionsDialog.tsx @@ -0,0 +1,155 @@ +/* eslint-disable @typescript-eslint/no-misused-promises */ + +import { useState } from 'react'; +import { + ModalActionsContainer, + ModalBasicHeader, + ModalBottomButtonWithBorder, + SessionWrapperModal, +} from '../SessionWrapperModal'; +import { SessionButtonColor, SessionButtonType } from '../basic/SessionButton'; +import { OpenGroupData } from '../../data/opengroups'; +import { sogsV3SetRoomPermissions } from '../../session/apis/open_group_api/sogsv3/sogsV3RoomPermissions'; +import { ConvoHub } from '../../session/conversations'; +import { tr } from '../../localization/localeTools'; +import { PanelToggleButton } from '../buttons/panel/PanelToggleButton'; +import { PanelButtonGroup, PanelButtonTextWithSubText } from '../buttons/panel/PanelButton'; +import { ModalDescription } from './shared/ModalDescriptionContainer'; +import { ModalFlexContainer } from './shared/ModalFlexContainer'; +import { getAppDispatch } from '../../state/dispatch'; +import { updateCommunityPermissionsModal } from '../../state/ducks/modalDialog'; +import type { WithConvoId } from '../../session/types/with'; + +export function UpdateCommunityPermissionsDialog(props: WithConvoId) { + const [defaultRead, setDefaultRead] = useState(true); + const [defaultWrite, setDefaultWrite] = useState(true); + const [defaultAccessible, setDefaultAccessible] = useState(true); + const [defaultUpload, setDefaultUpload] = useState(true); + const dispatch = getAppDispatch(); + + const convo = ConvoHub.use().get(props.conversationId); + + function onClickOK() { + if (convo.isOpenGroupV2()) { + const roomInfos = OpenGroupData.getV2OpenGroupRoom(convo.id); + if (!roomInfos) { + return; + } + void sogsV3SetRoomPermissions(roomInfos, { + default_accessible: defaultAccessible, + default_read: defaultRead, + default_upload: defaultUpload, + default_write: defaultWrite, + }); + } + + closeDialog(); + } + + function closeDialog() { + dispatch(updateCommunityPermissionsModal(null)); + } + + return ( + } + onClose={closeDialog} + buttonChildren={ + + + + + } + > + + + + + + } + active={defaultAccessible} + onClick={async () => setDefaultAccessible(!defaultAccessible)} + rowDataTestId="invalid-data-testid" + toggleDataTestId="invalid-data-testid" + /> + + } + active={defaultRead} + onClick={async () => setDefaultRead(!defaultRead)} + rowDataTestId="invalid-data-testid" + toggleDataTestId="invalid-data-testid" + /> + + } + active={defaultWrite} + onClick={async () => setDefaultWrite(!defaultWrite)} + rowDataTestId="invalid-data-testid" + toggleDataTestId="invalid-data-testid" + /> + + } + active={defaultUpload} + onClick={async () => setDefaultUpload(!defaultUpload)} + rowDataTestId="invalid-data-testid" + toggleDataTestId="invalid-data-testid" + /> + + + + ); +} + +// private onKeyUp(event: any) { +// switch (event.key) { +// case 'Enter': +// this.onClickOK(); +// break; +// case 'Esc': +// case 'Escape': +// this.closeDialog(); +// break; +// default: +// } +// } diff --git a/ts/components/dialog/conversationSettings/conversationSettingsItems.tsx b/ts/components/dialog/conversationSettings/conversationSettingsItems.tsx index 1f610aad3d..12bc6af21b 100644 --- a/ts/components/dialog/conversationSettings/conversationSettingsItems.tsx +++ b/ts/components/dialog/conversationSettings/conversationSettingsItems.tsx @@ -28,7 +28,6 @@ import { useShowInviteContactToGroupCb } from '../../menuAndSettingsHooks/useSho import { useShowCopyAccountIdCb } from '../../menuAndSettingsHooks/useCopyAccountId'; import { useShowCopyCommunityUrlCb } from '../../menuAndSettingsHooks/useCopyCommunityUrl'; import { useBanUserCb } from '../../menuAndSettingsHooks/useBanUser'; -import { useUnbanUserCb } from '../../menuAndSettingsHooks/useUnbanUser'; import { useAddModeratorsCb } from '../../menuAndSettingsHooks/useAddModerators'; import { useRemoveModeratorsCb } from '../../menuAndSettingsHooks/useRemoveModerators'; import { useShowLeaveCommunityCb } from '../../menuAndSettingsHooks/useShowLeaveCommunity'; @@ -40,6 +39,7 @@ import { useShowAttachments } from '../../menuAndSettingsHooks/useShowAttachment import { useGroupCommonNoShow } from '../../menuAndSettingsHooks/useGroupCommonNoShow'; import { useShowConversationSettingsFor } from '../../menuAndSettingsHooks/useShowConversationSettingsFor'; import { useShowNoteToSelfCb } from '../../menuAndSettingsHooks/useShowNoteToSelf'; +import { useChangeCommunityPermissionsCb } from '../../menuAndSettingsHooks/useChangeCommunityPermissions'; import { useTogglePinConversationHandler } from '../../menuAndSettingsHooks/UseTogglePinConversationHandler'; import { PLURAL_COUNT_OTHER } from '../../../localization/localeTools'; @@ -294,6 +294,22 @@ export function UpdateDisappearingMessagesButton({ ); } +export function ChangeCommunityPermissionsButton({ conversationId }: WithConvoId) { + const cb = useChangeCommunityPermissionsCb(conversationId); + + if (!cb) { + return null; + } + return ( + } + text={{ token: 'communityChangePermissionsDev' }} + onClick={cb} + dataTestId="edit-community-permissions" + /> + ); +} + export function AddAdminCommunityButton({ conversationId }: WithConvoId) { const cb = useAddModeratorsCb(conversationId); @@ -336,7 +352,7 @@ export function RemoveAdminCommunityButton({ conversationId }: WithConvoId) { } export function BanFromCommunityButton({ conversationId }: WithConvoId) { - const showBanUserCb = useBanUserCb(conversationId); + const showBanUserCb = useBanUserCb({ banType: 'ban', conversationId }); if (!showBanUserCb) { return null; @@ -352,7 +368,7 @@ export function BanFromCommunityButton({ conversationId }: WithConvoId) { } export function UnbanFromCommunityButton({ conversationId }: WithConvoId) { - const showUnbanUserCb = useUnbanUserCb(conversationId); + const showUnbanUserCb = useBanUserCb({ banType: 'unban', conversationId }); if (!showUnbanUserCb) { return null; diff --git a/ts/components/dialog/conversationSettings/pages/default/defaultPage.tsx b/ts/components/dialog/conversationSettings/pages/default/defaultPage.tsx index 43371f60c1..afe069a005 100644 --- a/ts/components/dialog/conversationSettings/pages/default/defaultPage.tsx +++ b/ts/components/dialog/conversationSettings/pages/default/defaultPage.tsx @@ -37,6 +37,7 @@ import { AddAdminCommunityButton, RemoveAdminCommunityButton, ShowNoteToSelfButton, + ChangeCommunityPermissionsButton, DeleteDestroyedOrKickedGroupButton, } from '../../conversationSettingsItems'; import { useCloseActionFromPage, useTitleFromPage } from '../conversationSettingsHooks'; @@ -85,6 +86,7 @@ function CommunityAdminActions({ conversationId }: WithConvoId) { + diff --git a/ts/components/dialog/debug/constants.ts b/ts/components/dialog/debug/constants.ts index 72712ef199..5ab5657b24 100644 --- a/ts/components/dialog/debug/constants.ts +++ b/ts/components/dialog/debug/constants.ts @@ -12,7 +12,13 @@ type DebugFeatureFlagsType = { export const DEBUG_FEATURE_FLAGS: DebugFeatureFlagsType = { // NOTE Put new feature flags in here during development so they are not available in production environments. Remove from here when they are ready for QA and production - DEV: ['showPopoverAnchors', 'debugInputCommands', 'proAvailable', 'useTestProBackend'], + DEV: [ + 'showPopoverAnchors', + 'debugInputCommands', + 'proAvailable', + 'useTestProBackend', + 'useDevCommunityActions', + ], UNSUPPORTED: ['useTestNet', 'useLocalDevNet', 'fsTTL30s', 'proGroupsAvailable'], UNTESTED: ['disableOnionRequests', 'replaceLocalizedStringsWithKeys'], }; diff --git a/ts/components/menu/ConversationListItemContextMenu.tsx b/ts/components/menu/ConversationListItemContextMenu.tsx index b2fd2115da..7cf2db54ae 100644 --- a/ts/components/menu/ConversationListItemContextMenu.tsx +++ b/ts/components/menu/ConversationListItemContextMenu.tsx @@ -23,6 +23,8 @@ import { ShowNoteToSelfMenuItem, ShowUserProfileMenuItem, UnbanMenuItem, + ServerBanMenuItem, + ServerUnbanMenuItem, } from './Menu'; import { CopyCommunityUrlMenuItem } from './items/CopyCommunityUrl/CopyCommunityUrlMenuItem'; import { CopyAccountIdMenuItem } from './items/CopyAccountId/CopyAccountIdMenuItem'; @@ -107,6 +109,8 @@ const ConversationListItemContextMenu = (props: PropsContextConversationItem) => {/* Communities actions */} + + diff --git a/ts/components/menu/Menu.tsx b/ts/components/menu/Menu.tsx index ef7f14b467..c9c45111e6 100644 --- a/ts/components/menu/Menu.tsx +++ b/ts/components/menu/Menu.tsx @@ -27,7 +27,6 @@ import { useShowDeletePrivateConversationCb } from '../menuAndSettingsHooks/useS import { useShowInviteContactToCommunity } from '../menuAndSettingsHooks/useShowInviteContactToCommunity'; import { useAddModeratorsCb } from '../menuAndSettingsHooks/useAddModerators'; import { useRemoveModeratorsCb } from '../menuAndSettingsHooks/useRemoveModerators'; -import { useUnbanUserCb } from '../menuAndSettingsHooks/useUnbanUser'; import { useBanUserCb } from '../menuAndSettingsHooks/useBanUser'; import { useSetNotificationsFor } from '../menuAndSettingsHooks/useSetNotificationsFor'; import { Localizer } from '../basic/Localizer'; @@ -131,7 +130,10 @@ export const AddModeratorsMenuItem = (): JSX.Element | null => { export const UnbanMenuItem = (): JSX.Element | null => { const convoId = useConvoIdFromContext(); - const showUnbanUserCb = useUnbanUserCb(convoId); + const showUnbanUserCb = useBanUserCb({ + conversationId: convoId, + banType: 'unban', + }); if (!showUnbanUserCb) { return null; @@ -142,7 +144,10 @@ export const UnbanMenuItem = (): JSX.Element | null => { export const BanMenuItem = (): JSX.Element | null => { const convoId = useConvoIdFromContext(); - const showBanUserCb = useBanUserCb(convoId); + const showBanUserCb = useBanUserCb({ + conversationId: convoId, + banType: 'ban', + }); if (!showBanUserCb) { return null; @@ -150,6 +155,40 @@ export const BanMenuItem = (): JSX.Element | null => { return {tr('banUser')}; }; +export const ServerUnbanMenuItem = (): JSX.Element | null => { + const convoId = useConvoIdFromContext(); + const showUnbanUserCb = useBanUserCb({ + conversationId: convoId, + banType: 'server-unban', + }); + + if (showUnbanUserCb) { + return ( + + + + ); + } + return null; +}; + +export const ServerBanMenuItem = (): JSX.Element | null => { + const convoId = useConvoIdFromContext(); + const showUnbanUserCb = useBanUserCb({ + conversationId: convoId, + banType: 'server-ban', + }); + + if (showUnbanUserCb) { + return ( + + + + ); + } + return null; +}; + export const MarkAllReadMenuItem = (): JSX.Element | null => { const convoId = useConvoIdFromContext(); const isIncomingRequest = useIsIncomingRequest(convoId); diff --git a/ts/components/menuAndSettingsHooks/useAddUserPermissions.tsx b/ts/components/menuAndSettingsHooks/useAddUserPermissions.tsx new file mode 100644 index 0000000000..96e21123cc --- /dev/null +++ b/ts/components/menuAndSettingsHooks/useAddUserPermissions.tsx @@ -0,0 +1,69 @@ +import { + sogsV3AddPermissions, + sogsV3ClearPermissions, + type OpenGroupPermissionType, +} from '../../session/apis/open_group_api/sogsv3/sogsV3UserPermissions'; +import { ConvoHub } from '../../session/conversations'; +import { PubKey } from '../../session/types'; +import { ToastUtils } from '../../session/utils'; +import { getFeatureFlag } from '../../state/ducks/types/releasedFeaturesReduxTypes'; + +export function useAddUserPermissions( + sender: string | undefined, + convoId: string | undefined, + permissions: Array +) { + if (!sender || !convoId || !permissions.length || !getFeatureFlag('useDevCommunityActions')) { + return null; + } + return async () => { + try { + const user = PubKey.cast(sender); + const convo = ConvoHub.use().getOrThrow(convoId); + + const roomInfo = convo.toOpenGroupV2(); + const res = await sogsV3AddPermissions([user], roomInfo, permissions); + if (!res) { + window?.log?.warn('failed to add user permissions:', res); + + ToastUtils.pushFailedToChangeUserPermissions(); + } else { + window?.log?.info(`${user.key} given permissions ${permissions.join(', ')}...`); + ToastUtils.pushUserPermissionsChanged(); + } + } catch (e) { + window?.log?.error('Got error while adding user permissions:', e); + ToastUtils.pushFailedToChangeUserPermissions(); + } + }; +} + +export function useClearUserPermissions( + sender: string | undefined, + convoId: string | undefined, + permissions: Array +) { + if (!sender || !convoId || !permissions.length || !getFeatureFlag('useDevCommunityActions')) { + return null; + } + return async () => { + try { + const user = PubKey.cast(sender); + const convo = ConvoHub.use().getOrThrow(convoId); + + const roomInfo = convo.toOpenGroupV2(); + const res = await sogsV3ClearPermissions([user], roomInfo, permissions); + if (!res) { + window?.log?.warn('failed to clear user permissions:', res); + + ToastUtils.pushFailedToChangeUserPermissions(); + } else { + window?.log?.info(`${user.key} given permissions ${permissions.join(', ')}...`); + ToastUtils.pushUserPermissionsChanged(); + } + } catch (e) { + window?.log?.error('Got error while clearing user permissions:', e); + ToastUtils.pushFailedToChangeUserPermissions(); + } + }; +} diff --git a/ts/components/menuAndSettingsHooks/useBanUser.ts b/ts/components/menuAndSettingsHooks/useBanUser.ts index a1c98a9c75..d6a36e6706 100644 --- a/ts/components/menuAndSettingsHooks/useBanUser.ts +++ b/ts/components/menuAndSettingsHooks/useBanUser.ts @@ -1,18 +1,37 @@ import { getAppDispatch } from '../../state/dispatch'; import { useIsPublic } from '../../hooks/useParamSelector'; -import { updateBanOrUnbanUserModal } from '../../state/ducks/modalDialog'; +import { + isServerBanUnban, + updateBanOrUnbanUserModal, + type BanType, +} from '../../state/ducks/modalDialog'; import { useWeAreCommunityAdminOrModerator } from '../../state/selectors/conversations'; +import { getFeatureFlag } from '../../state/ducks/types/releasedFeaturesReduxTypes'; -export function useBanUserCb(conversationId?: string, pubkey?: string) { +export function useBanUserCb({ + conversationId, + banType, + pubkey, +}: { + banType: BanType; + conversationId?: string; + pubkey?: string; +}) { const dispatch = getAppDispatch(); const isPublic = useIsPublic(conversationId); - const weAreCommunityAdminOrModerator = useWeAreCommunityAdminOrModerator(conversationId); + const weAreAdminOrMod = useWeAreCommunityAdminOrModerator(conversationId); + const hasDevCommunityActions = getFeatureFlag('useDevCommunityActions'); - if (!isPublic || !weAreCommunityAdminOrModerator || !conversationId) { + if ( + !isPublic || + !weAreAdminOrMod || + !conversationId || + (isServerBanUnban(banType) && !hasDevCommunityActions) + ) { return null; } return () => { - dispatch(updateBanOrUnbanUserModal({ banType: 'ban', conversationId, pubkey })); + dispatch(updateBanOrUnbanUserModal({ banType, conversationId, pubkey })); }; } diff --git a/ts/components/menuAndSettingsHooks/useChangeCommunityPermissions.ts b/ts/components/menuAndSettingsHooks/useChangeCommunityPermissions.ts new file mode 100644 index 0000000000..1d45fcce6c --- /dev/null +++ b/ts/components/menuAndSettingsHooks/useChangeCommunityPermissions.ts @@ -0,0 +1,19 @@ +import { useDispatch } from 'react-redux'; +import { useIsPublic, useWeAreAdmin } from '../../hooks/useParamSelector'; +import { updateCommunityPermissionsModal } from '../../state/ducks/modalDialog'; +import { getFeatureFlag } from '../../state/ducks/types/releasedFeaturesReduxTypes'; + +export function useChangeCommunityPermissionsCb(conversationId?: string) { + const dispatch = useDispatch(); + const isPublic = useIsPublic(conversationId); + const weAreAdmin = useWeAreAdmin(conversationId); + const hasDevCommunityActions = getFeatureFlag('useDevCommunityActions'); + + if (!isPublic || !weAreAdmin || !conversationId || !hasDevCommunityActions) { + return null; + } + + return () => { + dispatch(updateCommunityPermissionsModal({ conversationId })); + }; +} diff --git a/ts/components/menuAndSettingsHooks/useCopyAccountId.ts b/ts/components/menuAndSettingsHooks/useCopyAccountId.ts index 2ceaa00ceb..2e59b0d748 100644 --- a/ts/components/menuAndSettingsHooks/useCopyAccountId.ts +++ b/ts/components/menuAndSettingsHooks/useCopyAccountId.ts @@ -1,11 +1,17 @@ import { useIsPrivate } from '../../hooks/useParamSelector'; import { PubKey } from '../../session/types'; import { ToastUtils } from '../../session/utils'; +import { getFeatureFlag } from '../../state/ducks/types/releasedFeaturesReduxTypes'; function useShowCopyAccountId(conversationId?: string) { const isPrivate = useIsPrivate(conversationId); - return conversationId && isPrivate && !PubKey.isBlinded(conversationId); + return ( + conversationId && + isPrivate && + (!PubKey.isBlinded(conversationId) || + (PubKey.isBlinded(conversationId) && getFeatureFlag('useDevCommunityActions'))) + ); } export function useShowCopyAccountIdCb(conversationId?: string) { diff --git a/ts/components/menuAndSettingsHooks/useUnbanUser.ts b/ts/components/menuAndSettingsHooks/useUnbanUser.ts deleted file mode 100644 index 427d6e6b8d..0000000000 --- a/ts/components/menuAndSettingsHooks/useUnbanUser.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { getAppDispatch } from '../../state/dispatch'; -import { useIsPublic } from '../../hooks/useParamSelector'; -import { updateBanOrUnbanUserModal } from '../../state/ducks/modalDialog'; -import { useWeAreCommunityAdminOrModerator } from '../../state/selectors/conversations'; - -export function useUnbanUserCb(conversationId?: string, pubkey?: string) { - const dispatch = getAppDispatch(); - const isPublic = useIsPublic(conversationId); - const weAreCommunityAdminOrModerator = useWeAreCommunityAdminOrModerator(conversationId); - - if (!isPublic || !weAreCommunityAdminOrModerator || !conversationId) { - return null; - } - - return () => { - dispatch(updateBanOrUnbanUserModal({ banType: 'unban', conversationId, pubkey })); - }; -} diff --git a/ts/localization b/ts/localization index 1b1337602c..f2620de9c8 160000 --- a/ts/localization +++ b/ts/localization @@ -1 +1 @@ -Subproject commit 1b1337602c88edef62e33b201177a7cd2b0fb215 +Subproject commit f2620de9c8dc757ae7a131c55f60cdfd0074f47f diff --git a/ts/models/message.ts b/ts/models/message.ts index bc9d51eb4c..089e866b31 100644 --- a/ts/models/message.ts +++ b/ts/models/message.ts @@ -814,7 +814,6 @@ export class MessageModel extends Model { // we can only send a single preview const firstPreviewWithData = previewWithData?.[0] || null; - // we want to go for the v1, if this is an OpenGroupV1 or not an open group at all if (conversation?.isOpenGroupV2()) { const openGroupV2 = conversation.toOpenGroupV2(); attachmentPromise = uploadAttachmentsV3(finalAttachments, openGroupV2); diff --git a/ts/react.d.ts b/ts/react.d.ts index 1a59ee7ec5..0f84c1c36b 100644 --- a/ts/react.d.ts +++ b/ts/react.d.ts @@ -324,6 +324,7 @@ declare module 'react' { | 'disappearing-messages' | 'group-members' | 'edit-group-name' + | 'edit-community-permissions' // SessionRadioGroup & SessionRadio | 'password-input-confirm' diff --git a/ts/session/apis/open_group_api/sogsv3/sogsApiV3.ts b/ts/session/apis/open_group_api/sogsv3/sogsApiV3.ts index 7e3ace7562..949176349b 100644 --- a/ts/session/apis/open_group_api/sogsv3/sogsApiV3.ts +++ b/ts/session/apis/open_group_api/sogsv3/sogsApiV3.ts @@ -630,8 +630,11 @@ export const handleBatchPollResults = async ( case 'deleteMessage': case 'banUnbanUser': case 'deleteAllPosts': + case 'deleteAllUserPosts': case 'updateRoom': case 'deleteReaction': + case 'updateRoomPerms': + case 'updateRoomUserPerms': // Could save new user permissions here // we do nothing for all of those, but let's make sure if we ever add something batch polled for, we include it's handling here. // the assertUnreachable will fail to compile every time we add a new batch poll endpoint without taking care of it. break; diff --git a/ts/session/apis/open_group_api/sogsv3/sogsV3AddRemoveMods.ts b/ts/session/apis/open_group_api/sogsv3/sogsV3AddRemoveMods.ts index 443e2850f3..1006a16dc6 100644 --- a/ts/session/apis/open_group_api/sogsv3/sogsV3AddRemoveMods.ts +++ b/ts/session/apis/open_group_api/sogsv3/sogsV3AddRemoveMods.ts @@ -2,7 +2,7 @@ import AbortController from 'abort-controller'; import { PubKey } from '../../../types'; -import { batchFirstSubIsSuccess, sogsBatchSend } from './sogsV3BatchPoll'; +import { batchEverySubIsSuccess, batchFirstSubIsSuccess, sogsBatchSend } from './sogsV3BatchPoll'; import { OpenGroupRequestCommonType } from '../../../../data/types'; import { DURATION } from '../../../constants'; @@ -63,7 +63,7 @@ export const sogsV3RemoveAdmins = async ( 'batch', 10 * DURATION.SECONDS ); - const isSuccess = batchSendResponse?.body?.every(m => m?.code === 200) || false; + const isSuccess = batchEverySubIsSuccess(batchSendResponse); if (!isSuccess) { window.log.warn('remove mods failed with body', batchSendResponse?.body); } diff --git a/ts/session/apis/open_group_api/sogsv3/sogsV3BanUnban.ts b/ts/session/apis/open_group_api/sogsv3/sogsV3BanUnban.ts index f80cf7a87a..d81283b279 100644 --- a/ts/session/apis/open_group_api/sogsv3/sogsV3BanUnban.ts +++ b/ts/session/apis/open_group_api/sogsv3/sogsV3BanUnban.ts @@ -3,61 +3,93 @@ import { PubKey } from '../../../types'; import { batchFirstSubIsSuccess, OpenGroupBatchRow, sogsBatchSend } from './sogsV3BatchPoll'; import { OpenGroupRequestCommonType } from '../../../../data/types'; import { DURATION } from '../../../constants'; +import { + isServerBanUnban, + type BanServerWideOrNot, + type UnbanServerWideOrNot, +} from '../../../../state/ducks/modalDialog'; -export const sogsV3BanUser = async ( - userToBan: PubKey, - roomInfos: OpenGroupRequestCommonType, - deleteAllMessages: boolean -): Promise => { +export const sogsV3BanUser = async (args: { + roomInfos: OpenGroupRequestCommonType; + userToBan: PubKey; + deleteAllMessages: boolean; + banType: BanServerWideOrNot; +}): Promise => { const sequence: Array = [ { type: 'banUnbanUser', banUnbanUser: { - sessionId: userToBan.key, - roomId: roomInfos.roomId, - type: 'ban', + sessionId: args.userToBan.key, + type: args.banType, + roomId: args.roomInfos.roomId, }, }, ]; - if (deleteAllMessages) { - sequence.push({ - type: 'deleteAllPosts', - deleteAllPosts: { sessionId: userToBan.key, roomId: roomInfos.roomId }, - }); + if (args.deleteAllMessages) { + sequence.push( + isServerBanUnban(args.banType) + ? { + type: 'deleteAllUserPosts', + deleteAllUserPosts: { sessionId: args.userToBan.key }, + } + : { + type: 'deleteAllPosts', + deleteAllPosts: { sessionId: args.userToBan.key, roomId: args.roomInfos.roomId }, + } + ); } const batchSendResponse = await sogsBatchSend( - roomInfos.serverUrl, - new Set([roomInfos.roomId]), + args.roomInfos.serverUrl, + new Set([args.roomInfos.roomId]), new AbortController().signal, sequence, 'sequence', 10 * DURATION.SECONDS ); - return batchFirstSubIsSuccess(batchSendResponse); + const ret = batchFirstSubIsSuccess(batchSendResponse); + + if (!ret) { + window.log.warn( + `sogsV3BanUser failed with statuses:`, + batchSendResponse?.body?.map(m => m.code) + ); + } + + return ret; }; -export const sogsV3UnbanUser = async ( - userToBan: PubKey, - roomInfos: OpenGroupRequestCommonType -): Promise => { +export const sogsV3UnbanUser = async (args: { + userToUnban: PubKey; + roomInfos: OpenGroupRequestCommonType; + banType: UnbanServerWideOrNot; +}): Promise => { const batchSendResponse = await sogsBatchSend( - roomInfos.serverUrl, - new Set([roomInfos.roomId]), + args.roomInfos.serverUrl, + new Set([args.roomInfos.roomId]), new AbortController().signal, [ { type: 'banUnbanUser', banUnbanUser: { - sessionId: userToBan.key, - roomId: roomInfos.roomId, - type: 'unban', + sessionId: args.userToUnban.key, + type: args.banType, + roomId: args.roomInfos.roomId, }, }, ], 'batch', 10 * DURATION.SECONDS ); - return batchFirstSubIsSuccess(batchSendResponse); + const ret = batchFirstSubIsSuccess(batchSendResponse); + + if (!ret) { + window.log.warn( + `sogsV3UnbanUser failed with statuses:`, + batchSendResponse?.body?.map(m => m.code) + ); + } + + return ret; }; diff --git a/ts/session/apis/open_group_api/sogsv3/sogsV3BatchPoll.ts b/ts/session/apis/open_group_api/sogsv3/sogsV3BatchPoll.ts index 4c3c8592b0..6b5f24b25b 100644 --- a/ts/session/apis/open_group_api/sogsv3/sogsV3BatchPoll.ts +++ b/ts/session/apis/open_group_api/sogsv3/sogsV3BatchPoll.ts @@ -11,6 +11,8 @@ import { OpenGroupRequestHeaders, } from '../opengroupV2/OpenGroupPollingUtils'; import { addJsonContentTypeToHeaders } from './sogsV3SendMessage'; +import { OpenGroupPermissionType } from './sogsV3UserPermissions'; +import { hasDuplicates } from '../../../../shared/array_utils'; import type { WithImageId, WithMessageId, @@ -20,6 +22,13 @@ import type { WithSessionIds, } from './sogsWith'; import { FetchDestination } from '../../../utils/InsecureNodeFetch'; +import { + isBan, + isServerBanUnban, + type BanUnbanRoomWideOnly, + type BanUnbanServerWideOnly, +} from '../../../../state/ducks/modalDialog'; +import { ed25519Str } from '../../../utils/String'; type BatchFetchRequestOptions = { method: 'POST' | 'PUT' | 'GET' | 'DELETE'; @@ -131,6 +140,10 @@ export function batchFirstSubIsSuccess(response?: BatchSogsResponse | null): boo return Boolean(status && isNumber(status) && status >= 200 && status <= 300); } +export function batchEverySubIsSuccess(response?: BatchSogsResponse | null): boolean { + return response?.body?.every(m => isNumber(m.code) && m?.code >= 200 && m?.code <= 300) ?? false; +} + export type SubRequestCapabilitiesType = { type: 'capabilities' }; export type SubRequestMessagesObjectType = @@ -190,12 +203,22 @@ export type SubRequestAddRemoveModeratorType = { export type SubRequestBanUnbanUserType = { type: 'banUnbanUser'; banUnbanUser: { - type: 'ban' | 'unban'; sessionId: string; // can be blinded id or not - roomId: string; - }; + } & ( + | { + roomId: string; + type: BanUnbanRoomWideOnly; + } + | { type: BanUnbanServerWideOnly } + ); }; +export function isServerBanUnbanAction( + action: SubRequestBanUnbanUserType['banUnbanUser'] +): action is Extract { + return isServerBanUnban(action.type); +} + export type SubRequestDeleteAllUserPostsType = { type: 'deleteAllPosts'; deleteAllPosts: { @@ -204,6 +227,13 @@ export type SubRequestDeleteAllUserPostsType = { }; }; +export type SubRequestDeleteAllUserServerPostsType = { + type: 'deleteAllUserPosts'; + deleteAllUserPosts: { + sessionId: string; // can be blinded id or not + }; +}; + export type SubRequestUpdateRoomType = { type: 'updateRoom'; updateRoom: WithRoomId & @@ -215,6 +245,19 @@ export type SubRequestUpdateRoomType = { >; }; +export type SubRequestUpdateRoomPermsType = { + type: 'updateRoomPerms'; + updateRoomPerms: { + roomId: string; + permsToSet: { + default_read?: boolean; + default_write?: boolean; + default_upload?: boolean; + default_accessible?: boolean; + }; + }; +}; + export type SubRequestDeleteReactionType = { type: 'deleteReaction'; deleteReaction: { @@ -224,6 +267,17 @@ export type SubRequestDeleteReactionType = { }; }; +export type SubRequestUpdateUserRoomPermissionsType = { + type: 'updateRoomUserPerms'; + updateUserRoomPerms: { + sessionId: string; + roomId: string; + permsToAdd?: Array; + permsToRemove?: Array; + permsToClear?: Array; + }; +}; + export type OpenGroupBatchRow = | SubRequestCapabilitiesType | SubRequestMessagesType @@ -234,13 +288,37 @@ export type OpenGroupBatchRow = | SubRequestAddRemoveModeratorType | SubRequestBanUnbanUserType | SubRequestDeleteAllUserPostsType + | SubRequestDeleteAllUserServerPostsType | SubRequestUpdateRoomType - | SubRequestDeleteReactionType; + | SubRequestDeleteReactionType + | SubRequestUpdateUserRoomPermissionsType + | SubRequestUpdateRoomPermsType; + +type OpenGroupPermissionSelection = Partial< + Record<`${Prefix}${OpenGroupPermissionType}`, boolean> +>; + +function makePermissionSelection( + permissions: Array | undefined, + choice: boolean, + prefix: string = '' +): OpenGroupPermissionSelection { + return ( + permissions?.reduce( + (aggregatePermissions, newPermission) => ({ + ...aggregatePermissions, + [`${prefix}${newPermission}`]: choice, + }), + {} + ) ?? {} + ); +} /** * * @param options Array of subRequest options to be made. */ +// tslint:disable-next-line: cyclomatic-complexity const makeBatchRequestPayload = ( options: OpenGroupBatchRow ): BatchSubRequest | Array | null => { @@ -318,16 +396,31 @@ const makeBatchRequestPayload = ( }, })); case 'banUnbanUser': - const isBan = Boolean(options.banUnbanUser.type === 'ban'); + const details = options.banUnbanUser; + const isBanAction = isBan(details.type); + const isServerWideAction = isServerBanUnbanAction(details); + window?.log?.info( + `banUnbanUser: ${ed25519Str(details.sessionId)}, server-wide: ${isServerWideAction}` + ); + const path = `/user/${details.sessionId}/${isBanAction ? 'ban' : 'unban'}`; + if (isServerWideAction) { + // Issue server-wide (un)ban. + return { + method: 'POST', + path, + json: { + global: true, + }, + }; + } + + // Issue room-wide (un)ban. return { method: 'POST', - path: `/user/${options.banUnbanUser.sessionId}/${isBan ? 'ban' : 'unban'}`, + path, json: { - rooms: [options.banUnbanUser.roomId], - + rooms: [details.roomId], // watch out ban and unban user do not allow the same args - // global: false, // for now we do not support the global argument, rooms cannot be set if we use it - // timeout: null, // for now we do not support the timeout argument }, }; case 'deleteAllPosts': @@ -335,6 +428,11 @@ const makeBatchRequestPayload = ( method: 'DELETE', path: `/room/${options.deleteAllPosts.roomId}/all/${options.deleteAllPosts.sessionId}`, }; + case 'deleteAllUserPosts': + return { + method: 'DELETE', + path: `/rooms/all/${options.deleteAllUserPosts.sessionId}`, + }; case 'updateRoom': if ( !options.updateRoom.imageId && @@ -355,12 +453,36 @@ const makeBatchRequestPayload = ( path: `/room/${options.updateRoom.roomId}`, json, }; - + case 'updateRoomPerms': + return { + method: 'PUT', + path: `/room/${options.updateRoomPerms.roomId}`, + json: { ...options.updateRoomPerms.permsToSet }, + }; case 'deleteReaction': return { method: 'DELETE', path: `/room/${options.deleteReaction.roomId}/reactions/${options.deleteReaction.messageId}/${options.deleteReaction.reaction}`, }; + case 'updateRoomUserPerms': + if ( + hasDuplicates([ + ...(options.updateUserRoomPerms.permsToAdd ?? []), + ...(options.updateUserRoomPerms.permsToRemove ?? []), + ...(options.updateUserRoomPerms.permsToClear ?? []), + ]) + ) { + throw new Error('Cannot change the same permission in more than one way'); + } + return { + method: 'POST', + path: `/room/${options.updateUserRoomPerms.roomId}/permissions/${options.updateUserRoomPerms.sessionId}`, + json: { + ...makePermissionSelection(options.updateUserRoomPerms.permsToAdd, true), + ...makePermissionSelection(options.updateUserRoomPerms.permsToRemove, false), + ...makePermissionSelection(options.updateUserRoomPerms.permsToClear, true, 'default_'), + }, + }; default: assertUnreachable(type, 'Invalid batch request row'); } diff --git a/ts/session/apis/open_group_api/sogsv3/sogsV3RoomPermissions.ts b/ts/session/apis/open_group_api/sogsv3/sogsV3RoomPermissions.ts new file mode 100644 index 0000000000..09e50317dc --- /dev/null +++ b/ts/session/apis/open_group_api/sogsv3/sogsV3RoomPermissions.ts @@ -0,0 +1,50 @@ +/** + * @file + * Set certain default room permissions. + */ + +import AbortController from 'abort-controller'; +import { OpenGroupRequestCommonType } from '../../../../data/types'; +import { batchEverySubIsSuccess, sogsBatchSend } from './sogsV3BatchPoll'; +import { DURATION } from '../../../constants'; + +export type CommunityRoomPermissionType = + | 'default_read' + | 'default_write' + | 'default_accessible' + | 'default_upload'; + +export type OpenGroupRoomPermissionSetType = Record; + +type PermsToSet = Partial; + +export const sogsV3SetRoomPermissions = async ( + roomInfos: OpenGroupRequestCommonType, + permissions: OpenGroupRoomPermissionSetType +): Promise => { + const permsToSet: PermsToSet = {}; + Object.entries(permissions).forEach(([permission, value]) => { + permsToSet[permission as CommunityRoomPermissionType] = value; + }); + const batchSendResponse = await sogsBatchSend( + roomInfos.serverUrl, + new Set([roomInfos.roomId]), + new AbortController().signal, + [ + { + type: 'updateRoomPerms', + updateRoomPerms: { + roomId: roomInfos.roomId, + permsToSet, + }, + }, + ], + 'batch', + 10 * DURATION.SECONDS + ); + const isSuccess = batchEverySubIsSuccess(batchSendResponse); + if (!isSuccess) { + window.log.warn('set permissions failed with body', batchSendResponse?.body); + } + return isSuccess; +}; diff --git a/ts/session/apis/open_group_api/sogsv3/sogsV3UserPermissions.ts b/ts/session/apis/open_group_api/sogsv3/sogsV3UserPermissions.ts new file mode 100644 index 0000000000..24d122e0b9 --- /dev/null +++ b/ts/session/apis/open_group_api/sogsv3/sogsV3UserPermissions.ts @@ -0,0 +1,66 @@ +/** + * @file + * Add, remove, or clear certain per-room user permissions. + */ + +import AbortController from 'abort-controller'; +import { PubKey } from '../../../types'; +import { OpenGroupRequestCommonType } from '../../../../data/types'; +import { batchEverySubIsSuccess, sogsBatchSend } from './sogsV3BatchPoll'; +import { DURATION } from '../../../constants'; + +export type OpenGroupPermissionType = 'access' | 'read' | 'upload' | 'write'; + +export const sogsV3AddPermissions = async ( + usersToAddPermissionsTo: Array, + roomInfos: OpenGroupRequestCommonType, + permissions: Array +): Promise => { + const batchSendResponse = await sogsBatchSend( + roomInfos.serverUrl, + new Set([roomInfos.roomId]), + new AbortController().signal, + usersToAddPermissionsTo.map(user => ({ + type: 'updateRoomUserPerms', + updateUserRoomPerms: { + roomId: roomInfos.roomId, + sessionId: user.key, + permsToAdd: permissions, + }, + })), + 'batch', + 10 * DURATION.SECONDS + ); + const isSuccess = batchEverySubIsSuccess(batchSendResponse); + if (!isSuccess) { + window.log.warn('add permissions failed with body', batchSendResponse?.body); + } + return isSuccess; +}; + +export const sogsV3ClearPermissions = async ( + usersToClearPermissionsFor: Array, + roomInfos: OpenGroupRequestCommonType, + permissions: Array +): Promise => { + const batchSendResponse = await sogsBatchSend( + roomInfos.serverUrl, + new Set([roomInfos.roomId]), + new AbortController().signal, + usersToClearPermissionsFor.map(user => ({ + type: 'updateRoomUserPerms', + updateUserRoomPerms: { + roomId: roomInfos.roomId, + sessionId: user.key, + permsToClear: permissions, + }, + })), + 'batch', + 10 * DURATION.SECONDS + ); + const isSuccess = batchEverySubIsSuccess(batchSendResponse); + if (!isSuccess) { + window.log.warn('add permissions failed with body', batchSendResponse?.body); + } + return isSuccess; +}; diff --git a/ts/session/constants.ts b/ts/session/constants.ts index 7294a12ed5..04bf2417d9 100644 --- a/ts/session/constants.ts +++ b/ts/session/constants.ts @@ -88,7 +88,6 @@ export const CONVERSATION = { LAST_JOINED_FALLBACK_TIMESTAMP: 1, /** * the maximum chars that can be typed/pasted in the composition box. - * Same as android. */ MAX_MESSAGE_CHAR_COUNT_STANDARD: 2_000, MAX_MESSAGE_CHAR_COUNT_PRO: 10_000, diff --git a/ts/session/utils/Toast.tsx b/ts/session/utils/Toast.tsx index 4210d5caac..4846df133b 100644 --- a/ts/session/utils/Toast.tsx +++ b/ts/session/utils/Toast.tsx @@ -90,6 +90,10 @@ export function pushUserBanFailure() { pushToastError('userBanFailed', tStripped('banErrorFailed')); } +export function pushGlobalUserBanFailure() { + pushToastError('globalUserBanFailed', tStripped('globalUserBanFailedDev')); +} + export function pushUserUnbanSuccess() { pushToastSuccess('userUnbanned', tStripped('banUnbanUserUnbanned')); } @@ -98,6 +102,10 @@ export function pushUserUnbanFailure() { pushToastError('userUnbanFailed', tStripped('banUnbanErrorFailed')); } +export function pushGlobalUserUnbanFailure() { + pushToastError('globalUserUnbanFailed', tStripped('globalUserUnbanFailedDev')); +} + export function pushMessageDeleteForbidden() { pushToastError( 'messageDeletionForbidden', @@ -241,6 +249,14 @@ export function pushUserRemovedFromModerators(names: Array) { pushToastSuccess('adminRemovedUser', localizedString); } +export function pushFailedToChangeUserPermissions() { + pushToastWarning('failedToChangeUserPermissions', tStripped('failedToChangeUserPermissionsDev')); +} + +export function pushUserPermissionsChanged() { + pushToastSuccess('userPermissionsChanged', tStripped('userPermissionsChangedDev')); +} + export function pushInvalidPubKey() { pushToastSuccess('accountIdErrorInvalid', tStripped('accountIdErrorInvalid')); } diff --git a/ts/shared/array_utils.ts b/ts/shared/array_utils.ts new file mode 100644 index 0000000000..5850d2ec15 --- /dev/null +++ b/ts/shared/array_utils.ts @@ -0,0 +1,3 @@ +export function hasDuplicates(array: Array): boolean { + return new Set(array).size < array.length; +} diff --git a/ts/state/ducks/modalDialog.tsx b/ts/state/ducks/modalDialog.tsx index 05c6b84134..f02656f554 100644 --- a/ts/state/ducks/modalDialog.tsx +++ b/ts/state/ducks/modalDialog.tsx @@ -18,7 +18,24 @@ import { CTAVariant } from '../../components/dialog/cta/types'; import { CTAInteraction, registerCtaInteraction } from '../../util/ctaHistory'; import { closeContextMenus } from '../../util/contextMenu'; -export type BanType = 'ban' | 'unban'; +export type BanType = 'ban' | 'unban' | 'server-ban' | 'server-unban'; + +export type BanUnbanRoomWideOnly = Extract; +export type BanServerWideOrNot = Extract; +export type UnbanServerWideOrNot = Extract; +export type BanUnbanServerWideOnly = Extract; + +export function isBan(banType: BanType): banType is BanServerWideOrNot { + return ['ban', 'server-ban'].includes(banType); +} + +export function isUnban(banType: BanType): banType is UnbanServerWideOrNot { + return ['unban', 'server-unban'].includes(banType); +} + +export function isServerBanUnban(banType: BanType): banType is BanUnbanServerWideOnly { + return ['server-ban', 'server-unban'].includes(banType); +} export type UserSettingsPage = | 'default' @@ -69,9 +86,12 @@ export type BanOrUnbanUserModalState = pubkey?: string; }) | null; + export type AddModeratorsModalState = InviteContactModalState; export type RemoveModeratorsModalState = InviteContactModalState; export type UpdateGroupMembersModalState = InviteContactModalState; +// export type UpdateGroupNameModalState = WithConvoId | null; +export type UpdateCommunityPermissionsModalState = InviteContactModalState; type UpdateConversationDetailsModalState = WithConvoId | null; export type ChangeNickNameModalState = InviteContactModalState; export type UserSettingsModalState = WithUserSettingsPage | null; @@ -150,6 +170,7 @@ export type ModalId = | 'addModeratorsModal' | 'updateConversationDetailsModal' | 'groupMembersModal' + | 'communityPermissionsModal' | 'userProfileModal' | 'nickNameModal' | 'userSettingsModal' @@ -177,6 +198,7 @@ export type ModalState = { addModeratorsModal: AddModeratorsModalState; updateConversationDetailsModal: UpdateConversationDetailsModalState; groupMembersModal: UpdateGroupMembersModalState; + communityPermissionsModal: UpdateCommunityPermissionsModalState; userProfileModal: UserProfileModalState; nickNameModal: ChangeNickNameModalState; userSettingsModal: UserSettingsModalState; @@ -207,6 +229,7 @@ export const initialModalState: ModalState = { blockOrUnblockModal: null, updateConversationDetailsModal: null, groupMembersModal: null, + communityPermissionsModal: null, userProfileModal: null, nickNameModal: null, userSettingsModal: null, @@ -277,6 +300,7 @@ const ModalSlice = createSlice({ updateBanOrUnbanUserModal(state, action: PayloadAction) { return pushOrPopModal(state, 'banOrUnbanUserModal', action.payload); }, + updateBlockOrUnblockModal(state, action: PayloadAction) { return pushOrPopModal(state, 'blockOrUnblockModal', action.payload); }, @@ -295,6 +319,12 @@ const ModalSlice = createSlice({ updateGroupMembersModal(state, action: PayloadAction) { return pushOrPopModal(state, 'groupMembersModal', action.payload); }, + updateCommunityPermissionsModal( + state, + action: PayloadAction + ) { + return pushOrPopModal(state, 'communityPermissionsModal', action.payload); + }, updateUserProfileModal(state, action: PayloadAction) { return pushOrPopModal(state, 'userProfileModal', action.payload); }, @@ -373,6 +403,7 @@ export const { updateRemoveModeratorsModal, updateConversationDetailsModal, updateGroupMembersModal, + updateCommunityPermissionsModal, updateUserProfileModal, changeNickNameModal, userSettingsModal, diff --git a/ts/state/ducks/types/defaultFeatureFlags.ts b/ts/state/ducks/types/defaultFeatureFlags.ts index 6cffe955d5..f665036142 100644 --- a/ts/state/ducks/types/defaultFeatureFlags.ts +++ b/ts/state/ducks/types/defaultFeatureFlags.ts @@ -9,6 +9,7 @@ export const defaultProBooleanFeatureFlags = { proAvailable: !isEmpty(process.env.SESSION_PRO), proGroupsAvailable: !isEmpty(process.env.SESSION_PRO_GROUPS), useTestProBackend: !isEmpty(process.env.TEST_PRO_BACKEND), + useDevCommunityActions: !isEmpty(process.env.DEV_COMMUNITY_ACTIONS), mockCurrentUserHasProPlatformRefundExpired: !isEmpty( process.env.SESSION_USER_HAS_PRO_PLATFORM_REFUND_EXPIRED ), diff --git a/ts/state/ducks/types/releasedFeaturesReduxTypes.ts b/ts/state/ducks/types/releasedFeaturesReduxTypes.ts index 03c8d911fb..e360c29b6b 100644 --- a/ts/state/ducks/types/releasedFeaturesReduxTypes.ts +++ b/ts/state/ducks/types/releasedFeaturesReduxTypes.ts @@ -14,6 +14,7 @@ type SessionBaseBooleanFeatureFlags = { useDeterministicEncryption: boolean; useTestNet: boolean; useTestProBackend: boolean; + useDevCommunityActions: boolean; useClosedGroupV2QAButtons: boolean; alwaysShowRemainingChars: boolean; showPopoverAnchors: boolean; diff --git a/ts/state/selectors/modal.ts b/ts/state/selectors/modal.ts index c00dcdb372..bbaa26814d 100644 --- a/ts/state/selectors/modal.ts +++ b/ts/state/selectors/modal.ts @@ -1,6 +1,7 @@ import { useSelector } from 'react-redux'; -import { ModalState, type ModalId } from '../ducks/modalDialog'; +import { ModalState, ModalId } from '../ducks/modalDialog'; + import { StateType } from '../reducer'; const getModal = (state: StateType): ModalState => { @@ -35,6 +36,9 @@ export function useUpdateConversationDetailsModal() { return useSelector((state: StateType) => getModal(state).updateConversationDetailsModal); } +export const getBlockOrUnblockUserModalState = (state: StateType) => + getModal(state).blockOrUnblockModal; + export function useUpdateGroupMembersModal() { return useSelector((state: StateType) => getModal(state).groupMembersModal); } @@ -47,6 +51,10 @@ export function useChangeNickNameDialog() { return useSelector((state: StateType) => getModal(state).nickNameModal); } +export function useUpdateCommunityPermissionsModalState() { + return useSelector((state: StateType) => getModal(state).communityPermissionsModal); +} + export function useUserSettingsModal() { return useSelector((state: StateType) => getModal(state).userSettingsModal); } diff --git a/ts/webworker/workers/browser/libsession_worker_interface.ts b/ts/webworker/workers/browser/libsession_worker_interface.ts index 326e923c73..80bed4d28c 100644 --- a/ts/webworker/workers/browser/libsession_worker_interface.ts +++ b/ts/webworker/workers/browser/libsession_worker_interface.ts @@ -794,16 +794,16 @@ export const BlindingActions: BlindingActionsCalls = { callLibSessionWorker(['Blinding', 'blindVersionPubkey', opts]) as Promise< ReturnType >, - blindVersionSign: async (opts: Parameters[0]) => - callLibSessionWorker(['Blinding', 'blindVersionSign', opts]) as Promise< - ReturnType - >, blindVersionSignRequest: async ( opts: Parameters[0] ) => callLibSessionWorker(['Blinding', 'blindVersionSignRequest', opts]) as Promise< ReturnType >, + blindVersionSign: async (opts: Parameters[0]) => + callLibSessionWorker(['Blinding', 'blindVersionSign', opts]) as Promise< + ReturnType + >, }; export const UtilitiesActions: UtilitiesWrapperActionsCalls = {