Release Note Channel: Mute/Unmute UI, hide UI elements

Co-authored-by: yash-signal <yash@signal.org>
This commit is contained in:
automated-signal 2025-02-07 09:47:11 -06:00 committed by GitHub
parent 141cf2d482
commit d93f488cbb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 305 additions and 150 deletions

View file

@ -29,6 +29,8 @@
padding-bottom: 8px;
padding-top: 12px;
user-select: text;
display: flex;
align-items: center;
}
&__title-contact-icon {

View file

@ -0,0 +1,30 @@
// Copyright 2025 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
@use '../mixins';
@use '../variables';
.SignalConversationMuteToggle {
@include mixins.light-theme() {
background-color: variables.$color-white;
border-top: 0.5px solid variables.$color-black-alpha-16;
}
@include mixins.dark-theme() {
background-color: variables.$color-gray-95;
border-top: 0.5px solid variables.$color-gray-65;
}
height: variables.$header-height;
display: flex;
justify-content: center;
align-items: center;
font-size: 14px;
color: variables.$color-ultramarine-light;
&__text {
@include mixins.button-reset;
}
}

View file

@ -159,6 +159,7 @@
@use 'components/SendStoryModal.scss';
@use 'components/ShortcutGuide.scss';
@use 'components/SignalConnectionsModal.scss';
@use 'components/SignalConversationMuteToggle.scss';
@use 'components/Slider.scss';
@use 'components/SpinnerV2.scss';
@use 'components/StagedLinkPreview.scss';

View file

@ -1,7 +1,7 @@
// Copyright 2020 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React, { useContext } from 'react';
import React, { useContext, useState } from 'react';
import { action } from '@storybook/addon-actions';
import type { Meta } from '@storybook/react';
import { IMAGE_JPEG } from '../types/MIME';
@ -125,6 +125,10 @@ export default {
selectedMessageIds: undefined,
toggleSelectMode: action('toggleSelectMode'),
toggleForwardMessagesModal: action('toggleForwardMessagesModal'),
// Signal Conversation
isSignalConversation: false,
isMuted: false,
setMuteExpiration: action('setMuteExpiration'),
},
} satisfies Meta<Props>;
@ -263,3 +267,21 @@ export function NoFormattingMenu(args: Props): JSX.Element {
<CompositionArea {...args} theme={theme} isFormattingEnabled={false} />
);
}
export function SignalConversationMuteToggle(args: Props): JSX.Element {
const theme = useContext(StorybookThemeContext);
const [isMuted, setIsMuted] = useState(true);
function setIsMutedByTime(_: string, muteExpiresAt: number) {
setIsMuted(muteExpiresAt > Date.now());
}
return (
<CompositionArea
{...args}
theme={theme}
isSignalConversation
isMuted={isMuted}
setMuteExpiration={setIsMutedByTime}
/>
);
}

View file

