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