mirror of
https://github.com/signalapp/Signal-Desktop.git
synced 2026-04-02 00:07:56 +01:00
Introduce new permission for group member labels
This commit is contained in:
@@ -5340,30 +5340,54 @@
|
||||
"messageformat": "An admin changed who can edit group info to \"All members.\"",
|
||||
"description": "Shown in timeline or conversation preview when v2 group changes"
|
||||
},
|
||||
"icu:GroupV2--access-members--admins--other": {
|
||||
"messageformat": "{adminName} changed who can edit group membership to \"Only admins.\"",
|
||||
"description": "Shown in timeline or conversation preview when v2 group changes"
|
||||
},
|
||||
"icu:GroupV2--access-members--admins--you": {
|
||||
"messageformat": "You changed who can edit group membership to \"Only admins.\"",
|
||||
"description": "Shown in timeline or conversation preview when v2 group changes"
|
||||
},
|
||||
"icu:GroupV2--access-members--admins--other": {
|
||||
"messageformat": "{adminName} changed who can edit group membership to \"Only admins.\"",
|
||||
"description": "Shown in timeline or conversation preview when v2 group changes"
|
||||
},
|
||||
"icu:GroupV2--access-members--admins--unknown": {
|
||||
"messageformat": "An admin changed who can edit group membership to \"Only admins.\"",
|
||||
"description": "Shown in timeline or conversation preview when v2 group changes"
|
||||
},
|
||||
"icu:GroupV2--access-members--all--other": {
|
||||
"messageformat": "{adminName} changed who can edit group membership to \"All members.\"",
|
||||
"description": "Shown in timeline or conversation preview when v2 group changes"
|
||||
},
|
||||
"icu:GroupV2--access-members--all--you": {
|
||||
"messageformat": "You changed who can edit group membership to \"All members.\"",
|
||||
"description": "Shown in timeline or conversation preview when v2 group changes"
|
||||
},
|
||||
"icu:GroupV2--access-members--all--other": {
|
||||
"messageformat": "{adminName} changed who can edit group membership to \"All members.\"",
|
||||
"description": "Shown in timeline or conversation preview when v2 group changes"
|
||||
},
|
||||
"icu:GroupV2--access-members--all--unknown": {
|
||||
"messageformat": "An admin changed who can edit group membership to \"All members.\"",
|
||||
"description": "Shown in timeline or conversation preview when v2 group changes"
|
||||
},
|
||||
"icu:GroupV2--access-member-label--admins--you": {
|
||||
"messageformat": "You changed who can add member labels to \"Only admins.\"",
|
||||
"description": "Shown in timeline or conversation preview when v2 group changes"
|
||||
},
|
||||
"icu:GroupV2--access-member-label--admins--other": {
|
||||
"messageformat": "{adminName} changed who can add member labels to \"Only admins.\"",
|
||||
"description": "Shown in timeline or conversation preview when v2 group changes"
|
||||
},
|
||||
"icu:GroupV2--access-member-label--admins--unknown": {
|
||||
"messageformat": "An admin changed who can add member labels to \"Only admins.\"",
|
||||
"description": "Shown in timeline or conversation preview when v2 group changes"
|
||||
},
|
||||
"icu:GroupV2--access-member-label--all--you": {
|
||||
"messageformat": "You changed who can add member labels to \"All members.\"",
|
||||
"description": "Shown in timeline or conversation preview when v2 group changes"
|
||||
},
|
||||
"icu:GroupV2--access-member-label--all--other": {
|
||||
"messageformat": "{adminName} changed who can add member labels to \"All members.\"",
|
||||
"description": "Shown in timeline or conversation preview when v2 group changes"
|
||||
},
|
||||
"icu:GroupV2--access-member-label--all--unknown": {
|
||||
"messageformat": "An admin changed who can add member labels to \"All members.\"",
|
||||
"description": "Shown in timeline or conversation preview when v2 group changes"
|
||||
},
|
||||
"icu:GroupV2--access-invite-link--disabled--you": {
|
||||
"messageformat": "You disabled admin approval for the group link.",
|
||||
"description": "Shown in timeline or conversation preview when v2 group changes"
|
||||
@@ -6384,6 +6408,14 @@
|
||||
"messageformat": "Choose who can send messages to the group.",
|
||||
"description": "This is the additional info for the 'who can send messages' panel"
|
||||
},
|
||||
"icu:ConversationDetails--member-label--label": {
|
||||
"messageformat": "Who can add member labels",
|
||||
"description": "A label for a select box to choose whether all members or just admins can set Member Labels in groups"
|
||||
},
|
||||
"icu:ConversationDetails--member-label--info": {
|
||||
"messageformat": "Choose who can add member labels in this group.",
|
||||
"description": "More information about the setting for member labels."
|
||||
},
|
||||
"icu:ConversationDetails--label-clear-warning--title": {
|
||||
"messageformat": "Member labels will be cleared",
|
||||
"description": "(Deleted 2026/03/04) When the user changes the 'edit group info' permission to 'Admins only', this dialog shows. Title of dialog."
|
||||
|
||||
@@ -370,6 +370,7 @@ message Group {
|
||||
AccessRequired attributes = 1;
|
||||
AccessRequired members = 2;
|
||||
AccessRequired addFromInviteLink = 3;
|
||||
AccessRequired memberLabel = 4;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1052,6 +1053,7 @@ message GroupChangeChatUpdate {
|
||||
GroupAvatarUpdate groupAvatarUpdate = 4;
|
||||
GroupDescriptionUpdate groupDescriptionUpdate = 5;
|
||||
GroupMembershipAccessLevelChangeUpdate groupMembershipAccessLevelChangeUpdate = 6;
|
||||
GroupMemberLabelAccessLevelChangeUpdate groupMemberLabelAccessLevelChangeUpdate = 35;
|
||||
GroupAttributesAccessLevelChangeUpdate groupAttributesAccessLevelChangeUpdate = 7;
|
||||
GroupAnnouncementOnlyChangeUpdate groupAnnouncementOnlyChangeUpdate = 8;
|
||||
GroupAdminStatusUpdate groupAdminStatusUpdate = 9;
|
||||
@@ -1080,6 +1082,7 @@ message GroupChangeChatUpdate {
|
||||
GroupV2MigrationDroppedMembersUpdate groupV2MigrationDroppedMembersUpdate = 32;
|
||||
GroupSequenceOfRequestsAndCancelsUpdate groupSequenceOfRequestsAndCancelsUpdate = 33;
|
||||
GroupExpirationTimerUpdate groupExpirationTimerUpdate = 34;
|
||||
// next: 36
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1126,6 +1129,11 @@ message GroupMembershipAccessLevelChangeUpdate {
|
||||
GroupV2AccessLevel accessLevel = 2;
|
||||
}
|
||||
|
||||
message GroupMemberLabelAccessLevelChangeUpdate {
|
||||
optional bytes updaterAci = 1;
|
||||
GroupV2AccessLevel accessLevel = 2;
|
||||
}
|
||||
|
||||
message GroupAttributesAccessLevelChangeUpdate {
|
||||
optional bytes updaterAci = 1;
|
||||
GroupV2AccessLevel accessLevel = 2;
|
||||
|
||||
@@ -67,6 +67,7 @@ message AccessControl {
|
||||
AccessRequired attributes = 1;
|
||||
AccessRequired members = 2;
|
||||
AccessRequired addFromInviteLink = 3;
|
||||
AccessRequired member_label = 4;
|
||||
}
|
||||
|
||||
message Group {
|
||||
@@ -224,6 +225,10 @@ message GroupChange {
|
||||
AccessControl.AccessRequired addFromInviteLinkAccess = 1;
|
||||
}
|
||||
|
||||
message ModifyMemberLabelAccessControlAction {
|
||||
AccessControl.AccessRequired member_label_access = 1;
|
||||
}
|
||||
|
||||
message ModifyInviteLinkPasswordAction {
|
||||
bytes inviteLinkPassword = 1;
|
||||
}
|
||||
@@ -252,6 +257,7 @@ message GroupChange {
|
||||
ModifyAttributesAccessControlAction modifyAttributesAccess = 13;
|
||||
ModifyMembersAccessControlAction modifyMemberAccess = 14;
|
||||
ModifyAddFromInviteLinkAccessControlAction modifyAddFromInviteLinkAccess = 15; // change epoch = 1
|
||||
ModifyMemberLabelAccessControlAction modify_member_label_access = 27; // change epoch = 6
|
||||
repeated AddMemberPendingAdminApprovalAction addMembersPendingAdminApproval = 16; // change epoch = 1
|
||||
repeated DeleteMemberPendingAdminApprovalAction deleteMembersPendingAdminApproval = 17; // change epoch = 1
|
||||
repeated PromoteMemberPendingAdminApprovalAction promoteMembersPendingAdminApproval = 18; // change epoch = 1
|
||||
@@ -261,7 +267,7 @@ message GroupChange {
|
||||
repeated AddMemberBannedAction add_members_banned = 22; // change epoch = 4
|
||||
repeated DeleteMemberBannedAction delete_members_banned = 23; // change epoch = 4
|
||||
repeated PromoteMemberPendingPniAciProfileKeyAction promote_members_pending_pni_aci_profile_key = 24; // change epoch = 5
|
||||
// next: 27
|
||||
// next: 28
|
||||
}
|
||||
|
||||
bytes actions = 1;
|
||||
|
||||
@@ -418,7 +418,6 @@
|
||||
&__right {
|
||||
position: relative;
|
||||
color: variables.$color-gray-45;
|
||||
min-width: 100px;
|
||||
}
|
||||
|
||||
&__actions {
|
||||
|
||||
@@ -23,12 +23,14 @@ export default {
|
||||
} satisfies Meta<PropsType>;
|
||||
|
||||
const createProps = (): PropsType => ({
|
||||
group: getDefaultConversation({ type: 'group' }),
|
||||
me: getDefaultConversation({ type: 'direct' }),
|
||||
canAddLabel: true,
|
||||
existingLabelEmoji: '🐘',
|
||||
existingLabelString: 'Good Memory',
|
||||
getPreferredBadge: () => undefined,
|
||||
group: getDefaultConversation({ type: 'group' }),
|
||||
i18n,
|
||||
isActive: true,
|
||||
me: getDefaultConversation({ type: 'direct' }),
|
||||
membersWithLabel: [],
|
||||
ourColor: '160',
|
||||
popPanelForConversation: action('popPanelForConversation'),
|
||||
@@ -96,17 +98,7 @@ export function ThrowsErrorOnSave(): React.JSX.Element {
|
||||
export function PermissionsError(): React.JSX.Element {
|
||||
const props: PropsType = createProps();
|
||||
|
||||
return (
|
||||
<GroupMemberLabelEditor
|
||||
{...props}
|
||||
group={{
|
||||
...props.group,
|
||||
areWeAdmin: false,
|
||||
accessControlAttributes:
|
||||
Proto.AccessControl.AccessRequired.ADMINISTRATOR,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
return <GroupMemberLabelEditor {...props} canAddLabel={false} />;
|
||||
}
|
||||
|
||||
export function PermissionsRestrictedButAdmin(): React.JSX.Element {
|
||||
|
||||
@@ -44,10 +44,12 @@ import type { Location } from '../../../types/Nav.std.js';
|
||||
import { usePrevious } from '../../../hooks/usePrevious.std.js';
|
||||
|
||||
export type PropsDataType = {
|
||||
canAddLabel: boolean;
|
||||
existingLabelEmoji: string | undefined;
|
||||
existingLabelString: string | undefined;
|
||||
group: ConversationType;
|
||||
i18n: LocalizerType;
|
||||
isActive: boolean;
|
||||
me: ConversationType;
|
||||
membersWithLabel: Array<{
|
||||
contactNameColor: string;
|
||||
@@ -90,12 +92,14 @@ function getEmojiVariantKey(value: string): EmojiVariantKey | undefined {
|
||||
}
|
||||
|
||||
export function GroupMemberLabelEditor({
|
||||
canAddLabel,
|
||||
group,
|
||||
me,
|
||||
existingLabelEmoji,
|
||||
existingLabelString,
|
||||
getPreferredBadge,
|
||||
i18n,
|
||||
isActive,
|
||||
membersWithLabel,
|
||||
ourColor,
|
||||
popPanelForConversation,
|
||||
@@ -104,6 +108,8 @@ export function GroupMemberLabelEditor({
|
||||
}: PropsType): React.JSX.Element {
|
||||
const [isShowingGeneralError, setIsShowingGeneralError] =
|
||||
React.useState(false);
|
||||
const [isShowingPermissionsError, setIsShowingPermissionsError] =
|
||||
React.useState(false);
|
||||
|
||||
const messageContainer = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
@@ -131,6 +137,17 @@ export function GroupMemberLabelEditor({
|
||||
? { labelEmoji, labelString: labelStringForSave }
|
||||
: undefined;
|
||||
|
||||
useEffect(() => {
|
||||
if (!canAddLabel && isActive) {
|
||||
setIsShowingPermissionsError(true);
|
||||
}
|
||||
}, [
|
||||
canAddLabel,
|
||||
isActive,
|
||||
isShowingPermissionsError,
|
||||
setIsShowingPermissionsError,
|
||||
]);
|
||||
|
||||
const tryClose = React.useRef<() => void | undefined>();
|
||||
const [confirmDiscardModal, confirmDiscardIf] = useConfirmDiscard({
|
||||
i18n,
|
||||
@@ -397,7 +414,7 @@ export function GroupMemberLabelEditor({
|
||||
</div>
|
||||
{confirmDiscardModal}
|
||||
<AxoAlertDialog.Root
|
||||
open={isShowingGeneralError}
|
||||
open={isShowingGeneralError && isActive}
|
||||
onOpenChange={value => {
|
||||
if (!value) {
|
||||
setIsShowingGeneralError(false);
|
||||
@@ -426,6 +443,38 @@ export function GroupMemberLabelEditor({
|
||||
</AxoAlertDialog.Footer>
|
||||
</AxoAlertDialog.Content>
|
||||
</AxoAlertDialog.Root>
|
||||
<AxoAlertDialog.Root
|
||||
open={isShowingPermissionsError && isActive}
|
||||
onOpenChange={value => {
|
||||
if (!value) {
|
||||
setIsShowingPermissionsError(false);
|
||||
popPanelForConversation();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<AxoAlertDialog.Content escape="cancel-is-noop">
|
||||
<AxoAlertDialog.Body>
|
||||
<AxoAlertDialog.Title>
|
||||
{i18n('icu:ConversationDetails--member-label--error-title')}
|
||||
</AxoAlertDialog.Title>
|
||||
<AxoAlertDialog.Description>
|
||||
{i18n('icu:ConversationDetails--member-label--error-permissions')}
|
||||
</AxoAlertDialog.Description>
|
||||
</AxoAlertDialog.Body>
|
||||
<AxoAlertDialog.Footer>
|
||||
<AxoAlertDialog.Action
|
||||
variant="primary"
|
||||
arrow={false}
|
||||
onClick={() => {
|
||||
popPanelForConversation();
|
||||
setIsShowingPermissionsError(false);
|
||||
}}
|
||||
>
|
||||
{i18n('icu:ok')}
|
||||
</AxoAlertDialog.Action>
|
||||
</AxoAlertDialog.Footer>
|
||||
</AxoAlertDialog.Content>
|
||||
</AxoAlertDialog.Root>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -32,6 +32,9 @@ const createProps = (): PropsType => ({
|
||||
'setAccessControlAttributesSetting'
|
||||
),
|
||||
setAccessControlMembersSetting: action('setAccessControlMembersSetting'),
|
||||
setAccessControlMemberLabelSetting: action(
|
||||
'setAccessControlMemberLabelSetting'
|
||||
),
|
||||
setAnnouncementsOnly: action('setAnnouncementsOnly'),
|
||||
});
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import { SignalService as Proto } from '../../../protobuf/index.std.js';
|
||||
import { PanelRow } from './PanelRow.dom.js';
|
||||
import { PanelSection } from './PanelSection.dom.js';
|
||||
import { Select } from '../../Select.dom.js';
|
||||
import { AxoAlertDialog } from '../../../axo/AxoAlertDialog.dom.js';
|
||||
|
||||
export type PropsDataType = {
|
||||
conversation?: ConversationType;
|
||||
@@ -17,6 +18,7 @@ export type PropsDataType = {
|
||||
type PropsActionType = {
|
||||
setAccessControlAttributesSetting: (id: string, value: number) => void;
|
||||
setAccessControlMembersSetting: (id: string, value: number) => void;
|
||||
setAccessControlMemberLabelSetting: (id: string, value: number) => void;
|
||||
setAnnouncementsOnly: (id: string, value: boolean) => void;
|
||||
};
|
||||
|
||||
@@ -27,18 +29,34 @@ export function GroupV2Permissions({
|
||||
i18n,
|
||||
setAccessControlAttributesSetting,
|
||||
setAccessControlMembersSetting,
|
||||
setAccessControlMemberLabelSetting,
|
||||
setAnnouncementsOnly,
|
||||
}: PropsType): React.JSX.Element {
|
||||
const AccessControlEnum = Proto.AccessControl.AccessRequired;
|
||||
|
||||
const [isWarningAboutClearingLabels, setIsWarningAboutClearingLabels] =
|
||||
React.useState(false);
|
||||
const addMembersSelectId = useId();
|
||||
const groupInfoSelectId = useId();
|
||||
const announcementSelectId = useId();
|
||||
const memberLabelSelectId = useId();
|
||||
|
||||
if (conversation === undefined) {
|
||||
throw new Error('GroupV2Permissions rendered without a conversation');
|
||||
}
|
||||
const nonAdminsHaveLabels = conversation.memberships?.some(
|
||||
membership => !membership.isAdmin && membership.labelString
|
||||
);
|
||||
|
||||
const updateAccessControlMemberLabel = (value: string) => {
|
||||
const newValue = Number(value);
|
||||
if (newValue === AccessControlEnum.ADMINISTRATOR && nonAdminsHaveLabels) {
|
||||
setIsWarningAboutClearingLabels(true);
|
||||
return;
|
||||
}
|
||||
|
||||
setAccessControlMemberLabelSetting(conversation.id, Number(value));
|
||||
};
|
||||
const updateAccessControlAttributes = (value: string) => {
|
||||
setAccessControlAttributesSetting(conversation.id, Number(value));
|
||||
};
|
||||
@@ -114,6 +132,68 @@ export function GroupV2Permissions({
|
||||
}
|
||||
/>
|
||||
)}
|
||||
<PanelRow
|
||||
label={
|
||||
<label htmlFor={memberLabelSelectId}>
|
||||
{i18n('icu:ConversationDetails--member-label--label')}
|
||||
</label>
|
||||
}
|
||||
info={i18n('icu:ConversationDetails--member-label--info')}
|
||||
right={
|
||||
<Select
|
||||
id={memberLabelSelectId}
|
||||
onChange={updateAccessControlMemberLabel}
|
||||
options={accessControlOptions}
|
||||
value={String(conversation.accessControlMemberLabel)}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
<AxoAlertDialog.Root
|
||||
open={isWarningAboutClearingLabels}
|
||||
onOpenChange={value => {
|
||||
if (!value) {
|
||||
setIsWarningAboutClearingLabels(false);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<AxoAlertDialog.Content escape="cancel-is-noop">
|
||||
<AxoAlertDialog.Body>
|
||||
<AxoAlertDialog.Title>
|
||||
{i18n('icu:ConversationDetails--label-clear-warning--title')}
|
||||
</AxoAlertDialog.Title>
|
||||
<AxoAlertDialog.Description>
|
||||
{i18n(
|
||||
'icu:ConversationDetails--label-clear-warning--description'
|
||||
)}
|
||||
</AxoAlertDialog.Description>
|
||||
</AxoAlertDialog.Body>
|
||||
<AxoAlertDialog.Footer>
|
||||
<AxoAlertDialog.Action
|
||||
variant="secondary"
|
||||
arrow={false}
|
||||
onClick={() => {
|
||||
setIsWarningAboutClearingLabels(false);
|
||||
}}
|
||||
>
|
||||
{i18n('icu:cancel')}
|
||||
</AxoAlertDialog.Action>
|
||||
<AxoAlertDialog.Action
|
||||
variant="primary"
|
||||
arrow={false}
|
||||
onClick={() => {
|
||||
setAccessControlMemberLabelSetting(
|
||||
conversation.id,
|
||||
AccessControlEnum.ADMINISTRATOR
|
||||
);
|
||||
setIsWarningAboutClearingLabels(false);
|
||||
}}
|
||||
>
|
||||
{i18n('icu:ConversationDetails--label-clear-warning--continue')}
|
||||
</AxoAlertDialog.Action>
|
||||
</AxoAlertDialog.Footer>
|
||||
</AxoAlertDialog.Content>
|
||||
</AxoAlertDialog.Root>
|
||||
</PanelSection>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -276,6 +276,36 @@ function renderChangeDetail<T extends string | React.JSX.Element>(
|
||||
);
|
||||
return '';
|
||||
}
|
||||
if (detail.type === 'access-member-label') {
|
||||
const { newPrivilege } = detail;
|
||||
|
||||
if (newPrivilege === AccessControlEnum.ADMINISTRATOR) {
|
||||
if (fromYou) {
|
||||
return i18n('icu:GroupV2--access-member-label--admins--you');
|
||||
}
|
||||
if (from) {
|
||||
return i18n('icu:GroupV2--access-member-label--admins--other', {
|
||||
adminName: renderContact(from),
|
||||
});
|
||||
}
|
||||
return i18n('icu:GroupV2--access-member-label--admins--unknown');
|
||||
}
|
||||
if (newPrivilege === AccessControlEnum.MEMBER) {
|
||||
if (fromYou) {
|
||||
return i18n('icu:GroupV2--access-member-label--all--you');
|
||||
}
|
||||
if (from) {
|
||||
return i18n('icu:GroupV2--access-member-label--all--other', {
|
||||
adminName: renderContact(from),
|
||||
});
|
||||
}
|
||||
return i18n('icu:GroupV2--access-member-label--all--unknown');
|
||||
}
|
||||
log.warn(
|
||||
`access-member-label change type, privilege ${newPrivilege} is unknown`
|
||||
);
|
||||
return '';
|
||||
}
|
||||
if (detail.type === 'member-add') {
|
||||
const { aci } = detail;
|
||||
const weAreJoiner = isOurServiceId(aci);
|
||||
|
||||
@@ -861,12 +861,6 @@ export function buildAccessControlAttributesChange(
|
||||
new Proto.GroupChange.Actions.ModifyAttributesAccessControlAction();
|
||||
accessControlAction.attributesAccess = newValue;
|
||||
|
||||
if (!group.secretParams) {
|
||||
throw new Error(
|
||||
'buildAccessControlAttributesChange: group was missing secretParams!'
|
||||
);
|
||||
}
|
||||
|
||||
const actions = new Proto.GroupChange.Actions();
|
||||
actions.version = (group.revision || 0) + 1;
|
||||
actions.modifyAttributesAccess = accessControlAction;
|
||||
@@ -889,6 +883,64 @@ export function buildAccessControlMembersChange(
|
||||
return actions;
|
||||
}
|
||||
|
||||
export function buildAccessControlMemberLabelChange(
|
||||
group: ConversationAttributesType,
|
||||
value: AccessRequiredEnum
|
||||
): Proto.GroupChange.Actions {
|
||||
const ACCESS_ENUM = Proto.AccessControl.AccessRequired;
|
||||
const ROLE_ENUM = Proto.Member.Role;
|
||||
|
||||
if (!group.secretParams) {
|
||||
throw new Error(
|
||||
'buildAccessControlMemberLabelChange: group was missing secretParams!'
|
||||
);
|
||||
}
|
||||
|
||||
const accessControlAction =
|
||||
new Proto.GroupChange.Actions.ModifyMemberLabelAccessControlAction();
|
||||
accessControlAction.memberLabelAccess = value;
|
||||
|
||||
const actions = new Proto.GroupChange.Actions();
|
||||
actions.version = (group.revision || 0) + 1;
|
||||
actions.modifyMemberLabelAccess = accessControlAction;
|
||||
|
||||
// Clear out all non-admin labels
|
||||
const previousValue = group.accessControl?.memberLabel;
|
||||
if (
|
||||
previousValue !== ACCESS_ENUM.ADMINISTRATOR &&
|
||||
value === ACCESS_ENUM.ADMINISTRATOR
|
||||
) {
|
||||
const clientZkGroupCipher = getClientZkGroupCipher(group.secretParams);
|
||||
|
||||
const modifyLabelActions = (group.membersV2 || [])
|
||||
.map(member => {
|
||||
if (member.role === ROLE_ENUM.ADMINISTRATOR) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (!member.labelString && !member.labelEmoji) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const modifyLabel =
|
||||
new Proto.GroupChange.Actions.ModifyMemberLabelAction();
|
||||
modifyLabel.userId = encryptServiceId(clientZkGroupCipher, member.aci);
|
||||
|
||||
return modifyLabel;
|
||||
})
|
||||
.filter(isNotNil);
|
||||
|
||||
if (modifyLabelActions.length) {
|
||||
log.info(
|
||||
`buildAccessControlMemberLabelChange: Found ${modifyLabelActions.length} non-admins with labels. Clearing.`
|
||||
);
|
||||
actions.modifyMemberLabels = modifyLabelActions;
|
||||
}
|
||||
}
|
||||
|
||||
return actions;
|
||||
}
|
||||
|
||||
export function _maybeBuildAddBannedMemberActions({
|
||||
clientZkGroupCipher,
|
||||
group,
|
||||
@@ -1234,6 +1286,25 @@ export function buildModifyMemberRoleChange({
|
||||
actions.version = (group.revision || 0) + 1;
|
||||
actions.modifyMemberRoles = [toggleAdmin];
|
||||
|
||||
const membership = group.membersV2?.find(member => member.aci === serviceId);
|
||||
const onlyAdminsCanAddMemberLabel =
|
||||
group.accessControl?.memberLabel ===
|
||||
Proto.AccessControl.AccessRequired.ADMINISTRATOR;
|
||||
const wasPreviouslyAnAdmin =
|
||||
membership?.role === Proto.Member.Role.ADMINISTRATOR;
|
||||
const nowNotAnAdmin = role !== Proto.Member.Role.ADMINISTRATOR;
|
||||
|
||||
if (
|
||||
membership?.labelString &&
|
||||
onlyAdminsCanAddMemberLabel &&
|
||||
wasPreviouslyAnAdmin &&
|
||||
nowNotAnAdmin
|
||||
) {
|
||||
const modifyLabel = new Proto.GroupChange.Actions.ModifyMemberLabelAction();
|
||||
modifyLabel.userId = userIdCipherText;
|
||||
actions.modifyMemberLabels = [modifyLabel];
|
||||
}
|
||||
|
||||
return actions;
|
||||
}
|
||||
|
||||
@@ -1836,6 +1907,7 @@ export async function createGroupV2(
|
||||
attributes: ACCESS_ENUM.MEMBER,
|
||||
members: ACCESS_ENUM.MEMBER,
|
||||
addFromInviteLink: ACCESS_ENUM.UNSATISFIABLE,
|
||||
memberLabel: ACCESS_ENUM.MEMBER,
|
||||
},
|
||||
membersV2,
|
||||
pendingMembersV2,
|
||||
@@ -2351,6 +2423,7 @@ export async function initiateMigrationToGroupV2(
|
||||
attributes: ACCESS_ENUM.MEMBER,
|
||||
members: ACCESS_ENUM.MEMBER,
|
||||
addFromInviteLink: ACCESS_ENUM.UNSATISFIABLE,
|
||||
memberLabel: ACCESS_ENUM.MEMBER,
|
||||
},
|
||||
membersV2,
|
||||
pendingMembersV2,
|
||||
@@ -4574,6 +4647,17 @@ function extractDiffs({
|
||||
newPrivilege: current.accessControl.members,
|
||||
});
|
||||
}
|
||||
if (
|
||||
current.accessControl &&
|
||||
old.accessControl &&
|
||||
old.accessControl.memberLabel !== undefined &&
|
||||
old.accessControl.memberLabel !== current.accessControl.memberLabel
|
||||
) {
|
||||
details.push({
|
||||
type: 'access-member-label',
|
||||
newPrivilege: current.accessControl.memberLabel,
|
||||
});
|
||||
}
|
||||
|
||||
const linkPreviouslyEnabled = isAccessControlEnabled(
|
||||
old.accessControl?.addFromInviteLink
|
||||
@@ -5446,6 +5530,7 @@ async function applyGroupChange({
|
||||
members: ACCESS_ENUM.MEMBER,
|
||||
attributes: ACCESS_ENUM.MEMBER,
|
||||
addFromInviteLink: ACCESS_ENUM.UNSATISFIABLE,
|
||||
memberLabel: ACCESS_ENUM.MEMBER,
|
||||
};
|
||||
|
||||
// modifyAttributesAccess?:
|
||||
@@ -5477,6 +5562,16 @@ async function applyGroupChange({
|
||||
};
|
||||
}
|
||||
|
||||
// modify_member_label_access?:
|
||||
// GroupChange.Actions.ModifyMemberLabelAccessControlAction;
|
||||
if (actions.modifyMemberLabelAccess) {
|
||||
result.accessControl = {
|
||||
...result.accessControl,
|
||||
memberLabel:
|
||||
actions.modifyMemberLabelAccess.memberLabelAccess || ACCESS_ENUM.MEMBER,
|
||||
};
|
||||
}
|
||||
|
||||
// addMembersPendingAdminApproval?: Array<
|
||||
// GroupChange.Actions.AddMemberPendingAdminApprovalAction
|
||||
// >;
|
||||
@@ -5881,6 +5976,8 @@ async function applyGroupState({
|
||||
addFromInviteLink:
|
||||
(accessControl && accessControl.addFromInviteLink) ||
|
||||
ACCESS_ENUM.UNSATISFIABLE,
|
||||
memberLabel:
|
||||
(accessControl && accessControl.memberLabel) || ACCESS_ENUM.MEMBER,
|
||||
};
|
||||
|
||||
// Optimization: we assume we have left the group unless we are found in members
|
||||
@@ -6227,6 +6324,7 @@ type DecryptedGroupChangeActions = {
|
||||
| 'modifyAttributesAccess'
|
||||
| 'modifyMemberAccess'
|
||||
| 'modifyAddFromInviteLinkAccess'
|
||||
| 'modifyMemberLabelAccess'
|
||||
| 'modifyAvatar'
|
||||
>;
|
||||
|
||||
@@ -6694,6 +6792,21 @@ function decryptGroupChange(
|
||||
};
|
||||
}
|
||||
|
||||
// modifyMemberLabelAccess?: GroupChange.Actions.ModifyMemberLabelAccessControlAction;
|
||||
if (actions.modifyMemberLabelAccess) {
|
||||
const memberLabelAccess = dropNull(
|
||||
actions.modifyMemberLabelAccess.memberLabelAccess
|
||||
);
|
||||
strictAssert(
|
||||
isValidAccess(memberLabelAccess),
|
||||
`decryptGroupChange: modifyMemberLabelAccess.memberLabelAccess was not valid: ${actions.modifyMemberLabelAccess.memberLabelAccess}`
|
||||
);
|
||||
|
||||
result.modifyMemberLabelAccess = {
|
||||
memberLabelAccess,
|
||||
};
|
||||
}
|
||||
|
||||
// addMemberPendingAdminApprovals?: Array<
|
||||
// GroupChange.Actions.AddMemberPendingAdminApprovalAction
|
||||
// >;
|
||||
@@ -6973,6 +7086,7 @@ type DecryptedGroupState = {
|
||||
attributes: number;
|
||||
members: number;
|
||||
addFromInviteLink: number;
|
||||
memberLabel: number;
|
||||
};
|
||||
version?: number;
|
||||
members?: ReadonlyArray<DecryptedMember>;
|
||||
@@ -7042,6 +7156,8 @@ function decryptGroupState(
|
||||
const addFromInviteLink =
|
||||
accessControl.addFromInviteLink ??
|
||||
Proto.AccessControl.AccessRequired.UNKNOWN;
|
||||
const memberLabel =
|
||||
accessControl.memberLabel ?? Proto.AccessControl.AccessRequired.UNKNOWN;
|
||||
|
||||
strictAssert(
|
||||
isValidAccess(attributes),
|
||||
@@ -7060,6 +7176,7 @@ function decryptGroupState(
|
||||
attributes,
|
||||
members,
|
||||
addFromInviteLink,
|
||||
memberLabel,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
1
ts/model-types.d.ts
vendored
1
ts/model-types.d.ts
vendored
@@ -502,6 +502,7 @@ export type ConversationAttributesType = {
|
||||
attributes: AccessRequiredEnum;
|
||||
members: AccessRequiredEnum;
|
||||
addFromInviteLink: AccessRequiredEnum;
|
||||
memberLabel: AccessRequiredEnum;
|
||||
};
|
||||
announcementsOnly?: boolean;
|
||||
avatar?: ContactAvatarType | null;
|
||||
|
||||
@@ -238,6 +238,7 @@ import {
|
||||
applyNewAvatar,
|
||||
buildAccessControlAddFromInviteLinkChange,
|
||||
buildAccessControlAttributesChange,
|
||||
buildAccessControlMemberLabelChange,
|
||||
buildAccessControlMembersChange,
|
||||
buildAddBannedMemberChange,
|
||||
buildAddMember,
|
||||
@@ -4634,8 +4635,6 @@ export class ConversationModel {
|
||||
createGroupChange: async () =>
|
||||
buildInviteLinkPasswordChange(this.attributes, groupInviteLinkPassword),
|
||||
});
|
||||
|
||||
this.set({ groupInviteLinkPassword });
|
||||
}
|
||||
|
||||
async toggleGroupLink(value: boolean): Promise<void> {
|
||||
@@ -4678,18 +4677,6 @@ export class ConversationModel {
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
this.set({
|
||||
accessControl: {
|
||||
addFromInviteLink,
|
||||
attributes: this.get('accessControl')?.attributes || ACCESS_ENUM.MEMBER,
|
||||
members: this.get('accessControl')?.members || ACCESS_ENUM.MEMBER,
|
||||
},
|
||||
});
|
||||
|
||||
if (shouldCreateNewGroupLink) {
|
||||
this.set({ groupInviteLinkPassword });
|
||||
}
|
||||
}
|
||||
|
||||
async updateAccessControlAddFromInviteLink(value: boolean): Promise<void> {
|
||||
@@ -4712,14 +4699,6 @@ export class ConversationModel {
|
||||
addFromInviteLink
|
||||
),
|
||||
});
|
||||
|
||||
this.set({
|
||||
accessControl: {
|
||||
addFromInviteLink,
|
||||
attributes: this.get('accessControl')?.attributes || ACCESS_ENUM.MEMBER,
|
||||
members: this.get('accessControl')?.members || ACCESS_ENUM.MEMBER,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async updateAccessControlAttributes(value: number): Promise<void> {
|
||||
@@ -4733,16 +4712,6 @@ export class ConversationModel {
|
||||
createGroupChange: async () =>
|
||||
buildAccessControlAttributesChange(this.attributes, value),
|
||||
});
|
||||
|
||||
const ACCESS_ENUM = Proto.AccessControl.AccessRequired;
|
||||
this.set({
|
||||
accessControl: {
|
||||
addFromInviteLink:
|
||||
this.get('accessControl')?.addFromInviteLink || ACCESS_ENUM.MEMBER,
|
||||
attributes: value,
|
||||
members: this.get('accessControl')?.members || ACCESS_ENUM.MEMBER,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async updateAccessControlMembers(value: number): Promise<void> {
|
||||
@@ -4756,15 +4725,18 @@ export class ConversationModel {
|
||||
createGroupChange: async () =>
|
||||
buildAccessControlMembersChange(this.attributes, value),
|
||||
});
|
||||
}
|
||||
|
||||
const ACCESS_ENUM = Proto.AccessControl.AccessRequired;
|
||||
this.set({
|
||||
accessControl: {
|
||||
addFromInviteLink:
|
||||
this.get('accessControl')?.addFromInviteLink || ACCESS_ENUM.MEMBER,
|
||||
attributes: this.get('accessControl')?.attributes || ACCESS_ENUM.MEMBER,
|
||||
members: value,
|
||||
},
|
||||
async updateAccessControlMemberLabel(value: number): Promise<void> {
|
||||
if (!isGroupV2(this.attributes)) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.modifyGroupV2({
|
||||
name: 'updateAccessControlMembers',
|
||||
usingCredentialsFrom: [],
|
||||
createGroupChange: async () =>
|
||||
buildAccessControlMemberLabelChange(this.attributes, value),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -4779,8 +4751,6 @@ export class ConversationModel {
|
||||
createGroupChange: async () =>
|
||||
buildAnnouncementsOnlyChange(this.attributes, value),
|
||||
});
|
||||
|
||||
this.set({ announcementsOnly: value });
|
||||
}
|
||||
|
||||
async updateExpirationTimer(
|
||||
|
||||
@@ -2390,6 +2390,16 @@ export class BackupExportStream extends Readable {
|
||||
|
||||
update.groupInviteLinkAdminApprovalUpdate = innerUpdate;
|
||||
updates.push(update);
|
||||
} else if (type === 'access-member-label') {
|
||||
const innerUpdate =
|
||||
new Backups.GroupMemberLabelAccessLevelChangeUpdate();
|
||||
if (from) {
|
||||
innerUpdate.updaterAci = this.#aciToBytesOrUndefined(from);
|
||||
}
|
||||
innerUpdate.accessLevel = detail.newPrivilege;
|
||||
|
||||
update.groupMemberLabelAccessLevelChangeUpdate = innerUpdate;
|
||||
updates.push(update);
|
||||
} else if (type === 'announcements-only') {
|
||||
const innerUpdate = new Backups.GroupAnnouncementOnlyChangeUpdate();
|
||||
if (from) {
|
||||
|
||||
@@ -1261,6 +1261,9 @@ export class BackupImportStream extends Writable {
|
||||
addFromInviteLink:
|
||||
dropNull(accessControl.addFromInviteLink) ??
|
||||
SignalService.AccessControl.AccessRequired.UNKNOWN,
|
||||
memberLabel:
|
||||
dropNull(accessControl.memberLabel) ??
|
||||
SignalService.AccessControl.AccessRequired.UNKNOWN,
|
||||
}
|
||||
: undefined,
|
||||
membersV2: members?.map(
|
||||
@@ -3120,6 +3123,19 @@ export class BackupImportStream extends Writable {
|
||||
SignalService.AccessControl.AccessRequired.UNKNOWN,
|
||||
});
|
||||
}
|
||||
if (update.groupMemberLabelAccessLevelChangeUpdate) {
|
||||
const { updaterAci, accessLevel } =
|
||||
update.groupMemberLabelAccessLevelChangeUpdate;
|
||||
if (updaterAci) {
|
||||
from = fromAciObject(Aci.fromUuidBytes(updaterAci));
|
||||
}
|
||||
details.push({
|
||||
type: 'access-member-label',
|
||||
newPrivilege:
|
||||
dropNull(accessLevel) ??
|
||||
SignalService.AccessControl.AccessRequired.UNKNOWN,
|
||||
});
|
||||
}
|
||||
if (update.groupAttributesAccessLevelChangeUpdate) {
|
||||
const { updaterAci, accessLevel } =
|
||||
update.groupAttributesAccessLevelChangeUpdate;
|
||||
|
||||
@@ -390,6 +390,7 @@ export type ConversationType = ReadonlyDeep<
|
||||
accessControlAddFromInviteLink?: number;
|
||||
accessControlAttributes?: number;
|
||||
accessControlMembers?: number;
|
||||
accessControlMemberLabel?: number;
|
||||
announcementsOnly?: boolean;
|
||||
announcementsOnlyReady?: boolean;
|
||||
expireTimer?: DurationInSeconds;
|
||||
@@ -1253,6 +1254,7 @@ export const actions = {
|
||||
setAccessControlAddFromInviteLinkSetting,
|
||||
setAccessControlAttributesSetting,
|
||||
setAccessControlMembersSetting,
|
||||
setAccessControlMemberLabelSetting,
|
||||
setAnnouncementsOnly,
|
||||
setCenterMessage,
|
||||
setComposeGroupAvatar,
|
||||
@@ -1731,6 +1733,30 @@ function setAccessControlMembersSetting(
|
||||
};
|
||||
}
|
||||
|
||||
function setAccessControlMemberLabelSetting(
|
||||
conversationId: string,
|
||||
value: number
|
||||
): ThunkAction<void, RootStateType, unknown, NoopActionType> {
|
||||
return async dispatch => {
|
||||
const conversation = window.ConversationController.get(conversationId);
|
||||
if (!conversation) {
|
||||
throw new Error(
|
||||
'setAccessControlMemberLabelSetting: No conversation found'
|
||||
);
|
||||
}
|
||||
|
||||
await longRunningTaskWrapper({
|
||||
name: 'updateAccessControlMemberLabel',
|
||||
idForLogging: conversation.idForLogging(),
|
||||
task: async () => conversation.updateAccessControlMemberLabel(value),
|
||||
});
|
||||
dispatch({
|
||||
type: 'NOOP',
|
||||
payload: null,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function setAccessControlAttributesSetting(
|
||||
conversationId: string,
|
||||
value: number
|
||||
|
||||
@@ -242,6 +242,7 @@ export const ConversationPanel = memo(function ConversationPanel({
|
||||
<PanelContainer
|
||||
key={getPanelKey(prevPanel)}
|
||||
conversationId={conversationId}
|
||||
isActive={false}
|
||||
panel={prevPanel}
|
||||
ref={animateRef}
|
||||
/>
|
||||
@@ -257,6 +258,7 @@ export const ConversationPanel = memo(function ConversationPanel({
|
||||
lastPanelDoneAnimating !== prevPanel &&
|
||||
prevPanel && (
|
||||
<PanelContainer
|
||||
isActive={false}
|
||||
conversationId={conversationId}
|
||||
panel={prevPanel}
|
||||
key={getPanelKey(prevPanel)}
|
||||
@@ -283,85 +285,94 @@ export const ConversationPanel = memo(function ConversationPanel({
|
||||
|
||||
type PanelPropsType = {
|
||||
conversationId: string;
|
||||
isActive: boolean;
|
||||
panel: PanelArgsType;
|
||||
};
|
||||
|
||||
const PanelContainer = forwardRef<
|
||||
HTMLDivElement,
|
||||
PanelPropsType & { isActive?: boolean }
|
||||
>(function PanelContainerInner(
|
||||
{ conversationId, isActive, panel },
|
||||
ref
|
||||
): React.JSX.Element {
|
||||
const i18n = useSelector(getIntl);
|
||||
const { popPanelForConversation } = useNavActions();
|
||||
const conversationTitle = getConversationTitleForPanelType(i18n, panel.type);
|
||||
const PanelContainer = forwardRef<HTMLDivElement, PanelPropsType>(
|
||||
function PanelContainerInner(
|
||||
{ conversationId, isActive, panel },
|
||||
ref
|
||||
): React.JSX.Element {
|
||||
const i18n = useSelector(getIntl);
|
||||
const { popPanelForConversation } = useNavActions();
|
||||
const conversationTitle = getConversationTitleForPanelType(
|
||||
i18n,
|
||||
panel.type
|
||||
);
|
||||
|
||||
let info: React.JSX.Element | undefined;
|
||||
if (panel.type === PanelType.AllMedia) {
|
||||
info = <SmartAllMediaHeader />;
|
||||
} else if (conversationTitle != null) {
|
||||
info = (
|
||||
<div className="ConversationPanel__header__info">
|
||||
<div className="ConversationPanel__header__info__title">
|
||||
{conversationTitle}
|
||||
let info: React.JSX.Element | undefined;
|
||||
if (panel.type === PanelType.AllMedia) {
|
||||
info = <SmartAllMediaHeader />;
|
||||
} else if (conversationTitle != null) {
|
||||
info = (
|
||||
<div className="ConversationPanel__header__info">
|
||||
<div className="ConversationPanel__header__info__title">
|
||||
{conversationTitle}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const focusRef = useRef<HTMLDivElement | null>(null);
|
||||
useEffect(() => {
|
||||
if (!isActive) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (panel.type === PanelType.GroupMemberLabelEditor) {
|
||||
return;
|
||||
}
|
||||
|
||||
const focusNode = focusRef.current;
|
||||
if (!focusNode) {
|
||||
return;
|
||||
}
|
||||
|
||||
const elements =
|
||||
focusNode.querySelectorAll<HTMLElement>(focusableSelector);
|
||||
if (!elements.length) {
|
||||
return;
|
||||
}
|
||||
elements[0]?.focus();
|
||||
}, [isActive, panel]);
|
||||
|
||||
return (
|
||||
<div className="ConversationPanel" ref={ref}>
|
||||
<div className="ConversationPanel__header">
|
||||
<button
|
||||
aria-label={i18n('icu:goBack')}
|
||||
className="ConversationPanel__header__back-button"
|
||||
onClick={popPanelForConversation}
|
||||
type="button"
|
||||
/>
|
||||
{info}
|
||||
</div>
|
||||
<SmartMiniPlayer shouldFlow />
|
||||
<div
|
||||
className={classNames(
|
||||
'ConversationPanel__body',
|
||||
panel.type !== PanelType.PinnedMessages &&
|
||||
panel.type !== PanelType.AllMedia &&
|
||||
panel.type !== PanelType.GroupMemberLabelEditor &&
|
||||
'ConversationPanel__body--padding'
|
||||
)}
|
||||
ref={focusRef}
|
||||
>
|
||||
<PanelElement
|
||||
isActive={isActive}
|
||||
conversationId={conversationId}
|
||||
panel={panel}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const focusRef = useRef<HTMLDivElement | null>(null);
|
||||
useEffect(() => {
|
||||
if (!isActive) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (panel.type === PanelType.GroupMemberLabelEditor) {
|
||||
return;
|
||||
}
|
||||
|
||||
const focusNode = focusRef.current;
|
||||
if (!focusNode) {
|
||||
return;
|
||||
}
|
||||
|
||||
const elements = focusNode.querySelectorAll<HTMLElement>(focusableSelector);
|
||||
if (!elements.length) {
|
||||
return;
|
||||
}
|
||||
elements[0]?.focus();
|
||||
}, [isActive, panel]);
|
||||
|
||||
return (
|
||||
<div className="ConversationPanel" ref={ref}>
|
||||
<div className="ConversationPanel__header">
|
||||
<button
|
||||
aria-label={i18n('icu:goBack')}
|
||||
className="ConversationPanel__header__back-button"
|
||||
onClick={popPanelForConversation}
|
||||
type="button"
|
||||
/>
|
||||
{info}
|
||||
</div>
|
||||
<SmartMiniPlayer shouldFlow />
|
||||
<div
|
||||
className={classNames(
|
||||
'ConversationPanel__body',
|
||||
panel.type !== PanelType.PinnedMessages &&
|
||||
panel.type !== PanelType.AllMedia &&
|
||||
panel.type !== PanelType.GroupMemberLabelEditor &&
|
||||
'ConversationPanel__body--padding'
|
||||
)}
|
||||
ref={focusRef}
|
||||
>
|
||||
<PanelElement conversationId={conversationId} panel={panel} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
);
|
||||
|
||||
function PanelElement({
|
||||
conversationId,
|
||||
isActive,
|
||||
panel,
|
||||
}: PanelPropsType): React.JSX.Element | null {
|
||||
if (panel.type === PanelType.AllMedia) {
|
||||
@@ -396,7 +407,12 @@ function PanelElement({
|
||||
}
|
||||
|
||||
if (panel.type === PanelType.GroupMemberLabelEditor) {
|
||||
return <SmartGroupMemberLabelEditor conversationId={conversationId} />;
|
||||
return (
|
||||
<SmartGroupMemberLabelEditor
|
||||
conversationId={conversationId}
|
||||
isActive={isActive}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (panel.type === PanelType.GroupPermissions) {
|
||||
|
||||
@@ -15,16 +15,19 @@ import { createLogger } from '../../logging/log.std.js';
|
||||
import { getPreferredBadgeSelector } from '../selectors/badges.preload.js';
|
||||
import { isNotNil } from '../../util/isNotNil.std.js';
|
||||
import { useNavActions } from '../ducks/nav.std.js';
|
||||
import { getCanAddLabel } from '../../types/GroupMemberLabels.std.js';
|
||||
|
||||
const log = createLogger('SmartGroupMemberLabelEditor');
|
||||
|
||||
export type SmartGroupMemberLabelEditorProps = Readonly<{
|
||||
conversationId: string;
|
||||
isActive: boolean;
|
||||
}>;
|
||||
|
||||
export const SmartGroupMemberLabelEditor = memo(
|
||||
function SmartGroupMemberLabelEditor({
|
||||
conversationId,
|
||||
isActive,
|
||||
}: SmartGroupMemberLabelEditorProps) {
|
||||
const i18n = useSelector(getIntl);
|
||||
const theme = useSelector(getTheme);
|
||||
@@ -47,13 +50,9 @@ export const SmartGroupMemberLabelEditor = memo(
|
||||
const ourMembership = conversation.memberships?.find(
|
||||
membership => membership?.aci === ourAci
|
||||
);
|
||||
if (!ourMembership) {
|
||||
log.warn('User was not found in group, leaving this pane!');
|
||||
popPanelForConversation();
|
||||
return null;
|
||||
}
|
||||
const { labelEmoji: existingLabelEmoji, labelString: existingLabelString } =
|
||||
ourMembership;
|
||||
ourMembership || {};
|
||||
const canAddLabel = getCanAddLabel(conversation, ourMembership);
|
||||
|
||||
const membersWithLabel = (conversation.memberships || [])
|
||||
.map(membership => {
|
||||
@@ -94,11 +93,13 @@ export const SmartGroupMemberLabelEditor = memo(
|
||||
|
||||
return (
|
||||
<GroupMemberLabelEditor
|
||||
canAddLabel={canAddLabel}
|
||||
existingLabelEmoji={existingLabelEmoji}
|
||||
existingLabelString={existingLabelString}
|
||||
getPreferredBadge={getPreferredBadge}
|
||||
group={conversation}
|
||||
i18n={i18n}
|
||||
isActive={isActive}
|
||||
me={me}
|
||||
membersWithLabel={membersWithLabel}
|
||||
ourColor={ourColor}
|
||||
|
||||
@@ -21,6 +21,7 @@ export const SmartGroupV2Permissions = memo(function SmartGroupV2Permissions({
|
||||
const {
|
||||
setAccessControlAttributesSetting,
|
||||
setAccessControlMembersSetting,
|
||||
setAccessControlMemberLabelSetting,
|
||||
setAnnouncementsOnly,
|
||||
} = useConversationsActions();
|
||||
return (
|
||||
@@ -29,6 +30,7 @@ export const SmartGroupV2Permissions = memo(function SmartGroupV2Permissions({
|
||||
conversation={conversation}
|
||||
setAccessControlAttributesSetting={setAccessControlAttributesSetting}
|
||||
setAccessControlMembersSetting={setAccessControlMembersSetting}
|
||||
setAccessControlMemberLabelSetting={setAccessControlMemberLabelSetting}
|
||||
setAnnouncementsOnly={setAnnouncementsOnly}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -5,6 +5,7 @@ import type {
|
||||
ConversationType,
|
||||
MembershipType,
|
||||
} from '../state/ducks/conversations.preload.js';
|
||||
import { SignalService as Proto } from '../protobuf/index.std.js';
|
||||
|
||||
export const missingEmojiPlaceholder = '⍰';
|
||||
|
||||
@@ -25,5 +26,11 @@ export function getCanAddLabel(
|
||||
conversation: ConversationType,
|
||||
membership: MembershipType | undefined
|
||||
): boolean {
|
||||
return Boolean(membership && conversation.type === 'group');
|
||||
return Boolean(
|
||||
membership &&
|
||||
conversation.type === 'group' &&
|
||||
(membership.isAdmin ||
|
||||
conversation.accessControlMemberLabel ===
|
||||
Proto.AccessControl.AccessRequired.MEMBER)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -21,6 +21,10 @@ type GroupV2AccessInviteLinkChangeType = {
|
||||
type: 'access-invite-link';
|
||||
newPrivilege: number;
|
||||
};
|
||||
type GroupV2AccessMemberLabelChangeType = {
|
||||
type: 'access-member-label';
|
||||
newPrivilege: number;
|
||||
};
|
||||
type GroupV2AnnouncementsOnlyChangeType = {
|
||||
type: 'announcements-only';
|
||||
announcementsOnly: boolean;
|
||||
@@ -127,6 +131,7 @@ export type GroupV2ChangeDetailType =
|
||||
| GroupV2AccessAttributesChangeType
|
||||
| GroupV2AccessCreateChangeType
|
||||
| GroupV2AccessInviteLinkChangeType
|
||||
| GroupV2AccessMemberLabelChangeType
|
||||
| GroupV2AccessMembersChangeType
|
||||
| GroupV2AdminApprovalAddOneChangeType
|
||||
| GroupV2AdminApprovalRemoveOneChangeType
|
||||
|
||||
@@ -223,6 +223,7 @@ export function getConversation(model: ConversationModel): ConversationType {
|
||||
accessControlAddFromInviteLink: attributes.accessControl?.addFromInviteLink,
|
||||
accessControlAttributes: attributes.accessControl?.attributes,
|
||||
accessControlMembers: attributes.accessControl?.members,
|
||||
accessControlMemberLabel: attributes.accessControl?.memberLabel,
|
||||
announcementsOnly: Boolean(attributes.announcementsOnly),
|
||||
announcementsOnlyReady: canBeAnnouncementGroup(attributes),
|
||||
expireTimer: attributes.expireTimer,
|
||||
|
||||
Reference in New Issue
Block a user