Introduce new permission for group member labels

Co-authored-by: Scott Nonnenberg <scott@signal.org>
This commit is contained in:
automated-signal
2026-03-05 16:21:30 -06:00
committed by GitHub
parent 00c123c41a
commit c904502a20
21 changed files with 519 additions and 148 deletions

View File

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

View File

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

View File

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

View File

@@ -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}
/>
);