@ -76,6 +76,7 @@ import type { ShowToastAction } from '../state/ducks/toast';
import type { DraftEditMessageType } from '../model-types.d';
import type { ForwardMessagesPayload } from '../state/ducks/globalModals';
import { ForwardMessagesModalType } from './ForwardMessagesModal';
import { SignalConversationMuteToggle } from './conversation/SignalConversationMuteToggle';
export type OwnProps = Readonly<{
acceptedMessageRequest: boolean | null;
@ -118,6 +119,7 @@ export type OwnProps = Readonly<{
recordingState: RecordingState;
messageCompositionId: string;
shouldHidePopovers: boolean | null;
isMuted: boolean;
isSmsOnlyOrUnregistered: boolean | null;
left: boolean | null;
linkPreviewLoading: boolean;
@ -130,6 +132,7 @@ export type OwnProps = Readonly<{
conversationId: string;
files: ReadonlyArray<File>;
}) => unknown;
setMuteExpiration(conversationId: string, muteExpiresAt: number): unknown;
setMediaQualitySetting(conversationId: string, isHQ: boolean): unknown;
sendStickerMessage(
id: string,
@ -239,6 +242,7 @@ export const CompositionArea = memo(function CompositionArea({
imageToBlurHash,
isDisabled,
isSignalConversation,
isMuted,
isActive,
lastEditableMessageId,
messageCompositionId,
@ -254,6 +258,7 @@ export const CompositionArea = memo(function CompositionArea({
shouldHidePopovers,
showToast,
theme,
setMuteExpiration,
// AttachmentList
draftAttachments,
@ -737,8 +742,14 @@ export const CompositionArea = memo(function CompositionArea({
useEscapeHandling(handleEscape);
if (isSignalConversation) {
// TODO DESKTOP-4547
return <div />;
return (
<SignalConversationMuteToggle
conversationId={conversationId}
isMuted={isMuted}
i18n={i18n}
setMuteExpiration={setMuteExpiration}
/>
);
}
if (selectedMessageIds != null) {

View file

@ -47,6 +47,7 @@ function HeaderInfoTitle({
type,
i18n,
isMe,
isSignalConversation,
headerRef,
}: {
name: string | null;
@ -54,8 +55,18 @@ function HeaderInfoTitle({
type: ConversationTypeType;
i18n: LocalizerType;
isMe: boolean;
isSignalConversation: boolean;
headerRef: React.RefObject<HTMLDivElement>;
}) {
if (isSignalConversation) {
return (
<div className="module-ConversationHeader__header__info__title">
<UserText text={title} />
<span className="ContactModal__official-badge" />
</div>
);
}
if (isMe) {
return (
<div className="module-ConversationHeader__header__info__title">
@ -294,6 +305,7 @@ export const ConversationHeader = memo(function ConversationHeader({
theme={theme}
onViewUserStories={onViewUserStories}
onViewConversationDetails={onViewConversationDetails}
isSignalConversation={isSignalConversation ?? false}
/>
{!isSmsOnlyOrUnregistered && !isSignalConversation && (
<OutgoingCallButtons
@ -415,6 +427,7 @@ function HeaderContent({
i18n,
sharedGroupNames,
theme,
isSignalConversation,
onViewUserStories,
onViewConversationDetails,
}: {
@ -425,6 +438,7 @@ function HeaderContent({
i18n: LocalizerType;
sharedGroupNames: ReadonlyArray<string>;
theme: ThemeType;
isSignalConversation: boolean;
onViewUserStories: () => void;
onViewConversationDetails: () => void;
}) {
@ -476,6 +490,7 @@ function HeaderContent({
type={conversation.type}
i18n={i18n}
isMe={conversation.isMe}
isSignalConversation={isSignalConversation}
headerRef={headerRef}
/>
{(conversation.expireTimer != null || conversation.isVerified) && (

View file

@ -0,0 +1,34 @@
// Copyright 2025 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React from 'react';
import type { LocalizerType } from '../../types/I18N';
type Props = {
isMuted: boolean;
i18n: LocalizerType;
setMuteExpiration: (conversationId: string, muteExpiresAt: number) => unknown;
conversationId: string;
};
export function SignalConversationMuteToggle({
isMuted,
i18n,
setMuteExpiration,
conversationId,
}: Props): JSX.Element {
const onMuteToggleClicked = () => {
setMuteExpiration(conversationId, isMuted ? 0 : Number.MAX_SAFE_INTEGER);
};
return (
<div className="SignalConversationMuteToggle">
<button
onClick={onMuteToggleClicked}
type="button"
className="SignalConversationMuteToggle__text"
>
{isMuted ? i18n('icu:unmute') : i18n('icu:mute')}
</button>
</div>
);
}

View file

@ -65,6 +65,7 @@ const createProps = (
i18n,
isAdmin: false,
isGroup: true,
isSignalConversation: false,
leaveGroup: action('leaveGroup'),
loadRecentMediaItems: action('loadRecentMediaItems'),
memberships: times(32, i => ({
@ -244,3 +245,11 @@ export function InAnotherCallIndividual(): JSX.Element {
return <ConversationDetails {...props} hasActiveCall isGroup={false} />;
}
export function SignalConversation(): JSX.Element {
const props = createProps();
return (
<ConversationDetails {...props} isSignalConversation isGroup={false} />
);
}

View file

@ -84,6 +84,7 @@ export type StateProps = {
i18n: LocalizerType;
isAdmin: boolean;
isGroup: boolean;
isSignalConversation: boolean;
groupsInCommon: ReadonlyArray<ConversationType>;
maxGroupSize: number;
maxRecommendedGroupSize: number;
@ -181,6 +182,7 @@ export function ConversationDetails({
i18n,
isAdmin,
isGroup,
isSignalConversation,
leaveGroup,
loadRecentMediaItems,
memberships,
@ -397,6 +399,7 @@ export function ConversationDetails({
i18n={i18n}
isMe={conversation.isMe}
isGroup={isGroup}
isSignalConversation={isSignalConversation}
membersCount={conversation.membersCount ?? null}
startEditing={(isGroupTitle: boolean) => {
setModalState(
@ -424,7 +427,7 @@ export function ConversationDetails({
{i18n('icu:ConversationDetails__HeaderButton--Message')}
</Button>
)}
{!conversation.isMe && (
{!conversation.isMe && !isSignalConversation && (
<>
<ConversationDetailsCallButton
hasActiveCall={hasActiveCall}
@ -477,152 +480,157 @@ export function ConversationDetails({
/>
)}
<PanelSection>
{!isGroup || canEditGroupInfo ? (
<PanelRow
icon={
<ConversationDetailsIcon
ariaLabel={i18n(
'icu:ConversationDetails--disappearing-messages-label'
)}
icon={IconType.timer}
/>
}
info={
isGroup
? i18n(
'icu:ConversationDetails--disappearing-messages-info--group'
)
: i18n(
'icu:ConversationDetails--disappearing-messages-info--direct'
)
}
label={i18n('icu:ConversationDetails--disappearing-messages-label')}
right={
<DisappearingTimerSelect
i18n={i18n}
value={conversation.expireTimer || DurationInSeconds.ZERO}
onChange={value =>
setDisappearingMessages(conversation.id, value)
}
/>
}
/>
) : null}
{canHaveNicknameAndNote(conversation) && (
<PanelRow
icon={
<ConversationDetailsIcon
ariaLabel={i18n('icu:ConversationDetails--nickname-label')}
icon={IconType.edit}
/>
}
label={i18n('icu:ConversationDetails--nickname-label')}
onClick={onOpenEditNicknameAndNoteModal}
actions={
(conversation.nicknameGivenName ||
conversation.nicknameFamilyName ||
conversation.note) && (
<ContextMenu
{!isSignalConversation && (
<PanelSection>
{!isGroup || canEditGroupInfo ? (
<PanelRow
icon={
<ConversationDetailsIcon
ariaLabel={i18n(
'icu:ConversationDetails--disappearing-messages-label'
)}
icon={IconType.timer}
/>
}
info={
isGroup
? i18n(
'icu:ConversationDetails--disappearing-messages-info--group'
)
: i18n(
'icu:ConversationDetails--disappearing-messages-info--direct'
)
}
label={i18n(
'icu:ConversationDetails--disappearing-messages-label'
)}
right={
<DisappearingTimerSelect
i18n={i18n}
portalToRoot
popperOptions={{
placement: 'bottom',
strategy: 'absolute',
}}
menuOptions={[
{
icon: 'ConversationDetails--nickname-actions--delete',
label: i18n(
'icu:ConversationDetails--nickname-actions--delete'
),
onClick: () => {
setModalState(ModalState.ConfirmDeleteNicknameAndNote);
value={conversation.expireTimer || DurationInSeconds.ZERO}
onChange={value =>
setDisappearingMessages(conversation.id, value)
}
/>
}
/>
) : null}
{canHaveNicknameAndNote(conversation) && (
<PanelRow
icon={
<ConversationDetailsIcon
ariaLabel={i18n('icu:ConversationDetails--nickname-label')}
icon={IconType.edit}
/>
}
label={i18n('icu:ConversationDetails--nickname-label')}
onClick={onOpenEditNicknameAndNoteModal}
actions={
(conversation.nicknameGivenName ||
conversation.nicknameFamilyName ||
conversation.note) && (
<ContextMenu
i18n={i18n}
portalToRoot
popperOptions={{
placement: 'bottom',
strategy: 'absolute',
}}
menuOptions={[
{
icon: 'ConversationDetails--nickname-actions--delete',
label: i18n(
'icu:ConversationDetails--nickname-actions--delete'
),
onClick: () => {
setModalState(
ModalState.ConfirmDeleteNicknameAndNote
);
},
},
},
]}
>
{({ onClick }) => {
return (
<button
type="button"
className="ConversationDetails--nickname-actions"
onClick={onClick}
>
<span className="ConversationDetails--nickname-actions-label">
{i18n('icu:ConversationDetails--nickname-actions')}
</span>
</button>
);
]}
>
{({ onClick }) => {
return (
<button
type="button"
className="ConversationDetails--nickname-actions"
onClick={onClick}
>
<span className="ConversationDetails--nickname-actions-label">
{i18n('icu:ConversationDetails--nickname-actions')}
</span>
</button>
);
}}
</ContextMenu>
)
}
/>
)}
{selectedNavTab === NavTab.Chats && (
<PanelRow
icon={
<ConversationDetailsIcon
ariaLabel={i18n('icu:showChatColorEditor')}
icon={IconType.color}
/>
}
label={i18n('icu:showChatColorEditor')}
onClick={() => {
pushPanelForConversation({
type: PanelType.ChatColorEditor,
});
}}
right={
<div
className={`ConversationDetails__chat-color ConversationDetails__chat-color--${conversation.conversationColor}`}
style={{
...getCustomColorStyle(conversation.customColor),
}}
</ContextMenu>
)
}
/>
)}
{selectedNavTab === NavTab.Chats && (
<PanelRow
icon={
<ConversationDetailsIcon
ariaLabel={i18n('icu:showChatColorEditor')}
icon={IconType.color}
/>
}
label={i18n('icu:showChatColorEditor')}
onClick={() => {
pushPanelForConversation({
type: PanelType.ChatColorEditor,
});
}}
right={
<div
className={`ConversationDetails__chat-color ConversationDetails__chat-color--${conversation.conversationColor}`}
style={{
...getCustomColorStyle(conversation.customColor),
}}
/>
}
/>
)}
{isGroup && (
<PanelRow
icon={
<ConversationDetailsIcon
ariaLabel={i18n('icu:ConversationDetails--notifications')}
icon={IconType.notifications}
/>
}
label={i18n('icu:ConversationDetails--notifications')}
onClick={() =>
pushPanelForConversation({
type: PanelType.NotificationSettings,
})
}
right={
conversation.muteExpiresAt
? getMutedUntilText(conversation.muteExpiresAt, i18n)
: undefined
}
/>
)}
{!isGroup && !conversation.isMe && (
<PanelRow
onClick={() => toggleSafetyNumberModal(conversation.id)}
icon={
<ConversationDetailsIcon
ariaLabel={i18n('icu:ConversationDetails__viewSafetyNumber')}
icon={IconType.verify}
/>
}
label={
<div className="ConversationDetails__safety-number">
{i18n('icu:ConversationDetails__viewSafetyNumber')}
</div>
}
/>
)}
</PanelSection>
/>
}
/>
)}
{isGroup && (
<PanelRow
icon={
<ConversationDetailsIcon
ariaLabel={i18n('icu:ConversationDetails--notifications')}
icon={IconType.notifications}
/>
}
label={i18n('icu:ConversationDetails--notifications')}
onClick={() =>
pushPanelForConversation({
type: PanelType.NotificationSettings,
})
}
right={
conversation.muteExpiresAt
? getMutedUntilText(conversation.muteExpiresAt, i18n)
: undefined
}
/>
)}
{!isGroup && !conversation.isMe && (
<PanelRow
onClick={() => toggleSafetyNumberModal(conversation.id)}
icon={
<ConversationDetailsIcon
ariaLabel={i18n('icu:ConversationDetails__viewSafetyNumber')}
icon={IconType.verify}
/>
}
label={
<div className="ConversationDetails__safety-number">
{i18n('icu:ConversationDetails__viewSafetyNumber')}
</div>
}
/>
)}
</PanelSection>
)}
{isGroup && (
<ConversationDetailsMembershipList
canAddNewMembers={canAddNewMembers}
@ -705,7 +713,7 @@ export function ConversationDetails({
showLightbox={showLightbox}
/>
{!isGroup && !conversation.isMe && (
{!isGroup && !conversation.isMe && !isSignalConversation && (
<ConversationDetailsGroups
contactId={conversation.id}
i18n={i18n}

View file

@ -44,6 +44,7 @@ function Wrapper(overrideProps: Partial<Props>) {
membersCount={0}
isGroup
isMe={false}
isSignalConversation={false}
theme={theme}
toggleAboutContactModal={action('toggleAboutContactModal')}
{...overrideProps}

View file

@ -25,6 +25,7 @@ export type Props = {
i18n: LocalizerType;
isGroup: boolean;
isMe: boolean;
isSignalConversation: boolean;
membersCount: number | null;
startEditing: (isGroupTitle: boolean) => void;
toggleAboutContactModal: (contactId: string) => void;
@ -44,6 +45,7 @@ export function ConversationDetailsHeader({
i18n,
isGroup,
isMe,
isSignalConversation,
membersCount,
startEditing,
toggleAboutContactModal,
@ -194,6 +196,13 @@ export function ConversationDetailsHeader({
<span className="ContactModal__official-badge__large" />
</div>
);
} else if (isSignalConversation) {
title = (
<div className="ConversationDetailsHeader__title">
<UserText text={conversation.title} />
<span className="ContactModal__official-badge__large" />
</div>
);
} else if (isGroup) {
title = (
<div className="ConversationDetailsHeader__title">

View file

@ -396,6 +396,7 @@ export class ReleaseNotesFetcher {
await this.#scheduleForNextRun();
this.setTimeoutForNextRun();
window.SignalCI?.handleEvent('release_notes_fetcher_complete', {});
} catch (error) {
const errorString =
error instanceof HTTPError

View file

@ -67,6 +67,7 @@ import { useToastActions } from '../ducks/toast';
import { isShowingAnyModal } from '../selectors/globalModals';
import { isConversationEverUnregistered } from '../../util/isConversationUnregistered';
import { isDirectConversation } from '../../util/whatTypeOfConversation';
import { isConversationMuted } from '../../util/isConversationMuted';
function renderSmartCompositionRecording(
recProps: SmartCompositionRecordingProps
@ -232,6 +233,7 @@ export const SmartCompositionArea = memo(function SmartCompositionArea({
toggleSelectMode,
scrollToMessage,
setMessageToEdit,
setMuteExpiration,
showConversation,
} = useConversationsActions();
const { cancelRecording, completeRecording, startRecording, errorRecording } =
@ -325,7 +327,6 @@ export const SmartCompositionArea = memo(function SmartCompositionArea({
(isConversationSMSOnly(conversation) ||
isConversationEverUnregistered(conversation))
}
isSignalConversation={isSignalConversation(conversation)}
isFetchingUUID={conversation.isFetchingUUID ?? null}
isMissingMandatoryProfileSharing={isMissingRequiredProfileSharing(
conversation
@ -335,6 +336,10 @@ export const SmartCompositionArea = memo(function SmartCompositionArea({
blockConversation={blockConversation}
reportSpam={reportSpam}
deleteConversation={deleteConversation}
// Signal Conversation
isSignalConversation={isSignalConversation(conversation)}
isMuted={isConversationMuted(conversation)}
setMuteExpiration={setMuteExpiration}
// Groups
groupVersion={conversation.groupVersion ?? null}
isGroupV1AndDisabled={conversation.isGroupV1AndDisabled ?? null}

View file

@ -40,6 +40,7 @@ import { useCallingActions } from '../ducks/calling';
import { useSearchActions } from '../ducks/search';
import { useGlobalModalActions } from '../ducks/globalModals';
import { useLightboxActions } from '../ducks/lightbox';
import { isSignalConversation } from '../../util/isSignalConversation';
export type SmartConversationDetailsProps = {
conversationId: string;
@ -193,6 +194,7 @@ export const SmartConversationDetails = memo(function SmartConversationDetails({
i18n={i18n}
isAdmin={isAdmin}
isGroup={isGroup}
isSignalConversation={isSignalConversation(conversation)}
leaveGroup={leaveGroup}
loadRecentMediaItems={loadRecentMediaItems}
maxGroupSize={maxGroupSize}

View file

@ -129,6 +129,10 @@ export class App extends EventEmitter {
return this.#waitForEvent('receipts');
}
public async waitForReleaseNotesFetcher(): Promise<void> {
return this.#waitForEvent('release_notes_fetcher_complete');
}
public async waitForStorageService(): Promise<StorageServiceInfoType> {
return this.#waitForEvent('storageServiceComplete');
}

View file

@ -45,6 +45,7 @@ describe('release notes', function (this: Mocha.Suite) {
it('shows release notes with an image and body ranges', async () => {
const firstWindow = await app.getWindow();
await app.waitForReleaseNotesFetcher();
await firstWindow.evaluate('window.SignalCI.resetReleaseNotesFetcher()');
await app.close();

View file

@ -11,7 +11,7 @@ export function isSignalConversation(conversation: {
const { id, serviceId } = conversation;
if (serviceId) {
return serviceId === SIGNAL_ACI;
return isSignalServiceId(serviceId);
}
return window.ConversationController.isSignalConversationId(id);