Merge branch 'main' into HEAD

This commit is contained in:
Scott Nonnenberg 2024-07-30 15:53:28 -07:00
commit fed6bbfc8b
1127 changed files with 263697 additions and 302446 deletions

View file

@ -23,7 +23,7 @@ import type {
} from './conversations';
import * as log from '../../logging/log';
import { isAudio } from '../../types/Attachment';
import { getAttachmentUrlForPath } from '../selectors/message';
import { getLocalAttachmentUrl } from '../../util/getLocalAttachmentUrl';
import { assertDev } from '../../util/assert';
// State
@ -623,7 +623,7 @@ export function reducer(
return state;
}
const url = getAttachmentUrlForPath(attachment.path);
const url = getLocalAttachmentUrl(attachment);
// if we got the url for the current message
if (

View file

@ -2,6 +2,7 @@
// SPDX-License-Identifier: AGPL-3.0-only
import type { ThunkAction } from 'redux-thunk';
import { v4 as generateUuid } from 'uuid';
import type { ReadonlyDeep } from 'type-fest';
import * as log from '../../logging/log';
@ -23,7 +24,7 @@ import {
// State
export type AudioPlayerStateType = ReadonlyDeep<{
export type AudioRecorderStateType = ReadonlyDeep<{
recordingState: RecordingState;
errorDialogAudioRecorderType?: ErrorDialogAudioRecorderType;
}>;
@ -65,6 +66,10 @@ type AudioPlayerActionType = ReadonlyDeep<
| StartRecordingAction
>;
export function getIsRecording(audioRecorder: AudioRecorderStateType): boolean {
return audioRecorder.recordingState === RecordingState.Recording;
}
// Action Creators
export const actions = {
@ -168,6 +173,7 @@ export function completeRecording(
const voiceNoteAttachment: InMemoryAttachmentDraftType = {
pending: false,
clientUuid: generateUuid(),
contentType: stringToMIMEType(blob.type),
data,
size: data.byteLength,
@ -211,16 +217,16 @@ function errorRecording(
// Reducer
export function getEmptyState(): AudioPlayerStateType {
export function getEmptyState(): AudioRecorderStateType {
return {
recordingState: RecordingState.Idle,
};
}
export function reducer(
state: Readonly<AudioPlayerStateType> = getEmptyState(),
state: Readonly<AudioRecorderStateType> = getEmptyState(),
action: Readonly<AudioPlayerActionType>
): AudioPlayerStateType {
): AudioRecorderStateType {
if (action.type === START_RECORDING) {
return {
...state,

View file

@ -4,6 +4,7 @@
import type { ThunkAction } from 'redux-thunk';
import { isEqual, mapValues } from 'lodash';
import type { ReadonlyDeep } from 'type-fest';
import { DataWriter } from '../../sql/Client';
import type { StateType as RootStateType } from '../reducer';
import type { BadgeType, BadgeImageType } from '../../badges/types';
import { getOwn } from '../../util/getOwn';
@ -70,7 +71,7 @@ function updateOrCreate(
// check (e.g., due to a crash), we won't download its image files. In the unlikely
// event that this happens, we'll repair it the next time we check for undownloaded
// image files.
await window.Signal.Data.updateOrCreateBadges(badges);
await DataWriter.updateOrCreateBadges(badges);
dispatch({
type: UPDATE_OR_CREATE,

View file

@ -5,16 +5,24 @@ import type { ReadonlyDeep } from 'type-fest';
import type { ThunkAction } from 'redux-thunk';
import { omit } from 'lodash';
import type { StateType as RootStateType } from '../reducer';
import { clearCallHistoryDataAndSync } from '../../util/callDisposition';
import {
clearCallHistoryDataAndSync,
markAllCallHistoryReadAndSync,
} from '../../util/callDisposition';
import type { BoundActionCreatorsMapObject } from '../../hooks/useBoundActions';
import { useBoundActions } from '../../hooks/useBoundActions';
import type { ToastActionType } from './toast';
import { showToast } from './toast';
import { DataReader, DataWriter } from '../../sql/Client';
import { ToastType } from '../../types/Toast';
import type { CallHistoryDetails } from '../../types/CallDisposition';
import * as log from '../../logging/log';
import * as Errors from '../../types/errors';
import { drop } from '../../util/drop';
import {
getCallHistoryLatestCall,
getCallHistorySelector,
} from '../selectors/callHistory';
export type CallHistoryState = ReadonlyDeep<{
// This informs the app that underlying call history data has changed.
@ -70,7 +78,7 @@ function updateCallHistoryUnreadCount(): ThunkAction<
> {
return async dispatch => {
try {
const unreadCount = await window.Signal.Data.getCallHistoryUnreadCount();
const unreadCount = await DataReader.getCallHistoryUnreadCount();
dispatch({ type: CALL_HISTORY_UPDATE_UNREAD, payload: unreadCount });
} catch (error) {
log.error(
@ -87,7 +95,7 @@ function markCallHistoryRead(
): ThunkAction<void, RootStateType, unknown, CallHistoryUpdateUnread> {
return async dispatch => {
try {
await window.Signal.Data.markCallHistoryRead(callId);
await DataWriter.markCallHistoryRead(callId);
drop(window.ConversationController.get(conversationId)?.updateUnread());
} catch (error) {
log.error(
@ -100,30 +108,41 @@ function markCallHistoryRead(
};
}
function markCallsTabViewed(): ThunkAction<
void,
RootStateType,
unknown,
CallHistoryUpdateUnread
> {
return async dispatch => {
export function markCallHistoryReadInConversation(
callId: string
): ThunkAction<void, RootStateType, unknown, CallHistoryUpdateUnread> {
return async (dispatch, getState) => {
const callHistorySelector = getCallHistorySelector(getState());
const callHistory = callHistorySelector(callId);
if (callHistory == null) {
return;
}
try {
const conversationIds = await window.Signal.Data.markAllCallHistoryRead();
for (const conversationId of conversationIds) {
drop(window.ConversationController.get(conversationId)?.updateUnread());
}
} catch (error) {
log.error(
'markCallsTabViewed: Error marking all call history read',
Errors.toLogFormat(error)
);
await markAllCallHistoryReadAndSync(callHistory, true);
} finally {
dispatch(updateCallHistoryUnreadCount());
}
};
}
function addCallHistory(callHistory: CallHistoryDetails): CallHistoryAdd {
function markCallsTabViewed(): ThunkAction<
void,
RootStateType,
unknown,
CallHistoryUpdateUnread
> {
return async (dispatch, getState) => {
const latestCall = getCallHistoryLatestCall(getState());
if (latestCall != null) {
await markAllCallHistoryReadAndSync(latestCall, false);
dispatch(updateCallHistoryUnreadCount());
}
};
}
export function addCallHistory(
callHistory: CallHistoryDetails
): CallHistoryAdd {
return {
type: CALL_HISTORY_ADD,
payload: callHistory,
@ -149,10 +168,13 @@ function clearAllCallHistory(): ThunkAction<
unknown,
CallHistoryReset | ToastActionType
> {
return async dispatch => {
return async (dispatch, getState) => {
try {
await clearCallHistoryDataAndSync();
dispatch(showToast({ toastType: ToastType.CallHistoryCleared }));
const latestCall = getCallHistoryLatestCall(getState());
if (latestCall != null) {
await clearCallHistoryDataAndSync(latestCall);
dispatch(showToast({ toastType: ToastType.CallHistoryCleared }));
}
} catch (error) {
log.error('Error clearing call history', Errors.toLogFormat(error));
} finally {

File diff suppressed because it is too large Load diff

View file

@ -46,3 +46,12 @@ export const isAnybodyElseInGroupCall = (
peekInfo: undefined | Readonly<Pick<GroupCallPeekInfoType, 'acis'>>,
ourAci: AciString
): boolean => Boolean(peekInfo?.acis.some(id => id !== ourAci));
export const isAnybodyInGroupCall = (
peekInfo: undefined | Readonly<Pick<GroupCallPeekInfoType, 'acis'>>
): boolean => {
if (!peekInfo?.acis) {
return false;
}
return peekInfo.acis.length > 0;
};

View file

@ -18,11 +18,11 @@ import {
isVideoAttachment,
isImageAttachment,
} from '../../types/Attachment';
import { DataReader, DataWriter } from '../../sql/Client';
import type { BoundActionCreatorsMapObject } from '../../hooks/useBoundActions';
import type { DraftBodyRanges } from '../../types/BodyRange';
import { BodyRange } from '../../types/BodyRange';
import type { LinkPreviewType } from '../../types/message/LinkPreviews';
import type { MessageAttributesType } from '../../model-types.d';
import type { ReadonlyMessageAttributesType } from '../../model-types.d';
import type { NoopActionType } from './noop';
import type { ShowToastActionType } from './toast';
import type { StateType as RootStateType } from '../reducer';
@ -34,8 +34,7 @@ import {
} from './linkPreviews';
import { LinkPreviewSourceType } from '../../types/LinkPreview';
import type { AciString } from '../../types/ServiceId';
import { completeRecording } from './audioRecorder';
import { RecordingState } from '../../types/AudioRecorder';
import { completeRecording, getIsRecording } from './audioRecorder';
import { SHOW_TOAST } from './toast';
import type { AnyToast } from '../../types/Toast';
import { ToastType } from '../../types/Toast';
@ -70,8 +69,8 @@ import { resolveDraftAttachmentOnDisk } from '../../util/resolveDraftAttachmentO
import { shouldShowInvalidMessageToast } from '../../util/shouldShowInvalidMessageToast';
import { writeDraftAttachment } from '../../util/writeDraftAttachment';
import { __DEPRECATED$getMessageById } from '../../messages/getMessageById';
import { canReply } from '../selectors/message';
import { getContactId } from '../../messages/helpers';
import { canReply, isNormalBubble } from '../selectors/message';
import { getAuthorId } from '../../messages/helpers';
import { getConversationSelector } from '../selectors/conversations';
import { enqueueReactionForSend } from '../../reactions/enqueueReactionForSend';
import { useBoundActions } from '../../hooks/useBoundActions';
@ -90,8 +89,6 @@ import { drop } from '../../util/drop';
import { strictAssert } from '../../util/assert';
import { makeQuote } from '../../util/makeQuote';
import { sendEditedMessage as doSendEditedMessage } from '../../util/sendEditedMessage';
import { maybeBlockSendForFormattingModal } from '../../util/maybeBlockSendForFormattingModal';
import { maybeBlockSendForEditWarningModal } from '../../util/maybeBlockSendForEditWarningModal';
import { Sound, SoundType } from '../../util/Sound';
import {
isImageTypeSupported,
@ -107,14 +104,17 @@ type ComposerStateByConversationType = {
linkPreviewLoading: boolean;
linkPreviewResult?: LinkPreviewType;
messageCompositionId: string;
quotedMessage?: Pick<MessageAttributesType, 'conversationId' | 'quote'>;
quotedMessage?: Pick<
ReadonlyMessageAttributesType,
'conversationId' | 'quote'
>;
sendCounter: number;
shouldSendHighQualityAttachments?: boolean;
};
// eslint-disable-next-line local-rules/type-alias-readonlydeep
export type QuotedMessageType = Pick<
MessageAttributesType,
ReadonlyMessageAttributesType,
'conversationId' | 'quote'
>;
@ -246,6 +246,7 @@ export const actions = {
removeAttachment,
replaceAttachments,
resetComposer,
saveDraftRecordingIfNeeded,
scrollToQuotedMessage,
sendEditedMessage,
sendMultiMediaMessage,
@ -339,12 +340,12 @@ function scrollToQuotedMessage({
ShowToastActionType | ScrollToMessageActionType
> {
return async (dispatch, getState) => {
const messages = await window.Signal.Data.getMessagesBySentAt(sentAt);
const messages = await DataReader.getMessagesBySentAt(sentAt);
const message = messages.find(item =>
Boolean(
item.conversationId === conversationId &&
authorId &&
getContactId(item) === authorId
getAuthorId(item) === authorId
)
);
@ -366,23 +367,33 @@ function scrollToQuotedMessage({
};
}
export function handleLeaveConversation(
conversationId: string
): ThunkAction<void, RootStateType, unknown, never> {
export function saveDraftRecordingIfNeeded(): ThunkAction<
void,
RootStateType,
unknown,
never
> {
return (dispatch, getState) => {
const { audioRecorder } = getState();
const { conversations, audioRecorder } = getState();
const { selectedConversationId: conversationId } = conversations;
if (audioRecorder.recordingState !== RecordingState.Recording) {
if (!getIsRecording(audioRecorder) || !conversationId) {
return;
}
// save draft of voice note
dispatch(
completeRecording(conversationId, attachment => {
dispatch(
addPendingAttachment(conversationId, { ...attachment, pending: true })
);
dispatch(addAttachment(conversationId, attachment));
const conversation = window.ConversationController.get(conversationId);
if (!conversation) {
throw new Error('saveDraftRecordingIfNeeded: No conversation found');
}
drop(conversation.updateLastMessage());
})
);
};
@ -390,9 +401,7 @@ export function handleLeaveConversation(
// eslint-disable-next-line local-rules/type-alias-readonlydeep
type WithPreSendChecksOptions = Readonly<{
bodyRanges?: DraftBodyRanges;
message?: string;
isEditedMessage?: boolean;
voiceNoteAttachment?: InMemoryAttachmentDraftType;
}>;
@ -416,7 +425,7 @@ async function withPreSendChecks(
conversation.attributes,
]);
const { bodyRanges, isEditedMessage, message, voiceNoteAttachment } = options;
const { message, voiceNoteAttachment } = options;
try {
dispatch(setComposerDisabledState(conversationId, true));
@ -438,45 +447,6 @@ async function withPreSendChecks(
return;
}
try {
const hasFormatting = bodyRanges?.some(BodyRange.isFormatting);
if (hasFormatting && !window.storage.get('formattingWarningShown')) {
const sendAnyway = await maybeBlockSendForFormattingModal();
if (!sendAnyway) {
dispatch(setComposerDisabledState(conversationId, false));
return;
}
drop(window.storage.put('formattingWarningShown', true));
}
} catch (error) {
log.error(
'withPreSendChecks block for formatting modal:',
Errors.toLogFormat(error)
);
return;
}
try {
if (
isEditedMessage &&
!window.storage.get('sendEditWarningShown') &&
!window.SignalCI
) {
const sendAnyway = await maybeBlockSendForEditWarningModal();
if (!sendAnyway) {
dispatch(setComposerDisabledState(conversationId, false));
return;
}
drop(window.storage.put('sendEditWarningShown', true));
}
} catch (error) {
log.error(
'withPreSendChecks block for send edit warning modal:',
Errors.toLogFormat(error)
);
return;
}
const toast = shouldShowInvalidMessageToast(conversation.attributes);
if (toast != null) {
dispatch({
@ -510,6 +480,7 @@ async function withPreSendChecks(
function sendEditedMessage(
conversationId: string,
options: WithPreSendChecksOptions & {
bodyRanges?: DraftBodyRanges;
targetMessageId: string;
quoteAuthorAci?: AciString;
quoteSentAt?: number;
@ -534,39 +505,35 @@ function sendEditedMessage(
targetMessageId,
} = options;
await withPreSendChecks(
conversationId,
{ ...options, isEditedMessage: true },
dispatch,
async () => {
try {
await doSendEditedMessage(conversationId, {
body: message,
bodyRanges,
preview: getLinkPreviewForSend(message),
quoteAuthorAci,
quoteSentAt,
targetMessageId,
await withPreSendChecks(conversationId, options, dispatch, async () => {
try {
await doSendEditedMessage(conversationId, {
body: message,
bodyRanges,
preview: getLinkPreviewForSend(message),
quoteAuthorAci,
quoteSentAt,
targetMessageId,
});
} catch (error) {
log.error('sendEditedMessage', Errors.toLogFormat(error));
if (error.toastType) {
dispatch({
type: SHOW_TOAST,
payload: {
toastType: error.toastType,
},
});
} catch (error) {
log.error('sendEditedMessage', Errors.toLogFormat(error));
if (error.toastType) {
dispatch({
type: SHOW_TOAST,
payload: {
toastType: error.toastType,
},
});
}
}
}
);
});
};
}
function sendMultiMediaMessage(
conversationId: string,
options: WithPreSendChecksOptions & {
bodyRanges?: DraftBodyRanges;
draftAttachments?: ReadonlyArray<AttachmentDraftType>;
timestamp?: number;
}
@ -780,7 +747,7 @@ export function setQuoteByMessageId(
return;
}
if (message && !message.isNormalBubble()) {
if (message && !isNormalBubble(message.attributes)) {
return;
}
@ -802,7 +769,7 @@ export function setQuoteByMessageId(
timestamp,
});
window.Signal.Data.updateConversation(conversation.attributes);
await DataWriter.updateConversation(conversation.attributes);
}
if (message) {
@ -836,6 +803,7 @@ function addAttachment(
// We do async operations first so multiple in-process addAttachments don't stomp on
// each other.
const onDisk = await writeDraftAttachment(attachment);
const toAdd = { ...onDisk, clientUuid: generateUuid() };
const state = getState();
@ -859,7 +827,7 @@ function addAttachment(
// User has canceled the draft so we don't need to continue processing
if (!hasDraftAttachmentPending) {
await deleteDraftAttachment(onDisk);
await deleteDraftAttachment(toAdd);
return;
}
@ -872,9 +840,9 @@ function addAttachment(
log.warn(
`addAttachment: Failed to find pending attachment with path ${attachment.path}`
);
nextAttachments = [...draftAttachments, onDisk];
nextAttachments = [...draftAttachments, toAdd];
} else {
nextAttachments = replaceIndex(draftAttachments, index, onDisk);
nextAttachments = replaceIndex(draftAttachments, index, toAdd);
}
replaceAttachments(conversationId, nextAttachments)(
@ -902,7 +870,7 @@ function addAttachment(
});
}
window.Signal.Data.updateConversation(conversation.attributes);
await DataWriter.updateConversation(conversation.attributes);
}
};
}
@ -940,7 +908,7 @@ function addPendingAttachment(
if (conversation) {
conversation.attributes.draftAttachments = nextAttachments;
conversation.attributes.draftChanged = true;
window.Signal.Data.updateConversation(conversation.attributes);
drop(DataWriter.updateConversation(conversation.attributes));
}
};
}
@ -1061,11 +1029,9 @@ function processAttachments({
return;
}
const state = getState();
const isRecording =
state.audioRecorder.recordingState === RecordingState.Recording;
const { audioRecorder } = getState();
if (hasLinkPreviewLoaded() || isRecording) {
if (hasLinkPreviewLoaded() || getIsRecording(audioRecorder)) {
return;
}
@ -1204,6 +1170,7 @@ function getPendingAttachment(file: File): AttachmentDraftType | undefined {
return {
contentType: fileType,
clientUuid: generateUuid(),
fileName,
size: file.size,
path: file.name,
@ -1238,7 +1205,7 @@ function removeAttachment(
if (conversation) {
conversation.attributes.draftAttachments = nextAttachments;
conversation.attributes.draftChanged = true;
window.Signal.Data.updateConversation(conversation.attributes);
await DataWriter.updateConversation(conversation.attributes);
}
replaceAttachments(conversationId, nextAttachments)(
@ -1349,7 +1316,7 @@ function saveDraft(
draftChanged: true,
draftBodyRanges: [],
});
window.Signal.Data.updateConversation(conversation.attributes);
drop(DataWriter.updateConversation(conversation.attributes));
return;
}
@ -1373,7 +1340,7 @@ function saveDraft(
draftChanged: true,
timestamp,
});
window.Signal.Data.updateConversation(conversation.attributes);
drop(DataWriter.updateConversation(conversation.attributes));
}
}

File diff suppressed because it is too large Load diff

View file

@ -11,6 +11,8 @@ import type { StateType as RootStateType } from '../reducer';
import { showToast } from './toast';
import type { ShowToastActionType } from './toast';
import type { PromiseAction } from '../util';
import type { BoundActionCreatorsMapObject } from '../../hooks/useBoundActions';
import { useBoundActions } from '../../hooks/useBoundActions';
// State
@ -44,6 +46,10 @@ export const actions = {
eraseCrashReports,
};
export const useCrashReportsActions = (): BoundActionCreatorsMapObject<
typeof actions
> => useBoundActions(actions);
function setCrashReportCount(count: number): SetCrashReportCountActionType {
return { type: SET_COUNT, payload: count };
}

View file

@ -5,11 +5,11 @@ import { take, uniq } from 'lodash';
import type { ThunkAction } from 'redux-thunk';
import type { ReadonlyDeep } from 'type-fest';
import type { EmojiPickDataType } from '../../components/emoji/EmojiPicker';
import dataInterface from '../../sql/Client';
import { DataWriter } from '../../sql/Client';
import type { BoundActionCreatorsMapObject } from '../../hooks/useBoundActions';
import { useBoundActions } from '../../hooks/useBoundActions';
const { updateEmojiUsage } = dataInterface;
const { updateEmojiUsage } = DataWriter;
// State
@ -33,8 +33,9 @@ export const actions = {
useEmoji,
};
export const useActions = (): BoundActionCreatorsMapObject<typeof actions> =>
useBoundActions(actions);
export const useEmojisActions = (): BoundActionCreatorsMapObject<
typeof actions
> => useBoundActions(actions);
function onUseEmoji({
shortName,

View file

@ -6,7 +6,7 @@ import type { ReadonlyDeep } from 'type-fest';
import type { ExplodePromiseResultType } from '../../util/explodePromise';
import type {
GroupV2PendingMemberType,
MessageAttributesType,
ReadonlyMessageAttributesType,
} from '../../model-types.d';
import type {
MessageChangedActionType,
@ -18,7 +18,6 @@ import type { RecipientsByConversation } from './stories';
import type { SafetyNumberChangeSource } from '../../components/SafetyNumberChangeDialog';
import type { EditState as ProfileEditorEditState } from '../../components/ProfileEditor';
import type { StateType as RootStateType } from '../reducer';
import * as Errors from '../../types/errors';
import * as SingleServePromise from '../../services/singleServePromise';
import * as Stickers from '../../types/Stickers';
import { UsernameOnboardingState } from '../../types/globalModals';
@ -28,26 +27,37 @@ import type { BoundActionCreatorsMapObject } from '../../hooks/useBoundActions';
import { longRunningTaskWrapper } from '../../util/longRunningTaskWrapper';
import { useBoundActions } from '../../hooks/useBoundActions';
import { isGroupV1 } from '../../util/whatTypeOfConversation';
import { authorizeArtCreator } from '../../textsecure/authorizeArtCreator';
import type { AuthorizeArtCreatorOptionsType } from '../../textsecure/authorizeArtCreator';
import { getGroupMigrationMembers } from '../../groups';
import { ToastType } from '../../types/Toast';
import {
MESSAGE_CHANGED,
MESSAGE_DELETED,
MESSAGE_EXPIRED,
actions as conversationsActions,
} from './conversations';
import { SHOW_TOAST } from './toast';
import type { ShowToastActionType } from './toast';
import { isDownloaded } from '../../types/Attachment';
import type { ButtonVariant } from '../../components/Button';
import type { MessageRequestState } from '../../components/conversation/MessageRequestActionsConfirmation';
import type { MessageForwardDraft } from '../../types/ForwardDraft';
import { hydrateRanges } from '../../types/BodyRange';
import {
getConversationSelector,
type GetConversationByIdType,
} from '../selectors/conversations';
import { missingCaseError } from '../../util/missingCaseError';
import { ForwardMessagesModalType } from '../../components/ForwardMessagesModal';
import type { CallLinkType } from '../../types/CallLink';
import type { LocalizerType } from '../../types/I18N';
import { linkCallRoute } from '../../util/signalRoutes';
import type { StartCallData } from '../../components/ConfirmLeaveCallModal';
// State
export type EditHistoryMessagesType = ReadonlyDeep<
Array<MessageAttributesType>
Array<ReadonlyMessageAttributesType>
>;
export type EditNicknameAndNoteModalPropsType = ReadonlyDeep<{
conversationId: string;
}>;
export type DeleteMessagesPropsType = ReadonlyDeep<{
conversationId: string;
messageIds: ReadonlyArray<string>;
@ -55,21 +65,21 @@ export type DeleteMessagesPropsType = ReadonlyDeep<{
}>;
export type ForwardMessagePropsType = ReadonlyDeep<MessagePropsType>;
export type ForwardMessagesPropsType = ReadonlyDeep<{
messages: Array<ForwardMessagePropsType>;
type: ForwardMessagesModalType;
messageDrafts: Array<MessageForwardDraft>;
onForward?: () => void;
}>;
export type MessageRequestActionsConfirmationPropsType = ReadonlyDeep<{
conversationId: string;
state: MessageRequestState;
}>;
export type NotePreviewModalPropsType = ReadonlyDeep<{
conversationId: string;
}>;
export type SafetyNumberChangedBlockingDataType = ReadonlyDeep<{
promiseUuid: SingleServePromise.SingleServePromiseIdString;
source?: SafetyNumberChangeSource;
}>;
export type FormattingWarningDataType = ReadonlyDeep<{
explodedPromise: ExplodePromiseResultType<boolean>;
}>;
export type SendEditWarningDataType = ReadonlyDeep<{
explodedPromise: ExplodePromiseResultType<boolean>;
}>;
export type AuthorizeArtCreatorDataType =
ReadonlyDeep<AuthorizeArtCreatorOptionsType>;
type MigrateToGV2PropsType = ReadonlyDeep<{
areWeInvited: boolean;
@ -82,31 +92,33 @@ type MigrateToGV2PropsType = ReadonlyDeep<{
export type GlobalModalsStateType = ReadonlyDeep<{
addUserToAnotherGroupModalContactId?: string;
aboutContactModalContactId?: string;
authArtCreatorData?: AuthorizeArtCreatorDataType;
callLinkAddNameModalRoomId: string | null;
callLinkEditModalRoomId: string | null;
confirmLeaveCallModalState: StartCallData | null;
contactModalState?: ContactModalStateType;
deleteMessagesProps?: DeleteMessagesPropsType;
editHistoryMessages?: EditHistoryMessagesType;
editNicknameAndNoteModalProps: EditNicknameAndNoteModalPropsType | null;
errorModalProps?: {
buttonVariant?: ButtonVariant;
description?: string;
title?: string;
};
formattingWarningData?: FormattingWarningDataType;
forwardMessagesProps?: ForwardMessagesPropsType;
gv2MigrationProps?: MigrateToGV2PropsType;
hasConfirmationModal: boolean;
isAuthorizingArtCreator?: boolean;
isProfileEditorVisible: boolean;
isShortcutGuideModalVisible: boolean;
isSignalConnectionsVisible: boolean;
isStoriesSettingsVisible: boolean;
isWhatsNewVisible: boolean;
messageRequestActionsConfirmationProps: MessageRequestActionsConfirmationPropsType | null;
notePreviewModalProps: NotePreviewModalPropsType | null;
usernameOnboardingState: UsernameOnboardingState;
profileEditorHasError: boolean;
profileEditorInitialEditState: ProfileEditorEditState | undefined;
safetyNumberChangedBlockingData?: SafetyNumberChangedBlockingDataType;
safetyNumberModalContactId?: string;
sendEditWarningData?: SendEditWarningDataType;
stickerPackPreviewId?: string;
userNotFoundModalState?: UserNotFoundModalStateType;
}>;
@ -127,12 +139,16 @@ const TOGGLE_DELETE_MESSAGES_MODAL =
'globalModals/TOGGLE_DELETE_MESSAGES_MODAL';
const TOGGLE_FORWARD_MESSAGES_MODAL =
'globalModals/TOGGLE_FORWARD_MESSAGES_MODAL';
const TOGGLE_NOTE_PREVIEW_MODAL = 'globalModals/TOGGLE_NOTE_PREVIEW_MODAL';
const TOGGLE_PROFILE_EDITOR = 'globalModals/TOGGLE_PROFILE_EDITOR';
export const TOGGLE_PROFILE_EDITOR_ERROR =
'globalModals/TOGGLE_PROFILE_EDITOR_ERROR';
const TOGGLE_SAFETY_NUMBER_MODAL = 'globalModals/TOGGLE_SAFETY_NUMBER_MODAL';
const TOGGLE_ADD_USER_TO_ANOTHER_GROUP_MODAL =
'globalModals/TOGGLE_ADD_USER_TO_ANOTHER_GROUP_MODAL';
const TOGGLE_CALL_LINK_ADD_NAME_MODAL =
'globalModals/TOGGLE_CALL_LINK_ADD_NAME_MODAL';
const TOGGLE_CALL_LINK_EDIT_MODAL = 'globalModals/TOGGLE_CALL_LINK_EDIT_MODAL';
const TOGGLE_ABOUT_MODAL = 'globalModals/TOGGLE_ABOUT_MODAL';
const TOGGLE_SIGNAL_CONNECTIONS_MODAL =
'globalModals/TOGGLE_SIGNAL_CONNECTIONS_MODAL';
@ -144,22 +160,18 @@ const SHOW_STICKER_PACK_PREVIEW = 'globalModals/SHOW_STICKER_PACK_PREVIEW';
const CLOSE_STICKER_PACK_PREVIEW = 'globalModals/CLOSE_STICKER_PACK_PREVIEW';
const CLOSE_ERROR_MODAL = 'globalModals/CLOSE_ERROR_MODAL';
export const SHOW_ERROR_MODAL = 'globalModals/SHOW_ERROR_MODAL';
const SHOW_FORMATTING_WARNING_MODAL =
'globalModals/SHOW_FORMATTING_WARNING_MODAL';
const SHOW_SEND_EDIT_WARNING_MODAL =
'globalModals/SHOW_SEND_EDIT_WARNING_MODAL';
const TOGGLE_EDIT_NICKNAME_AND_NOTE_MODAL =
'globalModals/TOGGLE_EDIT_NICKNAME_AND_NOTE_MODAL';
const TOGGLE_MESSAGE_REQUEST_ACTIONS_CONFIRMATION =
'globalModals/TOGGLE_MESSAGE_REQUEST_ACTIONS_CONFIRMATION';
const CLOSE_SHORTCUT_GUIDE_MODAL = 'globalModals/CLOSE_SHORTCUT_GUIDE_MODAL';
const SHOW_SHORTCUT_GUIDE_MODAL = 'globalModals/SHOW_SHORTCUT_GUIDE_MODAL';
const SHOW_AUTH_ART_CREATOR = 'globalModals/SHOW_AUTH_ART_CREATOR';
const TOGGLE_CONFIRMATION_MODAL = 'globalModals/TOGGLE_CONFIRMATION_MODAL';
const CANCEL_AUTH_ART_CREATOR = 'globalModals/CANCEL_AUTH_ART_CREATOR';
const CONFIRM_AUTH_ART_CREATOR_PENDING =
'globalModals/CONFIRM_AUTH_ART_CREATOR_PENDING';
const CONFIRM_AUTH_ART_CREATOR_FULFILLED =
'globalModals/CONFIRM_AUTH_ART_CREATOR_FULFILLED';
const SHOW_EDIT_HISTORY_MODAL = 'globalModals/SHOW_EDIT_HISTORY_MODAL';
const CLOSE_EDIT_HISTORY_MODAL = 'globalModals/CLOSE_EDIT_HISTORY_MODAL';
const TOGGLE_USERNAME_ONBOARDING = 'globalModals/TOGGLE_USERNAME_ONBOARDING';
const TOGGLE_CONFIRM_LEAVE_CALL_MODAL =
'globalModals/TOGGLE_CONFIRM_LEAVE_CALL_MODAL';
export type ContactModalStateType = ReadonlyDeep<{
contactId: string;
@ -213,6 +225,16 @@ type ToggleForwardMessagesModalActionType = ReadonlyDeep<{
payload: ForwardMessagesPropsType | undefined;
}>;
export type ToggleConfirmLeaveCallModalActionType = ReadonlyDeep<{
type: typeof TOGGLE_CONFIRM_LEAVE_CALL_MODAL;
payload: StartCallData | null;
}>;
type ToggleNotePreviewModalActionType = ReadonlyDeep<{
type: typeof TOGGLE_NOTE_PREVIEW_MODAL;
payload: NotePreviewModalPropsType | null;
}>;
type ToggleProfileEditorActionType = ReadonlyDeep<{
type: typeof TOGGLE_PROFILE_EDITOR;
payload: {
@ -234,6 +256,16 @@ type ToggleAddUserToAnotherGroupModalActionType = ReadonlyDeep<{
payload: string | undefined;
}>;
type ToggleCallLinkAddNameModalActionType = ReadonlyDeep<{
type: typeof TOGGLE_CALL_LINK_ADD_NAME_MODAL;
payload: string | null;
}>;
type ToggleCallLinkEditModalActionType = ReadonlyDeep<{
type: typeof TOGGLE_CALL_LINK_EDIT_MODAL;
payload: string | null;
}>;
type ToggleAboutContactModalActionType = ReadonlyDeep<{
type: typeof TOGGLE_ABOUT_MODAL;
payload: string | undefined;
@ -256,20 +288,6 @@ type ShowStoriesSettingsActionType = ReadonlyDeep<{
type: typeof SHOW_STORIES_SETTINGS;
}>;
type ShowFormattingWarningModalActionType = ReadonlyDeep<{
type: typeof SHOW_FORMATTING_WARNING_MODAL;
payload: {
explodedPromise: ExplodePromiseResultType<boolean> | undefined;
};
}>;
type ShowSendEditWarningModalActionType = ReadonlyDeep<{
type: typeof SHOW_SEND_EDIT_WARNING_MODAL;
payload: {
explodedPromise: ExplodePromiseResultType<boolean> | undefined;
};
}>;
type HideStoriesSettingsActionType = ReadonlyDeep<{
type: typeof HIDE_STORIES_SETTINGS;
}>;
@ -316,6 +334,16 @@ export type ShowErrorModalActionType = ReadonlyDeep<{
};
}>;
type ToggleEditNicknameAndNoteModalActionType = ReadonlyDeep<{
type: typeof TOGGLE_EDIT_NICKNAME_AND_NOTE_MODAL;
payload: EditNicknameAndNoteModalPropsType | null;
}>;
type ToggleMessageRequestActionsConfirmationActionType = ReadonlyDeep<{
type: typeof TOGGLE_MESSAGE_REQUEST_ACTIONS_CONFIRMATION;
payload: MessageRequestActionsConfirmationPropsType | null;
}>;
type CloseShortcutGuideModalActionType = ReadonlyDeep<{
type: typeof CLOSE_SHORTCUT_GUIDE_MODAL;
}>;
@ -324,23 +352,6 @@ type ShowShortcutGuideModalActionType = ReadonlyDeep<{
type: typeof SHOW_SHORTCUT_GUIDE_MODAL;
}>;
export type ShowAuthArtCreatorActionType = ReadonlyDeep<{
type: typeof SHOW_AUTH_ART_CREATOR;
payload: AuthorizeArtCreatorDataType;
}>;
type CancelAuthArtCreatorActionType = ReadonlyDeep<{
type: typeof CANCEL_AUTH_ART_CREATOR;
}>;
type ConfirmAuthArtCreatorPendingActionType = ReadonlyDeep<{
type: typeof CONFIRM_AUTH_ART_CREATOR_PENDING;
}>;
type ConfirmAuthArtCreatorFulfilledActionType = ReadonlyDeep<{
type: typeof CONFIRM_AUTH_ART_CREATOR_FULFILLED;
}>;
type ShowEditHistoryModalActionType = ReadonlyDeep<{
type: typeof SHOW_EDIT_HISTORY_MODAL;
payload: {
@ -353,14 +364,11 @@ type CloseEditHistoryModalActionType = ReadonlyDeep<{
}>;
export type GlobalModalsActionType = ReadonlyDeep<
| CancelAuthArtCreatorActionType
| CloseEditHistoryModalActionType
| CloseErrorModalActionType
| CloseGV2MigrationDialogActionType
| CloseShortcutGuideModalActionType
| CloseStickerPackPreviewActionType
| ConfirmAuthArtCreatorFulfilledActionType
| ConfirmAuthArtCreatorPendingActionType
| HideContactModalActionType
| HideSendAnywayDialogActiontype
| HideStoriesSettingsActionType
@ -369,13 +377,12 @@ export type GlobalModalsActionType = ReadonlyDeep<
| MessageChangedActionType
| MessageDeletedActionType
| MessageExpiredActionType
| ShowAuthArtCreatorActionType
| ShowContactModalActionType
| ShowEditHistoryModalActionType
| ShowErrorModalActionType
| ShowFormattingWarningModalActionType
| ToggleEditNicknameAndNoteModalActionType
| ToggleMessageRequestActionsConfirmationActionType
| ShowSendAnywayDialogActionType
| ShowSendEditWarningModalActionType
| ShowShortcutGuideModalActionType
| ShowStickerPackPreviewActionType
| ShowStoriesSettingsActionType
@ -384,9 +391,13 @@ export type GlobalModalsActionType = ReadonlyDeep<
| StartMigrationToGV2ActionType
| ToggleAboutContactModalActionType
| ToggleAddUserToAnotherGroupModalActionType
| ToggleCallLinkAddNameModalActionType
| ToggleCallLinkEditModalActionType
| ToggleConfirmationModalActionType
| ToggleConfirmLeaveCallModalActionType
| ToggleDeleteMessagesModalActionType
| ToggleForwardMessagesModalActionType
| ToggleNotePreviewModalActionType
| ToggleProfileEditorActionType
| ToggleProfileEditorErrorActionType
| ToggleSafetyNumberModalActionType
@ -397,26 +408,24 @@ export type GlobalModalsActionType = ReadonlyDeep<
// Action Creators
export const actions = {
cancelAuthorizeArtCreator,
closeEditHistoryModal,
closeErrorModal,
closeGV2MigrationDialog,
closeShortcutGuideModal,
closeStickerPackPreview,
confirmAuthorizeArtCreator,
hideBlockingSafetyNumberChangeDialog,
hideContactModal,
hideStoriesSettings,
hideUserNotFoundModal,
hideWhatsNewModal,
showAuthorizeArtCreator,
showBlockingSafetyNumberChangeDialog,
showContactModal,
showEditHistoryModal,
showErrorModal,
showFormattingWarningModal,
showSendEditWarningModal,
toggleEditNicknameAndNoteModal,
toggleMessageRequestActionsConfirmation,
showGV2MigrationDialog,
showShareCallLinkViaSignal,
showShortcutGuideModal,
showStickerPackPreview,
showStoriesSettings,
@ -424,9 +433,13 @@ export const actions = {
showWhatsNewModal,
toggleAboutContactModal,
toggleAddUserToAnotherGroupModal,
toggleCallLinkAddNameModal,
toggleCallLinkEditModal,
toggleConfirmationModal,
toggleConfirmLeaveCallModal,
toggleDeleteMessagesModal,
toggleForwardMessagesModal,
toggleNotePreviewModal,
toggleProfileEditor,
toggleProfileEditorHasError,
toggleSafetyNumberModal,
@ -492,18 +505,6 @@ function showStoriesSettings(): ShowStoriesSettingsActionType {
return { type: SHOW_STORIES_SETTINGS };
}
function showFormattingWarningModal(
explodedPromise: ExplodePromiseResultType<boolean> | undefined
): ShowFormattingWarningModalActionType {
return { type: SHOW_FORMATTING_WARNING_MODAL, payload: { explodedPromise } };
}
function showSendEditWarningModal(
explodedPromise: ExplodePromiseResultType<boolean> | undefined
): ShowSendEditWarningModalActionType {
return { type: SHOW_SEND_EDIT_WARNING_MODAL, payload: { explodedPromise } };
}
function showGV2MigrationDialog(
conversationId: string
): ThunkAction<void, RootStateType, unknown, StartMigrationToGV2ActionType> {
@ -564,8 +565,34 @@ function toggleDeleteMessagesModal(
};
}
function toMessageForwardDraft(
props: ForwardMessagePropsType,
getConversation: GetConversationByIdType
): MessageForwardDraft {
return {
attachments: props.attachments ?? [],
bodyRanges: hydrateRanges(props.bodyRanges, getConversation),
hasContact: Boolean(props.contact),
isSticker: Boolean(props.isSticker),
messageBody: props.text,
originalMessageId: props.id,
previews: props.previews ?? [],
};
}
export type ForwardMessagesPayload = ReadonlyDeep<
| {
type: ForwardMessagesModalType.Forward;
messageIds: ReadonlyArray<string>;
}
| {
type: ForwardMessagesModalType.ShareCallLink;
draft: MessageForwardDraft;
}
>;
function toggleForwardMessagesModal(
messageIds?: ReadonlyArray<string>,
payload: ForwardMessagesPayload | null,
onForward?: () => void
): ThunkAction<
void,
@ -574,7 +601,7 @@ function toggleForwardMessagesModal(
ToggleForwardMessagesModalActionType
> {
return async (dispatch, getState) => {
if (!messageIds) {
if (payload == null) {
dispatch({
type: TOGGLE_FORWARD_MESSAGES_MODAL,
payload: undefined,
@ -582,35 +609,108 @@ function toggleForwardMessagesModal(
return;
}
const messagesProps = await Promise.all(
messageIds.map(async messageId => {
const messageAttributes = await window.MessageCache.resolveAttributes(
'toggleForwardMessagesModal',
messageId
);
let messageDrafts: ReadonlyArray<MessageForwardDraft>;
const { attachments = [] } = messageAttributes;
if (!attachments.every(isDownloaded)) {
dispatch(
conversationsActions.kickOffAttachmentDownload({ messageId })
if (payload.type === ForwardMessagesModalType.Forward) {
messageDrafts = await Promise.all(
payload.messageIds.map(async messageId => {
const messageAttributes = await window.MessageCache.resolveAttributes(
'toggleForwardMessagesModal',
messageId
);
}
const messagePropsSelector = getMessagePropsSelector(getState());
const messageProps = messagePropsSelector(messageAttributes);
const { attachments = [] } = messageAttributes;
return messageProps;
})
);
if (!attachments.every(isDownloaded)) {
dispatch(
conversationsActions.kickOffAttachmentDownload({ messageId })
);
}
const state = getState();
const messagePropsSelector = getMessagePropsSelector(state);
const conversationSelector = getConversationSelector(state);
const messageProps = messagePropsSelector(messageAttributes);
const messageDraft = toMessageForwardDraft(
messageProps,
conversationSelector
);
return messageDraft;
})
);
} else if (payload.type === ForwardMessagesModalType.ShareCallLink) {
messageDrafts = [payload.draft];
} else {
throw missingCaseError(payload);
}
dispatch({
type: TOGGLE_FORWARD_MESSAGES_MODAL,
payload: { messages: messagesProps, onForward },
payload: { type: payload.type, messageDrafts, onForward },
});
};
}
function showShareCallLinkViaSignal(
callLink: CallLinkType,
i18n: LocalizerType
): ThunkAction<
void,
RootStateType,
unknown,
ToggleForwardMessagesModalActionType
> {
return dispatch => {
const url = linkCallRoute
.toWebUrl({
key: callLink.rootKey,
})
.toString();
dispatch(
toggleForwardMessagesModal({
type: ForwardMessagesModalType.ShareCallLink,
draft: {
originalMessageId: null,
hasContact: false,
isSticker: false,
previews: [
{
title: callLink.name,
url,
isCallLink: true,
},
],
messageBody: i18n(
'icu:ShareCallLinkViaSignal__DraftMessageText',
{ url },
{ bidi: 'strip' }
),
},
})
);
};
}
export function toggleConfirmLeaveCallModal(
payload: StartCallData | null
): ToggleConfirmLeaveCallModalActionType {
return {
type: TOGGLE_CONFIRM_LEAVE_CALL_MODAL,
payload,
};
}
function toggleNotePreviewModal(
payload: NotePreviewModalPropsType | null
): ToggleNotePreviewModalActionType {
return {
type: TOGGLE_NOTE_PREVIEW_MODAL,
payload,
};
}
function toggleProfileEditor(
initialEditState?: ProfileEditorEditState
): ToggleProfileEditorActionType {
@ -639,6 +739,24 @@ function toggleAddUserToAnotherGroupModal(
};
}
function toggleCallLinkAddNameModal(
roomId: string | null
): ToggleCallLinkAddNameModalActionType {
return {
type: TOGGLE_CALL_LINK_ADD_NAME_MODAL,
payload: roomId,
};
}
function toggleCallLinkEditModal(
roomId: string | null
): ToggleCallLinkEditModalActionType {
return {
type: TOGGLE_CALL_LINK_EDIT_MODAL,
payload: roomId,
};
}
function toggleAboutContactModal(
contactId?: string
): ToggleAboutContactModalActionType {
@ -750,6 +868,27 @@ function showErrorModal({
};
}
function toggleEditNicknameAndNoteModal(
payload: EditNicknameAndNoteModalPropsType | null
): ToggleEditNicknameAndNoteModalActionType {
return {
type: TOGGLE_EDIT_NICKNAME_AND_NOTE_MODAL,
payload,
};
}
function toggleMessageRequestActionsConfirmation(
payload: {
conversationId: string;
state: MessageRequestState;
} | null
): ToggleMessageRequestActionsConfirmationActionType {
return {
type: TOGGLE_MESSAGE_REQUEST_ACTIONS_CONFIRMATION,
payload,
};
}
function closeShortcutGuideModal(): CloseShortcutGuideModalActionType {
return {
type: CLOSE_SHORTCUT_GUIDE_MODAL,
@ -762,27 +901,8 @@ function showShortcutGuideModal(): ShowShortcutGuideModalActionType {
};
}
function cancelAuthorizeArtCreator(): ThunkAction<
void,
RootStateType,
unknown,
CancelAuthArtCreatorActionType
> {
return async (dispatch, getState) => {
const data = getState().globalModals.authArtCreatorData;
if (!data) {
return;
}
dispatch({
type: CANCEL_AUTH_ART_CREATOR,
});
};
}
function copyOverMessageAttributesIntoEditHistory(
messageAttributes: ReadonlyDeep<MessageAttributesType>
messageAttributes: ReadonlyDeep<ReadonlyMessageAttributesType>
): EditHistoryMessagesType | undefined {
if (!messageAttributes.editHistory) {
return;
@ -832,64 +952,16 @@ function closeEditHistoryModal(): CloseEditHistoryModalActionType {
};
}
export function showAuthorizeArtCreator(
data: AuthorizeArtCreatorDataType
): ShowAuthArtCreatorActionType {
return {
type: SHOW_AUTH_ART_CREATOR,
payload: data,
};
}
export function confirmAuthorizeArtCreator(): ThunkAction<
void,
RootStateType,
unknown,
| ConfirmAuthArtCreatorPendingActionType
| ConfirmAuthArtCreatorFulfilledActionType
| CancelAuthArtCreatorActionType
| ShowToastActionType
> {
return async (dispatch, getState) => {
const data = getState().globalModals.authArtCreatorData;
if (!data) {
dispatch({ type: CANCEL_AUTH_ART_CREATOR });
return;
}
dispatch({
type: CONFIRM_AUTH_ART_CREATOR_PENDING,
});
try {
await authorizeArtCreator(data);
} catch (err) {
log.error('authorizeArtCreator failed', Errors.toLogFormat(err));
dispatch({
type: SHOW_TOAST,
payload: {
toastType: ToastType.Error,
},
});
}
dispatch({
type: CONFIRM_AUTH_ART_CREATOR_FULFILLED,
});
};
}
function copyOverMessageAttributesIntoForwardMessages(
messagesProps: ReadonlyArray<ForwardMessagePropsType>,
attributes: ReadonlyDeep<MessageAttributesType>
): ReadonlyArray<ForwardMessagePropsType> {
return messagesProps.map(messageProps => {
if (messageProps.id !== attributes.id) {
return messageProps;
messageDrafts: ReadonlyArray<MessageForwardDraft>,
attributes: ReadonlyDeep<ReadonlyMessageAttributesType>
): ReadonlyArray<MessageForwardDraft> {
return messageDrafts.map(messageDraft => {
if (messageDraft.originalMessageId !== attributes.id) {
return messageDraft;
}
return {
...messageProps,
...messageDraft,
attachments: attributes.attachments,
};
});
@ -900,6 +972,10 @@ function copyOverMessageAttributesIntoForwardMessages(
export function getEmptyState(): GlobalModalsStateType {
return {
hasConfirmationModal: false,
callLinkAddNameModalRoomId: null,
callLinkEditModalRoomId: null,
confirmLeaveCallModalState: null,
editNicknameAndNoteModalProps: null,
isProfileEditorVisible: false,
isShortcutGuideModalVisible: false,
isSignalConnectionsVisible: false,
@ -908,6 +984,8 @@ export function getEmptyState(): GlobalModalsStateType {
usernameOnboardingState: UsernameOnboardingState.NeverShown,
profileEditorHasError: false,
profileEditorInitialEditState: undefined,
messageRequestActionsConfirmationProps: null,
notePreviewModalProps: null,
};
}
@ -922,6 +1000,20 @@ export function reducer(
};
}
if (action.type === TOGGLE_CONFIRM_LEAVE_CALL_MODAL) {
return {
...state,
confirmLeaveCallModalState: action.payload,
};
}
if (action.type === TOGGLE_NOTE_PREVIEW_MODAL) {
return {
...state,
notePreviewModalProps: action.payload,
};
}
if (action.type === TOGGLE_PROFILE_EDITOR) {
return {
...state,
@ -1003,6 +1095,20 @@ export function reducer(
};
}
if (action.type === TOGGLE_CALL_LINK_ADD_NAME_MODAL) {
return {
...state,
callLinkAddNameModalRoomId: action.payload,
};
}
if (action.type === TOGGLE_CALL_LINK_EDIT_MODAL) {
return {
...state,
callLinkEditModalRoomId: action.payload,
};
}
if (action.type === TOGGLE_DELETE_MESSAGES_MODAL) {
return {
...state,
@ -1081,36 +1187,6 @@ export function reducer(
};
}
if (action.type === SHOW_FORMATTING_WARNING_MODAL) {
const { explodedPromise } = action.payload;
if (!explodedPromise) {
return {
...state,
formattingWarningData: undefined,
};
}
return {
...state,
formattingWarningData: { explodedPromise },
};
}
if (action.type === SHOW_SEND_EDIT_WARNING_MODAL) {
const { explodedPromise } = action.payload;
if (!explodedPromise) {
return {
...state,
sendEditWarningData: undefined,
};
}
return {
...state,
sendEditWarningData: { explodedPromise },
};
}
if (action.type === SHOW_STICKER_PACK_PREVIEW) {
return {
...state,
@ -1132,6 +1208,20 @@ export function reducer(
};
}
if (action.type === TOGGLE_EDIT_NICKNAME_AND_NOTE_MODAL) {
return {
...state,
editNicknameAndNoteModalProps: action.payload,
};
}
if (action.type === TOGGLE_MESSAGE_REQUEST_ACTIONS_CONFIRMATION) {
return {
...state,
messageRequestActionsConfirmationProps: action.payload,
};
}
if (action.type === CLOSE_SHORTCUT_GUIDE_MODAL) {
return {
...state,
@ -1146,36 +1236,6 @@ export function reducer(
};
}
if (action.type === CANCEL_AUTH_ART_CREATOR) {
return {
...state,
authArtCreatorData: undefined,
};
}
if (action.type === SHOW_AUTH_ART_CREATOR) {
return {
...state,
isAuthorizingArtCreator: false,
authArtCreatorData: action.payload,
};
}
if (action.type === CONFIRM_AUTH_ART_CREATOR_PENDING) {
return {
...state,
isAuthorizingArtCreator: true,
};
}
if (action.type === CONFIRM_AUTH_ART_CREATOR_FULFILLED) {
return {
...state,
isAuthorizingArtCreator: false,
authArtCreatorData: undefined,
};
}
if (action.type === SHOW_EDIT_HISTORY_MODAL) {
return {
...state,
@ -1193,8 +1253,8 @@ export function reducer(
if (state.forwardMessagesProps != null) {
if (action.type === MESSAGE_CHANGED) {
if (
!state.forwardMessagesProps.messages.some(message => {
return message.id === action.payload.id;
!state.forwardMessagesProps.messageDrafts.some(message => {
return message.originalMessageId === action.payload.id;
})
) {
return state;
@ -1204,8 +1264,8 @@ export function reducer(
...state,
forwardMessagesProps: {
...state.forwardMessagesProps,
messages: copyOverMessageAttributesIntoForwardMessages(
state.forwardMessagesProps.messages,
messageDrafts: copyOverMessageAttributesIntoForwardMessages(
state.forwardMessagesProps.messageDrafts,
action.payload.data
),
},

View file

@ -18,12 +18,16 @@ import type { StateType as RootStateType } from '../reducer';
import * as log from '../../logging/log';
import { __DEPRECATED$getMessageById } from '../../messages/getMessageById';
import type { MessageAttributesType } from '../../model-types.d';
import type { ReadonlyMessageAttributesType } from '../../model-types.d';
import { isGIF } from '../../types/Attachment';
import {
isImageTypeSupported,
isVideoTypeSupported,
} from '../../util/GoogleChrome';
import {
getLocalAttachmentUrl,
AttachmentDisposition,
} from '../../util/getLocalAttachmentUrl';
import { isTapToView } from '../selectors/message';
import { SHOW_TOAST } from './toast';
import { ToastType } from '../../types/Toast';
@ -35,7 +39,7 @@ import {
} from './conversations';
import { showStickerPackPreview } from './globalModals';
import { useBoundActions } from '../../hooks/useBoundActions';
import dataInterface from '../../sql/Client';
import { DataReader } from '../../sql/Client';
// eslint-disable-next-line local-rules/type-alias-readonlydeep
export type LightboxStateType =
@ -194,11 +198,8 @@ function showLightboxForViewOnceMedia(
);
}
const {
copyIntoTempDirectory,
getAbsoluteAttachmentPath,
getAbsoluteTempPath,
} = window.Signal.Migrations;
const { copyIntoTempDirectory, getAbsoluteAttachmentPath } =
window.Signal.Migrations;
const absolutePath = getAbsoluteAttachmentPath(firstAttachment.path);
const { path: tempPath } = await copyIntoTempDirectory(absolutePath);
@ -209,12 +210,14 @@ function showLightboxForViewOnceMedia(
await message.markViewOnceMessageViewed();
const { path, contentType } = tempAttachment;
const { contentType } = tempAttachment;
const media = [
{
attachment: tempAttachment,
objectURL: getAbsoluteTempPath(path),
objectURL: getLocalAttachmentUrl(tempAttachment, {
disposition: AttachmentDisposition.Temporary,
}),
contentType,
index: 0,
message: {
@ -242,7 +245,7 @@ function showLightboxForViewOnceMedia(
}
function filterValidAttachments(
attributes: MessageAttributesType
attributes: ReadonlyMessageAttributesType
): Array<AttachmentType> {
return (attributes.attachments ?? []).filter(
item => item.thumbnail && !item.pending && !item.error
@ -291,8 +294,6 @@ function showLightbox(opts: {
const attachments = filterValidAttachments(message.attributes);
const loop = isGIF(attachments);
const { getAbsoluteAttachmentPath } = window.Signal.Migrations;
const authorId =
window.ConversationController.lookupOrCreate({
serviceId: message.get('sourceServiceId'),
@ -303,7 +304,7 @@ function showLightbox(opts: {
const sentAt = message.get('sent_at');
const media = attachments.map((item, index) => ({
objectURL: getAbsoluteAttachmentPath(item.path ?? ''),
objectURL: getLocalAttachmentUrl(item),
path: item.path,
contentType: item.contentType,
loop,
@ -318,8 +319,9 @@ function showLightbox(opts: {
},
attachment: item,
thumbnailObjectUrl:
item.thumbnail?.objectUrl ||
getAbsoluteAttachmentPath(item.thumbnail?.path ?? ''),
item.thumbnail?.objectUrl || item.thumbnail
? getLocalAttachmentUrl(item.thumbnail)
: undefined,
}));
if (!media.length) {
@ -347,7 +349,7 @@ function showLightbox(opts: {
}
const { older, newer } =
await dataInterface.getConversationRangeCenteredOnMessage({
await DataReader.getConversationRangeCenteredOnMessage({
conversationId: message.get('conversationId'),
messageId,
receivedAt,
@ -434,8 +436,8 @@ function showLightboxForAdjacentMessage(
const [adjacent] =
direction === AdjacentMessageDirection.Previous
? await dataInterface.getOlderMessagesByConversation(options)
: await dataInterface.getNewerMessagesByConversation(options);
? await DataReader.getOlderMessagesByConversation(options)
: await DataReader.getNewerMessagesByConversation(options);
if (!adjacent) {
log.warn(

View file

@ -15,7 +15,7 @@ import type { MIMEType } from '../../types/MIME';
import type { MediaItemType } from '../../types/MediaItem';
import type { StateType as RootStateType } from '../reducer';
import dataInterface from '../../sql/Client';
import { DataReader, DataWriter } from '../../sql/Client';
import {
CONVERSATION_UNLOADED,
MESSAGE_CHANGED,
@ -25,6 +25,7 @@ import {
import { VERSION_NEEDED_FOR_DISPLAY } from '../../types/Message2';
import { isDownloading, hasFailed } from '../../types/Attachment';
import { isNotNil } from '../../util/isNotNil';
import { getLocalAttachmentUrl } from '../../util/getLocalAttachmentUrl';
import { useBoundActions } from '../../hooks/useBoundActions';
// eslint-disable-next-line local-rules/type-alias-readonlydeep
@ -74,8 +75,7 @@ function loadMediaItems(
conversationId: string
): ThunkAction<void, RootStateType, unknown, LoadMediaItemslActionType> {
return async dispatch => {
const { getAbsoluteAttachmentPath, upgradeMessageSchema } =
window.Signal.Migrations;
const { upgradeMessageSchema } = window.Signal.Migrations;
// We fetch more documents than media as they dont require to be loaded
// into memory right away. Revisit this once we have infinite scrolling:
@ -84,13 +84,13 @@ function loadMediaItems(
const ourAci = window.textsecure.storage.user.getCheckedAci();
const rawMedia = await dataInterface.getMessagesWithVisualMediaAttachments(
const rawMedia = await DataReader.getMessagesWithVisualMediaAttachments(
conversationId,
{
limit: DEFAULT_MEDIA_FETCH_COUNT,
}
);
const rawDocuments = await dataInterface.getMessagesWithFileAttachments(
const rawDocuments = await DataReader.getMessagesWithFileAttachments(
conversationId,
{
limit: DEFAULT_DOCUMENTS_FETCH_COUNT,
@ -111,7 +111,7 @@ function loadMediaItems(
const upgradedMsgAttributes = await upgradeMessageSchema(message);
model.set(upgradedMsgAttributes);
await dataInterface.saveMessage(upgradedMsgAttributes, { ourAci });
await DataWriter.saveMessage(upgradedMsgAttributes, { ourAci });
}
})
);
@ -133,9 +133,9 @@ function loadMediaItems(
const { thumbnail } = attachment;
const result = {
path: attachment.path,
objectURL: getAbsoluteAttachmentPath(attachment.path),
objectURL: getLocalAttachmentUrl(attachment),
thumbnailObjectUrl: thumbnail?.path
? getAbsoluteAttachmentPath(thumbnail.path)
? getLocalAttachmentUrl(thumbnail)
: undefined,
contentType: attachment.contentType,
index,

View file

@ -5,35 +5,33 @@ import type { ReadonlyDeep } from 'type-fest';
import { SocketStatus } from '../../types/SocketStatus';
import { trigger } from '../../shims/events';
import { assignWithNoUnnecessaryAllocation } from '../../util/assignWithNoUnnecessaryAllocation';
import type { BoundActionCreatorsMapObject } from '../../hooks/useBoundActions';
import { useBoundActions } from '../../hooks/useBoundActions';
// State
export type NetworkStateType = ReadonlyDeep<{
isOnline: boolean;
isOutage: boolean;
socketStatus: SocketStatus;
withinConnectingGracePeriod: boolean;
challengeStatus: 'required' | 'pending' | 'idle';
}>;
// Actions
const CHECK_NETWORK_STATUS = 'network/CHECK_NETWORK_STATUS';
const CLOSE_CONNECTING_GRACE_PERIOD = 'network/CLOSE_CONNECTING_GRACE_PERIOD';
const SET_NETWORK_STATUS = 'network/SET_NETWORK_STATUS';
const RELINK_DEVICE = 'network/RELINK_DEVICE';
const SET_CHALLENGE_STATUS = 'network/SET_CHALLENGE_STATUS';
const SET_OUTAGE = 'network/SET_OUTAGE';
export type CheckNetworkStatusPayloadType = ReadonlyDeep<{
export type SetNetworkStatusPayloadType = ReadonlyDeep<{
isOnline: boolean;
socketStatus: SocketStatus;
}>;
type CheckNetworkStatusAction = ReadonlyDeep<{
type: 'network/CHECK_NETWORK_STATUS';
payload: CheckNetworkStatusPayloadType;
}>;
type CloseConnectingGracePeriodActionType = ReadonlyDeep<{
type: 'network/CLOSE_CONNECTING_GRACE_PERIOD';
type SetNetworkStatusAction = ReadonlyDeep<{
type: 'network/SET_NETWORK_STATUS';
payload: SetNetworkStatusPayloadType;
}>;
type RelinkDeviceActionType = ReadonlyDeep<{
@ -47,30 +45,31 @@ type SetChallengeStatusActionType = ReadonlyDeep<{
};
}>;
type SetOutageActionType = ReadonlyDeep<{
type: 'network/SET_OUTAGE';
payload: {
isOutage: boolean;
};
}>;
export type NetworkActionType = ReadonlyDeep<
| CheckNetworkStatusAction
| CloseConnectingGracePeriodActionType
| SetNetworkStatusAction
| RelinkDeviceActionType
| SetChallengeStatusActionType
| SetOutageActionType
>;
// Action Creators
function checkNetworkStatus(
payload: CheckNetworkStatusPayloadType
): CheckNetworkStatusAction {
function setNetworkStatus(
payload: SetNetworkStatusPayloadType
): SetNetworkStatusAction {
return {
type: CHECK_NETWORK_STATUS,
type: SET_NETWORK_STATUS,
payload,
};
}
function closeConnectingGracePeriod(): CloseConnectingGracePeriodActionType {
return {
type: CLOSE_CONNECTING_GRACE_PERIOD,
};
}
function relinkDevice(): RelinkDeviceActionType {
trigger('setupAsNewDevice');
@ -88,20 +87,31 @@ function setChallengeStatus(
};
}
function setOutage(isOutage: boolean): SetOutageActionType {
return {
type: SET_OUTAGE,
payload: { isOutage },
};
}
export const actions = {
checkNetworkStatus,
closeConnectingGracePeriod,
setNetworkStatus,
relinkDevice,
setChallengeStatus,
setOutage,
};
export const useNetworkActions = (): BoundActionCreatorsMapObject<
typeof actions
> => useBoundActions(actions);
// Reducer
export function getEmptyState(): NetworkStateType {
return {
isOnline: navigator.onLine,
isOnline: true,
isOutage: false,
socketStatus: SocketStatus.OPEN,
withinConnectingGracePeriod: true,
challengeStatus: 'idle',
};
}
@ -110,7 +120,7 @@ export function reducer(
state: Readonly<NetworkStateType> = getEmptyState(),
action: Readonly<NetworkActionType>
): NetworkStateType {
if (action.type === CHECK_NETWORK_STATUS) {
if (action.type === SET_NETWORK_STATUS) {
const { isOnline, socketStatus } = action.payload;
// This action is dispatched frequently. We avoid allocating a new object if nothing
@ -121,13 +131,6 @@ export function reducer(
});
}
if (action.type === CLOSE_CONNECTING_GRACE_PERIOD) {
return {
...state,
withinConnectingGracePeriod: false,
};
}
if (action.type === SET_CHALLENGE_STATUS) {
return {
...state,
@ -135,5 +138,16 @@ export function reducer(
};
}
if (action.type === SET_OUTAGE) {
const { isOutage } = action.payload;
// This action is dispatched frequently when offline.
// We avoid allocating a new object if nothing has changed to
// avoid an unnecessary re-render.
return assignWithNoUnnecessaryAllocation(state, {
isOutage,
});
}
return state;
}

View file

@ -101,8 +101,9 @@ export const actions = {
selectDraftEmojiToBeReplaced,
};
export const useActions = (): BoundActionCreatorsMapObject<typeof actions> =>
useBoundActions(actions);
export const usePreferredReactionsActions = (): BoundActionCreatorsMapObject<
typeof actions
> => useBoundActions(actions);
function cancelCustomizePreferredReactionsModal(): CancelCustomizePreferredReactionsModalActionType {
return { type: CANCEL_CUSTOMIZE_PREFERRED_REACTIONS_MODAL };

View file

@ -15,6 +15,8 @@ import {
import * as log from '../../logging/log';
import * as Errors from '../../types/errors';
import type { StateType as RootStateType } from '../reducer';
import type { BoundActionCreatorsMapObject } from '../../hooks/useBoundActions';
import { useBoundActions } from '../../hooks/useBoundActions';
export type SafetyNumberContactType = ReadonlyDeep<{
safetyNumber: SafetyNumberType;
@ -174,6 +176,10 @@ export const actions = {
toggleVerified,
};
export const useSafetyNumberActions = (): BoundActionCreatorsMapObject<
typeof actions
> => useBoundActions(actions);
export function getEmptyState(): SafetyNumberStateType {
return {
contacts: {},

View file

@ -6,12 +6,9 @@ import { debounce, omit, reject } from 'lodash';
import type { ReadonlyDeep } from 'type-fest';
import type { StateType as RootStateType } from '../reducer';
import { filterAndSortConversationsByRecent } from '../../util/filterAndSortConversations';
import type {
ClientSearchResultMessageType,
ClientInterface,
} from '../../sql/Interface';
import dataInterface from '../../sql/Client';
import { filterAndSortConversations } from '../../util/filterAndSortConversations';
import type { ClientSearchResultMessageType } from '../../sql/Interface';
import { DataReader } from '../../sql/Client';
import { makeLookup } from '../../util/makeLookup';
import { isNotNil } from '../../util/isNotNil';
import type { ServiceIdString } from '../../types/ServiceId';
@ -44,7 +41,7 @@ import * as log from '../../logging/log';
import { searchConversationTitles } from '../../util/searchConversationTitles';
import { isDirectConversation } from '../../util/whatTypeOfConversation';
const { searchMessages: dataSearchMessages }: ClientInterface = dataInterface;
const { searchMessages: dataSearchMessages } = DataReader;
// State
@ -361,12 +358,11 @@ async function queryConversationsAndContacts(
}
);
const searchResults: Array<ConversationType> =
filterAndSortConversationsByRecent(
visibleConversations,
normalizedQuery,
regionCode
);
const searchResults: Array<ConversationType> = filterAndSortConversations(
visibleConversations,
normalizedQuery,
regionCode
);
// Split into two groups - active conversations and items just from address book
let conversationIds: Array<string> = [];

View file

@ -9,7 +9,7 @@ import type {
StickerType as StickerDBType,
StickerPackType as StickerPackDBType,
} from '../../sql/Interface';
import dataInterface from '../../sql/Client';
import { DataReader, DataWriter } from '../../sql/Client';
import type { RecentStickerType } from '../../types/Stickers';
import {
downloadStickerPack as externalDownloadStickerPack,
@ -22,8 +22,11 @@ import { ERASE_STORAGE_SERVICE } from './user';
import type { EraseStorageServiceStateAction } from './user';
import type { NoopActionType } from './noop';
import type { BoundActionCreatorsMapObject } from '../../hooks/useBoundActions';
import { useBoundActions } from '../../hooks/useBoundActions';
const { getRecentStickers, updateStickerLastUsed } = dataInterface;
const { getRecentStickers } = DataReader;
const { updateStickerLastUsed } = DataWriter;
// State
@ -154,6 +157,10 @@ export const actions = {
useSticker,
};
export const useStickersActions = (): BoundActionCreatorsMapObject<
typeof actions
> => useBoundActions(actions);
function removeStickerPack(id: string): StickerPackRemovedAction {
return {
type: 'stickers/REMOVE_STICKER_PACK',
@ -208,7 +215,11 @@ function downloadStickerPack(
function installStickerPack(
packId: string,
packKey: string,
options: { fromSync?: boolean; fromStorageService?: boolean } = {}
options: {
fromSync?: boolean;
fromStorageService?: boolean;
fromBackup?: boolean;
} = {}
): InstallStickerPackAction {
return {
type: 'stickers/INSTALL_STICKER_PACK',
@ -218,19 +229,27 @@ function installStickerPack(
async function doInstallStickerPack(
packId: string,
packKey: string,
options: { fromSync?: boolean; fromStorageService?: boolean } = {}
options: {
fromSync?: boolean;
fromStorageService?: boolean;
fromBackup?: boolean;
} = {}
): Promise<InstallStickerPackPayloadType> {
const { fromSync = false, fromStorageService = false } = options;
const {
fromSync = false,
fromStorageService = false,
fromBackup = false,
} = options;
const timestamp = Date.now();
await dataInterface.installStickerPack(packId, timestamp);
await DataWriter.installStickerPack(packId, timestamp);
if (!fromSync && !fromStorageService) {
if (!fromSync && !fromStorageService && !fromBackup) {
// Kick this off, but don't wait for it
void sendStickerPackSync(packId, packKey, true);
}
if (!fromStorageService) {
if (!fromStorageService && !fromBackup) {
storageServiceUploadJob();
}
@ -265,7 +284,7 @@ async function doUninstallStickerPack(
const { fromSync = false, fromStorageService = false } = options;
const timestamp = Date.now();
await dataInterface.uninstallStickerPack(packId, timestamp);
await DataWriter.uninstallStickerPack(packId, timestamp);
// If there are no more references, it should be removed
await maybeDeletePack(packId);

View file

@ -8,7 +8,7 @@ import type { ReadonlyDeep } from 'type-fest';
import * as Errors from '../../types/errors';
import type { AttachmentType } from '../../types/Attachment';
import type { DraftBodyRanges } from '../../types/BodyRange';
import type { MessageAttributesType } from '../../model-types.d';
import type { ReadonlyMessageAttributesType } from '../../model-types.d';
import type {
MessageChangedActionType,
MessageDeletedActionType,
@ -25,7 +25,7 @@ import { isAciString } from '../../util/isAciString';
import * as log from '../../logging/log';
import { TARGETED_CONVERSATION_CHANGED } from './conversations';
import { SIGNAL_ACI } from '../../types/SignalConversation';
import dataInterface from '../../sql/Client';
import { DataReader, DataWriter } from '../../sql/Client';
import { ReadStatus } from '../../messages/MessageReadStatus';
import { SendStatus } from '../../messages/MessageSendState';
import { SafetyNumberChangeSource } from '../../components/SafetyNumberChangeDialog';
@ -69,6 +69,7 @@ import {
conversationQueueJobEnum,
} from '../../jobs/conversationJobQueue';
import { ReceiptType } from '../../types/Receipt';
import { singleProtoJobQueue } from '../../jobs/singleProtoJobQueue';
export type StoryDataType = ReadonlyDeep<
{
@ -78,7 +79,7 @@ export type StoryDataType = ReadonlyDeep<
messageId: string;
startedDownload?: boolean;
} & Pick<
MessageAttributesType,
ReadonlyMessageAttributesType,
| 'bodyRanges'
| 'canReplyToStory'
| 'conversationId'
@ -123,31 +124,33 @@ export type AddStoryData = ReadonlyDeep<
| undefined
>;
// eslint-disable-next-line local-rules/type-alias-readonlydeep
export type RecipientsByConversation = Record<
string, // conversationId
{
serviceIds: Array<ServiceIdString>;
export type RecipientEntry = ReadonlyDeep<{
serviceIds: Array<ServiceIdString>;
byDistributionId?: Record<
StoryDistributionIdString,
{
serviceIds: Array<ServiceIdString>;
}
>;
}
byDistributionId?: Record<
StoryDistributionIdString,
{
serviceIds: Array<ServiceIdString>;
}
>;
}>;
export type RecipientsByConversation = ReadonlyDeep<
Record<
string, // conversationId
RecipientEntry
>
>;
// State
// eslint-disable-next-line local-rules/type-alias-readonlydeep
export type StoriesStateType = Readonly<{
export type StoriesStateType = ReadonlyDeep<{
addStoryData: AddStoryData;
hasAllStoriesUnmuted: boolean;
lastOpenedAtTimestamp: number | undefined;
replyState?: Readonly<{
messageId: string;
replies: Array<MessageAttributesType>;
replies: Array<ReadonlyMessageAttributesType>;
}>;
selectedStoryData?: SelectedStoryDataType;
sendStoryModalData?: RecipientsByConversation;
@ -187,14 +190,13 @@ type ListMembersVerified = ReadonlyDeep<{
};
}>;
// eslint-disable-next-line local-rules/type-alias-readonlydeep
type LoadStoryRepliesActionType = {
type LoadStoryRepliesActionType = ReadonlyDeep<{
type: typeof LOAD_STORY_REPLIES;
payload: {
messageId: string;
replies: Array<MessageAttributesType>;
replies: Array<ReadonlyMessageAttributesType>;
};
};
}>;
type MarkStoryReadActionType = ReadonlyDeep<{
type: typeof MARK_STORY_READ;
@ -284,7 +286,7 @@ function deleteGroupStoryReply(
messageId: string
): ThunkAction<void, RootStateType, unknown, StoryReplyDeletedActionType> {
return async dispatch => {
await window.Signal.Data.removeMessage(messageId);
await DataWriter.removeMessage(messageId, { singleProtoJobQueue });
dispatch({
type: STORY_REPLY_DELETED,
payload: messageId,
@ -336,7 +338,7 @@ function loadStoryReplies(
): ThunkAction<void, RootStateType, unknown, LoadStoryRepliesActionType> {
return async (dispatch, getState) => {
const conversation = getConversationSelector(getState())(conversationId);
const replies = await dataInterface.getOlderMessagesByConversation({
const replies = await DataReader.getOlderMessagesByConversation({
conversationId,
limit: 9000,
storyId: messageId,
@ -420,7 +422,7 @@ function markStoryRead(
message.set(markViewed(message.attributes, storyReadDate));
drop(
dataInterface.saveMessage(message.attributes, {
DataWriter.saveMessage(message.attributes, {
ourAci: window.textsecure.storage.user.getCheckedAci(),
})
);
@ -458,7 +460,7 @@ function markStoryRead(
);
}
await dataInterface.addNewStoryRead({
await DataWriter.addNewStoryRead({
authorId,
conversationId: message.attributes.conversationId,
storyId: messageId,
@ -512,10 +514,9 @@ function queueStoryDownload(
return;
}
// isDownloading checks for the downloadJobId which is set by
// queueAttachmentDownloads but we optimistically set story.startedDownload
// in redux to prevent race conditions from queuing up multiple attachment
// downloads before the attachment save takes place.
// isDownloading checks if the download is pending but we optimistically set
// story.startedDownload in redux to prevent race conditions from queuing up multiple
// attachment downloads before the attachment save takes place.
if (isDownloading(attachment) || story.startedDownload) {
return;
}
@ -1409,10 +1410,7 @@ function removeAllContactStories(
log.info(`${logId}: removing ${messages.length} stories`);
await Promise.all([
messages.map(m => m.cleanup()),
await dataInterface.removeMessages(messageIds),
]);
await DataWriter.removeMessages(messageIds, { singleProtoJobQueue });
dispatch({
type: 'NOOP',

View file

@ -10,7 +10,7 @@ import type { StoryDistributionWithMembersType } from '../../sql/Interface';
import type { StoryDistributionIdString } from '../../types/StoryDistributionId';
import type { ServiceIdString } from '../../types/ServiceId';
import * as log from '../../logging/log';
import dataInterface from '../../sql/Client';
import { DataReader, DataWriter } from '../../sql/Client';
import { MY_STORY_ID } from '../../types/Stories';
import { generateStoryDistributionId } from '../../types/StoryDistributionId';
import { deleteStoryForEveryone } from '../../util/deleteStoryForEveryone';
@ -113,7 +113,7 @@ function allowsRepliesChanged(
): ThunkAction<void, RootStateType, null, AllowRepliesChangedActionType> {
return async dispatch => {
const storyDistribution =
await dataInterface.getStoryDistributionWithMembers(listId);
await DataReader.getStoryDistributionWithMembers(listId);
if (!storyDistribution) {
log.warn(
@ -131,7 +131,7 @@ function allowsRepliesChanged(
return;
}
await dataInterface.modifyStoryDistribution({
await DataWriter.modifyStoryDistribution({
...storyDistribution,
allowsReplies,
storageNeedsSync: true,
@ -178,7 +178,7 @@ function createDistributionList(
};
if (shouldSave) {
await dataInterface.createNewStoryDistribution(storyDistribution);
await DataWriter.createNewStoryDistribution(storyDistribution);
}
if (storyDistribution.storageNeedsSync) {
@ -208,14 +208,14 @@ function deleteDistributionList(
const deletedAtTimestamp = Date.now();
const storyDistribution =
await dataInterface.getStoryDistributionWithMembers(listId);
await DataReader.getStoryDistributionWithMembers(listId);
if (!storyDistribution) {
log.warn('No story distribution found for id', listId);
return;
}
await dataInterface.modifyStoryDistributionWithMembers(
await DataWriter.modifyStoryDistributionWithMembers(
{
...storyDistribution,
deletedAtTimestamp,
@ -266,9 +266,8 @@ function hideMyStoriesFrom(
memberServiceIds: Array<ServiceIdString>
): ThunkAction<void, RootStateType, null, HideMyStoriesFromActionType> {
return async dispatch => {
const myStories = await dataInterface.getStoryDistributionWithMembers(
MY_STORY_ID
);
const myStories =
await DataReader.getStoryDistributionWithMembers(MY_STORY_ID);
if (!myStories) {
log.error(
@ -279,7 +278,7 @@ function hideMyStoriesFrom(
const toAdd = new Set<ServiceIdString>(memberServiceIds);
await dataInterface.modifyStoryDistributionWithMembers(
await DataWriter.modifyStoryDistributionWithMembers(
{
...myStories,
isBlockList: true,
@ -316,7 +315,7 @@ function removeMembersFromDistributionList(
}
const storyDistribution =
await dataInterface.getStoryDistributionWithMembers(listId);
await DataReader.getStoryDistributionWithMembers(listId);
if (!storyDistribution) {
log.warn(
@ -343,7 +342,7 @@ function removeMembersFromDistributionList(
await window.storage.put('hasSetMyStoriesPrivacy', true);
}
await dataInterface.modifyStoryDistributionWithMembers(
await DataWriter.modifyStoryDistributionWithMembers(
{
...storyDistribution,
isBlockList,
@ -385,9 +384,8 @@ function setMyStoriesToAllSignalConnections(): ThunkAction<
ResetMyStoriesActionType
> {
return async dispatch => {
const myStories = await dataInterface.getStoryDistributionWithMembers(
MY_STORY_ID
);
const myStories =
await DataReader.getStoryDistributionWithMembers(MY_STORY_ID);
if (!myStories) {
log.error(
@ -397,7 +395,7 @@ function setMyStoriesToAllSignalConnections(): ThunkAction<
}
if (myStories.isBlockList || myStories.members.length > 0) {
await dataInterface.modifyStoryDistributionWithMembers(
await DataWriter.modifyStoryDistributionWithMembers(
{
...myStories,
isBlockList: true,
@ -426,7 +424,7 @@ function updateStoryViewers(
): ThunkAction<void, RootStateType, null, ViewersChangedActionType> {
return async dispatch => {
const storyDistribution =
await dataInterface.getStoryDistributionWithMembers(listId);
await DataReader.getStoryDistributionWithMembers(listId);
if (!storyDistribution) {
log.warn(
@ -456,7 +454,7 @@ function updateStoryViewers(
}
});
await dataInterface.modifyStoryDistributionWithMembers(
await DataWriter.modifyStoryDistributionWithMembers(
{
...storyDistribution,
isBlockList: false,
@ -489,7 +487,7 @@ function removeMemberFromAllDistributionLists(
): ThunkAction<void, RootStateType, null, ModifyListActionType> {
return async dispatch => {
const logId = `removeMemberFromAllDistributionLists(${member})`;
const lists = await dataInterface.getAllStoryDistributionsWithMembers();
const lists = await DataReader.getAllStoryDistributionsWithMembers();
const listsWithMember = lists.filter(({ members }) =>
members.includes(member)

View file

@ -3,7 +3,6 @@
import type { ReadonlyDeep } from 'type-fest';
import { trigger } from '../../shims/events';
import type { LocaleMessagesType } from '../../types/I18N';
import type { LocalizerType } from '../../types/Util';
import type { MenuOptionsType } from '../../types/menu';
@ -11,6 +10,8 @@ import type { NoopActionType } from './noop';
import type { AciString, PniString } from '../../types/ServiceId';
import OS from '../../util/os/osMain';
import { ThemeType } from '../../types/Util';
import type { BoundActionCreatorsMapObject } from '../../hooks/useBoundActions';
import { useBoundActions } from '../../hooks/useBoundActions';
// State
@ -73,6 +74,10 @@ export const actions = {
manualReconnect,
};
export const useUserActions = (): BoundActionCreatorsMapObject<
typeof actions
> => useBoundActions(actions);
function eraseStorageServiceState(): EraseStorageServiceStateAction {
return {
type: ERASE_STORAGE_SERVICE,

View file

@ -37,27 +37,32 @@ import type { StoryDistributionListDataType } from './ducks/storyDistributionLis
import OS from '../util/os/osMain';
import { getEmojiReducerState as emojis } from '../util/loadRecentEmojis';
import { getInitialState as stickers } from '../types/Stickers';
import { getThemeType } from '../util/getThemeType';
import { getInteractionMode } from '../services/InteractionMode';
import { makeLookup } from '../util/makeLookup';
import type { CallHistoryDetails } from '../types/CallDisposition';
import type { ThemeType } from '../types/Util';
import type { CallLinkType } from '../types/CallLink';
export function getInitialState({
badges,
callLinks,
callsHistory,
callsHistoryUnreadCount,
stories,
storyDistributionLists,
mainWindowStats,
menuOptions,
theme,
}: {
badges: BadgesStateType;
callLinks: ReadonlyArray<CallLinkType>;
callsHistory: ReadonlyArray<CallHistoryDetails>;
callsHistoryUnreadCount: number;
stories: Array<StoryDataType>;
storyDistributionLists: Array<StoryDistributionListDataType>;
mainWindowStats: MainWindowStatsType;
menuOptions: MenuOptionsType;
theme: ThemeType;
}): StateType {
const items = window.storage.getItemsState();
@ -72,8 +77,6 @@ export function getInitialState({
window.ConversationController.getOurConversationId();
const ourDeviceId = window.textsecure.storage.user.getDeviceId();
const theme = getThemeType();
let osName: 'windows' | 'macos' | 'linux' | undefined;
if (OS.isWindows()) {
@ -95,7 +98,10 @@ export function getInitialState({
callHistoryByCallId: makeLookup(callsHistory, 'callId'),
unreadCount: callsHistoryUnreadCount,
},
calling: calling(),
calling: {
...calling(),
callLinks: makeLookup(callLinks, 'roomId'),
},
composer: composer(),
conversations: {
...conversations(),

View file

@ -11,8 +11,11 @@ import type { StoryDistributionListDataType } from './ducks/storyDistributionLis
import { actionCreators } from './actions';
import { createStore } from './createStore';
import { getInitialState } from './getInitialState';
import type { ThemeType } from '../types/Util';
import type { CallLinkType } from '../types/CallLink';
export function initializeRedux({
callLinks,
callsHistory,
callsHistoryUnreadCount,
initialBadgesState,
@ -20,7 +23,9 @@ export function initializeRedux({
menuOptions,
stories,
storyDistributionLists,
theme,
}: {
callLinks: ReadonlyArray<CallLinkType>;
callsHistory: ReadonlyArray<CallHistoryDetails>;
callsHistoryUnreadCount: number;
initialBadgesState: BadgesStateType;
@ -28,15 +33,18 @@ export function initializeRedux({
menuOptions: MenuOptionsType;
stories: Array<StoryDataType>;
storyDistributionLists: Array<StoryDistributionListDataType>;
theme: ThemeType;
}): void {
const initialState = getInitialState({
badges: initialBadgesState,
callLinks,
callsHistory,
callsHistoryUnreadCount,
mainWindowStats,
menuOptions,
stories,
storyDistributionLists,
theme,
});
const store = createStore(initialState);

View file

@ -9,12 +9,12 @@ import { Provider } from 'react-redux';
import type { Store } from 'redux';
import { ModalHost } from '../../components/ModalHost';
import type { PropsType } from '../smart/GroupV2JoinDialog';
import type { SmartGroupV2JoinDialogProps } from '../smart/GroupV2JoinDialog';
import { SmartGroupV2JoinDialog } from '../smart/GroupV2JoinDialog';
export const createGroupV2JoinModal = (
store: Store,
props: PropsType
props: SmartGroupV2JoinDialogProps
): React.ReactElement => {
const { onClose } = props;

14
ts/state/selectors/app.ts Normal file
View file

@ -0,0 +1,14 @@
// Copyright 2024 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { createSelector } from 'reselect';
import type { StateType } from '../reducer';
import type { AppStateType } from '../ducks/app';
export const getApp = (state: StateType): AppStateType => state.app;
export const getHasInitialLoadCompleted = createSelector(
getApp,
({ hasInitialLoadCompleted }) => hasInitialLoadCompleted
);
export const getAppView = createSelector(getApp, ({ appView }) => appView);

View file

@ -8,12 +8,7 @@ import {
getUserConversationId,
getUserNumber,
} from './user';
import {
getAttachmentUrlForPath,
getMessagePropStatus,
getSource,
getSourceServiceId,
} from './message';
import { getMessagePropStatus, getSource, getSourceServiceId } from './message';
import {
getConversationByIdSelector,
getConversations,
@ -22,8 +17,9 @@ import {
} from './conversations';
import type { StateType } from '../reducer';
import * as log from '../../logging/log';
import { getLocalAttachmentUrl } from '../../util/getLocalAttachmentUrl';
import type { MessageWithUIFieldsType } from '../ducks/conversations';
import type { MessageAttributesType } from '../../model-types.d';
import type { ReadonlyMessageAttributesType } from '../../model-types.d';
import { getMessageIdForLogging } from '../../util/idForLogging';
import * as Attachment from '../../types/Attachment';
import type { ActiveAudioPlayerStateType } from '../ducks/audioPlayer';
@ -61,7 +57,7 @@ export const selectVoiceNoteTitle = createSelector(
(ourNumber, ourAci, ourConversationId, conversationSelector, i18n) => {
return (
message: Pick<
MessageAttributesType,
ReadonlyMessageAttributesType,
'type' | 'source' | 'sourceServiceId'
>
) => {
@ -79,7 +75,7 @@ export const selectVoiceNoteTitle = createSelector(
);
export function extractVoiceNoteForPlayback(
message: MessageAttributesType,
message: ReadonlyMessageAttributesType,
ourConversationId: string | undefined
): VoiceNoteForPlayback | undefined {
const { type } = message;
@ -94,7 +90,7 @@ export function extractVoiceNoteForPlayback(
return;
}
const voiceNoteUrl = attachment.path
? getAttachmentUrlForPath(attachment.path)
? getLocalAttachmentUrl(attachment)
: undefined;
const status = getMessagePropStatus(message, ourConversationId);

View file

@ -0,0 +1,23 @@
// Copyright 2024 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { createSelector } from 'reselect';
import type { StateType } from '../reducer';
import type { AudioRecorderStateType } from '../ducks/audioRecorder';
export function getAudioRecorder(state: StateType): AudioRecorderStateType {
return state.audioRecorder;
}
export const getErrorDialogAudioRecorderType = createSelector(
getAudioRecorder,
audioRecorder => {
return audioRecorder.errorDialogAudioRecorderType;
}
);
export const getRecordingState = createSelector(
getAudioRecorder,
audioRecorder => {
return audioRecorder.recordingState;
}
);

View file

@ -4,7 +4,11 @@
import { createSelector } from 'reselect';
import type { CallHistoryState } from '../ducks/callHistory';
import type { StateType } from '../reducer';
import type { CallHistoryDetails } from '../../types/CallDisposition';
import {
AdhocCallStatus,
CallType,
type CallHistoryDetails,
} from '../../types/CallDisposition';
import { getOwn } from '../../util/getOwn';
const getCallHistory = (state: StateType): CallHistoryState =>
@ -36,3 +40,28 @@ export const getCallHistoryUnreadCount = createSelector(
return callHistory.unreadCount;
}
);
export const getCallHistoryLatestCall = createSelector(
getCallHistory,
callHistory => {
let latestCall = null;
for (const callId of Object.keys(callHistory.callHistoryByCallId)) {
const call = callHistory.callHistoryByCallId[callId];
// Skip unused call links
if (
call.type === CallType.Adhoc &&
call.status === AdhocCallStatus.Pending
) {
continue;
}
if (latestCall == null || call.timestamp > latestCall.timestamp) {
latestCall = call;
}
}
return latestCall;
}
);

View file

@ -23,6 +23,36 @@ export type CallStateType = DirectCallStateType | GroupCallStateType;
const getCalling = (state: StateType): CallingStateType => state.calling;
export const getAvailableMicrophones = createSelector(
getCalling,
({ availableMicrophones }) => availableMicrophones
);
export const getSelectedMicrophone = createSelector(
getCalling,
({ selectedMicrophone }) => selectedMicrophone
);
export const getAvailableSpeakers = createSelector(
getCalling,
({ availableSpeakers }) => availableSpeakers
);
export const getSelectedSpeaker = createSelector(
getCalling,
({ selectedSpeaker }) => selectedSpeaker
);
export const getAvailableCameras = createSelector(
getCalling,
({ availableCameras }) => availableCameras
);
export const getSelectedCamera = createSelector(
getCalling,
({ selectedCamera }) => selectedCamera
);
export const getActiveCallState = createSelector(
getCalling,
(state: CallingStateType) => state.activeCallState
@ -49,21 +79,13 @@ export type CallLinkSelectorType = (roomId: string) => CallLinkType | undefined;
export const getCallLinkSelector = createSelector(
getCallLinksByRoomId,
(callLinksByRoomId: CallLinksByRoomIdType): CallLinkSelectorType =>
(roomId: string): CallLinkType | undefined => {
const callLinkState = getOwn(callLinksByRoomId, roomId);
if (!callLinkState) {
return;
}
(roomId: string): CallLinkType | undefined =>
getOwn(callLinksByRoomId, roomId)
);
const { name, restrictions, rootKey, expiration } = callLinkState;
return {
roomId,
name,
restrictions,
rootKey,
expiration,
};
}
export const getAllCallLinks = createSelector(
getCallLinksByRoomId,
(lookup): Array<CallLinkType> => Object.values(lookup)
);
export type CallSelectorType = (

View file

@ -30,10 +30,7 @@ import { deconstructLookup } from '../../util/deconstructLookup';
import type { PropsDataType as TimelinePropsType } from '../../components/conversation/Timeline';
import { assertDev } from '../../util/assert';
import { isConversationUnregistered } from '../../util/isConversationUnregistered';
import {
filterAndSortConversationsAlphabetically,
filterAndSortConversationsByRecent,
} from '../../util/filterAndSortConversations';
import { filterAndSortConversations } from '../../util/filterAndSortConversations';
import type { ContactNameColorType } from '../../types/Colors';
import { ContactNameColors } from '../../types/Colors';
import type { AvatarDataType } from '../../types/Avatar';
@ -208,6 +205,12 @@ export const getTargetedMessage = createSelector(
};
}
);
export const getTargetedMessageSource = createSelector(
getConversations,
(state: ConversationsStateType): string | undefined => {
return state.targetedMessageSource;
}
);
export const getSelectedMessageIds = createSelector(
getConversations,
(state: ConversationsStateType): ReadonlyArray<string> | undefined => {
@ -300,8 +303,9 @@ const collator = new Intl.Collator();
// phone numbers and contacts from scratch here again.
export const _getConversationComparator = () => {
return (left: ConversationType, right: ConversationType): number => {
const leftTimestamp = left.timestamp;
const rightTimestamp = right.timestamp;
// These two fields can be sorted with each other; they are timestamps
const leftTimestamp = left.lastMessageReceivedAtMs || left.timestamp;
const rightTimestamp = right.lastMessageReceivedAtMs || right.timestamp;
if (leftTimestamp && !rightTimestamp) {
return -1;
}
@ -312,6 +316,19 @@ export const _getConversationComparator = () => {
return rightTimestamp - leftTimestamp;
}
// This field looks like a timestamp, but is actually a counter
const leftCounter = left.lastMessageReceivedAt;
const rightCounter = right.lastMessageReceivedAt;
if (leftCounter && !rightCounter) {
return -1;
}
if (rightCounter && !leftCounter) {
return 1;
}
if (leftCounter && rightCounter && leftCounter !== rightCounter) {
return rightCounter - leftCounter;
}
if (
typeof left.inboxPosition === 'number' &&
typeof right.inboxPosition === 'number'
@ -513,6 +530,13 @@ export const getComposerUUIDFetchState = createSelector(
}
);
export const getHasContactSpoofingReview = createSelector(
getConversations,
(state: ConversationsStateType): boolean => {
return state.hasContactSpoofingReview;
}
);
function isTrusted(conversation: ConversationType): boolean {
if (conversation.type === 'group') {
return true;
@ -711,11 +735,7 @@ export const getFilteredComposeContacts = createSelector(
contacts: ReadonlyArray<ConversationType>,
regionCode: string | undefined
): Array<ConversationType> => {
return filterAndSortConversationsAlphabetically(
contacts,
searchTerm,
regionCode
);
return filterAndSortConversations(contacts, searchTerm, regionCode);
}
);
@ -737,18 +757,16 @@ export const getFilteredComposeGroups = createSelector(
}>;
}
> => {
return filterAndSortConversationsAlphabetically(
groups,
searchTerm,
regionCode
).map(group => ({
...group,
// we don't disable groups when composing, already filtered
disabledReason: undefined,
// should always be populated for a group
membersCount: group.membersCount ?? 0,
memberships: group.memberships ?? [],
}));
return filterAndSortConversations(groups, searchTerm, regionCode).map(
group => ({
...group,
// we don't disable groups when composing, already filtered
disabledReason: undefined,
// should always be populated for a group
membersCount: group.membersCount ?? 0,
memberships: group.memberships ?? [],
})
);
}
);
@ -756,7 +774,7 @@ export const getFilteredCandidateContactsForNewGroup = createSelector(
getCandidateContactsForNewGroup,
getNormalizedComposerConversationSearchTerm,
getRegionCode,
filterAndSortConversationsByRecent
filterAndSortConversations
);
const getGroupCreationComposerState = createSelector(
@ -844,43 +862,66 @@ export const getCachedSelectorForConversation = createSelector(
}
);
export type GetConversationByIdType = (id?: string) => ConversationType;
export const getConversationSelector = createSelector(
getCachedSelectorForConversation,
export type GetConversationByAnyIdSelectorType = (
id?: string
) => ConversationType | undefined;
export const getConversationByAnyIdSelector = createSelector(
getConversationLookup,
getConversationsByServiceId,
getConversationsByE164,
getConversationsByGroupId,
(
selector: CachedConversationSelectorType,
byId: ConversationLookupType,
byServiceId: ConversationLookupType,
byE164: ConversationLookupType,
byGroupId: ConversationLookupType
): GetConversationByAnyIdSelectorType => {
return (id?: string) => {
if (!id) {
return undefined;
}
const onGroupId = getOwn(byGroupId, id);
if (onGroupId) {
return onGroupId;
}
const onServiceId = getOwn(
byServiceId,
normalizeServiceId(id, 'getConversationSelector')
);
if (onServiceId) {
return onServiceId;
}
const onE164 = getOwn(byE164, id);
if (onE164) {
return onE164;
}
const onId = getOwn(byId, id);
if (onId) {
return onId;
}
return undefined;
};
}
);
export type GetConversationByIdType = (id?: string) => ConversationType;
export const getConversationSelector = createSelector(
getCachedSelectorForConversation,
getConversationByAnyIdSelector,
(
selector: CachedConversationSelectorType,
getById: GetConversationByAnyIdSelectorType
): GetConversationByIdType => {
return (id?: string) => {
if (!id) {
return selector(undefined);
}
const onServiceId = getOwn(
byServiceId,
normalizeServiceId(id, 'getConversationSelector')
);
if (onServiceId) {
return selector(onServiceId);
}
const onE164 = getOwn(byE164, id);
if (onE164) {
return selector(onE164);
}
const onGroupId = getOwn(byGroupId, id);
if (onGroupId) {
return selector(onGroupId);
}
const onId = getOwn(byId, id);
if (onId) {
return selector(onId);
const byId = getById(id);
if (byId) {
return selector(byId);
}
log.warn(`getConversationSelector: No conversation found for id ${id}`);
@ -986,10 +1027,10 @@ export function _conversationMessagesSelector(
conversation: ConversationMessageType
): TimelinePropsType {
const {
isNearBottom,
isNearBottom = null,
messageChangeCounter,
messageIds,
messageLoadingState,
messageLoadingState = null,
metrics,
scrollToMessageCounter,
scrollToMessageId,
@ -1009,10 +1050,10 @@ export function _conversationMessagesSelector(
const oldestUnseenIndex = oldestUnseen
? messageIds.findIndex(id => id === oldestUnseen.id)
: undefined;
: null;
const scrollToIndex = scrollToMessageId
? messageIds.findIndex(id => id === scrollToMessageId)
: undefined;
: null;
const { totalUnseen } = metrics;
return {
@ -1025,9 +1066,9 @@ export function _conversationMessagesSelector(
oldestUnseenIndex:
isNumber(oldestUnseenIndex) && oldestUnseenIndex >= 0
? oldestUnseenIndex
: undefined,
: null,
scrollToIndex:
isNumber(scrollToIndex) && scrollToIndex >= 0 ? scrollToIndex : undefined,
isNumber(scrollToIndex) && scrollToIndex >= 0 ? scrollToIndex : null,
scrollToIndexCounter: scrollToMessageCounter,
totalUnseen,
};
@ -1065,6 +1106,9 @@ export const getConversationMessagesSelector = createSelector(
scrollToIndexCounter: 0,
totalUnseen: 0,
items: [],
isNearBottom: null,
oldestUnseenIndex: null,
scrollToIndex: null,
};
}

View file

@ -0,0 +1,18 @@
// Copyright 2024 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { createSelector } from 'reselect';
import type { StateType } from '../reducer';
import type { CrashReportsStateType } from '../ducks/crashReports';
const getCrashReports = (state: StateType): CrashReportsStateType =>
state.crashReports;
export const getCrashReportsIsPending = createSelector(
getCrashReports,
({ isPending }) => isPending
);
export const getCrashReportCount = createSelector(
getCrashReports,
({ count }) => count
);

View file

@ -21,3 +21,68 @@ export const isShowingAnyModal = createSelector(
return Boolean(value);
})
);
export const getCallLinkEditModalRoomId = createSelector(
getGlobalModalsState,
({ callLinkEditModalRoomId }) => callLinkEditModalRoomId
);
export const getCallLinkAddNameModalRoomId = createSelector(
getGlobalModalsState,
({ callLinkAddNameModalRoomId }) => callLinkAddNameModalRoomId
);
export const getConfirmLeaveCallModalState = createSelector(
getGlobalModalsState,
({ confirmLeaveCallModalState }) => confirmLeaveCallModalState
);
export const getContactModalState = createSelector(
getGlobalModalsState,
({ contactModalState }) => contactModalState
);
export const getIsStoriesSettingsVisible = createSelector(
getGlobalModalsState,
({ isStoriesSettingsVisible }) => isStoriesSettingsVisible
);
export const getSafetyNumberChangedBlockingData = createSelector(
getGlobalModalsState,
({ safetyNumberChangedBlockingData }) => safetyNumberChangedBlockingData
);
export const getDeleteMessagesProps = createSelector(
getGlobalModalsState,
({ deleteMessagesProps }) => deleteMessagesProps
);
export const getEditHistoryMessages = createSelector(
getGlobalModalsState,
({ editHistoryMessages }) => editHistoryMessages
);
export const getForwardMessagesProps = createSelector(
getGlobalModalsState,
({ forwardMessagesProps }) => forwardMessagesProps
);
export const getProfileEditorHasError = createSelector(
getGlobalModalsState,
({ profileEditorHasError }) => profileEditorHasError
);
export const getProfileEditorInitialEditState = createSelector(
getGlobalModalsState,
({ profileEditorInitialEditState }) => profileEditorInitialEditState
);
export const getEditNicknameAndNoteModalProps = createSelector(
getGlobalModalsState,
({ editNicknameAndNoteModalProps }) => editNicknameAndNoteModalProps
);
export const getNotePreviewModalProps = createSelector(
getGlobalModalsState,
({ notePreviewModalProps }) => notePreviewModalProps
);

View file

@ -0,0 +1,17 @@
// Copyright 2024 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { createSelector } from 'reselect';
import type { StateType } from '../reducer';
const getInboxState = (state: StateType) => state.inbox;
export const getInboxEnvelopeTimestamp = createSelector(
getInboxState,
({ envelopeTimestamp }) => envelopeTimestamp
);
export const getInboxFirstEnvelopeTimestamp = createSelector(
getInboxState,
({ firstEnvelopeTimestamp }) => firstEnvelopeTimestamp
);

View file

@ -0,0 +1,34 @@
// Copyright 2019 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { createSelector } from 'reselect';
import { getUserACI } from './user';
import { getConversationSelector } from './conversations';
import type { AciString } from '../../types/ServiceId';
import type { GetConversationByIdType } from './conversations';
export const getDeleteSyncSendEnabled = createSelector(
getUserACI,
getConversationSelector,
(
aci: AciString | undefined,
conversationSelector: GetConversationByIdType
): boolean => {
if (!aci) {
return false;
}
const ourConversation = conversationSelector(aci);
if (!ourConversation) {
return false;
}
const { capabilities } = ourConversation;
if (!capabilities || !capabilities.deleteSync) {
return false;
}
return true;
}
);

View file

@ -115,7 +115,7 @@ export const getUsernameLink = createSelector(
const content = Bytes.concatenate([entropy, serverId]);
return contactByEncryptedUsernameRoute
.toWebUrl({ encryptedUsername: Bytes.toBase64(content) })
.toWebUrl({ encryptedUsername: Bytes.toBase64url(content) })
.toString();
}
);
@ -228,3 +228,23 @@ export const getNavTabsCollapsed = createSelector(
getItems,
(state: ItemsStateType): boolean => Boolean(state.navTabsCollapsed ?? false)
);
export const getShowStickersIntroduction = createSelector(
getItems,
(state: ItemsStateType): boolean => {
return state.showStickersIntroduction ?? false;
}
);
export const getShowStickerPickerHint = createSelector(
getItems,
(state: ItemsStateType): boolean => {
return state.showStickerPickerHint ?? false;
}
);
export const getLocalDeleteWarningShown = createSelector(
getItems,
(state: ItemsStateType): boolean =>
Boolean(state.localDeleteWarningShown ?? false)
);

View file

@ -3,7 +3,6 @@
import { groupBy, isEmpty, isNumber, isObject, map } from 'lodash';
import { createSelector } from 'reselect';
import filesize from 'filesize';
import getDirection from 'direction';
import emojiRegex from 'emoji-regex';
import LinkifyIt from 'linkify-it';
@ -12,8 +11,9 @@ import type { ReadonlyDeep } from 'type-fest';
import type { StateType } from '../reducer';
import type {
LastMessageStatus,
MessageAttributesType,
ReadonlyMessageAttributesType,
MessageReactionType,
QuotedAttachmentType,
ShallowChallengeError,
} from '../../model-types.d';
@ -27,8 +27,10 @@ import type { PropsData as TimelineMessagePropsData } from '../../components/con
import { TextDirection } from '../../components/conversation/Message';
import type { PropsData as TimerNotificationProps } from '../../components/conversation/TimerNotification';
import type { PropsData as ChangeNumberNotificationProps } from '../../components/conversation/ChangeNumberNotification';
import type { PropsData as JoinedSignalNotificationProps } from '../../components/conversation/JoinedSignalNotification';
import type { PropsData as SafetyNumberNotificationProps } from '../../components/conversation/SafetyNumberNotification';
import type { PropsData as VerificationNotificationProps } from '../../components/conversation/VerificationNotification';
import type { PropsData as TitleTransitionNotificationProps } from '../../components/conversation/TitleTransitionNotification';
import type { PropsDataType as GroupsV2Props } from '../../components/conversation/GroupV2Change';
import type { PropsDataType as GroupV1MigrationPropsType } from '../../components/conversation/GroupV1Migration';
import type { PropsDataType as DeliveryIssuePropsType } from '../../components/conversation/DeliveryIssueNotification';
@ -40,7 +42,6 @@ import type {
ChangeType,
} from '../../components/conversation/GroupNotification';
import type { PropsType as ProfileChangeNotificationPropsType } from '../../components/conversation/ProfileChangeNotification';
import type { QuotedAttachmentType } from '../../components/conversation/Quote';
import { getDomain, isCallLink, isStickerPack } from '../../types/LinkPreview';
import type {
@ -57,8 +58,15 @@ import type { AssertProps } from '../../types/Util';
import type { LinkPreviewType } from '../../types/message/LinkPreviews';
import { getMentionsRegex } from '../../types/Message';
import { SignalService as Proto } from '../../protobuf';
import type { AttachmentType } from '../../types/Attachment';
import { isVoiceMessage, canBeDownloaded } from '../../types/Attachment';
import type {
AttachmentForUIType,
AttachmentType,
} from '../../types/Attachment';
import {
isVoiceMessage,
canBeDownloaded,
defaultBlurHash,
} from '../../types/Attachment';
import { type DefaultConversationColorType } from '../../types/Colors';
import { ReadStatus } from '../../messages/MessageReadStatus';
@ -70,6 +78,7 @@ import { isMoreRecentThan } from '../../util/timestamp';
import * as iterables from '../../util/iterables';
import { strictAssert } from '../../util/assert';
import { canEditMessage } from '../../util/canEditMessage';
import { getLocalAttachmentUrl } from '../../util/getLocalAttachmentUrl';
import { getAccountSelector } from './accounts';
import { getDefaultConversationColor } from './items';
@ -129,6 +138,7 @@ import type { AnyPaymentEvent } from '../../types/Payment';
import { isPaymentNotificationEvent } from '../../types/Payment';
import {
getTitleNoDefault,
getTitle,
getNumber,
renderNumber,
} from '../../util/getTitle';
@ -138,6 +148,8 @@ import { CallMode } from '../../types/Calling';
import { CallDirection } from '../../types/CallDisposition';
import { getCallIdFromEra } from '../../util/callDisposition';
import { LONG_MESSAGE } from '../../types/MIME';
import type { MessageRequestResponseNotificationData } from '../../components/conversation/MessageRequestResponseNotification';
import { formatFileSize } from '../../util/formatFileSize';
export { isIncoming, isOutgoing, isStory };
@ -152,7 +164,7 @@ type FormattedContact = Partial<ConversationType> &
| 'sharedGroupNames'
| 'title'
| 'type'
| 'unblurredAvatarPath'
| 'unblurredAvatarUrl'
>;
export type PropsForMessage = Omit<TimelineMessagePropsData, 'interactionMode'>;
export type MessagePropsType = Omit<
@ -189,7 +201,7 @@ export function hasErrors(
}
export function getSource(
message: Pick<MessageAttributesType, 'type' | 'source'>,
message: Pick<ReadonlyMessageAttributesType, 'type' | 'source'>,
ourNumber: string | undefined
): string | undefined {
if (isIncoming(message)) {
@ -221,7 +233,7 @@ export function getSourceDevice(
}
export function getSourceServiceId(
message: Pick<MessageAttributesType, 'type' | 'sourceServiceId'>,
message: Pick<ReadonlyMessageAttributesType, 'type' | 'sourceServiceId'>,
ourAci: AciString | undefined
): ServiceIdString | undefined {
if (isIncoming(message)) {
@ -241,7 +253,7 @@ export type GetContactOptions = Pick<
'conversationSelector' | 'ourConversationId' | 'ourNumber' | 'ourAci'
>;
export function getContactId(
export function getAuthorId(
message: MessageWithUIFieldsType,
{
conversationSelector,
@ -297,19 +309,15 @@ export const getAttachmentsForMessage = ({
if (sticker && sticker.data) {
const { data } = sticker;
// We don't show anything if we don't have the sticker or the blurhash...
if (!data.blurHash && (data.pending || !data.path)) {
return [];
}
return [
{
...data,
// We want to show the blurhash for stickers, not the spinner
pending: false,
url: data.path
? window.Signal.Migrations.getAbsoluteAttachmentPath(data.path)
: undefined,
// Stickers are not guaranteed to have a blurhash (e.g. if imported but
// undownloaded from backup), so we want to make sure we have something to show
blurHash: data.blurHash ?? defaultBlurHash(),
url: data.path ? getLocalAttachmentUrl(data) : undefined,
},
];
}
@ -343,7 +351,7 @@ const getAuthorForMessage = (
): PropsData['author'] => {
const {
acceptedMessageRequest,
avatarPath,
avatarUrl,
badges,
color,
id,
@ -353,12 +361,12 @@ const getAuthorForMessage = (
profileName,
sharedGroupNames,
title,
unblurredAvatarPath,
unblurredAvatarUrl,
} = getContact(message, options);
const unsafe = {
acceptedMessageRequest,
avatarPath,
avatarUrl,
badges,
color,
id,
@ -368,7 +376,7 @@ const getAuthorForMessage = (
profileName,
sharedGroupNames,
title,
unblurredAvatarPath,
unblurredAvatarUrl,
};
const safe: AssertProps<PropsData['author'], typeof unsafe> = unsafe;
@ -412,7 +420,7 @@ const getReactionsForMessage = (
const {
acceptedMessageRequest,
avatarPath,
avatarUrl,
badges,
color,
id,
@ -426,7 +434,7 @@ const getReactionsForMessage = (
const unsafe = {
acceptedMessageRequest,
avatarPath,
avatarUrl,
badges,
color,
id,
@ -701,7 +709,7 @@ export const getPropsForMessage = (
(message.reactions || []).find(re => re.fromId === ourConversationId) || {}
).emoji;
const authorId = getContactId(message, {
const authorId = getAuthorId(message, {
conversationSelector,
ourConversationId,
ourNumber,
@ -755,6 +763,7 @@ export const getPropsForMessage = (
isMessageRequestAccepted: conversation?.acceptedMessageRequest ?? true,
isSelected,
isSelectMode,
isSMS: message.sms === true,
isSpoilerExpanded: message.isSpoilerExpanded,
isSticker: Boolean(sticker),
isTargeted,
@ -768,7 +777,7 @@ export const getPropsForMessage = (
status: getMessagePropStatus(message, ourConversationId),
text: message.body,
textDirection: getTextDirection(message.body),
timestamp: getMessageSentTimestamp(message, { includeEdits: true, log }),
timestamp: getMessageSentTimestamp(message, { includeEdits: false, log }),
receivedAtMS: message.received_at_ms,
};
};
@ -790,18 +799,18 @@ export const getMessagePropsSelector = createSelector(
getSelectedMessageIds,
getDefaultConversationColor,
(
conversationSelector,
ourConversationId,
ourAci,
ourPni,
ourNumber,
regionCode,
accountSelector,
cachedConversationMemberColorsSelector,
targetedMessage,
selectedMessageIds,
defaultConversationColor
) =>
conversationSelector,
ourConversationId,
ourAci,
ourPni,
ourNumber,
regionCode,
accountSelector,
cachedConversationMemberColorsSelector,
targetedMessage,
selectedMessageIds,
defaultConversationColor
) =>
(message: MessageWithUIFieldsType) => {
const contactNameColors = cachedConversationMemberColorsSelector(
message.conversationId
@ -922,6 +931,20 @@ export function getPropsForBubble(
timestamp,
};
}
if (isJoinedSignalNotification(message)) {
return {
type: 'joinedSignalNotification',
data: getPropsForJoinedSignalNotification(message),
timestamp,
};
}
if (isTitleTransitionNotification(message)) {
return {
type: 'titleTransitionNotification',
data: getPropsForTitleTransitionNotification(message),
timestamp,
};
}
if (isChatSessionRefreshed(message)) {
return {
type: 'chatSessionRefreshed',
@ -962,6 +985,14 @@ export function getPropsForBubble(
};
}
if (isMessageRequestResponse(message)) {
return {
type: 'messageRequestResponse',
data: getPropsForMessageRequestResponse(message),
timestamp,
};
}
const data = getPropsForMessage(message, options);
return {
@ -971,6 +1002,30 @@ export function getPropsForBubble(
};
}
export function isNormalBubble(message: MessageWithUIFieldsType): boolean {
return (
!isCallHistory(message) &&
!isChatSessionRefreshed(message) &&
!isContactRemovedNotification(message) &&
!isConversationMerge(message) &&
!isEndSession(message) &&
!isExpirationTimerUpdate(message) &&
!isGroupUpdate(message) &&
!isGroupV1Migration(message) &&
!isGroupV2Change(message) &&
!isKeyChange(message) &&
!isPhoneNumberDiscovery(message) &&
!isTitleTransitionNotification(message) &&
!isProfileChange(message) &&
!isUniversalTimerNotification(message) &&
!isUnsupportedMessage(message) &&
!isVerifiedChange(message) &&
!isChangeNumberNotification(message) &&
!isJoinedSignalNotification(message) &&
!isDeliveryIssue(message)
);
}
function getPropsForPaymentEvent(
message: MessageAttributesWithPaymentEvent,
{ conversationSelector }: GetPropsForBubbleOptions
@ -1074,6 +1129,8 @@ function getPropsForGroupV1Migration(
conversationId: message.conversationId,
droppedMembers,
invitedMembers,
droppedMemberCount: droppedMembers.length,
invitedMemberCount: invitedMembers.length,
};
}
@ -1081,19 +1138,30 @@ function getPropsForGroupV1Migration(
areWeInvited,
droppedMemberIds,
invitedMembers: rawInvitedMembers,
droppedMemberCount: rawDroppedMemberCount,
invitedMemberCount: rawInvitedMemberCount,
} = migration;
const invitedMembers = rawInvitedMembers.map(item =>
conversationSelector(item.uuid)
);
const droppedMembers = droppedMemberIds.map(conversationId =>
conversationSelector(conversationId)
);
const droppedMembers = droppedMemberIds
? droppedMemberIds.map(conversationId =>
conversationSelector(conversationId)
)
: undefined;
const invitedMembers = rawInvitedMembers
? rawInvitedMembers.map(item => conversationSelector(item.uuid))
: undefined;
const droppedMemberCount =
rawDroppedMemberCount ?? droppedMemberIds?.length ?? 0;
const invitedMemberCount =
rawInvitedMemberCount ?? invitedMembers?.length ?? 0;
return {
areWeInvited,
conversationId: message.conversationId,
droppedMembers,
invitedMembers,
droppedMemberCount,
invitedMemberCount,
};
}
@ -1380,10 +1448,10 @@ export function getPropsForCallHistory(
const isSelectMode = selectedMessageIds != null;
let callCreator: ConversationType | null = null;
if (callHistory.ringerId) {
callCreator = conversationSelector(callHistory.ringerId);
} else if (callHistory.direction === CallDirection.Outgoing) {
if (callHistory.direction === CallDirection.Outgoing) {
callCreator = conversationSelector(ourConversationId);
} else if (callHistory.ringerId) {
callCreator = conversationSelector(callHistory.ringerId);
}
if (callHistory.mode === CallMode.Direct) {
@ -1452,6 +1520,24 @@ function getPropsForProfileChange(
} as ProfileChangeNotificationPropsType;
}
// Message Request Response Event
export function isMessageRequestResponse(
message: ReadonlyMessageAttributesType
): boolean {
return message.type === 'message-request-response-event';
}
function getPropsForMessageRequestResponse(
message: ReadonlyMessageAttributesType
): MessageRequestResponseNotificationData {
const { messageRequestResponseEvent } = message;
if (!messageRequestResponseEvent) {
throw new Error('getPropsForMessageRequestResponse: event is missing!');
}
return { messageRequestResponseEvent };
}
// Universal Timer Notification
// Note: smart, so props not generated here
@ -1490,6 +1576,47 @@ function getPropsForChangeNumberNotification(
};
}
// Joined Signal Notification
export function isJoinedSignalNotification(
message: MessageWithUIFieldsType
): boolean {
return message.type === 'joined-signal-notification';
}
function getPropsForJoinedSignalNotification(
message: MessageWithUIFieldsType
): JoinedSignalNotificationProps {
return {
timestamp: message.sent_at,
};
}
// Title Transition Notification
export function isTitleTransitionNotification(
message: MessageWithUIFieldsType
): boolean {
return (
message.type === 'title-transition-notification' &&
message.titleTransition != null
);
}
function getPropsForTitleTransitionNotification(
message: MessageWithUIFieldsType
): TitleTransitionNotificationProps {
strictAssert(
message.titleTransition != null,
'Invalid attributes for title-transition-notification'
);
const { renderInfo } = message.titleTransition;
const oldTitle = getTitle(renderInfo);
return {
oldTitle,
};
}
// Chat Session Refreshed
export function isChatSessionRefreshed(
@ -1678,7 +1805,7 @@ export function getPropsForEmbeddedContact(
message: MessageWithUIFieldsType,
regionCode: string | undefined,
accountSelector: (identifier?: string) => ServiceIdString | undefined
): EmbeddedContactType | undefined {
): ReadonlyDeep<EmbeddedContactType> | undefined {
const contacts = message.contact;
if (!contacts || !contacts.length) {
return undefined;
@ -1690,52 +1817,56 @@ export function getPropsForEmbeddedContact(
return embeddedContactSelector(firstContact, {
regionCode,
getAbsoluteAttachmentPath: getAttachmentUrlForPath,
firstNumber,
serviceId: accountSelector(firstNumber),
});
}
export function getAttachmentUrlForPath(path: string): string {
return window.Signal.Migrations.getAbsoluteAttachmentPath(path);
}
export function getPropsForAttachment(
attachment: AttachmentType
): AttachmentType | undefined {
): AttachmentForUIType | undefined {
if (!attachment) {
return undefined;
}
const { path, pending, size, screenshot, thumbnail } = attachment;
const { path, pending, size, screenshot, thumbnail, thumbnailFromBackup } =
attachment;
return {
...attachment,
fileSize: size ? filesize(size) : undefined,
fileSize: size ? formatFileSize(size) : undefined,
isVoiceMessage: isVoiceMessage(attachment),
pending,
url: path ? getAttachmentUrlForPath(path) : undefined,
url: path ? getLocalAttachmentUrl(attachment) : undefined,
thumbnailFromBackup: thumbnailFromBackup?.path
? {
...thumbnailFromBackup,
url: getLocalAttachmentUrl(thumbnailFromBackup),
}
: undefined,
screenshot: screenshot?.path
? {
...screenshot,
url: getAttachmentUrlForPath(screenshot.path),
url: getLocalAttachmentUrl({
// Legacy v1 screenshots
size: 0,
...screenshot,
}),
}
: undefined,
thumbnail: thumbnail?.path
? {
...thumbnail,
url: getAttachmentUrlForPath(thumbnail.path),
url: getLocalAttachmentUrl(thumbnail),
}
: undefined,
};
}
function processQuoteAttachment(
attachment: AttachmentType
): QuotedAttachmentType {
function processQuoteAttachment(attachment: QuotedAttachmentType) {
const { thumbnail } = attachment;
const path =
thumbnail && thumbnail.path && getAttachmentUrlForPath(thumbnail.path);
const path = thumbnail && thumbnail.path && getLocalAttachmentUrl(thumbnail);
const objectUrl = thumbnail && thumbnail.objectUrl;
const thumbnailWithObjectUrl =
@ -1960,7 +2091,7 @@ export function getLastChallengeError(
const getTargetedMessageForDetails = (
state: StateType
): MessageAttributesType | undefined =>
): ReadonlyMessageAttributesType | undefined =>
state.conversations.targetedMessageForDetails;
const OUTGOING_KEY_ERROR = 'OutgoingIdentityKeyError';
@ -2014,7 +2145,7 @@ export const getMessageDetails = createSelector(
let conversationIds: Array<string>;
if (isIncoming(message)) {
conversationIds = [
getContactId(message, {
getAuthorId(message, {
conversationSelector,
ourConversationId,
ourNumber,

View file

@ -6,23 +6,36 @@ import { createSelector } from 'reselect';
import type { StateType } from '../reducer';
import type { NetworkStateType } from '../ducks/network';
import { isDone } from '../../util/registration';
import { SocketStatus } from '../../types/SocketStatus';
const getNetwork = (state: StateType): NetworkStateType => state.network;
export const getNetworkIsOnline = createSelector(
getNetwork,
({ isOnline }) => isOnline
);
export const getNetworkIsOutage = createSelector(
getNetwork,
({ isOutage }) => isOutage
);
export const getNetworkSocketStatus = createSelector(
getNetwork,
({ socketStatus }) => socketStatus
);
export const hasNetworkDialog = createSelector(
getNetwork,
isDone,
(
{ isOnline, socketStatus, withinConnectingGracePeriod }: NetworkStateType,
{ isOnline, isOutage }: NetworkStateType,
isRegistrationDone: boolean
): boolean =>
isRegistrationDone &&
(!isOnline ||
(socketStatus === SocketStatus.CONNECTING &&
!withinConnectingGracePeriod) ||
socketStatus === SocketStatus.CLOSED ||
socketStatus === SocketStatus.CLOSING)
): boolean => isRegistrationDone && (!isOnline || isOutage)
);
export const getChallengeStatus = createSelector(
getNetwork,
({ challengeStatus }) => challengeStatus
);
export const isChallengePending = createSelector(

View file

@ -23,5 +23,14 @@ export const getContactSafetyNumber = createSelector(
(
{ contacts }: SafetyNumberStateType,
contactID: string
): SafetyNumberContactType => contacts[contactID]
): SafetyNumberContactType | void => contacts[contactID]
);
export const getContactSafetyNumberSelector = createSelector(
[getSafetyNumber],
({ contacts }) => {
return (contactId: string) => {
return contacts[contactId];
};
}
);

View file

@ -1,12 +1,15 @@
// Copyright 2019 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { join } from 'path';
import type { Dictionary } from 'lodash';
import { compact, filter, map, orderBy, reject, sortBy, values } from 'lodash';
import { createSelector } from 'reselect';
import type { RecentStickerType } from '../../types/Stickers';
import {
getLocalAttachmentUrl,
AttachmentDisposition,
} from '../../util/getLocalAttachmentUrl';
import type {
StickerType as StickerDBType,
StickerPackType as StickerPackDBType,
@ -17,14 +20,11 @@ import type {
StickerPackType,
StickerType,
} from '../ducks/stickers';
import { getStickersPath, getTempPath } from './user';
const getSticker = (
packs: Dictionary<StickerPackDBType>,
packId: string,
stickerId: number,
stickerPath: string,
tempPath: string
stickerId: number
): StickerType | undefined => {
const pack = packs[packId];
if (!pack) {
@ -38,32 +38,31 @@ const getSticker = (
const isEphemeral = pack.status === 'ephemeral';
return translateStickerFromDB(sticker, stickerPath, tempPath, isEphemeral);
return translateStickerFromDB(sticker, isEphemeral);
};
const translateStickerFromDB = (
sticker: StickerDBType,
stickerPath: string,
tempPath: string,
isEphemeral: boolean
): StickerType => {
const { id, packId, emoji, path } = sticker;
const prefix = isEphemeral ? tempPath : stickerPath;
const { id, packId, emoji } = sticker;
return {
id,
packId,
emoji,
url: join(prefix, path),
url: getLocalAttachmentUrl(sticker, {
disposition: isEphemeral
? AttachmentDisposition.Temporary
: AttachmentDisposition.Sticker,
}),
};
};
export const translatePackFromDB = (
pack: StickerPackDBType,
packs: Dictionary<StickerPackDBType>,
blessedPacks: Dictionary<boolean>,
stickersPath: string,
tempPath: string
blessedPacks: Dictionary<boolean>
): StickerPackType => {
const { id, stickers, status, coverStickerId } = pack;
const isEphemeral = status === 'ephemeral';
@ -75,13 +74,13 @@ export const translatePackFromDB = (
sticker => sticker.isCoverOnly
);
const translatedStickers = map(filteredStickers, sticker =>
translateStickerFromDB(sticker, stickersPath, tempPath, isEphemeral)
translateStickerFromDB(sticker, isEphemeral)
);
return {
...pack,
isBlessed: Boolean(blessedPacks[id]),
cover: getSticker(packs, id, coverStickerId, stickersPath, tempPath),
cover: getSticker(packs, id, coverStickerId),
stickers: sortBy(translatedStickers, sticker => sticker.id),
};
};
@ -90,16 +89,12 @@ const filterAndTransformPacks = (
packs: Dictionary<StickerPackDBType>,
packFilter: (sticker: StickerPackDBType) => boolean,
packSort: (sticker: StickerPackDBType) => number | undefined,
blessedPacks: Dictionary<boolean>,
stickersPath: string,
tempPath: string
blessedPacks: Dictionary<boolean>
): Array<StickerPackType> => {
const list = filter(packs, packFilter);
const sorted = orderBy<StickerPackDBType>(list, packSort, ['desc']);
return sorted.map(pack =>
translatePackFromDB(pack, packs, blessedPacks, stickersPath, tempPath)
);
return sorted.map(pack => translatePackFromDB(pack, packs, blessedPacks));
};
const getStickers = (state: StateType) => state.stickers;
@ -122,17 +117,13 @@ export const getBlessedPacks = createSelector(
export const getRecentStickers = createSelector(
getRecents,
getPacks,
getStickersPath,
getTempPath,
(
recents: ReadonlyArray<RecentStickerType>,
packs: Dictionary<StickerPackDBType>,
stickersPath: string,
tempPath: string
packs: Dictionary<StickerPackDBType>
) => {
return compact(
recents.map(({ packId, stickerId }) => {
return getSticker(packs, packId, stickerId, stickersPath, tempPath);
return getSticker(packs, packId, stickerId);
})
);
}
@ -141,21 +132,15 @@ export const getRecentStickers = createSelector(
export const getInstalledStickerPacks = createSelector(
getPacks,
getBlessedPacks,
getStickersPath,
getTempPath,
(
packs: Dictionary<StickerPackDBType>,
blessedPacks: Dictionary<boolean>,
stickersPath: string,
tempPath: string
blessedPacks: Dictionary<boolean>
): Array<StickerPackType> => {
return filterAndTransformPacks(
packs,
pack => pack.status === 'installed',
pack => pack.installedAt,
blessedPacks,
stickersPath,
tempPath
blessedPacks
);
}
);
@ -175,13 +160,9 @@ export const getRecentlyInstalledStickerPack = createSelector(
export const getReceivedStickerPacks = createSelector(
getPacks,
getBlessedPacks,
getStickersPath,
getTempPath,
(
packs: Dictionary<StickerPackDBType>,
blessedPacks: Dictionary<boolean>,
stickersPath: string,
tempPath: string
blessedPacks: Dictionary<boolean>
): Array<StickerPackType> => {
return filterAndTransformPacks(
packs,
@ -189,9 +170,7 @@ export const getReceivedStickerPacks = createSelector(
(pack.status === 'downloaded' || pack.status === 'pending') &&
!blessedPacks[pack.id],
pack => pack.createdAt,
blessedPacks,
stickersPath,
tempPath
blessedPacks
);
}
);
@ -199,21 +178,15 @@ export const getReceivedStickerPacks = createSelector(
export const getBlessedStickerPacks = createSelector(
getPacks,
getBlessedPacks,
getStickersPath,
getTempPath,
(
packs: Dictionary<StickerPackDBType>,
blessedPacks: Dictionary<boolean>,
stickersPath: string,
tempPath: string
blessedPacks: Dictionary<boolean>
): Array<StickerPackType> => {
return filterAndTransformPacks(
packs,
pack => blessedPacks[pack.id] && pack.status !== 'installed',
pack => pack.createdAt,
blessedPacks,
stickersPath,
tempPath
blessedPacks
);
}
);
@ -221,21 +194,15 @@ export const getBlessedStickerPacks = createSelector(
export const getKnownStickerPacks = createSelector(
getPacks,
getBlessedPacks,
getStickersPath,
getTempPath,
(
packs: Dictionary<StickerPackDBType>,
blessedPacks: Dictionary<boolean>,
stickersPath: string,
tempPath: string
blessedPacks: Dictionary<boolean>
): Array<StickerPackType> => {
return filterAndTransformPacks(
packs,
pack => !blessedPacks[pack.id] && pack.status === 'known',
pack => pack.createdAt,
blessedPacks,
stickersPath,
tempPath
blessedPacks
);
}
);

View file

@ -126,7 +126,7 @@ function getAvatarData(
): Pick<
ConversationType,
| 'acceptedMessageRequest'
| 'avatarPath'
| 'avatarUrl'
| 'badges'
| 'color'
| 'isMe'
@ -138,7 +138,7 @@ function getAvatarData(
> {
return pick(conversation, [
'acceptedMessageRequest',
'avatarPath',
'avatarUrl',
'badges',
'color',
'isMe',
@ -166,7 +166,7 @@ export function getStoryView(
conversationSelector(story.sourceServiceId || story.source),
[
'acceptedMessageRequest',
'avatarPath',
'avatarUrl',
'badges',
'color',
'firstName',
@ -253,7 +253,7 @@ export function getConversationStory(
const conversation = pick(conversationSelector(story.conversationId), [
'acceptedMessageRequest',
'avatarPath',
'avatarUrl',
'color',
'hideStory',
'id',

View file

@ -0,0 +1,11 @@
// Copyright 2024 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { createSelector } from 'reselect';
import type { StateType } from '../reducer';
import type { ToastStateType } from '../ducks/toast';
export function getToastState(state: StateType): ToastStateType {
return state.toast;
}
export const getToast = createSelector(getToastState, ({ toast }) => toast);

View file

@ -11,6 +11,26 @@ import type { UpdatesStateType } from '../ducks/updates';
export const getUpdatesState = (state: Readonly<StateType>): UpdatesStateType =>
state.updates;
export const getUpdateDialogType = createSelector(
getUpdatesState,
({ dialogType }) => dialogType
);
export const getUpdateVersion = createSelector(
getUpdatesState,
({ version }) => version
);
export const getUpdateDownloadSize = createSelector(
getUpdatesState,
({ downloadSize }) => downloadSize
);
export const getUpdateDownloadedSize = createSelector(
getUpdatesState,
({ downloadedSize }) => downloadedSize
);
export const isUpdateDialogVisible = createSelector(
getUpdatesState,
({ dialogType, didSnooze }) => {

View file

@ -3,12 +3,13 @@
import { createSelector } from 'reselect';
import type { LocalizerType, ThemeType } from '../../types/Util';
import { type LocalizerType, ThemeType } from '../../types/Util';
import type { AciString, PniString } from '../../types/ServiceId';
import type { LocaleMessagesType } from '../../types/I18N';
import type { MenuOptionsType } from '../../types/menu';
import type { StateType } from '../reducer';
import type { CallingStateType } from '../ducks/calling';
import type { UserStateType } from '../ducks/user';
import { isAlpha, isBeta } from '../../util/version';
@ -80,11 +81,26 @@ export const getTempPath = createSelector(
(state: UserStateType): string => state.tempPath
);
export const getTheme = createSelector(
export const getPreferredTheme = createSelector(
getUser,
(state: UserStateType): ThemeType => state.theme
);
// Also defined in calling selectors, redefined to avoid circular dependency
const getIsInFullScreenCall = createSelector(
(state: StateType): CallingStateType => state.calling,
(state: CallingStateType): boolean =>
Boolean(state.activeCallState && !state.activeCallState.pip)
);
export const getTheme = createSelector(
getPreferredTheme,
getIsInFullScreenCall,
(theme: ThemeType, isInCall: boolean): ThemeType => {
return isInCall ? ThemeType.dark : theme;
}
);
const getVersion = createSelector(
getUser,
(state: UserStateType) => state.version

View file

@ -1,9 +1,7 @@
// Copyright 2024 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React from 'react';
import React, { memo, useCallback } from 'react';
import { useSelector } from 'react-redux';
import { AboutContactModal } from '../../components/conversation/AboutContactModal';
import { isSignalConnection } from '../../util/getSignalConnections';
import { getIntl } from '../selectors/user';
@ -11,8 +9,9 @@ import { getGlobalModalsState } from '../selectors/globalModals';
import { getConversationSelector } from '../selectors/conversations';
import { useConversationsActions } from '../ducks/conversations';
import { useGlobalModalActions } from '../ducks/globalModals';
import { strictAssert } from '../../util/assert';
export function SmartAboutContactModal(): JSX.Element | null {
export const SmartAboutContactModal = memo(function SmartAboutContactModal() {
const i18n = useSelector(getIntl);
const globalModals = useSelector(getGlobalModalsState);
const { aboutContactModalContactId: contactId } = globalModals;
@ -20,18 +19,25 @@ export function SmartAboutContactModal(): JSX.Element | null {
const { updateSharedGroups, unblurAvatar } = useConversationsActions();
const conversation = getConversation(contactId);
const { id: conversationId } = conversation ?? {};
const {
toggleAboutContactModal,
toggleSignalConnectionsModal,
toggleSafetyNumberModal,
toggleNotePreviewModal,
} = useGlobalModalActions();
if (!contactId) {
const handleOpenNotePreviewModal = useCallback(() => {
strictAssert(conversationId != null, 'conversationId is required');
toggleNotePreviewModal({ conversationId });
}, [toggleNotePreviewModal, conversationId]);
if (conversation == null) {
return null;
}
const conversation = getConversation(contactId);
return (
<AboutContactModal
i18n={i18n}
@ -42,6 +48,7 @@ export function SmartAboutContactModal(): JSX.Element | null {
toggleSafetyNumberModal={toggleSafetyNumberModal}
isSignalConnection={isSignalConnection(conversation)}
onClose={toggleAboutContactModal}
onOpenNotePreviewModal={handleOpenNotePreviewModal}
/>
);
}
});

View file

@ -1,37 +1,47 @@
// Copyright 2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { connect } from 'react-redux';
import { mapDispatchToProps } from '../actions';
import { useSelector } from 'react-redux';
import React, { memo } from 'react';
import { AddUserToAnotherGroupModal } from '../../components/AddUserToAnotherGroupModal';
import type { StateType } from '../reducer';
import {
getAllGroupsWithInviteAccess,
getContactSelector,
} from '../selectors/conversations';
import { getIntl, getRegionCode, getTheme } from '../selectors/user';
import { getIntl, getRegionCode } from '../selectors/user';
import { useToastActions } from '../ducks/toast';
import { useGlobalModalActions } from '../ducks/globalModals';
import { useConversationsActions } from '../ducks/conversations';
export type Props = {
export type SmartAddUserToAnotherGroupModalProps = Readonly<{
contactID: string;
};
}>;
const mapStateToProps = (state: StateType, props: Props) => {
const candidateConversations = getAllGroupsWithInviteAccess(state);
const getContact = getContactSelector(state);
export const SmartAddUserToAnotherGroupModal = memo(
function SmartAddUserToAnotherGroupModal({
contactID,
}: SmartAddUserToAnotherGroupModalProps) {
const i18n = useSelector(getIntl);
const candidateConversations = useSelector(getAllGroupsWithInviteAccess);
const getContact = useSelector(getContactSelector);
const regionCode = useSelector(getRegionCode);
const regionCode = getRegionCode(state);
const { toggleAddUserToAnotherGroupModal } = useGlobalModalActions();
const { addMembersToGroup } = useConversationsActions();
const { showToast } = useToastActions();
return {
contact: getContact(props.contactID),
i18n: getIntl(state),
theme: getTheme(state),
candidateConversations,
regionCode,
};
};
const contact = getContact(contactID);
const smart = connect(mapStateToProps, mapDispatchToProps);
export const SmartAddUserToAnotherGroupModal = smart(
AddUserToAnotherGroupModal
return (
<AddUserToAnotherGroupModal
contact={contact}
i18n={i18n}
candidateConversations={candidateConversations}
regionCode={regionCode}
toggleAddUserToAnotherGroupModal={toggleAddUserToAnotherGroupModal}
addMembersToGroup={addMembersToGroup}
showToast={showToast}
/>
);
}
);

View file

@ -1,21 +1,20 @@
// Copyright 2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React from 'react';
import React, { memo } from 'react';
import { useSelector } from 'react-redux';
import { MediaGallery } from '../../components/conversation/media-gallery/MediaGallery';
import { getMediaGalleryState } from '../selectors/mediaGallery';
import { useConversationsActions } from '../ducks/conversations';
import { useLightboxActions } from '../ducks/lightbox';
import { useMediaGalleryActions } from '../ducks/mediaGallery';
export type PropsType = {
conversationId: string;
};
export function SmartAllMedia({ conversationId }: PropsType): JSX.Element {
export const SmartAllMedia = memo(function SmartAllMedia({
conversationId,
}: PropsType) {
const { media, documents } = useSelector(getMediaGalleryState);
const { loadMediaItems } = useMediaGalleryActions();
const { saveAttachment } = useConversationsActions();
@ -32,4 +31,4 @@ export function SmartAllMedia({ conversationId }: PropsType): JSX.Element {
saveAttachment={saveAttachment}
/>
);
}
});

View file

@ -1,87 +1,148 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React from 'react';
import React, { memo } from 'react';
import { useSelector } from 'react-redux';
import type { VerificationTransport } from '../../types/VerificationTransport';
import { DataWriter } from '../../sql/Client';
import { App } from '../../components/App';
import OS from '../../util/os/osMain';
import { getConversation } from '../../util/getConversation';
import { getChallengeURL } from '../../challenge';
import { writeProfile } from '../../services/writeProfile';
import { strictAssert } from '../../util/assert';
import { SmartCallManager } from './CallManager';
import { SmartGlobalModalContainer } from './GlobalModalContainer';
import { SmartLightbox } from './Lightbox';
import { SmartStoryViewer } from './StoryViewer';
import {
getTheme,
getIsMainWindowMaximized,
getIsMainWindowFullScreen,
getTheme,
} from '../selectors/user';
import { hasSelectedStoryData } from '../selectors/stories';
import type { StateType } from '../reducer';
import { hasSelectedStoryData as getHasSelectedStoryData } from '../selectors/stories';
import { useAppActions } from '../ducks/app';
import { useConversationsActions } from '../ducks/conversations';
import { useStoriesActions } from '../ducks/stories';
import { ErrorBoundary } from '../../components/ErrorBoundary';
import { ModalContainer } from '../../components/ModalContainer';
import { SmartInbox } from './Inbox';
import { getAppView } from '../selectors/app';
function renderInbox(): JSX.Element {
return <SmartInbox />;
}
export function SmartApp(): JSX.Element {
const app = useSelector((state: StateType) => state.app);
function renderCallManager(): JSX.Element {
return (
<ModalContainer className="module-calling__modal-container">
<SmartCallManager />
</ModalContainer>
);
}
function renderGlobalModalContainer(): JSX.Element {
return <SmartGlobalModalContainer />;
}
function renderLightbox(): JSX.Element {
return <SmartLightbox />;
}
function renderStoryViewer(closeView: () => unknown): JSX.Element {
return (
<ErrorBoundary name="App/renderStoryViewer" closeView={closeView}>
<SmartStoryViewer />
</ErrorBoundary>
);
}
async function getCaptchaToken(): Promise<string> {
const url = getChallengeURL('registration');
document.location.href = url;
if (!window.Signal.challengeHandler) {
throw new Error('Captcha handler is not ready!');
}
return window.Signal.challengeHandler.requestCaptcha({
reason: 'standalone registration',
});
}
function requestVerification(
number: string,
captcha: string,
transport: VerificationTransport
): Promise<{ sessionId: string }> {
const { server } = window.textsecure;
strictAssert(server !== undefined, 'WebAPI not available');
return server.requestVerification(number, captcha, transport);
}
function registerSingleDevice(
number: string,
code: string,
sessionId: string
): Promise<void> {
return window
.getAccountManager()
.registerSingleDevice(number, code, sessionId);
}
function readyForUpdates(): void {
window.IPC.readyForUpdates();
}
async function uploadProfile({
firstName,
lastName,
}: {
firstName: string;
lastName: string;
}): Promise<void> {
const us = window.ConversationController.getOurConversationOrThrow();
us.set('profileName', firstName);
us.set('profileFamilyName', lastName);
us.captureChange('standaloneProfile');
await DataWriter.updateConversation(us.attributes);
await writeProfile(getConversation(us), {
keepAvatar: true,
});
}
export const SmartApp = memo(function SmartApp() {
const appView = useSelector(getAppView);
const isMaximized = useSelector(getIsMainWindowMaximized);
const isFullScreen = useSelector(getIsMainWindowFullScreen);
const hasSelectedStoryData = useSelector(getHasSelectedStoryData);
const theme = useSelector(getTheme);
const { openInbox } = useAppActions();
const { scrollToMessage } = useConversationsActions();
const { viewStory } = useStoriesActions();
const osClassName = OS.getClassName();
return (
<App
{...app}
isMaximized={useSelector(getIsMainWindowMaximized)}
isFullScreen={useSelector(getIsMainWindowFullScreen)}
osClassName={OS.getClassName()}
renderCallManager={() => (
<ModalContainer className="module-calling__modal-container">
<SmartCallManager />
</ModalContainer>
)}
renderGlobalModalContainer={() => <SmartGlobalModalContainer />}
renderLightbox={() => <SmartLightbox />}
hasSelectedStoryData={useSelector(hasSelectedStoryData)}
renderStoryViewer={(closeView: () => unknown) => (
<ErrorBoundary name="App/renderStoryViewer" closeView={closeView}>
<SmartStoryViewer />
</ErrorBoundary>
)}
appView={appView}
isMaximized={isMaximized}
isFullScreen={isFullScreen}
getCaptchaToken={getCaptchaToken}
osClassName={osClassName}
renderCallManager={renderCallManager}
renderGlobalModalContainer={renderGlobalModalContainer}
renderLightbox={renderLightbox}
hasSelectedStoryData={hasSelectedStoryData}
readyForUpdates={readyForUpdates}
renderStoryViewer={renderStoryViewer}
renderInbox={renderInbox}
requestVerification={(
number: string,
captcha: string,
transport: VerificationTransport
): Promise<{ sessionId: string }> => {
const { server } = window.textsecure;
strictAssert(server !== undefined, 'WebAPI not available');
return server.requestVerification(number, captcha, transport);
}}
registerSingleDevice={(
number: string,
code: string,
sessionId: string
): Promise<void> => {
return window
.getAccountManager()
.registerSingleDevice(number, code, sessionId);
}}
theme={useSelector(getTheme)}
requestVerification={requestVerification}
registerSingleDevice={registerSingleDevice}
uploadProfile={uploadProfile}
theme={theme}
openInbox={openInbox}
scrollToMessage={scrollToMessage}
viewStory={viewStory}
/>
);
}
});

View file

@ -0,0 +1,65 @@
// Copyright 2024 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React, { memo, useCallback, useMemo } from 'react';
import { useSelector } from 'react-redux';
import { useCallingActions } from '../ducks/calling';
import { getCallLinkSelector } from '../selectors/calling';
import * as log from '../../logging/log';
import { getIntl } from '../selectors/user';
import { useGlobalModalActions } from '../ducks/globalModals';
import { getCallLinkAddNameModalRoomId } from '../selectors/globalModals';
import { strictAssert } from '../../util/assert';
import {
isCallLinkAdmin,
isCallLinksCreateEnabled,
} from '../../util/callLinks';
import { CallLinkAddNameModal } from '../../components/CallLinkAddNameModal';
export const SmartCallLinkAddNameModal = memo(
function SmartCallLinkAddNameModal(): JSX.Element | null {
strictAssert(isCallLinksCreateEnabled(), 'Call links creation is disabled');
const roomId = useSelector(getCallLinkAddNameModalRoomId);
strictAssert(roomId, 'Expected roomId to be set');
const i18n = useSelector(getIntl);
const callLinkSelector = useSelector(getCallLinkSelector);
const { updateCallLinkName } = useCallingActions();
const { toggleCallLinkAddNameModal } = useGlobalModalActions();
const callLink = useMemo(() => {
return callLinkSelector(roomId);
}, [callLinkSelector, roomId]);
const handleClose = useCallback(() => {
toggleCallLinkAddNameModal(null);
}, [toggleCallLinkAddNameModal]);
const handleUpdateCallLinkName = useCallback(
(newName: string) => {
updateCallLinkName(roomId, newName);
},
[roomId, updateCallLinkName]
);
if (!callLink) {
log.error(
'SmartCallLinkEditModal: No call link found for roomId',
roomId
);
return null;
}
strictAssert(isCallLinkAdmin(callLink), 'User is not an admin');
return (
<CallLinkAddNameModal
i18n={i18n}
callLink={callLink}
onClose={handleClose}
onUpdateCallLinkName={handleUpdateCallLinkName}
/>
);
}
);

View file

@ -0,0 +1,70 @@
// Copyright 2024 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React, { memo, useCallback } from 'react';
import { useSelector } from 'react-redux';
import type { CallHistoryGroup } from '../../types/CallDisposition';
import { getIntl } from '../selectors/user';
import { CallLinkDetails } from '../../components/CallLinkDetails';
import { getCallLinkSelector } from '../selectors/calling';
import { useGlobalModalActions } from '../ducks/globalModals';
import { useCallingActions } from '../ducks/calling';
import * as log from '../../logging/log';
import { strictAssert } from '../../util/assert';
import type { CallLinkRestrictions } from '../../types/CallLink';
export type SmartCallLinkDetailsProps = Readonly<{
roomId: string;
callHistoryGroup: CallHistoryGroup;
}>;
export const SmartCallLinkDetails = memo(function SmartCallLinkDetails({
roomId,
callHistoryGroup,
}: SmartCallLinkDetailsProps) {
const i18n = useSelector(getIntl);
const callLinkSelector = useSelector(getCallLinkSelector);
const { startCallLinkLobby, updateCallLinkRestrictions } =
useCallingActions();
const { toggleCallLinkAddNameModal, showShareCallLinkViaSignal } =
useGlobalModalActions();
const callLink = callLinkSelector(roomId);
const handleOpenCallLinkAddNameModal = useCallback(() => {
toggleCallLinkAddNameModal(roomId);
}, [roomId, toggleCallLinkAddNameModal]);
const handleShareCallLinkViaSignal = useCallback(() => {
strictAssert(callLink != null, 'callLink not found');
showShareCallLinkViaSignal(callLink, i18n);
}, [callLink, i18n, showShareCallLinkViaSignal]);
const handleStartCallLinkLobby = useCallback(() => {
strictAssert(callLink != null, 'callLink not found');
startCallLinkLobby({ rootKey: callLink.rootKey });
}, [callLink, startCallLinkLobby]);
const handleUpdateCallLinkRestrictions = useCallback(
(newRestrictions: CallLinkRestrictions) => {
updateCallLinkRestrictions(roomId, newRestrictions);
},
[roomId, updateCallLinkRestrictions]
);
if (callLink == null) {
log.error(`SmartCallLinkDetails: callLink not found for room ${roomId}`);
return null;
}
return (
<CallLinkDetails
callHistoryGroup={callHistoryGroup}
callLink={callLink}
i18n={i18n}
onOpenCallLinkAddNameModal={handleOpenCallLinkAddNameModal}
onStartCallLinkLobby={handleStartCallLinkLobby}
onShareCallLinkViaSignal={handleShareCallLinkViaSignal}
onUpdateCallLinkRestrictions={handleUpdateCallLinkRestrictions}
/>
);
});

View file

@ -0,0 +1,98 @@
// Copyright 2024 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React, { memo, useCallback, useMemo } from 'react';
import { useSelector } from 'react-redux';
import { CallLinkEditModal } from '../../components/CallLinkEditModal';
import { useCallingActions } from '../ducks/calling';
import { getCallLinkSelector } from '../selectors/calling';
import * as log from '../../logging/log';
import { getIntl } from '../selectors/user';
import { useGlobalModalActions } from '../ducks/globalModals';
import type { CallLinkRestrictions } from '../../types/CallLink';
import { getCallLinkEditModalRoomId } from '../selectors/globalModals';
import { strictAssert } from '../../util/assert';
import { linkCallRoute } from '../../util/signalRoutes';
import { copyCallLink } from '../../util/copyLinksWithToast';
import { drop } from '../../util/drop';
import { isCallLinksCreateEnabled } from '../../util/callLinks';
export const SmartCallLinkEditModal = memo(
function SmartCallLinkEditModal(): JSX.Element | null {
strictAssert(isCallLinksCreateEnabled(), 'Call links creation is disabled');
const roomId = useSelector(getCallLinkEditModalRoomId);
strictAssert(roomId, 'Expected roomId to be set');
const i18n = useSelector(getIntl);
const callLinkSelector = useSelector(getCallLinkSelector);
const { updateCallLinkRestrictions, startCallLinkLobby } =
useCallingActions();
const {
toggleCallLinkAddNameModal,
toggleCallLinkEditModal,
showShareCallLinkViaSignal,
} = useGlobalModalActions();
const callLink = useMemo(() => {
return callLinkSelector(roomId);
}, [callLinkSelector, roomId]);
const handleClose = useCallback(() => {
toggleCallLinkEditModal(null);
}, [toggleCallLinkEditModal]);
const handleCopyCallLink = useCallback(() => {
strictAssert(callLink != null, 'callLink not found');
const callLinkWebUrl = linkCallRoute
.toWebUrl({
key: callLink?.rootKey,
})
.toString();
drop(copyCallLink(callLinkWebUrl));
}, [callLink]);
const handleOpenCallLinkAddNameModal = useCallback(() => {
toggleCallLinkAddNameModal(roomId);
}, [roomId, toggleCallLinkAddNameModal]);
const handleUpdateCallLinkRestrictions = useCallback(
(newRestrictions: CallLinkRestrictions) => {
updateCallLinkRestrictions(roomId, newRestrictions);
},
[roomId, updateCallLinkRestrictions]
);
const handleShareCallLinkViaSignal = useCallback(() => {
strictAssert(callLink != null, 'callLink not found');
showShareCallLinkViaSignal(callLink, i18n);
}, [callLink, i18n, showShareCallLinkViaSignal]);
const handleStartCallLinkLobby = useCallback(() => {
strictAssert(callLink != null, 'callLink not found');
startCallLinkLobby({ rootKey: callLink.rootKey });
toggleCallLinkEditModal(null);
}, [callLink, startCallLinkLobby, toggleCallLinkEditModal]);
if (!callLink) {
log.error(
'SmartCallLinkEditModal: No call link found for roomId',
roomId
);
return null;
}
return (
<CallLinkEditModal
i18n={i18n}
callLink={callLink}
onClose={handleClose}
onCopyCallLink={handleCopyCallLink}
onOpenCallLinkAddNameModal={handleOpenCallLinkAddNameModal}
onUpdateCallLinkRestrictions={handleUpdateCallLinkRestrictions}
onShareCallLinkViaSignal={handleShareCallLinkViaSignal}
onStartCallLinkLobby={handleStartCallLinkLobby}
/>
);
}
);

View file

@ -1,23 +1,28 @@
// Copyright 2020 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React from 'react';
import { connect } from 'react-redux';
import { memoize } from 'lodash';
import { mapDispatchToProps } from '../actions';
import React, { memo } from 'react';
import { useSelector } from 'react-redux';
import type {
DirectIncomingCall,
GroupIncomingCall,
} from '../../components/CallManager';
import { CallManager } from '../../components/CallManager';
import { isConversationTooBigToRing as getIsConversationTooBigToRing } from '../../conversations/isConversationTooBigToRing';
import * as log from '../../logging/log';
import { calling as callingService } from '../../services/calling';
import { getIntl, getTheme } from '../selectors/user';
import { getMe, getConversationSelector } from '../selectors/conversations';
import { getActiveCall } from '../ducks/calling';
import type { ConversationType } from '../ducks/conversations';
import { getCallLinkSelector, getIncomingCall } from '../selectors/calling';
import { isGroupCallRaiseHandEnabled } from '../../util/isGroupCallRaiseHandEnabled';
import { isGroupCallReactionsEnabled } from '../../util/isGroupCallReactionsEnabled';
import {
FALLBACK_NOTIFICATION_TITLE,
NotificationSetting,
NotificationType,
notificationService,
} from '../../services/notifications';
import {
bounceAppIconStart,
bounceAppIconStop,
} from '../../shims/bounceAppIcon';
import type { CallLinkType } from '../../types/CallLink';
import type {
ActiveCallBaseType,
ActiveCallType,
@ -27,41 +32,35 @@ import type {
ConversationsByDemuxIdType,
GroupCallRemoteParticipantType,
} from '../../types/Calling';
import type { AciString } from '../../types/ServiceId';
import { CallMode, CallState } from '../../types/Calling';
import type { CallLinkType } from '../../types/CallLink';
import type { StateType } from '../reducer';
import { missingCaseError } from '../../util/missingCaseError';
import { SmartCallingDeviceSelection } from './CallingDeviceSelection';
import type { SafetyNumberProps } from '../../components/SafetyNumberChangeDialog';
import { SmartSafetyNumberViewer } from './SafetyNumberViewer';
import { callingTones } from '../../util/callingTones';
import {
bounceAppIconStart,
bounceAppIconStop,
} from '../../shims/bounceAppIcon';
import {
FALLBACK_NOTIFICATION_TITLE,
NotificationSetting,
NotificationType,
notificationService,
} from '../../services/notifications';
import * as log from '../../logging/log';
import { getPreferredBadgeSelector } from '../selectors/badges';
import { isConversationTooBigToRing } from '../../conversations/isConversationTooBigToRing';
import type { AciString } from '../../types/ServiceId';
import { strictAssert } from '../../util/assert';
import { callLinkToConversation } from '../../util/callLinks';
import { callingTones } from '../../util/callingTones';
import { isGroupCallRaiseHandEnabled } from '../../util/isGroupCallRaiseHandEnabled';
import { missingCaseError } from '../../util/missingCaseError';
import { useAudioPlayerActions } from '../ducks/audioPlayer';
import { getActiveCall, useCallingActions } from '../ducks/calling';
import type { ConversationType } from '../ducks/conversations';
import type { StateType } from '../reducer';
import { getHasInitialLoadCompleted } from '../selectors/app';
import {
getAvailableCameras,
getCallLinkSelector,
getIncomingCall,
} from '../selectors/calling';
import { getConversationSelector, getMe } from '../selectors/conversations';
import { getIntl } from '../selectors/user';
import { SmartCallingDeviceSelection } from './CallingDeviceSelection';
import { renderEmojiPicker } from './renderEmojiPicker';
import { renderReactionPicker } from './renderReactionPicker';
import { callLinkToConversation } from '../../util/callLinks';
import { isSharingPhoneNumberWithEverybody as getIsSharingPhoneNumberWithEverybody } from '../../util/phoneNumberSharingMode';
import { useGlobalModalActions } from '../ducks/globalModals';
function renderDeviceSelection(): JSX.Element {
return <SmartCallingDeviceSelection />;
}
function renderSafetyNumberViewer(props: SafetyNumberProps): JSX.Element {
return <SmartSafetyNumberViewer {...props} />;
}
const getGroupCallVideoFrameSource =
callingService.getGroupCallVideoFrameSource.bind(callingService);
@ -210,10 +209,10 @@ const mapStateToActiveCallProp = (
} satisfies ActiveDirectCallType;
case CallMode.Group:
case CallMode.Adhoc: {
const conversationsWithSafetyNumberChanges: Array<ConversationType> = [];
const groupMembers: Array<ConversationType> = [];
const remoteParticipants: Array<GroupCallRemoteParticipantType> = [];
const peekedParticipants: Array<ConversationType> = [];
const pendingParticipants: Array<ConversationType> = [];
const conversationsByDemuxId: ConversationsByDemuxIdType = new Map();
const { localDemuxId } = call;
const raisedHands: Set<number> = new Set(call.raisedHands ?? []);
@ -227,6 +226,7 @@ const mapStateToActiveCallProp = (
deviceCount: 0,
maxDevices: Infinity,
acis: [],
pendingAcis: [],
},
} = call;
@ -284,22 +284,6 @@ const mapStateToActiveCallProp = (
}
});
for (
let i = 0;
i < activeCallState.safetyNumberChangedAcis.length;
i += 1
) {
const aci = activeCallState.safetyNumberChangedAcis[i];
const remoteConversation = conversationSelectorByAci(aci);
if (!remoteConversation) {
log.error('Remote participant has no corresponding conversation');
continue;
}
conversationsWithSafetyNumberChanges.push(remoteConversation);
}
for (let i = 0; i < peekInfo.acis.length; i += 1) {
const peekedParticipantAci = peekInfo.acis[i];
@ -313,19 +297,33 @@ const mapStateToActiveCallProp = (
peekedParticipants.push(peekedConversation);
}
for (let i = 0; i < peekInfo.pendingAcis.length; i += 1) {
const aci = peekInfo.pendingAcis[i];
// In call links, pending users may be unknown until they share profile keys.
// conversationSelectorByAci should create conversations for new contacts.
const pendingConversation = conversationSelectorByAci(aci);
if (!pendingConversation) {
log.error('Pending participant has no corresponding conversation');
continue;
}
pendingParticipants.push(pendingConversation);
}
return {
...baseResult,
callMode: call.callMode,
connectionState: call.connectionState,
conversationsWithSafetyNumberChanges,
conversationsByDemuxId,
deviceCount: peekInfo.deviceCount,
groupMembers,
isConversationTooBigToRing: isConversationTooBigToRing(conversation),
isConversationTooBigToRing: getIsConversationTooBigToRing(conversation),
joinState: call.joinState,
localDemuxId,
maxDevices: peekInfo.maxDevices,
peekedParticipants,
pendingParticipants,
raisedHands,
remoteParticipants,
remoteAudioLevels: call.remoteAudioLevels || new Map<number, number>(),
@ -414,36 +412,112 @@ const mapStateToIncomingCallProp = (
}
};
const mapStateToProps = (state: StateType) => {
const incomingCall = mapStateToIncomingCallProp(state);
export const SmartCallManager = memo(function SmartCallManager() {
const i18n = useSelector(getIntl);
const activeCall = useSelector(mapStateToActiveCallProp);
const callLink = useSelector(mapStateToCallLinkProp);
const incomingCall = useSelector(mapStateToIncomingCallProp);
const availableCameras = useSelector(getAvailableCameras);
const hasInitialLoadCompleted = useSelector(getHasInitialLoadCompleted);
const me = useSelector(getMe);
const isConversationTooBigToRing = incomingCall
? getIsConversationTooBigToRing(incomingCall.conversation)
: false;
return {
activeCall: mapStateToActiveCallProp(state),
callLink: mapStateToCallLinkProp(state),
bounceAppIconStart,
bounceAppIconStop,
availableCameras: state.calling.availableCameras,
getGroupCallVideoFrameSource,
getPreferredBadge: getPreferredBadgeSelector(state),
i18n: getIntl(state),
isGroupCallRaiseHandEnabled: isGroupCallRaiseHandEnabled(),
isGroupCallReactionsEnabled: isGroupCallReactionsEnabled(),
incomingCall,
me: getMe(state),
notifyForCall,
playRingtone,
stopRingtone,
renderEmojiPicker,
renderReactionPicker,
renderDeviceSelection,
renderSafetyNumberViewer,
theme: getTheme(state),
isConversationTooBigToRing: incomingCall
? isConversationTooBigToRing(incomingCall.conversation)
: false,
};
};
const {
approveUser,
batchUserAction,
denyUser,
changeCallView,
closeNeedPermissionScreen,
getPresentingSources,
cancelCall,
startCall,
toggleParticipants,
acceptCall,
declineCall,
openSystemPreferencesAction,
removeClient,
blockClient,
sendGroupCallRaiseHand,
sendGroupCallReaction,
setGroupCallVideoRequest,
setIsCallActive,
setLocalAudio,
setLocalVideo,
setLocalPreview,
setOutgoingRing,
setPresenting,
setRendererCanvas,
switchToPresentationView,
switchFromPresentationView,
hangUpActiveCall,
togglePip,
toggleScreenRecordingPermissionsDialog,
toggleSettings,
} = useCallingActions();
const { pauseVoiceNotePlayer } = useAudioPlayerActions();
const { showContactModal, showShareCallLinkViaSignal } =
useGlobalModalActions();
const smart = connect(mapStateToProps, mapDispatchToProps);
export const SmartCallManager = smart(CallManager);
return (
<CallManager
acceptCall={acceptCall}
activeCall={activeCall}
approveUser={approveUser}
availableCameras={availableCameras}
batchUserAction={batchUserAction}
blockClient={blockClient}
bounceAppIconStart={bounceAppIconStart}
bounceAppIconStop={bounceAppIconStop}
callLink={callLink}
cancelCall={cancelCall}
changeCallView={changeCallView}
closeNeedPermissionScreen={closeNeedPermissionScreen}
declineCall={declineCall}
denyUser={denyUser}
getGroupCallVideoFrameSource={getGroupCallVideoFrameSource}
getIsSharingPhoneNumberWithEverybody={
getIsSharingPhoneNumberWithEverybody
}
getPresentingSources={getPresentingSources}
hangUpActiveCall={hangUpActiveCall}
hasInitialLoadCompleted={hasInitialLoadCompleted}
i18n={i18n}
incomingCall={incomingCall}
isConversationTooBigToRing={isConversationTooBigToRing}
isGroupCallRaiseHandEnabled={isGroupCallRaiseHandEnabled()}
me={me}
notifyForCall={notifyForCall}
openSystemPreferencesAction={openSystemPreferencesAction}
pauseVoiceNotePlayer={pauseVoiceNotePlayer}
playRingtone={playRingtone}
removeClient={removeClient}
renderDeviceSelection={renderDeviceSelection}
renderEmojiPicker={renderEmojiPicker}
renderReactionPicker={renderReactionPicker}
sendGroupCallRaiseHand={sendGroupCallRaiseHand}
sendGroupCallReaction={sendGroupCallReaction}
setGroupCallVideoRequest={setGroupCallVideoRequest}
setIsCallActive={setIsCallActive}
setLocalAudio={setLocalAudio}
setLocalPreview={setLocalPreview}
setLocalVideo={setLocalVideo}
setOutgoingRing={setOutgoingRing}
setPresenting={setPresenting}
setRendererCanvas={setRendererCanvas}
showContactModal={showContactModal}
showShareCallLinkViaSignal={showShareCallLinkViaSignal}
startCall={startCall}
stopRingtone={stopRingtone}
switchFromPresentationView={switchFromPresentationView}
switchToPresentationView={switchToPresentationView}
toggleParticipants={toggleParticipants}
togglePip={togglePip}
toggleScreenRecordingPermissionsDialog={
toggleScreenRecordingPermissionsDialog
}
toggleSettings={toggleSettings}
/>
);
});

View file

@ -1,34 +1,42 @@
// Copyright 2020 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { connect } from 'react-redux';
import { mapDispatchToProps } from '../actions';
import React, { memo } from 'react';
import { useSelector } from 'react-redux';
import { CallingDeviceSelection } from '../../components/CallingDeviceSelection';
import type { StateType } from '../reducer';
import { getIntl } from '../selectors/user';
import {
getAvailableCameras,
getAvailableMicrophones,
getAvailableSpeakers,
getSelectedCamera,
getSelectedMicrophone,
getSelectedSpeaker,
} from '../selectors/calling';
import { useCallingActions } from '../ducks/calling';
const mapStateToProps = (state: StateType) => {
const {
availableMicrophones,
availableSpeakers,
selectedMicrophone,
selectedSpeaker,
availableCameras,
selectedCamera,
} = state.calling;
return {
availableCameras,
availableMicrophones,
availableSpeakers,
i18n: getIntl(state),
selectedCamera,
selectedMicrophone,
selectedSpeaker,
};
};
const smart = connect(mapStateToProps, mapDispatchToProps);
export const SmartCallingDeviceSelection = smart(CallingDeviceSelection);
export const SmartCallingDeviceSelection = memo(
function SmartCallingDeviceSelection() {
const i18n = useSelector(getIntl);
const availableMicrophones = useSelector(getAvailableMicrophones);
const selectedMicrophone = useSelector(getSelectedMicrophone);
const availableSpeakers = useSelector(getAvailableSpeakers);
const selectedSpeaker = useSelector(getSelectedSpeaker);
const availableCameras = useSelector(getAvailableCameras);
const selectedCamera = useSelector(getSelectedCamera);
const { changeIODevice, toggleSettings } = useCallingActions();
return (
<CallingDeviceSelection
availableCameras={availableCameras}
availableMicrophones={availableMicrophones}
availableSpeakers={availableSpeakers}
changeIODevice={changeIODevice}
i18n={i18n}
selectedCamera={selectedCamera}
selectedMicrophone={selectedMicrophone}
selectedSpeaker={selectedSpeaker}
toggleSettings={toggleSettings}
/>
);
}
);

View file

@ -1,8 +1,8 @@
// Copyright 2023 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React, { useCallback, useEffect } from 'react';
import React, { memo, useCallback, useEffect, useMemo } from 'react';
import { useSelector } from 'react-redux';
import { DataReader } from '../../sql/Client';
import { useItemsActions } from '../ducks/items';
import {
getNavTabsCollapsed,
@ -15,7 +15,7 @@ import {
getAllConversations,
getConversationSelector,
} from '../selectors/conversations';
import { filterAndSortConversationsByRecent } from '../../util/filterAndSortConversations';
import { filterAndSortConversations } from '../../util/filterAndSortConversations';
import type {
CallHistoryFilter,
CallHistoryFilterOptions,
@ -26,50 +26,94 @@ import type { ConversationType } from '../ducks/conversations';
import { SmartConversationDetails } from './ConversationDetails';
import { SmartToastManager } from './ToastManager';
import { useCallingActions } from '../ducks/calling';
import { getActiveCallState } from '../selectors/calling';
import {
getActiveCallState,
getAdhocCallSelector,
getAllCallLinks,
getCallSelector,
getCallLinkSelector,
} from '../selectors/calling';
import { useCallHistoryActions } from '../ducks/callHistory';
import { getCallHistoryEdition } from '../selectors/callHistory';
import { getHasPendingUpdate } from '../selectors/updates';
import { getHasAnyFailedStorySends } from '../selectors/stories';
import { getOtherTabsUnreadStats } from '../selectors/nav';
import { SmartCallLinkDetails } from './CallLinkDetails';
import type { CallLinkType } from '../../types/CallLink';
import { filterCallLinks } from '../../util/filterCallLinks';
import { useGlobalModalActions } from '../ducks/globalModals';
import { isCallLinksCreateEnabled } from '../../util/callLinks';
function getCallHistoryFilter(
allConversations: Array<ConversationType>,
regionCode: string | undefined,
options: CallHistoryFilterOptions
): CallHistoryFilter | null {
function getCallHistoryFilter({
allCallLinks,
allConversations,
regionCode,
options,
}: {
allConversations: Array<ConversationType>;
allCallLinks: Array<CallLinkType>;
regionCode: string | undefined;
options: CallHistoryFilterOptions;
}): CallHistoryFilter | null {
const { status } = options;
const query = options.query.normalize().trim();
if (query !== '') {
const currentConversations = allConversations.filter(conversation => {
return conversation.removalStage == null;
});
const filteredConversations = filterAndSortConversationsByRecent(
currentConversations,
query,
regionCode
);
// If there are no matching conversations, then no calls will match.
if (filteredConversations.length === 0) {
return null;
}
if (query === '') {
return {
status: options.status,
conversationIds: filteredConversations.map(conversation => {
return conversation.id;
}),
status,
callLinkRoomIds: null,
conversationIds: null,
};
}
let callLinkRoomIds = null;
let conversationIds = null;
const currentConversations = allConversations.filter(conversation => {
return conversation.removalStage == null;
});
const filteredConversations = filterAndSortConversations(
currentConversations,
query,
regionCode
);
if (filteredConversations.length > 0) {
conversationIds = filteredConversations.map(conversation => {
return conversation.id;
});
}
const filteredCallLinks = filterCallLinks(allCallLinks, query);
if (filteredCallLinks.length > 0) {
callLinkRoomIds = filteredCallLinks.map(callLink => {
return callLink.roomId;
});
}
// If the search query resulted in no matching call links or conversations, then
// no calls will match.
if (callLinkRoomIds == null && conversationIds == null) {
return null;
}
return {
status: options.status,
conversationIds: null,
status,
callLinkRoomIds,
conversationIds,
};
}
function renderCallLinkDetails(
roomId: string,
callHistoryGroup: CallHistoryGroup
): JSX.Element {
return (
<SmartCallLinkDetails roomId={roomId} callHistoryGroup={callHistoryGroup} />
);
}
function renderConversationDetails(
conversationId: string,
callHistoryGroup: CallHistoryGroup | null
@ -88,16 +132,20 @@ function renderToastManager(props: {
return <SmartToastManager disableMegaphone {...props} />;
}
export function SmartCallsTab(): JSX.Element {
export const SmartCallsTab = memo(function SmartCallsTab() {
const i18n = useSelector(getIntl);
const navTabsCollapsed = useSelector(getNavTabsCollapsed);
const preferredLeftPaneWidth = useSelector(getPreferredLeftPaneWidth);
const { savePreferredLeftPaneWidth, toggleNavTabsCollapse } =
useItemsActions();
const allCallLinks = useSelector(getAllCallLinks);
const allConversations = useSelector(getAllConversations);
const regionCode = useSelector(getRegionCode);
const getConversation = useSelector(getConversationSelector);
const getAdhocCall = useSelector(getAdhocCallSelector);
const getCall = useSelector(getCallSelector);
const getCallLink = useSelector(getCallLinkSelector);
const activeCall = useSelector(getActiveCallState);
const callHistoryEdition = useSelector(getCallHistoryEdition);
@ -106,32 +154,43 @@ export function SmartCallsTab(): JSX.Element {
const hasFailedStorySends = useSelector(getHasAnyFailedStorySends);
const otherTabsUnreadStats = useSelector(getOtherTabsUnreadStats);
const canCreateCallLinks = useMemo(() => {
return isCallLinksCreateEnabled();
}, []);
const {
createCallLink,
hangUpActiveCall,
onOutgoingAudioCallInConversation,
onOutgoingVideoCallInConversation,
peekNotConnectedGroupCall,
startCallLinkLobbyByRoomId,
togglePip,
} = useCallingActions();
const {
clearAllCallHistory: clearCallHistory,
markCallHistoryRead,
markCallsTabViewed,
} = useCallHistoryActions();
const { toggleCallLinkEditModal, toggleConfirmLeaveCallModal } =
useGlobalModalActions();
const getCallHistoryGroupsCount = useCallback(
async (options: CallHistoryFilterOptions) => {
const callHistoryFilter = getCallHistoryFilter(
const callHistoryFilter = getCallHistoryFilter({
allCallLinks,
allConversations,
regionCode,
options
);
options,
});
if (callHistoryFilter == null) {
return 0;
}
const count = await window.Signal.Data.getCallHistoryGroupsCount(
callHistoryFilter
);
const count =
await DataReader.getCallHistoryGroupsCount(callHistoryFilter);
return count;
},
[allConversations, regionCode]
[allCallLinks, allConversations, regionCode]
);
const getCallHistoryGroups = useCallback(
@ -139,23 +198,30 @@ export function SmartCallsTab(): JSX.Element {
options: CallHistoryFilterOptions,
pagination: CallHistoryPagination
) => {
const callHistoryFilter = getCallHistoryFilter(
const callHistoryFilter = getCallHistoryFilter({
allCallLinks,
allConversations,
regionCode,
options
);
options,
});
if (callHistoryFilter == null) {
return [];
}
const results = await window.Signal.Data.getCallHistoryGroups(
const results = await DataReader.getCallHistoryGroups(
callHistoryFilter,
pagination
);
return results;
},
[allConversations, regionCode]
[allCallLinks, allConversations, regionCode]
);
const handleCreateCallLink = useCallback(() => {
createCallLink(roomId => {
toggleCallLinkEditModal(roomId);
});
}, [createCallLink, toggleCallLinkEditModal]);
useEffect(() => {
markCallsTabViewed();
}, [markCallsTabViewed]);
@ -168,7 +234,12 @@ export function SmartCallsTab(): JSX.Element {
getConversation={getConversation}
getCallHistoryGroupsCount={getCallHistoryGroupsCount}
getCallHistoryGroups={getCallHistoryGroups}
getAdhocCall={getAdhocCall}
getCall={getCall}
getCallLink={getCallLink}
callHistoryEdition={callHistoryEdition}
canCreateCallLinks={canCreateCallLinks}
hangUpActiveCall={hangUpActiveCall}
hasFailedStorySends={hasFailedStorySends}
hasPendingUpdate={hasPendingUpdate}
i18n={i18n}
@ -176,13 +247,19 @@ export function SmartCallsTab(): JSX.Element {
onClearCallHistory={clearCallHistory}
onMarkCallHistoryRead={markCallHistoryRead}
onToggleNavTabsCollapse={toggleNavTabsCollapse}
onCreateCallLink={handleCreateCallLink}
onOutgoingAudioCallInConversation={onOutgoingAudioCallInConversation}
onOutgoingVideoCallInConversation={onOutgoingVideoCallInConversation}
peekNotConnectedGroupCall={peekNotConnectedGroupCall}
preferredLeftPaneWidth={preferredLeftPaneWidth}
renderCallLinkDetails={renderCallLinkDetails}
renderConversationDetails={renderConversationDetails}
renderToastManager={renderToastManager}
regionCode={regionCode}
savePreferredLeftPaneWidth={savePreferredLeftPaneWidth}
startCallLinkLobbyByRoomId={startCallLinkLobbyByRoomId}
toggleConfirmLeaveCallModal={toggleConfirmLeaveCallModal}
togglePip={togglePip}
/>
);
}
});

View file

@ -1,29 +1,33 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { connect } from 'react-redux';
import { mapDispatchToProps } from '../actions';
import React, { memo, useCallback } from 'react';
import { useSelector } from 'react-redux';
import { CaptchaDialog } from '../../components/CaptchaDialog';
import type { StateType } from '../reducer';
import { getIntl } from '../selectors/user';
import { isChallengePending } from '../selectors/network';
import { getChallengeURL } from '../../challenge';
import * as log from '../../logging/log';
const mapStateToProps = (state: StateType) => {
return {
...state.updates,
isPending: isChallengePending(state),
i18n: getIntl(state),
export type SmartCaptchaDialogProps = Readonly<{
onSkip: () => void;
}>;
onContinue() {
const url = getChallengeURL('chat');
log.info(`CaptchaDialog: navigating to ${url}`);
document.location.href = url;
},
};
};
const smart = connect(mapStateToProps, mapDispatchToProps);
export const SmartCaptchaDialog = smart(CaptchaDialog);
export const SmartCaptchaDialog = memo(function SmartCaptchaDialog({
onSkip,
}: SmartCaptchaDialogProps) {
const i18n = useSelector(getIntl);
const isPending = useSelector(isChallengePending);
const handleContinue = useCallback(() => {
const url = getChallengeURL('chat');
log.info(`CaptchaDialog: navigating to ${url}`);
document.location.href = url;
}, []);
return (
<CaptchaDialog
i18n={i18n}
isPending={isPending}
onSkip={onSkip}
onContinue={handleContinue}
/>
);
});

View file

@ -1,53 +1,91 @@
// Copyright 2020 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { connect } from 'react-redux';
import { mapDispatchToProps } from '../actions';
import type { PropsDataType } from '../../components/ChatColorPicker';
import React, { memo, useCallback } from 'react';
import { useSelector } from 'react-redux';
import { ChatColorPicker } from '../../components/ChatColorPicker';
import type { StateType } from '../reducer';
import {
getConversationSelector,
getConversationsWithCustomColorSelector,
} from '../selectors/conversations';
import { getIntl } from '../selectors/user';
import { getDefaultConversationColor } from '../selectors/items';
import {
getCustomColors,
getDefaultConversationColor,
} from '../selectors/items';
import { getConversationColorAttributes } from '../../util/getConversationColorAttributes';
import {
useConversationsActions,
type ConversationType,
} from '../ducks/conversations';
import { useItemsActions } from '../ducks/items';
export type SmartChatColorPickerProps = {
export type SmartChatColorPickerProps = Readonly<{
conversationId?: string;
};
}>;
const mapStateToProps = (
state: StateType,
props: SmartChatColorPickerProps
): PropsDataType => {
const conversation = props.conversationId
? getConversationSelector(state)(props.conversationId)
export const SmartChatColorPicker = memo(function SmartChatColorPicker({
conversationId,
}: SmartChatColorPickerProps) {
const i18n = useSelector(getIntl);
const customColors = useSelector(getCustomColors) ?? {};
const defaultConversationColor = useSelector(getDefaultConversationColor);
const conversationSelector = useSelector(getConversationSelector);
const conversationWithCustomColorSelector = useSelector(
getConversationsWithCustomColorSelector
);
const {
addCustomColor,
removeCustomColor,
setGlobalDefaultConversationColor,
resetDefaultChatColor,
editCustomColor,
} = useItemsActions();
const {
colorSelected,
resetAllChatColors,
removeCustomColorOnConversations,
} = useConversationsActions();
const conversation = conversationId
? conversationSelector(conversationId)
: {};
const defaultConversationColor = getDefaultConversationColor(state);
const colorValues = getConversationColorAttributes(
conversation,
defaultConversationColor
);
const { customColors } = state.items;
return {
...props,
customColors: customColors ? customColors.colors : {},
getConversationsWithCustomColor: (colorId: string) =>
Promise.resolve(getConversationsWithCustomColorSelector(state)(colorId)),
i18n: getIntl(state),
selectedColor: colorValues.conversationColor,
selectedCustomColor: {
id: colorValues.customColorId,
value: colorValues.customColor,
},
const selectedColor = colorValues.conversationColor;
const selectedCustomColor = {
id: colorValues.customColorId,
value: colorValues.customColor,
};
};
const smart = connect(mapStateToProps, mapDispatchToProps);
const getConversationsWithCustomColor = useCallback(
async (colorId: string): Promise<Array<ConversationType>> => {
return conversationWithCustomColorSelector(colorId);
},
[conversationWithCustomColorSelector]
);
export const SmartChatColorPicker = smart(ChatColorPicker);
return (
<ChatColorPicker
addCustomColor={addCustomColor}
colorSelected={colorSelected}
conversationId={conversationId}
customColors={customColors}
editCustomColor={editCustomColor}
getConversationsWithCustomColor={getConversationsWithCustomColor}
i18n={i18n}
isGlobal={false}
removeCustomColor={removeCustomColor}
removeCustomColorOnConversations={removeCustomColorOnConversations}
resetAllChatColors={resetAllChatColors}
resetDefaultChatColor={resetDefaultChatColor}
selectedColor={selectedColor}
selectedCustomColor={selectedCustomColor}
setGlobalDefaultConversationColor={setGlobalDefaultConversationColor}
/>
);
});

View file

@ -1,7 +1,6 @@
// Copyright 2023 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React, { useEffect, useRef } from 'react';
import React, { memo, useEffect, useRef } from 'react';
import { useSelector } from 'react-redux';
import { ChatsTab } from '../../components/ChatsTab';
import { SmartConversationView } from './ConversationView';
@ -12,10 +11,8 @@ import { useGlobalModalActions } from '../ducks/globalModals';
import { getIntl } from '../selectors/user';
import { usePrevious } from '../../hooks/usePrevious';
import { TargetedMessageSource } from '../ducks/conversationsEnums';
import type { ConversationsStateType } from '../ducks/conversations';
import { useConversationsActions } from '../ducks/conversations';
import { useToastActions } from '../ducks/toast';
import type { StateType } from '../reducer';
import { strictAssert } from '../../util/assert';
import { ToastType } from '../../types/Toast';
import { getNavTabsCollapsed } from '../selectors/items';
@ -23,6 +20,11 @@ import { useItemsActions } from '../ducks/items';
import { getHasAnyFailedStorySends } from '../selectors/stories';
import { getHasPendingUpdate } from '../selectors/updates';
import { getOtherTabsUnreadStats } from '../selectors/nav';
import {
getSelectedConversationId,
getTargetedMessage,
getTargetedMessageSource,
} from '../selectors/conversations';
function renderConversationView() {
return <SmartConversationView />;
@ -36,17 +38,15 @@ function renderMiniPlayer(options: { shouldFlow: boolean }) {
return <SmartMiniPlayer {...options} />;
}
export function SmartChatsTab(): JSX.Element {
export const SmartChatsTab = memo(function SmartChatsTab() {
const i18n = useSelector(getIntl);
const navTabsCollapsed = useSelector(getNavTabsCollapsed);
const hasFailedStorySends = useSelector(getHasAnyFailedStorySends);
const hasPendingUpdate = useSelector(getHasPendingUpdate);
const otherTabsUnreadStats = useSelector(getOtherTabsUnreadStats);
const { selectedConversationId, targetedMessage, targetedMessageSource } =
useSelector<StateType, ConversationsStateType>(
state => state.conversations
);
const selectedConversationId = useSelector(getSelectedConversationId);
const targetedMessageId = useSelector(getTargetedMessage)?.id;
const targetedMessageSource = useSelector(getTargetedMessageSource);
const {
onConversationClosed,
@ -64,20 +64,20 @@ export function SmartChatsTab(): JSX.Element {
if (selectedConversationId !== lastOpenedConversationId.current) {
lastOpenedConversationId.current = selectedConversationId;
if (selectedConversationId) {
onConversationOpened(selectedConversationId, targetedMessage);
onConversationOpened(selectedConversationId, targetedMessageId);
}
} else if (
selectedConversationId &&
targetedMessage &&
targetedMessageId &&
targetedMessageSource !== TargetedMessageSource.Focus
) {
scrollToMessage(selectedConversationId, targetedMessage);
scrollToMessage(selectedConversationId, targetedMessageId);
}
}, [
onConversationOpened,
selectedConversationId,
scrollToMessage,
targetedMessage,
targetedMessageId,
targetedMessageSource,
]);
@ -157,4 +157,4 @@ export function SmartChatsTab(): JSX.Element {
showWhatsNewModal={showWhatsNewModal}
/>
);
}
});

View file

@ -1,26 +1,20 @@
// Copyright 2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { connect } from 'react-redux';
import type { StateType } from '../reducer';
import { mapDispatchToProps } from '../actions';
import React, { memo, useMemo } from 'react';
import { useSelector } from 'react-redux';
import { strictAssert } from '../../util/assert';
import { lookupConversationWithoutServiceId } from '../../util/lookupConversationWithoutServiceId';
import { getUsernameFromSearch } from '../../util/Username';
import type { StatePropsType } from '../../components/conversation/conversation-details/AddGroupMembersModal/ChooseGroupMembersModal';
import { ChooseGroupMembersModal } from '../../components/conversation/conversation-details/AddGroupMembersModal/ChooseGroupMembersModal';
import { getIntl, getTheme, getRegionCode } from '../selectors/user';
import {
getCandidateContactsForNewGroup,
getConversationByIdSelector,
getMe,
} from '../selectors/conversations';
import { getPreferredBadgeSelector } from '../selectors/badges';
import { useGlobalModalActions } from '../ducks/globalModals';
export type SmartChooseGroupMembersModalPropsType = {
export type SmartChooseGroupMembersModalPropsType = Readonly<{
conversationIdsAlreadyInGroup: Set<string>;
maxGroupSize: number;
confirmAdds: () => void;
@ -30,41 +24,63 @@ export type SmartChooseGroupMembersModalPropsType = {
selectedConversationIds: ReadonlyArray<string>;
setSearchTerm: (_: string) => void;
toggleSelectedContact: (conversationId: string) => void;
};
}>;
const mapStateToProps = (
state: StateType,
props: SmartChooseGroupMembersModalPropsType
): StatePropsType => {
const conversationSelector = getConversationByIdSelector(state);
export const SmartChooseGroupMembersModal = memo(
function SmartChooseGroupMembersModal({
conversationIdsAlreadyInGroup,
maxGroupSize,
confirmAdds,
onClose,
removeSelectedContact,
searchTerm,
selectedConversationIds,
setSearchTerm,
toggleSelectedContact,
}: SmartChooseGroupMembersModalPropsType) {
const i18n = useSelector(getIntl);
const theme = useSelector(getTheme);
const regionCode = useSelector(getRegionCode);
const me = useSelector(getMe);
const conversationSelector = useSelector(getConversationByIdSelector);
const candidateContacts = getCandidateContactsForNewGroup(state);
const selectedContacts = props.selectedConversationIds.map(conversationId => {
const convo = conversationSelector(conversationId);
strictAssert(
convo,
'<SmartChooseGroupMemberModal> selected conversation not found'
const candidateContacts = useSelector(getCandidateContactsForNewGroup);
const selectedContacts = selectedConversationIds.map(conversationId => {
const convo = conversationSelector(conversationId);
strictAssert(
convo,
'<SmartChooseGroupMemberModal> selected conversation not found'
);
return convo;
});
const { showUserNotFoundModal } = useGlobalModalActions();
const username = useMemo(() => {
return getUsernameFromSearch(searchTerm);
}, [searchTerm]);
return (
<ChooseGroupMembersModal
regionCode={regionCode}
candidateContacts={candidateContacts}
confirmAdds={confirmAdds}
conversationIdsAlreadyInGroup={conversationIdsAlreadyInGroup}
i18n={i18n}
maxGroupSize={maxGroupSize}
onClose={onClose}
ourE164={me.e164}
ourUsername={me.username}
removeSelectedContact={removeSelectedContact}
searchTerm={searchTerm}
selectedContacts={selectedContacts}
setSearchTerm={setSearchTerm}
theme={theme}
toggleSelectedContact={toggleSelectedContact}
lookupConversationWithoutServiceId={lookupConversationWithoutServiceId}
showUserNotFoundModal={showUserNotFoundModal}
username={username}
/>
);
return convo;
});
const { searchTerm } = props;
return {
...props,
regionCode: getRegionCode(state),
candidateContacts,
getPreferredBadge: getPreferredBadgeSelector(state),
i18n: getIntl(state),
theme: getTheme(state),
ourE164: getMe(state).e164,
ourUsername: getMe(state).username,
selectedContacts,
lookupConversationWithoutServiceId,
username: getUsernameFromSearch(searchTerm),
};
};
const smart = connect(mapStateToProps, mapDispatchToProps);
export const SmartChooseGroupMembersModal = smart(ChooseGroupMembersModal);
}
);

View file

@ -1,9 +1,7 @@
// Copyright 2024 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React, { useMemo } from 'react';
import React, { memo, useMemo } from 'react';
import { useSelector } from 'react-redux';
import { CollidingAvatars } from '../../components/CollidingAvatars';
import { getIntl } from '../selectors/user';
import { getConversationSelector } from '../selectors/conversations';
@ -12,9 +10,9 @@ export type PropsType = Readonly<{
conversationIds: ReadonlyArray<string>;
}>;
export function SmartCollidingAvatars({
export const SmartCollidingAvatars = memo(function SmartCollidingAvatars({
conversationIds,
}: PropsType): JSX.Element {
}: PropsType) {
const i18n = useSelector(getIntl);
const getConversation = useSelector(getConversationSelector);
@ -25,4 +23,4 @@ export function SmartCollidingAvatars({
}, [conversationIds, getConversation]);
return <CollidingAvatars i18n={i18n} conversations={conversations} />;
}
});

View file

@ -1,35 +1,26 @@
// Copyright 2019 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React from 'react';
import { connect } from 'react-redux';
import { get } from 'lodash';
import { mapDispatchToProps } from '../actions';
import type { Props as ComponentPropsType } from '../../components/CompositionArea';
import React, { useCallback, useMemo, memo } from 'react';
import { useSelector } from 'react-redux';
import { CompositionArea } from '../../components/CompositionArea';
import type { StateType } from '../reducer';
import { useContactNameData } from '../../components/conversation/ContactName';
import type {
DraftBodyRanges,
HydratedBodyRangesType,
} from '../../types/BodyRange';
import { isConversationSMSOnly } from '../../util/isConversationSMSOnly';
import { dropNull } from '../../util/dropNull';
import { hydrateRanges } from '../../types/BodyRange';
import { strictAssert } from '../../util/assert';
import { getAddedByForOurPendingInvitation } from '../../util/getAddedByForOurPendingInvitation';
import { imageToBlurHash } from '../../util/imageToBlurHash';
import { isConversationSMSOnly } from '../../util/isConversationSMSOnly';
import { isSignalConversation } from '../../util/isSignalConversation';
import {
getErrorDialogAudioRecorderType,
getRecordingState,
} from '../selectors/audioRecorder';
import { getPreferredBadgeSelector } from '../selectors/badges';
import { selectRecentEmojis } from '../selectors/emojis';
import {
getIntl,
getPlatform,
getTheme,
getUserConversationId,
} from '../selectors/user';
import {
getDefaultConversationColor,
getEmojiSkinTone,
getTextFormattingEnabled,
} from '../selectors/items';
import { getComposerStateForConversationIdSelector } from '../selectors/composer';
import {
getConversationSelector,
getGroupAdminsSelector,
@ -38,71 +29,95 @@ import {
getSelectedMessageIds,
isMissingRequiredProfileSharing,
} from '../selectors/conversations';
import { selectRecentEmojis } from '../selectors/emojis';
import {
getDefaultConversationColor,
getEmojiSkinTone,
getShowStickerPickerHint,
getShowStickersIntroduction,
getTextFormattingEnabled,
} from '../selectors/items';
import { getPropsForQuote } from '../selectors/message';
import {
getBlessedStickerPacks,
getInstalledStickerPacks,
getKnownStickerPacks,
getReceivedStickerPacks,
getRecentlyInstalledStickerPack,
getRecentStickers,
getRecentlyInstalledStickerPack,
} from '../selectors/stickers';
import { isSignalConversation } from '../../util/isSignalConversation';
import { getComposerStateForConversationIdSelector } from '../selectors/composer';
import {
getIntl,
getPlatform,
getTheme,
getUserConversationId,
} from '../selectors/user';
import type { SmartCompositionRecordingProps } from './CompositionRecording';
import { SmartCompositionRecording } from './CompositionRecording';
import type { SmartCompositionRecordingDraftProps } from './CompositionRecordingDraft';
import { SmartCompositionRecordingDraft } from './CompositionRecordingDraft';
import { hydrateRanges } from '../../types/BodyRange';
import { useItemsActions } from '../ducks/items';
import { useComposerActions } from '../ducks/composer';
import { useConversationsActions } from '../ducks/conversations';
import { useAudioRecorderActions } from '../ducks/audioRecorder';
import { useEmojisActions } from '../ducks/emojis';
import { useGlobalModalActions } from '../ducks/globalModals';
import { useStickersActions } from '../ducks/stickers';
import { useToastActions } from '../ducks/toast';
import { isShowingAnyModal } from '../selectors/globalModals';
type ExternalProps = {
function renderSmartCompositionRecording(
recProps: SmartCompositionRecordingProps
) {
return <SmartCompositionRecording {...recProps} />;
}
function renderSmartCompositionRecordingDraft(
draftProps: SmartCompositionRecordingDraftProps
) {
return <SmartCompositionRecordingDraft {...draftProps} />;
}
export const SmartCompositionArea = memo(function SmartCompositionArea({
id,
}: {
id: string;
};
export type CompositionAreaPropsType = ExternalProps & ComponentPropsType;
const mapStateToProps = (state: StateType, props: ExternalProps) => {
const { id } = props;
const platform = getPlatform(state);
const shouldHidePopovers = getHasPanelOpen(state);
const conversationSelector = getConversationSelector(state);
}) {
const conversationSelector = useSelector(getConversationSelector);
const conversation = conversationSelector(id);
if (!conversation) {
throw new Error(`Conversation id ${id} not found!`);
}
strictAssert(conversation, `Conversation id ${id} not found!`);
const {
announcementsOnly,
areWeAdmin,
draftEditMessage,
draftText,
draftBodyRanges,
} = conversation;
const receivedPacks = getReceivedStickerPacks(state);
const installedPacks = getInstalledStickerPacks(state);
const blessedPacks = getBlessedStickerPacks(state);
const knownPacks = getKnownStickerPacks(state);
const installedPack = getRecentlyInstalledStickerPack(state);
const recentStickers = getRecentStickers(state);
const showIntroduction = get(
state.items,
['showStickersIntroduction'],
false
const i18n = useSelector(getIntl);
const theme = useSelector(getTheme);
const skinTone = useSelector(getEmojiSkinTone);
const recentEmojis = useSelector(selectRecentEmojis);
const selectedMessageIds = useSelector(getSelectedMessageIds);
const isFormattingEnabled = useSelector(getTextFormattingEnabled);
const lastEditableMessageId = useSelector(getLastEditableMessageId);
const receivedPacks = useSelector(getReceivedStickerPacks);
const installedPacks = useSelector(getInstalledStickerPacks);
const blessedPacks = useSelector(getBlessedStickerPacks);
const knownPacks = useSelector(getKnownStickerPacks);
const platform = useSelector(getPlatform);
const shouldHidePopovers = useSelector(getHasPanelOpen);
const installedPack = useSelector(getRecentlyInstalledStickerPack);
const recentStickers = useSelector(getRecentStickers);
const showStickersIntroduction = useSelector(getShowStickersIntroduction);
const showStickerPickerHint = useSelector(getShowStickerPickerHint);
const recordingState = useSelector(getRecordingState);
const errorDialogAudioRecorderType = useSelector(
getErrorDialogAudioRecorderType
);
const showPickerHint = Boolean(
get(state.items, ['showStickerPickerHint'], false) &&
receivedPacks.length > 0
const hasGlobalModalOpen = useSelector(isShowingAnyModal);
const hasPanelOpen = useSelector(getHasPanelOpen);
const getGroupAdmins = useSelector(getGroupAdminsSelector);
const getPreferredBadge = useSelector(getPreferredBadgeSelector);
const composerStateForConversationIdSelector = useSelector(
getComposerStateForConversationIdSelector
);
const composerStateForConversationIdSelector =
getComposerStateForConversationIdSelector(state);
const composerState = composerStateForConversationIdSelector(id);
const { announcementsOnly, areWeAdmin, draftEditMessage, draftBodyRanges } =
conversation;
const {
attachments: draftAttachments,
focusCounter,
@ -114,6 +129,38 @@ const mapStateToProps = (state: StateType, props: ExternalProps) => {
shouldSendHighQualityAttachments,
} = composerState;
const isActive = useMemo(() => {
return !hasGlobalModalOpen && !hasPanelOpen;
}, [hasGlobalModalOpen, hasPanelOpen]);
const groupAdmins = useMemo(() => {
return getGroupAdmins(id);
}, [getGroupAdmins, id]);
const addedBy = useMemo(() => {
if (conversation.type === 'group') {
return getAddedByForOurPendingInvitation(conversation);
}
return null;
}, [conversation]);
const conversationName = useContactNameData(conversation);
strictAssert(conversationName, 'conversationName is required');
const addedByName = useContactNameData(addedBy);
const hydratedDraftBodyRanges = useMemo(() => {
return hydrateRanges(draftBodyRanges, conversationSelector);
}, [conversationSelector, draftBodyRanges]);
const convertDraftBodyRangesIntoHydrated = useCallback(
(
bodyRanges: DraftBodyRanges | undefined
): HydratedBodyRangesType | undefined => {
return hydrateRanges(bodyRanges, conversationSelector);
},
[conversationSelector]
);
let { quotedMessage } = composerState;
if (!quotedMessage && draftEditMessage?.quote) {
quotedMessage = {
@ -122,117 +169,200 @@ const mapStateToProps = (state: StateType, props: ExternalProps) => {
};
}
const recentEmojis = selectRecentEmojis(state);
const ourConversationId = useSelector(getUserConversationId);
const defaultConversationColor = useSelector(getDefaultConversationColor);
const selectedMessageIds = getSelectedMessageIds(state);
const isFormattingEnabled = getTextFormattingEnabled(state);
const lastEditableMessageId = getLastEditableMessageId(state);
const convertDraftBodyRangesIntoHydrated = (
bodyRanges: DraftBodyRanges | undefined
): HydratedBodyRangesType | undefined => {
return hydrateRanges(bodyRanges, conversationSelector);
};
return {
// Base
conversationId: id,
draftEditMessage,
focusCounter,
getPreferredBadge: getPreferredBadgeSelector(state),
i18n: getIntl(state),
isDisabled,
isFormattingEnabled,
lastEditableMessageId,
messageCompositionId,
platform,
sendCounter,
shouldHidePopovers,
theme: getTheme(state),
convertDraftBodyRangesIntoHydrated,
// AudioCapture
errorDialogAudioRecorderType:
state.audioRecorder.errorDialogAudioRecorderType,
recordingState: state.audioRecorder.recordingState,
// AttachmentsList
draftAttachments,
// MediaEditor
imageToBlurHash,
// MediaQualitySelector
shouldSendHighQualityAttachments:
shouldSendHighQualityAttachments !== undefined
? shouldSendHighQualityAttachments
: window.storage.get('sent-media-quality') === 'high',
// StagedLinkPreview
linkPreviewLoading,
linkPreviewResult,
// Quote
quotedMessageId: quotedMessage?.quote?.messageId,
quotedMessageProps: quotedMessage
const quotedMessageProps = useMemo(() => {
return quotedMessage
? getPropsForQuote(quotedMessage, {
conversationSelector,
ourConversationId: getUserConversationId(state),
defaultConversationColor: getDefaultConversationColor(state),
ourConversationId,
defaultConversationColor,
})
: undefined,
quotedMessageAuthorAci: quotedMessage?.quote?.authorAci,
quotedMessageSentAt: quotedMessage?.quote?.id,
// Emojis
recentEmojis,
skinTone: getEmojiSkinTone(state),
// Stickers
receivedPacks,
installedPack,
blessedPacks,
knownPacks,
installedPacks,
recentStickers,
showIntroduction,
showPickerHint,
// Message Requests
...conversation,
conversationType: conversation.type,
isSMSOnly: Boolean(isConversationSMSOnly(conversation)),
isSignalConversation: isSignalConversation(conversation),
isFetchingUUID: conversation.isFetchingUUID,
isMissingMandatoryProfileSharing:
isMissingRequiredProfileSharing(conversation),
// Groups
announcementsOnly,
areWeAdmin,
groupAdmins: getGroupAdminsSelector(state)(conversation.id),
: undefined;
}, [
quotedMessage,
conversationSelector,
ourConversationId,
defaultConversationColor,
]);
draftText: dropNull(draftText),
draftBodyRanges: hydrateRanges(draftBodyRanges, conversationSelector),
renderSmartCompositionRecording: (
recProps: SmartCompositionRecordingProps
) => {
return <SmartCompositionRecording {...recProps} />;
},
renderSmartCompositionRecordingDraft: (
draftProps: SmartCompositionRecordingDraftProps
) => {
return <SmartCompositionRecordingDraft {...draftProps} />;
const { putItem, removeItem } = useItemsActions();
const onSetSkinTone = useCallback(
(tone: number) => {
putItem('skinTone', tone);
},
[putItem]
);
// Select Mode
selectedMessageIds,
};
};
const clearShowIntroduction = useCallback(() => {
removeItem('showStickersIntroduction');
}, [removeItem]);
const dispatchPropsMap = {
...mapDispatchToProps,
onSetSkinTone: (tone: number) => mapDispatchToProps.putItem('skinTone', tone),
clearShowIntroduction: () =>
mapDispatchToProps.removeItem('showStickersIntroduction'),
clearShowPickerHint: () =>
mapDispatchToProps.removeItem('showStickerPickerHint'),
onPickEmoji: mapDispatchToProps.onUseEmoji,
};
const clearShowPickerHint = useCallback(() => {
removeItem('showStickerPickerHint');
}, [removeItem]);
const smart = connect(mapStateToProps, dispatchPropsMap);
const {
onTextTooLong,
onCloseLinkPreview,
addAttachment,
removeAttachment,
onClearAttachments,
processAttachments,
setMediaQualitySetting,
setQuoteByMessageId,
cancelJoinRequest,
sendStickerMessage,
sendEditedMessage,
sendMultiMediaMessage,
setComposerFocus,
} = useComposerActions();
const {
pushPanelForConversation,
discardEditMessage,
acceptConversation,
blockAndReportSpam,
blockConversation,
reportSpam,
deleteConversation,
toggleSelectMode,
scrollToMessage,
setMessageToEdit,
showConversation,
} = useConversationsActions();
const { cancelRecording, completeRecording, startRecording, errorRecording } =
useAudioRecorderActions();
const { onUseEmoji } = useEmojisActions();
const { showGV2MigrationDialog, toggleForwardMessagesModal } =
useGlobalModalActions();
const { clearInstalledStickerPack } = useStickersActions();
const { showToast } = useToastActions();
const { onEditorStateChange } = useComposerActions();
export const SmartCompositionArea = smart(CompositionArea);
return (
<CompositionArea
// Base
conversationId={id}
draftEditMessage={draftEditMessage ?? null}
focusCounter={focusCounter}
getPreferredBadge={getPreferredBadge}
i18n={i18n}
isDisabled={isDisabled}
isFormattingEnabled={isFormattingEnabled}
isActive={isActive}
lastEditableMessageId={lastEditableMessageId ?? null}
messageCompositionId={messageCompositionId}
platform={platform}
sendCounter={sendCounter}
shouldHidePopovers={shouldHidePopovers}
theme={theme}
convertDraftBodyRangesIntoHydrated={convertDraftBodyRangesIntoHydrated}
onTextTooLong={onTextTooLong}
pushPanelForConversation={pushPanelForConversation}
discardEditMessage={discardEditMessage}
onCloseLinkPreview={onCloseLinkPreview}
onEditorStateChange={onEditorStateChange}
// AudioCapture
errorDialogAudioRecorderType={errorDialogAudioRecorderType ?? null}
recordingState={recordingState}
cancelRecording={cancelRecording}
completeRecording={completeRecording}
startRecording={startRecording}
errorRecording={errorRecording}
// AttachmentsList
draftAttachments={draftAttachments}
addAttachment={addAttachment}
removeAttachment={removeAttachment}
onClearAttachments={onClearAttachments}
processAttachments={processAttachments}
// MediaEditor
imageToBlurHash={imageToBlurHash}
// MediaQualitySelector
shouldSendHighQualityAttachments={
shouldSendHighQualityAttachments !== undefined
? shouldSendHighQualityAttachments
: window.storage.get('sent-media-quality') === 'high'
}
setMediaQualitySetting={setMediaQualitySetting}
// StagedLinkPreview
linkPreviewLoading={linkPreviewLoading}
linkPreviewResult={linkPreviewResult ?? null}
// Quote
quotedMessageId={quotedMessage?.quote?.messageId ?? null}
quotedMessageProps={quotedMessageProps ?? null}
quotedMessageAuthorAci={quotedMessage?.quote?.authorAci ?? null}
quotedMessageSentAt={quotedMessage?.quote?.id ?? null}
setQuoteByMessageId={setQuoteByMessageId}
// Emojis
recentEmojis={recentEmojis}
skinTone={skinTone}
onPickEmoji={onUseEmoji}
// Stickers
receivedPacks={receivedPacks}
installedPack={installedPack}
blessedPacks={blessedPacks}
knownPacks={knownPacks}
installedPacks={installedPacks}
recentStickers={recentStickers}
showIntroduction={showStickersIntroduction}
showPickerHint={showStickerPickerHint}
// Message Requests
acceptedMessageRequest={conversation.acceptedMessageRequest ?? null}
removalStage={conversation.removalStage ?? null}
addedByName={addedByName}
conversationName={conversationName}
conversationType={conversation.type}
isBlocked={conversation.isBlocked ?? false}
isReported={conversation.isReported ?? false}
isHidden={conversation.removalStage != null}
isSMSOnly={Boolean(isConversationSMSOnly(conversation))}
isSignalConversation={isSignalConversation(conversation)}
isFetchingUUID={conversation.isFetchingUUID ?? null}
isMissingMandatoryProfileSharing={isMissingRequiredProfileSharing(
conversation
)}
acceptConversation={acceptConversation}
blockAndReportSpam={blockAndReportSpam}
blockConversation={blockConversation}
reportSpam={reportSpam}
deleteConversation={deleteConversation}
// Groups
groupVersion={conversation.groupVersion ?? null}
isGroupV1AndDisabled={conversation.isGroupV1AndDisabled ?? null}
left={conversation.left ?? null}
announcementsOnly={announcementsOnly ?? null}
areWeAdmin={areWeAdmin ?? null}
areWePending={conversation.areWePending ?? null}
areWePendingApproval={conversation.areWePendingApproval ?? null}
groupAdmins={groupAdmins}
draftText={conversation.draftText ?? null}
draftBodyRanges={hydratedDraftBodyRanges ?? null}
renderSmartCompositionRecording={renderSmartCompositionRecording}
renderSmartCompositionRecordingDraft={
renderSmartCompositionRecordingDraft
}
showGV2MigrationDialog={showGV2MigrationDialog}
cancelJoinRequest={cancelJoinRequest}
sortedGroupMembers={conversation.sortedGroupMembers ?? null}
// Select Mode
selectedMessageIds={selectedMessageIds}
toggleSelectMode={toggleSelectMode}
toggleForwardMessagesModal={toggleForwardMessagesModal}
// Dispatch
onSetSkinTone={onSetSkinTone}
clearShowIntroduction={clearShowIntroduction}
clearInstalledStickerPack={clearInstalledStickerPack}
clearShowPickerHint={clearShowPickerHint}
showToast={showToast}
sendStickerMessage={sendStickerMessage}
sendEditedMessage={sendEditedMessage}
sendMultiMediaMessage={sendMultiMediaMessage}
scrollToMessage={scrollToMessage}
setComposerFocus={setComposerFocus}
setMessageToEdit={setMessageToEdit}
showConversation={showConversation}
/>
);
});

View file

@ -1,10 +1,9 @@
// Copyright 2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React, { useCallback } from 'react';
import React, { memo, useCallback } from 'react';
import { useSelector } from 'react-redux';
import { CompositionRecording } from '../../components/CompositionRecording';
import { mapDispatchToProps } from '../actions';
import { useAudioRecorderActions } from '../ducks/audioRecorder';
import { useComposerActions } from '../ducks/composer';
import { useToastActions } from '../ducks/toast';
@ -15,49 +14,55 @@ export type SmartCompositionRecordingProps = {
onBeforeSend: () => void;
};
export function SmartCompositionRecording({
onBeforeSend,
}: SmartCompositionRecordingProps): JSX.Element | null {
const i18n = useSelector(getIntl);
const selectedConversationId = useSelector(getSelectedConversationId);
const { cancelRecording, completeRecording } = useAudioRecorderActions();
const { sendMultiMediaMessage } = useComposerActions();
const { hideToast, showToast } = useToastActions();
const handleCancel = useCallback(() => {
cancelRecording();
}, [cancelRecording]);
const handleSend = useCallback(() => {
if (selectedConversationId) {
completeRecording(selectedConversationId, voiceNoteAttachment => {
onBeforeSend();
sendMultiMediaMessage(selectedConversationId, { voiceNoteAttachment });
});
}
}, [
selectedConversationId,
completeRecording,
export const SmartCompositionRecording = memo(
function SmartCompositionRecording({
onBeforeSend,
sendMultiMediaMessage,
]);
}: SmartCompositionRecordingProps) {
const i18n = useSelector(getIntl);
const selectedConversationId = useSelector(getSelectedConversationId);
const { errorRecording, cancelRecording, completeRecording } =
useAudioRecorderActions();
if (!selectedConversationId) {
return null;
const { sendMultiMediaMessage, addAttachment, saveDraftRecordingIfNeeded } =
useComposerActions();
const { hideToast, showToast } = useToastActions();
const handleCancel = useCallback(() => {
cancelRecording();
}, [cancelRecording]);
const handleSend = useCallback(() => {
if (selectedConversationId) {
completeRecording(selectedConversationId, voiceNoteAttachment => {
onBeforeSend();
sendMultiMediaMessage(selectedConversationId, {
voiceNoteAttachment,
});
});
}
}, [
selectedConversationId,
completeRecording,
onBeforeSend,
sendMultiMediaMessage,
]);
if (!selectedConversationId) {
return null;
}
return (
<CompositionRecording
i18n={i18n}
onCancel={handleCancel}
onSend={handleSend}
errorRecording={errorRecording}
addAttachment={addAttachment}
completeRecording={completeRecording}
saveDraftRecordingIfNeeded={saveDraftRecordingIfNeeded}
showToast={showToast}
hideToast={hideToast}
/>
);
}
return (
<CompositionRecording
i18n={i18n}
conversationId={selectedConversationId}
onCancel={handleCancel}
onSend={handleSend}
errorRecording={mapDispatchToProps.errorRecording}
addAttachment={mapDispatchToProps.addAttachment}
completeRecording={mapDispatchToProps.completeRecording}
showToast={showToast}
hideToast={hideToast}
/>
);
}
);

View file

@ -1,7 +1,7 @@
// Copyright 2023 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React, { useCallback } from 'react';
import React, { memo, useCallback } from 'react';
import { useSelector } from 'react-redux';
import { CompositionRecordingDraft } from '../../components/CompositionRecordingDraft';
import type { AttachmentDraftType } from '../../types/Attachment';
@ -21,136 +21,138 @@ export type SmartCompositionRecordingDraftProps = {
voiceNoteAttachment: AttachmentDraftType;
};
export function SmartCompositionRecordingDraft({
voiceNoteAttachment,
}: SmartCompositionRecordingDraftProps): JSX.Element {
const i18n = useSelector(getIntl);
const active = useSelector(selectAudioPlayerActive);
const selectedConversationId = useSelector(getSelectedConversationId);
const getConversationById = useSelector(getConversationByIdSelector);
const {
loadVoiceNoteDraftAudio,
unloadMessageAudio,
setIsPlaying,
setPosition,
} = useAudioPlayerActions();
const { sendMultiMediaMessage, removeAttachment } = useComposerActions();
export const SmartCompositionRecordingDraft = memo(
function SmartCompositionRecordingDraft({
voiceNoteAttachment,
}: SmartCompositionRecordingDraftProps) {
const i18n = useSelector(getIntl);
const active = useSelector(selectAudioPlayerActive);
const selectedConversationId = useSelector(getSelectedConversationId);
const getConversationById = useSelector(getConversationByIdSelector);
const {
loadVoiceNoteDraftAudio,
unloadMessageAudio,
setIsPlaying,
setPosition,
} = useAudioPlayerActions();
const { sendMultiMediaMessage, removeAttachment } = useComposerActions();
if (!selectedConversationId) {
throw new Error('No selected conversation');
}
if (!selectedConversationId) {
throw new Error('No selected conversation');
}
const playbackRate =
getConversationById(selectedConversationId)?.voiceNotePlaybackRate ?? 1;
const playbackRate =
getConversationById(selectedConversationId)?.voiceNotePlaybackRate ?? 1;
const audioUrl = !voiceNoteAttachment.pending
? voiceNoteAttachment.url
: undefined;
const content = active?.content;
const draftActive =
content && AudioPlayerContent.isDraft(content) && content.url === audioUrl
? active
const audioUrl = !voiceNoteAttachment.pending
? voiceNoteAttachment.url
: undefined;
const handlePlay = useCallback(
(positionAsRatio?: number) => {
if (!draftActive && audioUrl) {
loadVoiceNoteDraftAudio({
conversationId: selectedConversationId,
url: audioUrl,
startPosition: positionAsRatio ?? 0,
playbackRate,
const content = active?.content;
const draftActive =
content && AudioPlayerContent.isDraft(content) && content.url === audioUrl
? active
: undefined;
const handlePlay = useCallback(
(positionAsRatio?: number) => {
if (!draftActive && audioUrl) {
loadVoiceNoteDraftAudio({
conversationId: selectedConversationId,
url: audioUrl,
startPosition: positionAsRatio ?? 0,
playbackRate,
});
}
if (draftActive) {
if (positionAsRatio !== undefined) {
setPosition(positionAsRatio);
}
if (!draftActive.playing) {
setIsPlaying(true);
}
}
},
[
draftActive,
audioUrl,
loadVoiceNoteDraftAudio,
selectedConversationId,
playbackRate,
setPosition,
setIsPlaying,
]
);
const handlePause = useCallback(() => {
setIsPlaying(false);
}, [setIsPlaying]);
const handleSend = useCallback(() => {
if (selectedConversationId) {
sendMultiMediaMessage(selectedConversationId, {
draftAttachments: [voiceNoteAttachment],
});
}
if (draftActive) {
if (positionAsRatio !== undefined) {
}, [selectedConversationId, sendMultiMediaMessage, voiceNoteAttachment]);
const handleCancel = useCallback(() => {
unloadMessageAudio();
if (selectedConversationId && voiceNoteAttachment.path) {
removeAttachment(selectedConversationId, voiceNoteAttachment.path);
}
}, [
removeAttachment,
selectedConversationId,
unloadMessageAudio,
voiceNoteAttachment.path,
]);
const handleScrub = useCallback(
(positionAsRatio: number) => {
// if scrubbing when audio not loaded
if (!draftActive && audioUrl) {
loadVoiceNoteDraftAudio({
conversationId: selectedConversationId,
url: audioUrl,
startPosition: positionAsRatio,
playbackRate,
});
return;
}
// if scrubbing when audio is loaded
if (draftActive) {
setPosition(positionAsRatio);
if (draftActive?.playing) {
setIsPlaying(true);
}
}
if (!draftActive.playing) {
setIsPlaying(true);
}
}
},
[
draftActive,
audioUrl,
loadVoiceNoteDraftAudio,
selectedConversationId,
playbackRate,
setPosition,
setIsPlaying,
]
);
},
[
audioUrl,
draftActive,
loadVoiceNoteDraftAudio,
playbackRate,
selectedConversationId,
setIsPlaying,
setPosition,
]
);
const handlePause = useCallback(() => {
setIsPlaying(false);
}, [setIsPlaying]);
const handleSend = useCallback(() => {
if (selectedConversationId) {
sendMultiMediaMessage(selectedConversationId, {
draftAttachments: [voiceNoteAttachment],
});
}
}, [selectedConversationId, sendMultiMediaMessage, voiceNoteAttachment]);
const handleCancel = useCallback(() => {
unloadMessageAudio();
if (selectedConversationId && voiceNoteAttachment.path) {
removeAttachment(selectedConversationId, voiceNoteAttachment.path);
}
}, [
removeAttachment,
selectedConversationId,
unloadMessageAudio,
voiceNoteAttachment.path,
]);
const handleScrub = useCallback(
(positionAsRatio: number) => {
// if scrubbing when audio not loaded
if (!draftActive && audioUrl) {
loadVoiceNoteDraftAudio({
conversationId: selectedConversationId,
url: audioUrl,
startPosition: positionAsRatio,
playbackRate,
});
return;
}
// if scrubbing when audio is loaded
if (draftActive) {
setPosition(positionAsRatio);
if (draftActive?.playing) {
setIsPlaying(true);
}
}
},
[
audioUrl,
draftActive,
loadVoiceNoteDraftAudio,
playbackRate,
selectedConversationId,
setIsPlaying,
setPosition,
]
);
return (
<CompositionRecordingDraft
i18n={i18n}
audioUrl={audioUrl}
active={draftActive}
onCancel={handleCancel}
onSend={handleSend}
onPlay={handlePlay}
onPause={handlePause}
onScrub={handleScrub}
/>
);
}
return (
<CompositionRecordingDraft
i18n={i18n}
audioUrl={audioUrl}
active={draftActive}
onCancel={handleCancel}
onSend={handleSend}
onPlay={handlePlay}
onPause={handlePause}
onScrub={handleScrub}
/>
);
}
);

View file

@ -1,12 +1,11 @@
// Copyright 2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React from 'react';
import React, { memo } from 'react';
import { useSelector } from 'react-redux';
import type { CompositionTextAreaProps } from '../../components/CompositionTextArea';
import { CompositionTextArea } from '../../components/CompositionTextArea';
import { getIntl, getPlatform } from '../selectors/user';
import { useActions as useEmojiActions } from '../ducks/emojis';
import { useEmojisActions as useEmojiActions } from '../ducks/emojis';
import { useItemsActions } from '../ducks/items';
import { getPreferredBadgeSelector } from '../selectors/badges';
import { useComposerActions } from '../ducks/composer';
@ -16,6 +15,7 @@ export type SmartCompositionTextAreaProps = Pick<
CompositionTextAreaProps,
| 'bodyRanges'
| 'draftText'
| 'isActive'
| 'placeholder'
| 'onChange'
| 'onScroll'
@ -26,9 +26,9 @@ export type SmartCompositionTextAreaProps = Pick<
| 'scrollerRef'
>;
export function SmartCompositionTextArea(
export const SmartCompositionTextArea = memo(function SmartCompositionTextArea(
props: SmartCompositionTextAreaProps
): JSX.Element {
) {
const i18n = useSelector(getIntl);
const platform = useSelector(getPlatform);
@ -44,6 +44,7 @@ export function SmartCompositionTextArea(
{...props}
getPreferredBadge={getPreferredBadge}
i18n={i18n}
isActive
isFormattingEnabled={isFormattingEnabled}
onPickEmoji={onPickEmoji}
onSetSkinTone={onSetSkinTone}
@ -51,4 +52,4 @@ export function SmartCompositionTextArea(
platform={platform}
/>
);
}
});

View file

@ -1,16 +1,10 @@
// Copyright 2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { connect } from 'react-redux';
import type { StateType } from '../reducer';
import { mapDispatchToProps } from '../actions';
import React, { memo, useMemo } from 'react';
import { useSelector } from 'react-redux';
import { strictAssert } from '../../util/assert';
import type { StatePropsType } from '../../components/conversation/conversation-details/AddGroupMembersModal/ConfirmAdditionsModal';
import { ConfirmAdditionsModal } from '../../components/conversation/conversation-details/AddGroupMembersModal/ConfirmAdditionsModal';
import type { RequestState } from '../../components/conversation/conversation-details/util';
import { getIntl } from '../selectors/user';
import { getConversationByIdSelector } from '../selectors/conversations';
@ -22,28 +16,37 @@ export type SmartConfirmAdditionsModalPropsType = {
requestState: RequestState;
};
const mapStateToProps = (
state: StateType,
props: SmartConfirmAdditionsModalPropsType
): StatePropsType => {
const conversationSelector = getConversationByIdSelector(state);
export const SmartConfirmAdditionsModal = memo(
function SmartConfirmAdditionsModal({
selectedConversationIds,
groupTitle,
makeRequest,
onClose,
requestState,
}: SmartConfirmAdditionsModalPropsType) {
const i18n = useSelector(getIntl);
const conversationSelector = useSelector(getConversationByIdSelector);
const selectedContacts = props.selectedConversationIds.map(conversationId => {
const convo = conversationSelector(conversationId);
strictAssert(
convo,
'<SmartChooseGroupMemberModal> selected conversation not found'
const selectedContacts = useMemo(() => {
return selectedConversationIds.map(conversationId => {
const convo = conversationSelector(conversationId);
strictAssert(
convo,
'<SmartChooseGroupMemberModal> selected conversation not found'
);
return convo;
});
}, [conversationSelector, selectedConversationIds]);
return (
<ConfirmAdditionsModal
i18n={i18n}
selectedContacts={selectedContacts}
groupTitle={groupTitle}
makeRequest={makeRequest}
onClose={onClose}
requestState={requestState}
/>
);
return convo;
});
return {
...props,
selectedContacts,
i18n: getIntl(state),
};
};
const smart = connect(mapStateToProps, mapDispatchToProps);
export const SmartConfirmAdditionsModal = smart(ConfirmAdditionsModal);
}
);

View file

@ -0,0 +1,37 @@
// Copyright 2024 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React, { memo } from 'react';
import { useSelector } from 'react-redux';
import { useCallingActions } from '../ducks/calling';
import { getIntl } from '../selectors/user';
import { useGlobalModalActions } from '../ducks/globalModals';
import { getConfirmLeaveCallModalState } from '../selectors/globalModals';
import { ConfirmLeaveCallModal } from '../../components/ConfirmLeaveCallModal';
export const SmartConfirmLeaveCallModal = memo(
function SmartConfirmLeaveCallModal(): JSX.Element | null {
const i18n = useSelector(getIntl);
const confirmLeaveCallModalState = useSelector(
getConfirmLeaveCallModalState
);
const { leaveCurrentCallAndStartCallingLobby } = useCallingActions();
const { toggleConfirmLeaveCallModal } = useGlobalModalActions();
if (!confirmLeaveCallModalState) {
return null;
}
return (
<ConfirmLeaveCallModal
i18n={i18n}
data={confirmLeaveCallModalState}
leaveCurrentCallAndStartCallingLobby={
leaveCurrentCallAndStartCallingLobby
}
toggleConfirmLeaveCallModal={toggleConfirmLeaveCallModal}
/>
);
}
);

View file

@ -1,59 +1,107 @@
// Copyright 2020 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { connect } from 'react-redux';
import { mapDispatchToProps } from '../actions';
import type { PropsDataType } from '../../components/conversation/ContactModal';
import React, { memo, useCallback, useMemo } from 'react';
import { useSelector } from 'react-redux';
import { ContactModal } from '../../components/conversation/ContactModal';
import type { StateType } from '../reducer';
import { getAreWeASubscriber } from '../selectors/items';
import { getIntl, getTheme } from '../selectors/user';
import { getBadgesSelector } from '../selectors/badges';
import { getConversationSelector } from '../selectors/conversations';
import { getHasStoriesSelector } from '../selectors/stories2';
import { getActiveCallState } from '../selectors/calling';
import {
getActiveCallState,
isInFullScreenCall as getIsInFullScreenCall,
} from '../selectors/calling';
import { useStoriesActions } from '../ducks/stories';
import { useConversationsActions } from '../ducks/conversations';
import { useGlobalModalActions } from '../ducks/globalModals';
import { useCallingActions } from '../ducks/calling';
import { getContactModalState } from '../selectors/globalModals';
import { strictAssert } from '../../util/assert';
const mapStateToProps = (state: StateType): PropsDataType => {
const { contactId, conversationId } =
state.globalModals.contactModalState || {};
export const SmartContactModal = memo(function SmartContactModal() {
const i18n = useSelector(getIntl);
const theme = useSelector(getTheme);
const { conversationId, contactId } = useSelector(getContactModalState) ?? {};
const conversationSelector = useSelector(getConversationSelector);
const hasStoriesSelector = useSelector(getHasStoriesSelector);
const activeCallState = useSelector(getActiveCallState);
const isInFullScreenCall = useSelector(getIsInFullScreenCall);
const badgesSelector = useSelector(getBadgesSelector);
const areWeASubscriber = useSelector(getAreWeASubscriber);
const currentConversation = getConversationSelector(state)(conversationId);
const contact = getConversationSelector(state)(contactId);
const conversation = conversationSelector(conversationId);
const contact = conversationSelector(contactId);
const hasStories = hasStoriesSelector(contactId);
const hasActiveCall = activeCallState != null;
const badges = badgesSelector(contact.badges);
const areWeAdmin =
currentConversation && currentConversation.areWeAdmin
? currentConversation.areWeAdmin
: false;
const areWeAdmin = conversation?.areWeAdmin ?? false;
let isMember = false;
let isAdmin = false;
if (contact && currentConversation && currentConversation.memberships) {
currentConversation.memberships.forEach(membership => {
if (membership.aci === contact.serviceId) {
isMember = true;
isAdmin = membership.isAdmin;
}
const ourMembership = useMemo(() => {
return conversation?.memberships?.find(membership => {
return membership.aci === contact.serviceId;
});
}
}, [conversation?.memberships, contact]);
const hasStories = getHasStoriesSelector(state)(contactId);
const isMember = ourMembership != null;
const isAdmin = ourMembership?.isAdmin ?? false;
return {
areWeASubscriber: getAreWeASubscriber(state),
areWeAdmin,
badges: getBadgesSelector(state)(contact.badges),
hasActiveCall: Boolean(getActiveCallState(state)),
contact,
conversation: currentConversation,
hasStories,
i18n: getIntl(state),
isAdmin,
isMember,
theme: getTheme(state),
};
};
const {
removeMemberFromGroup,
showConversation,
updateConversationModelSharedGroups,
toggleAdmin,
blockConversation,
} = useConversationsActions();
const { viewUserStories } = useStoriesActions();
const {
toggleAboutContactModal,
toggleAddUserToAnotherGroupModal,
toggleSafetyNumberModal,
hideContactModal,
toggleEditNicknameAndNoteModal,
} = useGlobalModalActions();
const {
onOutgoingVideoCallInConversation,
onOutgoingAudioCallInConversation,
togglePip,
} = useCallingActions();
const smart = connect(mapStateToProps, mapDispatchToProps);
const handleOpenEditNicknameAndNoteModal = useCallback(() => {
strictAssert(contactId != null, 'Expected conversationId to be set');
toggleEditNicknameAndNoteModal({ conversationId: contactId });
}, [toggleEditNicknameAndNoteModal, contactId]);
export const SmartContactModal = smart(ContactModal);
return (
<ContactModal
areWeAdmin={areWeAdmin}
areWeASubscriber={areWeASubscriber}
badges={badges}
blockConversation={blockConversation}
contact={contact}
conversation={conversation}
hasActiveCall={hasActiveCall}
hasStories={hasStories}
hideContactModal={hideContactModal}
i18n={i18n}
isAdmin={isAdmin}
isInFullScreenCall={isInFullScreenCall}
isMember={isMember}
onOpenEditNicknameAndNoteModal={handleOpenEditNicknameAndNoteModal}
onOutgoingAudioCallInConversation={onOutgoingAudioCallInConversation}
onOutgoingVideoCallInConversation={onOutgoingVideoCallInConversation}
removeMemberFromGroup={removeMemberFromGroup}
showConversation={showConversation}
theme={theme}
toggleAboutContactModal={toggleAboutContactModal}
toggleAddUserToAnotherGroupModal={toggleAddUserToAnotherGroupModal}
toggleAdmin={toggleAdmin}
togglePip={togglePip}
toggleSafetyNumberModal={toggleSafetyNumberModal}
updateConversationModelSharedGroups={updateConversationModelSharedGroups}
viewUserStories={viewUserStories}
/>
);
});

View file

@ -1,46 +1,41 @@
// Copyright 2020 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import * as React from 'react';
import React, { memo, useCallback, useMemo } from 'react';
import { useSelector } from 'react-redux';
import type { StateType } from '../reducer';
import { ContactName } from '../../components/conversation/ContactName';
import { getIntl } from '../selectors/user';
import type { GetConversationByIdType } from '../selectors/conversations';
import {
getConversationSelector,
getSelectedConversationId,
} from '../selectors/conversations';
import type { LocalizerType } from '../../types/Util';
import { useGlobalModalActions } from '../ducks/globalModals';
type ExternalProps = {
contactId: string;
};
export function SmartContactName(props: ExternalProps): JSX.Element {
const { contactId } = props;
const i18n = useSelector<StateType, LocalizerType>(getIntl);
const getConversation = useSelector<StateType, GetConversationByIdType>(
getConversationSelector
);
const contact = getConversation(contactId) || {
title: i18n('icu:unknownContact'),
};
export const SmartContactName = memo(function SmartContactName({
contactId,
}: ExternalProps) {
const i18n = useSelector(getIntl);
const getConversation = useSelector(getConversationSelector);
const currentConversationId = useSelector(getSelectedConversationId);
const currentConversation = getConversation(currentConversationId);
const { showContactModal } = useGlobalModalActions();
const contact = useMemo(() => {
return getConversation(contactId);
}, [getConversation, contactId]);
const handleClick = useCallback(() => {
showContactModal(contactId, currentConversationId);
}, [showContactModal, contactId, currentConversationId]);
return (
<ContactName
firstName={contact.firstName}
title={contact.title}
onClick={() => showContactModal(contact.id, currentConversation.id)}
title={contact.title ?? i18n('icu:unknownContact')}
onClick={handleClick}
/>
);
}
});

View file

@ -1,15 +1,12 @@
// Copyright 2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React, { useCallback } from 'react';
import React, { memo, useCallback } from 'react';
import { useSelector } from 'react-redux';
import { mapValues } from 'lodash';
import type { StateType } from '../reducer';
import { ContactSpoofingReviewDialog } from '../../components/conversation/ContactSpoofingReviewDialog';
import { useConversationsActions } from '../ducks/conversations';
import type { GetConversationByIdType } from '../selectors/conversations';
import {
getConversationSelector,
getConversationByServiceIdSelector,
@ -33,114 +30,114 @@ export type PropsType = Readonly<{
onClose: () => void;
}>;
export function SmartContactSpoofingReviewDialog(
props: PropsType
): JSX.Element | null {
const { conversationId } = props;
export const SmartContactSpoofingReviewDialog = memo(
function SmartContactSpoofingReviewDialog(props: PropsType) {
const { conversationId } = props;
const getConversation = useSelector<StateType, GetConversationByIdType>(
getConversationSelector
);
const getConversation = useSelector(getConversationSelector);
const {
acceptConversation,
blockAndReportSpam,
blockConversation,
deleteConversation,
removeMember,
updateSharedGroups,
} = useConversationsActions();
const { showContactModal, toggleSignalConnectionsModal } =
useGlobalModalActions();
const getPreferredBadge = useSelector(getPreferredBadgeSelector);
const i18n = useSelector(getIntl);
const theme = useSelector(getTheme);
const getConversationByServiceId = useSelector(
getConversationByServiceIdSelector
);
const conversation = getConversation(conversationId);
// Just binding the options argument
const safeConversationSelector = useCallback(
(state: StateType) => {
return getSafeConversationWithSameTitle(state, {
possiblyUnsafeConversation: conversation,
});
},
[conversation]
);
const safeConvo = useSelector(safeConversationSelector);
const sharedProps = {
...props,
acceptConversation,
blockAndReportSpam,
blockConversation,
deleteConversation,
getPreferredBadge,
i18n,
removeMember,
updateSharedGroups,
showContactModal,
toggleSignalConnectionsModal,
theme,
};
if (conversation.type === 'group') {
const { memberships } = getGroupMemberships(
conversation,
getConversationByServiceId
const {
acceptConversation,
reportSpam,
blockAndReportSpam,
blockConversation,
deleteConversation,
removeMember,
updateSharedGroups,
} = useConversationsActions();
const { showContactModal, toggleSignalConnectionsModal } =
useGlobalModalActions();
const getPreferredBadge = useSelector(getPreferredBadgeSelector);
const i18n = useSelector(getIntl);
const theme = useSelector(getTheme);
const getConversationByServiceId = useSelector(
getConversationByServiceIdSelector
);
const groupNameCollisions = getCollisionsFromMemberships(memberships);
const conversation = getConversation(conversationId);
const previouslyAcknowledgedTitlesById = invertIdsByTitle(
conversation.acknowledgedGroupNameCollisions
// Just binding the options argument
const safeConversationSelector = useCallback(
(state: StateType) => {
return getSafeConversationWithSameTitle(state, {
possiblyUnsafeConversation: conversation,
});
},
[conversation]
);
const safeConvo = useSelector(safeConversationSelector);
const sharedProps = {
...props,
acceptConversation,
reportSpam,
blockAndReportSpam,
blockConversation,
deleteConversation,
getPreferredBadge,
i18n,
removeMember,
updateSharedGroups,
showContactModal,
toggleSignalConnectionsModal,
theme,
};
if (conversation.type === 'group') {
const { memberships } = getGroupMemberships(
conversation,
getConversationByServiceId
);
const groupNameCollisions = getCollisionsFromMemberships(memberships);
const previouslyAcknowledgedTitlesById = invertIdsByTitle(
conversation.acknowledgedGroupNameCollisions
);
const collisionInfoByTitle = mapValues(groupNameCollisions, collisions =>
collisions.map(collision => ({
conversation: collision,
isSignalConnection: isSignalConnection(collision),
oldName: getOwn(previouslyAcknowledgedTitlesById, collision.id),
}))
);
return (
<ContactSpoofingReviewDialog
{...sharedProps}
type={ContactSpoofingType.MultipleGroupMembersWithSameTitle}
group={conversation}
collisionInfoByTitle={collisionInfoByTitle}
/>
);
}
const possiblyUnsafeConvo = conversation;
assertDev(
possiblyUnsafeConvo.type === 'direct',
'DirectConversationWithSameTitle: expects possibly unsafe direct ' +
'conversation'
);
const collisionInfoByTitle = mapValues(groupNameCollisions, collisions =>
collisions.map(collision => ({
conversation: collision,
isSignalConnection: isSignalConnection(collision),
oldName: getOwn(previouslyAcknowledgedTitlesById, collision.id),
}))
);
if (!safeConvo) {
return null;
}
const possiblyUnsafe = {
conversation: possiblyUnsafeConvo,
isSignalConnection: isSignalConnection(possiblyUnsafeConvo),
};
const safe = {
conversation: safeConvo,
isSignalConnection: isSignalConnection(safeConvo),
};
return (
<ContactSpoofingReviewDialog
{...sharedProps}
type={ContactSpoofingType.MultipleGroupMembersWithSameTitle}
group={conversation}
collisionInfoByTitle={collisionInfoByTitle}
type={ContactSpoofingType.DirectConversationWithSameTitle}
possiblyUnsafe={possiblyUnsafe}
safe={safe}
/>
);
}
const possiblyUnsafeConvo = conversation;
assertDev(
possiblyUnsafeConvo.type === 'direct',
'DirectConversationWithSameTitle: expects possibly unsafe direct ' +
'conversation'
);
if (!safeConvo) {
return null;
}
const possiblyUnsafe = {
conversation: possiblyUnsafeConvo,
isSignalConnection: isSignalConnection(possiblyUnsafeConvo),
};
const safe = {
conversation: safeConvo,
isSignalConnection: isSignalConnection(safeConvo),
};
return (
<ContactSpoofingReviewDialog
{...sharedProps}
type={ContactSpoofingType.DirectConversationWithSameTitle}
possiblyUnsafe={possiblyUnsafe}
safe={safe}
/>
);
}
);

View file

@ -1,43 +1,45 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React from 'react';
import { connect } from 'react-redux';
import { sortBy } from 'lodash';
import type { StateType } from '../reducer';
import { mapDispatchToProps } from '../actions';
import type { StateProps } from '../../components/conversation/conversation-details/ConversationDetails';
import React, { memo, useCallback } from 'react';
import { useSelector } from 'react-redux';
import { ConversationDetails } from '../../components/conversation/conversation-details/ConversationDetails';
import {
getConversationByIdSelector,
getConversationByServiceIdSelector,
getAllComposableConversations,
} from '../selectors/conversations';
getGroupSizeHardLimit,
getGroupSizeRecommendedLimit,
} from '../../groups/limits';
import { SignalService as Proto } from '../../protobuf';
import type { CallHistoryGroup } from '../../types/CallDisposition';
import { assertDev } from '../../util/assert';
import { getConversationColorAttributes } from '../../util/getConversationColorAttributes';
import { getGroupMemberships } from '../../util/getGroupMemberships';
import { getActiveCallState } from '../selectors/calling';
import {
getAreWeASubscriber,
getDefaultConversationColor,
} from '../selectors/items';
import { getIntl, getTheme } from '../selectors/user';
import {
getBadgesSelector,
getPreferredBadgeSelector,
} from '../selectors/badges';
import { assertDev } from '../../util/assert';
import { SignalService as Proto } from '../../protobuf';
import { getConversationColorAttributes } from '../../util/getConversationColorAttributes';
import { getActiveCallState } from '../selectors/calling';
import {
getAllComposableConversations,
getConversationByIdSelector,
getConversationByServiceIdSelector,
} from '../selectors/conversations';
import {
getAreWeASubscriber,
getDefaultConversationColor,
} from '../selectors/items';
import { getSelectedNavTab } from '../selectors/nav';
import { getIntl, getTheme } from '../selectors/user';
import type { SmartChooseGroupMembersModalPropsType } from './ChooseGroupMembersModal';
import { SmartChooseGroupMembersModal } from './ChooseGroupMembersModal';
import type { SmartConfirmAdditionsModalPropsType } from './ConfirmAdditionsModal';
import { SmartConfirmAdditionsModal } from './ConfirmAdditionsModal';
import {
getGroupSizeRecommendedLimit,
getGroupSizeHardLimit,
} from '../../groups/limits';
import type { CallHistoryGroup } from '../../types/CallDisposition';
import { getSelectedNavTab } from '../selectors/nav';
import type { ConversationType } from '../ducks/conversations';
import { useConversationsActions } from '../ducks/conversations';
import { useCallingActions } from '../ducks/calling';
import { useSearchActions } from '../ducks/search';
import { useGlobalModalActions } from '../ducks/globalModals';
import { useLightboxActions } from '../ducks/lightbox';
export type SmartConversationDetailsProps = {
conversationId: string;
@ -58,79 +60,167 @@ const renderConfirmAdditionsModal = (
return <SmartConfirmAdditionsModal {...props} />;
};
const mapStateToProps = (
state: StateType,
props: SmartConversationDetailsProps
): StateProps => {
const conversationSelector = getConversationByIdSelector(state);
const conversation = conversationSelector(props.conversationId);
function getGroupsInCommonSorted(
conversation: ConversationType,
allComposableConversations: ReadonlyArray<ConversationType>
) {
if (conversation.type !== 'direct') {
return [];
}
const groupsInCommonUnsorted = allComposableConversations.filter(
otherConversation => {
if (otherConversation.type !== 'group') {
return false;
}
return otherConversation.memberships?.some(member => {
return member.aci === conversation.serviceId;
});
}
);
return sortBy(groupsInCommonUnsorted, 'title');
}
export const SmartConversationDetails = memo(function SmartConversationDetails({
conversationId,
callHistoryGroup,
}: SmartConversationDetailsProps) {
const i18n = useSelector(getIntl);
const theme = useSelector(getTheme);
const activeCall = useSelector(getActiveCallState);
const allComposableConversations = useSelector(getAllComposableConversations);
const areWeASubscriber = useSelector(getAreWeASubscriber);
const badgesSelector = useSelector(getBadgesSelector);
const conversationByServiceIdSelector = useSelector(
getConversationByServiceIdSelector
);
const conversationSelector = useSelector(getConversationByIdSelector);
const defaultConversationColor = useSelector(getDefaultConversationColor);
const getPreferredBadge = useSelector(getPreferredBadgeSelector);
const selectedNavTab = useSelector(getSelectedNavTab);
const {
acceptConversation,
addMembersToGroup,
blockConversation,
deleteAvatarFromDisk,
getProfilesForConversation,
leaveGroup,
loadRecentMediaItems,
pushPanelForConversation,
replaceAvatar,
saveAvatarToDisk,
setDisappearingMessages,
setMuteExpiration,
showConversation,
updateGroupAttributes,
updateNicknameAndNote,
} = useConversationsActions();
const {
onOutgoingAudioCallInConversation,
onOutgoingVideoCallInConversation,
} = useCallingActions();
const { searchInConversation } = useSearchActions();
const {
showContactModal,
toggleAboutContactModal,
toggleAddUserToAnotherGroupModal,
toggleEditNicknameAndNoteModal,
toggleSafetyNumberModal,
} = useGlobalModalActions();
const { showLightboxWithMedia } = useLightboxActions();
const conversation = conversationSelector(conversationId);
assertDev(
conversation,
'<SmartConversationDetails> expected a conversation to be found'
);
const conversationWithColorAttributes = {
...conversation,
...getConversationColorAttributes(conversation, defaultConversationColor),
};
const canEditGroupInfo = Boolean(conversation.canEditGroupInfo);
const canAddNewMembers = Boolean(conversation.canAddNewMembers);
const isAdmin = Boolean(conversation.areWeAdmin);
const hasGroupLink =
Boolean(conversation.groupLink) &&
conversation.accessControlAddFromInviteLink !== ACCESS_ENUM.UNSATISFIABLE;
const conversationByServiceIdSelector =
getConversationByServiceIdSelector(state);
const groupMemberships = getGroupMemberships(
conversation,
conversationByServiceIdSelector
);
const badges = getBadgesSelector(state)(conversation.badges);
const defaultConversationColor = getDefaultConversationColor(state);
const groupsInCommon =
conversation.type === 'direct'
? getAllComposableConversations(state).filter(
c =>
c.type === 'group' &&
(c.memberships ?? []).some(
member => member.aci === conversation.serviceId
)
)
: [];
const groupsInCommonSorted = sortBy(groupsInCommon, 'title');
const { memberships, pendingApprovalMemberships, pendingMemberships } =
groupMemberships;
const badges = badgesSelector(conversation.badges);
const canAddNewMembers = conversation.canAddNewMembers ?? false;
const canEditGroupInfo = conversation.canEditGroupInfo ?? false;
const groupsInCommon = getGroupsInCommonSorted(
conversation,
allComposableConversations
);
const hasActiveCall = activeCall != null;
const hasGroupLink =
conversation.groupLink != null &&
conversation.accessControlAddFromInviteLink !== ACCESS_ENUM.UNSATISFIABLE;
const isAdmin = conversation.areWeAdmin ?? false;
const isGroup = conversation.type === 'group';
const maxGroupSize = getGroupSizeHardLimit(1001);
const maxRecommendedGroupSize = getGroupSizeRecommendedLimit(151);
return {
...props,
const userAvatarData = conversation.avatars ?? [];
areWeASubscriber: getAreWeASubscriber(state),
badges,
canEditGroupInfo,
canAddNewMembers,
conversation: {
...conversation,
...getConversationColorAttributes(conversation, defaultConversationColor),
},
getPreferredBadge: getPreferredBadgeSelector(state),
hasActiveCall: Boolean(getActiveCallState(state)),
i18n: getIntl(state),
isAdmin,
...groupMemberships,
maxGroupSize,
maxRecommendedGroupSize,
userAvatarData: conversation.avatars || [],
hasGroupLink,
groupsInCommon: groupsInCommonSorted,
isGroup: conversation.type === 'group',
selectedNavTab: getSelectedNavTab(state),
theme: getTheme(state),
renderChooseGroupMembersModal,
renderConfirmAdditionsModal,
};
};
const handleDeleteNicknameAndNote = useCallback(() => {
updateNicknameAndNote(conversationId, { nickname: null, note: null });
}, [conversationId, updateNicknameAndNote]);
const smart = connect(mapStateToProps, mapDispatchToProps);
const handleOpenEditNicknameAndNoteModal = useCallback(() => {
toggleEditNicknameAndNoteModal({ conversationId });
}, [conversationId, toggleEditNicknameAndNoteModal]);
export const SmartConversationDetails = smart(ConversationDetails);
return (
<ConversationDetails
acceptConversation={acceptConversation}
addMembersToGroup={addMembersToGroup}
areWeASubscriber={areWeASubscriber}
badges={badges}
blockConversation={blockConversation}
callHistoryGroup={callHistoryGroup}
canAddNewMembers={canAddNewMembers}
canEditGroupInfo={canEditGroupInfo}
conversation={conversationWithColorAttributes}
deleteAvatarFromDisk={deleteAvatarFromDisk}
getPreferredBadge={getPreferredBadge}
getProfilesForConversation={getProfilesForConversation}
groupsInCommon={groupsInCommon}
hasActiveCall={hasActiveCall}
hasGroupLink={hasGroupLink}
i18n={i18n}
isAdmin={isAdmin}
isGroup={isGroup}
leaveGroup={leaveGroup}
loadRecentMediaItems={loadRecentMediaItems}
maxGroupSize={maxGroupSize}
maxRecommendedGroupSize={maxRecommendedGroupSize}
memberships={memberships}
onDeleteNicknameAndNote={handleDeleteNicknameAndNote}
onOpenEditNicknameAndNoteModal={handleOpenEditNicknameAndNoteModal}
onOutgoingAudioCallInConversation={onOutgoingAudioCallInConversation}
onOutgoingVideoCallInConversation={onOutgoingVideoCallInConversation}
pendingApprovalMemberships={pendingApprovalMemberships}
pendingMemberships={pendingMemberships}
pushPanelForConversation={pushPanelForConversation}
renderChooseGroupMembersModal={renderChooseGroupMembersModal}
renderConfirmAdditionsModal={renderConfirmAdditionsModal}
replaceAvatar={replaceAvatar}
saveAvatarToDisk={saveAvatarToDisk}
searchInConversation={searchInConversation}
selectedNavTab={selectedNavTab}
setDisappearingMessages={setDisappearingMessages}
setMuteExpiration={setMuteExpiration}
showContactModal={showContactModal}
showConversation={showConversation}
showLightboxWithMedia={showLightboxWithMedia}
theme={theme}
toggleAboutContactModal={toggleAboutContactModal}
toggleAddUserToAnotherGroupModal={toggleAddUserToAnotherGroupModal}
toggleSafetyNumberModal={toggleSafetyNumberModal}
updateGroupAttributes={updateGroupAttributes}
userAvatarData={userAvatarData}
/>
);
});

View file

@ -1,56 +1,62 @@
// Copyright 2020 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React from 'react';
import React, { memo, useCallback, useMemo } from 'react';
import { useSelector } from 'react-redux';
import { pick } from 'lodash';
import type { ConversationType } from '../ducks/conversations';
import type { StateType } from '../reducer';
import { useContactNameData } from '../../components/conversation/ContactName';
import {
ConversationHeader,
OutgoingCallButtonStyle,
} from '../../components/conversation/ConversationHeader';
import { getPreferredBadgeSelector } from '../selectors/badges';
import {
getConversationByServiceIdSelector,
getConversationSelector,
getHasPanelOpen,
getSelectedMessageIds,
isMissingRequiredProfileSharing,
} from '../selectors/conversations';
import { getCannotLeaveBecauseYouAreLastAdmin } from '../../components/conversation/conversation-details/ConversationDetails';
import { useMinimalConversation } from '../../hooks/useMinimalConversation';
import { CallMode } from '../../types/Calling';
import { getActiveCall, useCallingActions } from '../ducks/calling';
import { PanelType } from '../../types/Panels';
import { StoryViewModeType } from '../../types/Stories';
import { strictAssert } from '../../util/assert';
import { getAddedByForOurPendingInvitation } from '../../util/getAddedByForOurPendingInvitation';
import { getGroupMemberships } from '../../util/getGroupMemberships';
import { isConversationSMSOnly } from '../../util/isConversationSMSOnly';
import { isGroupOrAdhocCallState } from '../../util/isGroupOrAdhocCall';
import { isSignalConversation } from '../../util/isSignalConversation';
import { missingCaseError } from '../../util/missingCaseError';
import { useCallingActions } from '../ducks/calling';
import { isAnybodyElseInGroupCall } from '../ducks/callingHelpers';
import type { ConversationType } from '../ducks/conversations';
import {
getConversationCallMode,
useConversationsActions,
} from '../ducks/conversations';
import { getHasStoriesSelector } from '../selectors/stories2';
import { getOwn } from '../../util/getOwn';
import { getUserACI, getIntl, getTheme } from '../selectors/user';
import { isConversationSMSOnly } from '../../util/isConversationSMSOnly';
import { missingCaseError } from '../../util/missingCaseError';
import { strictAssert } from '../../util/assert';
import { isSignalConversation } from '../../util/isSignalConversation';
import { useSearchActions } from '../ducks/search';
import { useStoriesActions } from '../ducks/stories';
import { getCannotLeaveBecauseYouAreLastAdmin } from '../../components/conversation/conversation-details/ConversationDetails';
import { getGroupMemberships } from '../../util/getGroupMemberships';
import { isGroupOrAdhocCallState } from '../../util/isGroupOrAdhocCall';
import { getPreferredBadgeSelector } from '../selectors/badges';
import { getActiveCallState, getCallSelector } from '../selectors/calling';
import {
getConversationByServiceIdSelector,
getConversationSelector,
getHasPanelOpen,
isMissingRequiredProfileSharing as getIsMissingRequiredProfileSharing,
getSelectedMessageIds,
} from '../selectors/conversations';
import { getHasStoriesSelector } from '../selectors/stories2';
import { getIntl, getTheme, getUserACI } from '../selectors/user';
import { useItemsActions } from '../ducks/items';
import { getLocalDeleteWarningShown } from '../selectors/items';
import { getDeleteSyncSendEnabled } from '../selectors/items-extra';
export type OwnProps = {
id: string;
};
const getOutgoingCallButtonStyle = (
conversation: ConversationType,
state: StateType
const useOutgoingCallButtonStyle = (
conversation: ConversationType
): OutgoingCallButtonStyle => {
const { calling } = state;
const ourAci = getUserACI(state);
strictAssert(ourAci, 'getOutgoingCallButtonStyle missing our uuid');
const ourAci = useSelector(getUserACI);
const activeCall = useSelector(getActiveCallState);
const callSelector = useSelector(getCallSelector);
strictAssert(ourAci, 'useOutgoingCallButtonStyle missing our uuid');
if (getActiveCall(calling)) {
if (activeCall != null) {
return OutgoingCallButtonStyle.None;
}
@ -62,7 +68,7 @@ const getOutgoingCallButtonStyle = (
return OutgoingCallButtonStyle.Both;
case CallMode.Group:
case CallMode.Adhoc: {
const call = getOwn(calling.callsByConversation, conversation.id);
const call = callSelector(conversation.id);
if (
isGroupOrAdhocCallState(call) &&
isAnybodyElseInGroupCall(call.peekInfo, ourAci)
@ -76,7 +82,9 @@ const getOutgoingCallButtonStyle = (
}
};
export function SmartConversationHeader({ id }: OwnProps): JSX.Element {
export const SmartConversationHeader = memo(function SmartConversationHeader({
id,
}: OwnProps) {
const conversationSelector = useSelector(getConversationSelector);
const conversation = conversationSelector(id);
if (!conversation) {
@ -89,11 +97,8 @@ export function SmartConversationHeader({ id }: OwnProps): JSX.Element {
const badgeSelector = useSelector(getPreferredBadgeSelector);
const badge = badgeSelector(conversation.badges);
const i18n = useSelector(getIntl);
const hasPanelShowing = useSelector<StateType, boolean>(getHasPanelOpen);
const outgoingCallButtonStyle = useSelector<
StateType,
OutgoingCallButtonStyle
>(state => getOutgoingCallButtonStyle(conversation, state));
const hasPanelShowing = useSelector(getHasPanelOpen);
const outgoingCallButtonStyle = useOutgoingCallButtonStyle(conversation);
const theme = useSelector(getTheme);
const {
@ -102,12 +107,16 @@ export function SmartConversationHeader({ id }: OwnProps): JSX.Element {
onArchive,
onMarkUnread,
onMoveToInbox,
popPanelForConversation,
pushPanelForConversation,
setDisappearingMessages,
setMuteExpiration,
setPinned,
toggleSelectMode,
acceptConversation,
blockAndReportSpam,
blockConversation,
reportSpam,
deleteConversation,
} = useConversationsActions();
const {
onOutgoingAudioCallInConversation,
@ -129,61 +138,169 @@ export function SmartConversationHeader({ id }: OwnProps): JSX.Element {
const selectedMessageIds = useSelector(getSelectedMessageIds);
const isSelectMode = selectedMessageIds != null;
const addedBy = useMemo(() => {
if (conversation.type === 'group') {
return getAddedByForOurPendingInvitation(conversation);
}
return null;
}, [conversation]);
const addedByName = useContactNameData(addedBy);
const conversationName = useContactNameData(conversation);
strictAssert(conversationName, 'conversationName is required');
const isDeleteSyncSendEnabled = useSelector(getDeleteSyncSendEnabled);
const isMissingMandatoryProfileSharing =
getIsMissingRequiredProfileSharing(conversation);
const onConversationAccept = useCallback(() => {
acceptConversation(conversation.id);
}, [acceptConversation, conversation.id]);
const onConversationArchive = useCallback(() => {
onArchive(conversation.id);
}, [onArchive, conversation.id]);
const onConversationBlock = useCallback(() => {
blockConversation(conversation.id);
}, [blockConversation, conversation.id]);
const onConversationBlockAndReportSpam = useCallback(() => {
blockAndReportSpam(conversation.id);
}, [blockAndReportSpam, conversation.id]);
const onConversationDelete = useCallback(() => {
deleteConversation(conversation.id);
}, [deleteConversation, conversation.id]);
const onConversationDeleteMessages = useCallback(() => {
destroyMessages(conversation.id);
}, [destroyMessages, conversation.id]);
const onConversationDisappearingMessagesChange = useCallback(
seconds => {
setDisappearingMessages(conversation.id, seconds);
},
[setDisappearingMessages, conversation.id]
);
const onConversationLeaveGroup = useCallback(() => {
leaveGroup(conversation.id);
}, [leaveGroup, conversation.id]);
const onConversationMarkUnread = useCallback(() => {
onMarkUnread(conversation.id);
}, [onMarkUnread, conversation.id]);
const onConversationMuteExpirationChange = useCallback(
seconds => {
setMuteExpiration(conversation.id, seconds);
},
[setMuteExpiration, conversation.id]
);
const onConversationPin = useCallback(() => {
setPinned(conversation.id, true);
}, [setPinned, conversation.id]);
const onConversationReportSpam = useCallback(() => {
reportSpam(conversation.id);
}, [reportSpam, conversation.id]);
const onConversationUnarchive = useCallback(() => {
onMoveToInbox(conversation.id);
}, [onMoveToInbox, conversation.id]);
const onConversationUnpin = useCallback(() => {
setPinned(conversation.id, false);
}, [setPinned, conversation.id]);
const onOutgoingAudioCall = useCallback(() => {
onOutgoingAudioCallInConversation(conversation.id);
}, [onOutgoingAudioCallInConversation, conversation.id]);
const onOutgoingVideoCall = useCallback(() => {
onOutgoingVideoCallInConversation(conversation.id);
}, [onOutgoingVideoCallInConversation, conversation.id]);
const onSearchInConversation = useCallback(() => {
searchInConversation(conversation.id);
}, [searchInConversation, conversation.id]);
const onSelectModeEnter = useCallback(() => {
toggleSelectMode(true);
}, [toggleSelectMode]);
const onShowMembers = useCallback(() => {
pushPanelForConversation({ type: PanelType.GroupV1Members });
}, [pushPanelForConversation]);
const onViewConversationDetails = useCallback(() => {
pushPanelForConversation({ type: PanelType.ConversationDetails });
}, [pushPanelForConversation]);
const onViewRecentMedia = useCallback(() => {
pushPanelForConversation({ type: PanelType.AllMedia });
}, [pushPanelForConversation]);
const onViewUserStories = useCallback(() => {
viewUserStories({
conversationId: conversation.id,
storyViewMode: StoryViewModeType.User,
});
}, [viewUserStories, conversation.id]);
const minimalConversation = useMinimalConversation(conversation);
const localDeleteWarningShown = useSelector(getLocalDeleteWarningShown);
const { putItem } = useItemsActions();
const setLocalDeleteWarningShown = () =>
putItem('localDeleteWarningShown', true);
return (
<ConversationHeader
{...pick(conversation, [
'acceptedMessageRequest',
'announcementsOnly',
'areWeAdmin',
'avatarPath',
'canChangeTimer',
'color',
'expireTimer',
'groupVersion',
'isArchived',
'isMe',
'isPinned',
'isVerified',
'left',
'markedUnread',
'muteExpiresAt',
'name',
'phoneNumber',
'profileName',
'sharedGroupNames',
'title',
'type',
'unblurredAvatarPath',
])}
addedByName={addedByName}
badge={badge}
cannotLeaveBecauseYouAreLastAdmin={cannotLeaveBecauseYouAreLastAdmin}
destroyMessages={destroyMessages}
conversation={minimalConversation}
conversationName={conversationName}
hasPanelShowing={hasPanelShowing}
hasStories={hasStories}
i18n={i18n}
id={id}
isMissingMandatoryProfileSharing={isMissingRequiredProfileSharing(
conversation
)}
localDeleteWarningShown={localDeleteWarningShown}
isDeleteSyncSendEnabled={isDeleteSyncSendEnabled}
isMissingMandatoryProfileSharing={isMissingMandatoryProfileSharing}
isSelectMode={isSelectMode}
isSignalConversation={isSignalConversation(conversation)}
isSMSOnly={isConversationSMSOnly(conversation)}
leaveGroup={leaveGroup}
onArchive={onArchive}
onMarkUnread={onMarkUnread}
onMoveToInbox={onMoveToInbox}
onOutgoingAudioCallInConversation={onOutgoingAudioCallInConversation}
onOutgoingVideoCallInConversation={onOutgoingVideoCallInConversation}
onConversationAccept={onConversationAccept}
onConversationArchive={onConversationArchive}
onConversationBlock={onConversationBlock}
onConversationBlockAndReportSpam={onConversationBlockAndReportSpam}
onConversationDelete={onConversationDelete}
onConversationDeleteMessages={onConversationDeleteMessages}
onConversationDisappearingMessagesChange={
onConversationDisappearingMessagesChange
}
onConversationLeaveGroup={onConversationLeaveGroup}
onConversationMarkUnread={onConversationMarkUnread}
onConversationMuteExpirationChange={onConversationMuteExpirationChange}
onConversationPin={onConversationPin}
onConversationReportSpam={onConversationReportSpam}
onConversationUnarchive={onConversationUnarchive}
onConversationUnpin={onConversationUnpin}
onOutgoingAudioCall={onOutgoingAudioCall}
onOutgoingVideoCall={onOutgoingVideoCall}
onSearchInConversation={onSearchInConversation}
onSelectModeEnter={onSelectModeEnter}
onShowMembers={onShowMembers}
onViewConversationDetails={onViewConversationDetails}
onViewRecentMedia={onViewRecentMedia}
onViewUserStories={onViewUserStories}
outgoingCallButtonStyle={outgoingCallButtonStyle}
popPanelForConversation={popPanelForConversation}
pushPanelForConversation={pushPanelForConversation}
searchInConversation={searchInConversation}
setDisappearingMessages={setDisappearingMessages}
setMuteExpiration={setMuteExpiration}
setPinned={setPinned}
setLocalDeleteWarningShown={setLocalDeleteWarningShown}
sharedGroupNames={conversation.sharedGroupNames}
theme={theme}
isSelectMode={isSelectMode}
toggleSelectMode={toggleSelectMode}
viewUserStories={viewUserStories}
/>
);
}
});

View file

@ -1,38 +1,43 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { connect } from 'react-redux';
import { useSelector } from 'react-redux';
import React, { memo } from 'react';
import { ConversationNotificationsSettings } from '../../components/conversation/conversation-details/ConversationNotificationsSettings';
import type { StateType } from '../reducer';
import { getIntl } from '../selectors/user';
import { getConversationByIdSelector } from '../selectors/conversations';
import { strictAssert } from '../../util/assert';
import { mapDispatchToProps } from '../actions';
import { useConversationsActions } from '../ducks/conversations';
export type OwnProps = {
export type SmartConversationNotificationsSettingsProps = {
conversationId: string;
};
const mapStateToProps = (state: StateType, props: OwnProps) => {
const { conversationId } = props;
const conversationSelector = getConversationByIdSelector(state);
const conversation = conversationSelector(conversationId);
strictAssert(conversation, 'Expected a conversation to be found');
return {
id: conversationId,
conversationType: conversation.type,
dontNotifyForMentionsIfMuted: Boolean(
conversation.dontNotifyForMentionsIfMuted
),
i18n: getIntl(state),
muteExpiresAt: conversation.muteExpiresAt,
};
};
const smart = connect(mapStateToProps, mapDispatchToProps);
export const SmartConversationNotificationsSettings = smart(
ConversationNotificationsSettings
export const SmartConversationNotificationsSettings = memo(
function SmartConversationNotificationsSettings({
conversationId,
}: SmartConversationNotificationsSettingsProps) {
const i18n = useSelector(getIntl);
const conversationSelector = useSelector(getConversationByIdSelector);
const { setMuteExpiration, setDontNotifyForMentionsIfMuted } =
useConversationsActions();
const conversation = conversationSelector(conversationId);
strictAssert(conversation, 'Expected a conversation to be found');
const {
type: conversationType,
dontNotifyForMentionsIfMuted,
muteExpiresAt,
} = conversation;
return (
<ConversationNotificationsSettings
id={conversationId}
conversationType={conversationType}
dontNotifyForMentionsIfMuted={dontNotifyForMentionsIfMuted ?? false}
i18n={i18n}
muteExpiresAt={muteExpiresAt}
setMuteExpiration={setMuteExpiration}
setDontNotifyForMentionsIfMuted={setDontNotifyForMentionsIfMuted}
/>
);
}
);

View file

@ -4,6 +4,7 @@
import type { MutableRefObject } from 'react';
import React, {
forwardRef,
memo,
useCallback,
useEffect,
useRef,
@ -91,11 +92,11 @@ function doAnimate({
};
}
export function ConversationPanel({
export const ConversationPanel = memo(function ConversationPanel({
conversationId,
}: {
conversationId: string;
}): JSX.Element | null {
}) {
const panelInformation = useSelector(getPanelInformation);
const { panelAnimationDone, panelAnimationStarted } =
useConversationsActions();
@ -250,7 +251,7 @@ export function ConversationPanel({
}
return null;
}
});
type PanelPropsType = {
conversationId: string;

View file

@ -1,9 +1,8 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React from 'react';
import React, { memo, useCallback } from 'react';
import { useSelector } from 'react-redux';
import type { StateType } from '../reducer';
import { ConversationPanel } from './ConversationPanel';
import { ConversationView } from '../../components/conversation/ConversationView';
import { SmartCompositionArea } from './CompositionArea';
@ -17,52 +16,61 @@ import {
} from '../selectors/conversations';
import { useComposerActions } from '../ducks/composer';
import { useConversationsActions } from '../ducks/conversations';
import { isShowingAnyModal } from '../selectors/globalModals';
export function SmartConversationView(): JSX.Element {
const conversationId = useSelector(getSelectedConversationId);
if (!conversationId) {
throw new Error('SmartConversationView: No selected conversation');
}
const { toggleSelectMode } = useConversationsActions();
const selectedMessageIds = useSelector(getSelectedMessageIds);
const isSelectMode = selectedMessageIds != null;
const { processAttachments } = useComposerActions();
const hasOpenModal = useSelector((state: StateType) => {
return (
state.globalModals.forwardMessagesProps != null ||
state.globalModals.deleteMessagesProps != null ||
state.globalModals.hasConfirmationModal
);
});
const shouldHideConversationView = useSelector((state: StateType) => {
const activePanel = getActivePanel(state);
const isAnimating = getIsPanelAnimating(state);
return activePanel && !isAnimating;
});
return (
<ConversationView
conversationId={conversationId}
hasOpenModal={hasOpenModal}
isSelectMode={isSelectMode}
onExitSelectMode={() => {
toggleSelectMode(false);
}}
processAttachments={processAttachments}
renderCompositionArea={() => <SmartCompositionArea id={conversationId} />}
renderConversationHeader={() => (
<SmartConversationHeader id={conversationId} />
)}
renderTimeline={() => (
<SmartTimeline key={conversationId} id={conversationId} />
)}
renderPanel={() => <ConversationPanel conversationId={conversationId} />}
shouldHideConversationView={shouldHideConversationView}
/>
);
function renderCompositionArea(conversationId: string) {
return <SmartCompositionArea id={conversationId} />;
}
function renderConversationHeader(conversationId: string) {
return <SmartConversationHeader id={conversationId} />;
}
function renderTimeline(conversationId: string) {
return <SmartTimeline key={conversationId} id={conversationId} />;
}
function renderPanel(conversationId: string) {
return <ConversationPanel conversationId={conversationId} />;
}
export const SmartConversationView = memo(
function SmartConversationView(): JSX.Element {
const conversationId = useSelector(getSelectedConversationId);
if (!conversationId) {
throw new Error('SmartConversationView: No selected conversation');
}
const { toggleSelectMode } = useConversationsActions();
const selectedMessageIds = useSelector(getSelectedMessageIds);
const isSelectMode = selectedMessageIds != null;
const { processAttachments } = useComposerActions();
const hasOpenModal = useSelector(isShowingAnyModal);
const activePanel = useSelector(getActivePanel);
const isPanelAnimating = useSelector(getIsPanelAnimating);
const shouldHideConversationView = activePanel && !isPanelAnimating;
const onExitSelectMode = useCallback(() => {
toggleSelectMode(false);
}, [toggleSelectMode]);
return (
<ConversationView
conversationId={conversationId}
hasOpenModal={hasOpenModal}
hasOpenPanel={activePanel != null}
isSelectMode={isSelectMode}
onExitSelectMode={onExitSelectMode}
processAttachments={processAttachments}
renderCompositionArea={renderCompositionArea}
renderConversationHeader={renderConversationHeader}
renderTimeline={renderTimeline}
renderPanel={renderPanel}
shouldHideConversationView={shouldHideConversationView}
/>
);
}
);

View file

@ -1,19 +1,23 @@
// Copyright 2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { connect } from 'react-redux';
import { mapDispatchToProps } from '../actions';
import { useSelector } from 'react-redux';
import React, { memo } from 'react';
import { CrashReportDialog } from '../../components/CrashReportDialog';
import type { StateType } from '../reducer';
import { getIntl } from '../selectors/user';
import { useCrashReportsActions } from '../ducks/crashReports';
import { getCrashReportsIsPending } from '../selectors/crashReports';
const mapStateToProps = (state: StateType) => {
return {
...state.crashReports,
i18n: getIntl(state),
};
};
const smart = connect(mapStateToProps, mapDispatchToProps);
export const SmartCrashReportDialog = smart(CrashReportDialog);
export const SmartCrashReportDialog = memo(function SmartCrashReportDialog() {
const i18n = useSelector(getIntl);
const isPending = useSelector(getCrashReportsIsPending);
const { writeCrashReportsToLog, eraseCrashReports } =
useCrashReportsActions();
return (
<CrashReportDialog
i18n={i18n}
isPending={isPending}
writeCrashReportsToLog={writeCrashReportsToLog}
eraseCrashReports={eraseCrashReports}
/>
);
});

View file

@ -1,51 +1,66 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import * as React from 'react';
import React, { memo } from 'react';
import { useSelector } from 'react-redux';
import type { StateType } from '../reducer';
import type { LocalizerType } from '../../types/Util';
import { useActions as usePreferredReactionsActions } from '../ducks/preferredReactions';
import { usePreferredReactionsActions } from '../ducks/preferredReactions';
import { useItemsActions } from '../ducks/items';
import { getIntl } from '../selectors/user';
import { getEmojiSkinTone } from '../selectors/items';
import { useRecentEmojis } from '../selectors/emojis';
import { getCustomizeModalState } from '../selectors/preferredReactions';
import { CustomizingPreferredReactionsModal } from '../../components/CustomizingPreferredReactionsModal';
import { strictAssert } from '../../util/assert';
export function SmartCustomizingPreferredReactionsModal(): JSX.Element {
const preferredReactionsActions = usePreferredReactionsActions();
const { onSetSkinTone } = useItemsActions();
export const SmartCustomizingPreferredReactionsModal = memo(
function SmartCustomizingPreferredReactionsModal(): JSX.Element {
const i18n = useSelector(getIntl);
const customizeModalState = useSelector(getCustomizeModalState);
const skinTone = useSelector(getEmojiSkinTone);
const recentEmojis = useRecentEmojis();
const i18n = useSelector<StateType, LocalizerType>(getIntl);
const {
cancelCustomizePreferredReactionsModal,
deselectDraftEmoji,
replaceSelectedDraftEmoji,
resetDraftEmoji,
savePreferredReactions,
selectDraftEmojiToBeReplaced,
} = usePreferredReactionsActions();
const { onSetSkinTone } = useItemsActions();
const customizeModalState = useSelector<
StateType,
ReturnType<typeof getCustomizeModalState>
>(state => getCustomizeModalState(state));
const recentEmojis = useRecentEmojis();
const skinTone = useSelector<StateType, number>(state =>
getEmojiSkinTone(state)
);
if (!customizeModalState) {
throw new Error(
strictAssert(
customizeModalState != null,
'<SmartCustomizingPreferredReactionsModal> requires a modal'
);
}
return (
<CustomizingPreferredReactionsModal
i18n={i18n}
onSetSkinTone={onSetSkinTone}
recentEmojis={recentEmojis}
skinTone={skinTone}
{...preferredReactionsActions}
{...customizeModalState}
/>
);
}
const {
hadSaveError,
isSaving,
draftPreferredReactions,
originalPreferredReactions,
selectedDraftEmojiIndex,
} = customizeModalState;
return (
<CustomizingPreferredReactionsModal
cancelCustomizePreferredReactionsModal={
cancelCustomizePreferredReactionsModal
}
deselectDraftEmoji={deselectDraftEmoji}
draftPreferredReactions={draftPreferredReactions}
hadSaveError={hadSaveError}
i18n={i18n}
isSaving={isSaving}
onSetSkinTone={onSetSkinTone}
originalPreferredReactions={originalPreferredReactions}
recentEmojis={recentEmojis}
replaceSelectedDraftEmoji={replaceSelectedDraftEmoji}
resetDraftEmoji={resetDraftEmoji}
savePreferredReactions={savePreferredReactions}
selectDraftEmojiToBeReplaced={selectDraftEmojiToBeReplaced}
selectedDraftEmojiIndex={selectedDraftEmojiIndex}
skinTone={skinTone}
/>
);
}
);

View file

@ -1,9 +1,8 @@
// Copyright 2023 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React from 'react';
import React, { memo, useCallback } from 'react';
import { useSelector } from 'react-redux';
import type { DeleteMessagesPropsType } from '../ducks/globalModals';
import type { StateType } from '../reducer';
import { getIntl } from '../selectors/user';
import { useGlobalModalActions } from '../ducks/globalModals';
@ -12,56 +11,94 @@ import { strictAssert } from '../../util/assert';
import { canDeleteMessagesForEveryone } from '../selectors/message';
import { useConversationsActions } from '../ducks/conversations';
import { useToastActions } from '../ducks/toast';
import { getConversationSelector } from '../selectors/conversations';
import {
getConversationSelector,
getLastSelectedMessage,
} from '../selectors/conversations';
import { getDeleteMessagesProps } from '../selectors/globalModals';
import { useItemsActions } from '../ducks/items';
import { getLocalDeleteWarningShown } from '../selectors/items';
import { getDeleteSyncSendEnabled } from '../selectors/items-extra';
import { LocalDeleteWarningModal } from '../../components/LocalDeleteWarningModal';
export function SmartDeleteMessagesModal(): JSX.Element | null {
const deleteMessagesProps = useSelector<
StateType,
DeleteMessagesPropsType | undefined
>(state => state.globalModals.deleteMessagesProps);
strictAssert(
deleteMessagesProps != null,
'Cannot render delete messages modal without messages'
);
const { conversationId, messageIds, onDelete } = deleteMessagesProps;
const isMe = useSelector((state: StateType) => {
return getConversationSelector(state)(conversationId).isMe;
});
export const SmartDeleteMessagesModal = memo(
function SmartDeleteMessagesModal() {
const deleteMessagesProps = useSelector(getDeleteMessagesProps);
strictAssert(
deleteMessagesProps != null,
'Cannot render delete messages modal without messages'
);
const { conversationId, messageIds, onDelete } = deleteMessagesProps;
const conversationSelector = useSelector(getConversationSelector);
const conversation = conversationSelector(conversationId);
const { isMe } = conversation;
const canDeleteForEveryone = useSelector((state: StateType) => {
return canDeleteMessagesForEveryone(state, { messageIds, isMe });
});
const lastSelectedMessage = useSelector((state: StateType) => {
return state.conversations.lastSelectedMessage;
});
const i18n = useSelector(getIntl);
const { toggleDeleteMessagesModal } = useGlobalModalActions();
const { deleteMessages, deleteMessagesForEveryone } =
useConversationsActions();
const { showToast } = useToastActions();
const getCanDeleteForEveryone = useCallback(
(state: StateType) => {
return canDeleteMessagesForEveryone(state, { messageIds, isMe });
},
[messageIds, isMe]
);
const canDeleteForEveryone = useSelector(getCanDeleteForEveryone);
const isDeleteSyncSendEnabled = useSelector(getDeleteSyncSendEnabled);
const lastSelectedMessage = useSelector(getLastSelectedMessage);
const i18n = useSelector(getIntl);
const { toggleDeleteMessagesModal } = useGlobalModalActions();
const { deleteMessages, deleteMessagesForEveryone } =
useConversationsActions();
const { showToast } = useToastActions();
return (
<DeleteMessagesModal
isMe={isMe}
canDeleteForEveryone={canDeleteForEveryone}
i18n={i18n}
messageCount={deleteMessagesProps.messageIds.length}
onClose={() => {
toggleDeleteMessagesModal(undefined);
}}
onDeleteForMe={() => {
deleteMessages({
conversationId,
messageIds,
lastSelectedMessage,
});
onDelete?.();
}}
onDeleteForEveryone={() => {
deleteMessagesForEveryone(messageIds);
onDelete?.();
}}
showToast={showToast}
/>
);
}
const messageCount = deleteMessagesProps.messageIds.length;
const handleClose = useCallback(() => {
toggleDeleteMessagesModal(undefined);
}, [toggleDeleteMessagesModal]);
const handleDeleteForMe = useCallback(() => {
deleteMessages({
conversationId,
messageIds,
lastSelectedMessage,
});
onDelete?.();
}, [
conversationId,
deleteMessages,
lastSelectedMessage,
messageIds,
onDelete,
]);
const handleDeleteForEveryone = useCallback(() => {
deleteMessagesForEveryone(messageIds);
onDelete?.();
}, [deleteMessagesForEveryone, messageIds, onDelete]);
const localDeleteWarningShown = useSelector(getLocalDeleteWarningShown);
const { putItem } = useItemsActions();
if (!localDeleteWarningShown && isDeleteSyncSendEnabled) {
return (
<LocalDeleteWarningModal
i18n={i18n}
onClose={() => {
putItem('localDeleteWarningShown', true);
}}
/>
);
}
return (
<DeleteMessagesModal
isMe={isMe}
canDeleteForEveryone={canDeleteForEveryone}
i18n={i18n}
isDeleteSyncSendEnabled={isDeleteSyncSendEnabled}
messageCount={messageCount}
onClose={handleClose}
onDeleteForMe={handleDeleteForMe}
onDeleteForEveryone={handleDeleteForEveryone}
showToast={showToast}
/>
);
}
);

View file

@ -1,11 +1,9 @@
// Copyright 2023 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React, { useMemo } from 'react';
import React, { memo, useMemo } from 'react';
import { useSelector } from 'react-redux';
import type { GlobalModalsStateType } from '../ducks/globalModals';
import type { MessageAttributesType } from '../../model-types.d';
import type { StateType } from '../reducer';
import type { ReadonlyMessageAttributesType } from '../../model-types.d';
import { EditHistoryMessagesModal } from '../../components/EditHistoryMessagesModal';
import { getIntl, getPlatform } from '../selectors/user';
import { getMessagePropsSelector } from '../selectors/message';
@ -14,49 +12,48 @@ import { useConversationsActions } from '../ducks/conversations';
import { useGlobalModalActions } from '../ducks/globalModals';
import { useLightboxActions } from '../ducks/lightbox';
import { strictAssert } from '../../util/assert';
import { getEditHistoryMessages } from '../selectors/globalModals';
export function SmartEditHistoryMessagesModal(): JSX.Element {
const i18n = useSelector(getIntl);
const platform = useSelector(getPlatform);
export const SmartEditHistoryMessagesModal = memo(
function SmartEditHistoryMessagesModal(): JSX.Element {
const i18n = useSelector(getIntl);
const platform = useSelector(getPlatform);
const { closeEditHistoryModal } = useGlobalModalActions();
const { closeEditHistoryModal } = useGlobalModalActions();
const { kickOffAttachmentDownload } = useConversationsActions();
const { showLightbox } = useLightboxActions();
const { kickOffAttachmentDownload } = useConversationsActions();
const { showLightbox } = useLightboxActions();
const getPreferredBadge = useSelector(getPreferredBadgeSelector);
const messagesAttributes = useSelector(getEditHistoryMessages);
const messagePropsSelector = useSelector(getMessagePropsSelector);
const getPreferredBadge = useSelector(getPreferredBadgeSelector);
strictAssert(messagesAttributes, 'messages not provided');
const { editHistoryMessages: messagesAttributes } = useSelector<
StateType,
GlobalModalsStateType
>(state => state.globalModals);
const editHistoryMessages = useMemo(() => {
return messagesAttributes.map(messageAttributes => ({
...messagePropsSelector(
messageAttributes as ReadonlyMessageAttributesType
),
// Make sure the messages don't get an "edited" badge
isEditedMessage: false,
// Do not show the same reactions in the message history UI
reactions: undefined,
// Make sure that the timestamp is the correct timestamp from attributes
// not the one that the selector derives.
timestamp: messageAttributes.timestamp,
}));
}, [messagesAttributes, messagePropsSelector]);
const messagePropsSelector = useSelector(getMessagePropsSelector);
strictAssert(messagesAttributes, 'messages not provided');
const editHistoryMessages = useMemo(() => {
return messagesAttributes.map(messageAttributes => ({
...messagePropsSelector(messageAttributes as MessageAttributesType),
// Make sure the messages don't get an "edited" badge
isEditedMessage: false,
// Do not show the same reactions in the message history UI
reactions: undefined,
// Make sure that the timestamp is the correct timestamp from attributes
// not the one that the selector derives.
timestamp: messageAttributes.timestamp,
}));
}, [messagesAttributes, messagePropsSelector]);
return (
<EditHistoryMessagesModal
closeEditHistoryModal={closeEditHistoryModal}
editHistoryMessages={editHistoryMessages}
getPreferredBadge={getPreferredBadge}
i18n={i18n}
platform={platform}
kickOffAttachmentDownload={kickOffAttachmentDownload}
showLightbox={showLightbox}
/>
);
}
return (
<EditHistoryMessagesModal
closeEditHistoryModal={closeEditHistoryModal}
editHistoryMessages={editHistoryMessages}
getPreferredBadge={getPreferredBadge}
i18n={i18n}
platform={platform}
kickOffAttachmentDownload={kickOffAttachmentDownload}
showLightbox={showLightbox}
/>
);
}
);

View file

@ -0,0 +1,56 @@
// Copyright 2024 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React, { memo, useCallback } from 'react';
import { useSelector } from 'react-redux';
import { getConversationSelector } from '../selectors/conversations';
import { getEditNicknameAndNoteModalProps } from '../selectors/globalModals';
import { strictAssert } from '../../util/assert';
import { EditNicknameAndNoteModal } from '../../components/EditNicknameAndNoteModal';
import { getIntl } from '../selectors/user';
import { useGlobalModalActions } from '../ducks/globalModals';
import type { NicknameAndNote } from '../ducks/conversations';
import { useConversationsActions } from '../ducks/conversations';
export const SmartEditNicknameAndNoteModal = memo(
function SmartEditNicknameAndNoteModal(): JSX.Element {
const props = useSelector(getEditNicknameAndNoteModalProps);
strictAssert(props != null, 'EditNicknameAndNoteModal requires props');
const { conversationId } = props;
const i18n = useSelector(getIntl);
const conversationSelector = useSelector(getConversationSelector);
const conversation = conversationSelector(conversationId);
strictAssert(
conversation != null,
'EditNicknameAndNoteModal requires conversation'
);
const { toggleEditNicknameAndNoteModal, toggleNotePreviewModal } =
useGlobalModalActions();
const { updateNicknameAndNote } = useConversationsActions();
const handleSave = useCallback(
(nicknameAndNote: NicknameAndNote) => {
// Ensure we don't re-open the note preview modal if there's no note.
if (nicknameAndNote.note == null) {
toggleNotePreviewModal(null);
}
updateNicknameAndNote(conversationId, nicknameAndNote);
},
[conversationId, updateNicknameAndNote, toggleNotePreviewModal]
);
const handleClose = useCallback(() => {
toggleEditNicknameAndNoteModal(null);
}, [toggleEditNicknameAndNoteModal]);
return (
<EditNicknameAndNoteModal
i18n={i18n}
conversation={conversation}
onSave={handleSave}
onClose={handleClose}
/>
);
}
);

View file

@ -1,14 +1,9 @@
// Copyright 2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { connect } from 'react-redux';
import { mapDispatchToProps } from '../actions';
import type { PropsDataType } from '../../components/EditUsernameModalBody';
import React, { memo } from 'react';
import { useSelector } from 'react-redux';
import { EditUsernameModalBody } from '../../components/EditUsernameModalBody';
import { getMinNickname, getMaxNickname } from '../../util/Username';
import type { StateType } from '../reducer';
import { getIntl } from '../selectors/user';
import {
getUsernameReservationState,
@ -18,25 +13,55 @@ import {
} from '../selectors/username';
import { getUsernameCorrupted } from '../selectors/items';
import { getMe } from '../selectors/conversations';
import { useUsernameActions } from '../ducks/username';
import { useToastActions } from '../ducks/toast';
function mapStateToProps(state: StateType): PropsDataType {
const i18n = getIntl(state);
const { username } = getMe(state);
const usernameCorrupted = getUsernameCorrupted(state);
export type SmartEditUsernameModalBodyProps = Readonly<{
isRootModal: boolean;
onClose(): void;
}>;
return {
i18n,
usernameCorrupted,
currentUsername: usernameCorrupted ? undefined : username,
minNickname: getMinNickname(),
maxNickname: getMaxNickname(),
state: getUsernameReservationState(state),
recoveredUsername: getRecoveredUsername(state),
reservation: getUsernameReservationObject(state),
error: getUsernameReservationError(state),
};
}
const smart = connect(mapStateToProps, mapDispatchToProps);
export const SmartEditUsernameModalBody = smart(EditUsernameModalBody);
export const SmartEditUsernameModalBody = memo(
function SmartEditUsernameModalBody({
isRootModal,
onClose,
}: SmartEditUsernameModalBodyProps) {
const i18n = useSelector(getIntl);
const { username } = useSelector(getMe);
const usernameCorrupted = useSelector(getUsernameCorrupted);
const currentUsername = usernameCorrupted ? undefined : username;
const minNickname = getMinNickname();
const maxNickname = getMaxNickname();
const state = useSelector(getUsernameReservationState);
const recoveredUsername = useSelector(getRecoveredUsername);
const reservation = useSelector(getUsernameReservationObject);
const error = useSelector(getUsernameReservationError);
const {
setUsernameReservationError,
clearUsernameReservation,
reserveUsername,
confirmUsername,
} = useUsernameActions();
const { showToast } = useToastActions();
return (
<EditUsernameModalBody
i18n={i18n}
usernameCorrupted={usernameCorrupted}
currentUsername={currentUsername}
minNickname={minNickname}
maxNickname={maxNickname}
state={state}
recoveredUsername={recoveredUsername}
reservation={reservation}
error={error}
setUsernameReservationError={setUsernameReservationError}
clearUsernameReservation={clearUsernameReservation}
reserveUsername={reserveUsername}
confirmUsername={confirmUsername}
showToast={showToast}
isRootModal={isRootModal}
onClose={onClose}
/>
);
}
);

View file

@ -1,56 +1,53 @@
// Copyright 2020 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import * as React from 'react';
import React, { useCallback, forwardRef, memo } from 'react';
import { useSelector } from 'react-redux';
import type { StateType } from '../reducer';
import { useRecentEmojis } from '../selectors/emojis';
import { useActions as useEmojiActions } from '../ducks/emojis';
import { useEmojisActions as useEmojiActions } from '../ducks/emojis';
import type { Props as EmojiPickerProps } from '../../components/emoji/EmojiPicker';
import { EmojiPicker } from '../../components/emoji/EmojiPicker';
import { getIntl } from '../selectors/user';
import { getEmojiSkinTone } from '../selectors/items';
import type { LocalizerType } from '../../types/Util';
export const SmartEmojiPicker = React.forwardRef<
HTMLDivElement,
Pick<
EmojiPickerProps,
'onClickSettings' | 'onPickEmoji' | 'onSetSkinTone' | 'onClose' | 'style'
>
>(function SmartEmojiPickerInner(
{ onClickSettings, onPickEmoji, onSetSkinTone, onClose, style },
ref
) {
const i18n = useSelector<StateType, LocalizerType>(getIntl);
const skinTone = useSelector<StateType, number>(state =>
getEmojiSkinTone(state)
);
export const SmartEmojiPicker = memo(
forwardRef<
HTMLDivElement,
Pick<
EmojiPickerProps,
'onClickSettings' | 'onPickEmoji' | 'onSetSkinTone' | 'onClose' | 'style'
>
>(function SmartEmojiPickerInner(
{ onClickSettings, onPickEmoji, onSetSkinTone, onClose, style },
ref
) {
const i18n = useSelector(getIntl);
const skinTone = useSelector(getEmojiSkinTone);
const recentEmojis = useRecentEmojis();
const recentEmojis = useRecentEmojis();
const { onUseEmoji } = useEmojiActions();
const { onUseEmoji } = useEmojiActions();
const handlePickEmoji = useCallback(
data => {
onUseEmoji({ shortName: data.shortName });
onPickEmoji(data);
},
[onUseEmoji, onPickEmoji]
);
const handlePickEmoji = React.useCallback(
data => {
onUseEmoji({ shortName: data.shortName });
onPickEmoji(data);
},
[onUseEmoji, onPickEmoji]
);
return (
<EmojiPicker
ref={ref}
i18n={i18n}
skinTone={skinTone}
onClickSettings={onClickSettings}
onSetSkinTone={onSetSkinTone}
onPickEmoji={handlePickEmoji}
recentEmojis={recentEmojis}
onClose={onClose}
style={style}
/>
);
});
return (
<EmojiPicker
i18n={i18n}
onClickSettings={onClickSettings}
onClose={onClose}
onSetSkinTone={onSetSkinTone}
onPickEmoji={handlePickEmoji}
recentEmojis={recentEmojis}
ref={ref}
skinTone={skinTone}
style={style}
wasInvokedFromKeyboard={false}
/>
);
})
);

View file

@ -1,24 +1,17 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React, { useState } from 'react';
import React, { useCallback, useState } from 'react';
import { useSelector } from 'react-redux';
import type {
ForwardMessagePropsType,
ForwardMessagesPropsType,
} from '../ducks/globalModals';
import type { StateType } from '../reducer';
import type { ForwardMessagesPropsType } from '../ducks/globalModals';
import * as log from '../../logging/log';
import { ForwardMessagesModal } from '../../components/ForwardMessagesModal';
import { LinkPreviewSourceType } from '../../types/LinkPreview';
import * as Errors from '../../types/errors';
import type { GetConversationByIdType } from '../selectors/conversations';
import {
getAllComposableConversations,
getConversationSelector,
} from '../selectors/conversations';
import { getAllComposableConversations } from '../selectors/conversations';
import { getIntl, getTheme, getRegionCode } from '../selectors/user';
import { getLinkPreview } from '../selectors/linkPreviews';
import { isInFullScreenCall as getIsInFullScreenCall } from '../selectors/calling';
import { getPreferredBadgeSelector } from '../selectors/badges';
import { maybeForwardMessages } from '../../util/maybeForwardMessages';
import {
@ -29,7 +22,6 @@ import { useGlobalModalActions } from '../ducks/globalModals';
import { useLinkPreviewActions } from '../ducks/linkPreviews';
import { SmartCompositionTextArea } from './CompositionTextArea';
import { useToastActions } from '../ducks/toast';
import { hydrateRanges } from '../../types/BodyRange';
import { isDownloaded } from '../../types/Attachment';
import { __DEPRECATED$getMessageById } from '../../messages/getMessageById';
import { strictAssert } from '../../util/assert';
@ -37,35 +29,18 @@ import type {
ForwardMessageData,
MessageForwardDraft,
} from '../../types/ForwardDraft';
function toMessageForwardDraft(
props: ForwardMessagePropsType,
getConversation: GetConversationByIdType
): MessageForwardDraft {
return {
attachments: props.attachments ?? [],
bodyRanges: hydrateRanges(props.bodyRanges, getConversation),
hasContact: Boolean(props.contact),
isSticker: Boolean(props.isSticker),
messageBody: props.text,
originalMessageId: props.id,
previews: props.previews ?? [],
};
}
import { getForwardMessagesProps } from '../selectors/globalModals';
export function SmartForwardMessagesModal(): JSX.Element | null {
const forwardMessagesProps = useSelector<
StateType,
ForwardMessagesPropsType | undefined
>(state => state.globalModals.forwardMessagesProps);
const forwardMessagesProps = useSelector(getForwardMessagesProps);
if (forwardMessagesProps == null) {
return null;
}
if (
!forwardMessagesProps.messages.every(message => {
return message.attachments?.every(isDownloaded) ?? true;
!forwardMessagesProps.messageDrafts.every(messageDraft => {
return messageDraft.attachments?.every(isDownloaded) ?? true;
})
) {
return null;
@ -83,13 +58,15 @@ function SmartForwardMessagesModalInner({
}: {
forwardMessagesProps: ForwardMessagesPropsType;
}): JSX.Element | null {
const { type } = forwardMessagesProps;
const candidateConversations = useSelector(getAllComposableConversations);
const getPreferredBadge = useSelector(getPreferredBadgeSelector);
const getConversation = useSelector(getConversationSelector);
const i18n = useSelector(getIntl);
const linkPreviewForSource = useSelector(getLinkPreview);
const regionCode = useSelector(getRegionCode);
const theme = useSelector(getTheme);
const isInFullScreenCall = useSelector(getIsInFullScreenCall);
const { removeLinkPreview } = useLinkPreviewActions();
const { toggleForwardMessagesModal } = useGlobalModalActions();
@ -97,76 +74,95 @@ function SmartForwardMessagesModalInner({
const [drafts, setDrafts] = useState<ReadonlyArray<MessageForwardDraft>>(
() => {
return forwardMessagesProps.messages.map((props): MessageForwardDraft => {
return toMessageForwardDraft(props, getConversation);
});
return forwardMessagesProps.messageDrafts;
}
);
const handleChange = useCallback(
(
updatedDrafts: ReadonlyArray<MessageForwardDraft>,
caretLocation?: number
) => {
setDrafts(updatedDrafts);
const isLonelyDraft = updatedDrafts.length === 1;
const lonelyDraft = isLonelyDraft ? updatedDrafts[0] : null;
if (lonelyDraft == null) {
return;
}
const attachmentsLength = lonelyDraft.attachments?.length ?? 0;
if (attachmentsLength === 0) {
maybeGrabLinkPreview(
lonelyDraft.messageBody ?? '',
LinkPreviewSourceType.ForwardMessageModal,
{ caretLocation }
);
}
},
[]
);
const closeModal = useCallback(() => {
resetLinkPreview();
toggleForwardMessagesModal(null);
}, [toggleForwardMessagesModal]);
const doForwardMessages = useCallback(
async (
conversationIds: ReadonlyArray<string>,
finalDrafts: ReadonlyArray<MessageForwardDraft>
) => {
try {
const messages = await Promise.all(
finalDrafts.map(async (draft): Promise<ForwardMessageData> => {
if (draft.originalMessageId == null) {
return { draft, originalMessage: null };
}
const message = await __DEPRECATED$getMessageById(
draft.originalMessageId
);
strictAssert(message, 'no message found');
return {
draft,
originalMessage: message.attributes,
};
})
);
const didForwardSuccessfully = await maybeForwardMessages(
messages,
conversationIds
);
if (didForwardSuccessfully) {
closeModal();
forwardMessagesProps?.onForward?.();
}
} catch (err) {
log.warn('doForwardMessage', Errors.toLogFormat(err));
}
},
[forwardMessagesProps, closeModal]
);
if (!drafts.length) {
return null;
}
function closeModal() {
resetLinkPreview();
toggleForwardMessagesModal();
}
return (
<ForwardMessagesModal
drafts={drafts}
candidateConversations={candidateConversations}
doForwardMessages={async (conversationIds, finalDrafts) => {
try {
const messages = await Promise.all(
finalDrafts.map(async (draft): Promise<ForwardMessageData> => {
const message = await __DEPRECATED$getMessageById(
draft.originalMessageId
);
strictAssert(message, 'no message found');
return {
draft,
originalMessage: message.attributes,
};
})
);
const didForwardSuccessfully = await maybeForwardMessages(
messages,
conversationIds
);
if (didForwardSuccessfully) {
closeModal();
forwardMessagesProps?.onForward?.();
}
} catch (err) {
log.warn('doForwardMessage', Errors.toLogFormat(err));
}
}}
doForwardMessages={doForwardMessages}
linkPreviewForSource={linkPreviewForSource}
getPreferredBadge={getPreferredBadge}
i18n={i18n}
isInFullScreenCall={isInFullScreenCall}
onClose={closeModal}
onChange={(updatedDrafts, caretLocation) => {
setDrafts(updatedDrafts);
const isLonelyDraft = updatedDrafts.length === 1;
const lonelyDraft = isLonelyDraft ? updatedDrafts[0] : null;
if (lonelyDraft == null) {
return;
}
const attachmentsLength = lonelyDraft.attachments?.length ?? 0;
if (attachmentsLength === 0) {
maybeGrabLinkPreview(
lonelyDraft.messageBody ?? '',
LinkPreviewSourceType.ForwardMessageModal,
{ caretLocation }
);
}
}}
onChange={handleChange}
regionCode={regionCode}
RenderCompositionTextArea={SmartCompositionTextArea}
removeLinkPreview={removeLinkPreview}
type={type}
showToast={showToast}
theme={theme}
/>

View file

@ -1,7 +1,7 @@
// Copyright 2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React from 'react';
import React, { memo } from 'react';
import { useSelector } from 'react-redux';
import { ConversationDetailsMembershipList } from '../../components/conversation/conversation-details/ConversationDetailsMembershipList';
@ -19,7 +19,9 @@ export type PropsType = {
conversationId: string;
};
export function SmartGV1Members({ conversationId }: PropsType): JSX.Element {
export const SmartGV1Members = memo(function SmartGV1Members({
conversationId,
}: PropsType): JSX.Element {
const getPreferredBadge = useSelector(getPreferredBadgeSelector);
const i18n = useSelector(getIntl);
const theme = useSelector(getTheme);
@ -53,4 +55,4 @@ export function SmartGV1Members({ conversationId }: PropsType): JSX.Element {
theme={theme}
/>
);
}
});

View file

@ -1,11 +1,8 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React, { useCallback } from 'react';
import React, { memo, useCallback } from 'react';
import { useSelector } from 'react-redux';
import type { GlobalModalsStateType } from '../ducks/globalModals';
import type { StateType } from '../reducer';
import type { ButtonVariant } from '../../components/Button';
import { ErrorModal } from '../../components/ErrorModal';
import { GlobalModalContainer } from '../../components/GlobalModalContainer';
@ -25,11 +22,34 @@ import { getConversationsStoppingSend } from '../selectors/conversations';
import { getIntl, getTheme } from '../selectors/user';
import { useGlobalModalActions } from '../ducks/globalModals';
import { SmartDeleteMessagesModal } from './DeleteMessagesModal';
import { SmartMessageRequestActionsConfirmation } from './MessageRequestActionsConfirmation';
import { getGlobalModalsState } from '../selectors/globalModals';
import { SmartEditNicknameAndNoteModal } from './EditNicknameAndNoteModal';
import { SmartNotePreviewModal } from './NotePreviewModal';
import { SmartCallLinkEditModal } from './CallLinkEditModal';
import { SmartCallLinkAddNameModal } from './CallLinkAddNameModal';
import { SmartConfirmLeaveCallModal } from './ConfirmLeaveCallModal';
function renderCallLinkAddNameModal(): JSX.Element {
return <SmartCallLinkAddNameModal />;
}
function renderCallLinkEditModal(): JSX.Element {
return <SmartCallLinkEditModal />;
}
function renderConfirmLeaveCallModal(): JSX.Element {
return <SmartConfirmLeaveCallModal />;
}
function renderEditHistoryMessagesModal(): JSX.Element {
return <SmartEditHistoryMessagesModal />;
}
function renderEditNicknameAndNoteModal(): JSX.Element {
return <SmartEditNicknameAndNoteModal />;
}
function renderProfileEditor(): JSX.Element {
return <SmartProfileEditorModal />;
}
@ -50,6 +70,14 @@ function renderForwardMessagesModal(): JSX.Element {
return <SmartForwardMessagesModal />;
}
function renderMessageRequestActionsConfirmation(): JSX.Element {
return <SmartMessageRequestActionsConfirmation />;
}
function renderNotePreviewModal(): JSX.Element {
return <SmartNotePreviewModal />;
}
function renderStoriesSettings(): JSX.Element {
return <SmartStoriesSettingsModal />;
}
@ -66,141 +94,151 @@ function renderAboutContactModal(): JSX.Element {
return <SmartAboutContactModal />;
}
export function SmartGlobalModalContainer(): JSX.Element {
const conversationsStoppingSend = useSelector(getConversationsStoppingSend);
const i18n = useSelector(getIntl);
const theme = useSelector(getTheme);
export const SmartGlobalModalContainer = memo(
function SmartGlobalModalContainer() {
const conversationsStoppingSend = useSelector(getConversationsStoppingSend);
const i18n = useSelector(getIntl);
const theme = useSelector(getTheme);
const hasSafetyNumberChangeModal = conversationsStoppingSend.length > 0;
const hasSafetyNumberChangeModal = conversationsStoppingSend.length > 0;
const {
aboutContactModalContactId,
addUserToAnotherGroupModalContactId,
authArtCreatorData,
contactModalState,
deleteMessagesProps,
editHistoryMessages,
errorModalProps,
formattingWarningData,
forwardMessagesProps,
isAuthorizingArtCreator,
isProfileEditorVisible,
isShortcutGuideModalVisible,
isSignalConnectionsVisible,
isStoriesSettingsVisible,
isWhatsNewVisible,
usernameOnboardingState,
safetyNumberChangedBlockingData,
safetyNumberModalContactId,
sendEditWarningData,
stickerPackPreviewId,
userNotFoundModalState,
} = useSelector<StateType, GlobalModalsStateType>(
state => state.globalModals
);
const {
aboutContactModalContactId,
addUserToAnotherGroupModalContactId,
callLinkAddNameModalRoomId,
callLinkEditModalRoomId,
confirmLeaveCallModalState,
contactModalState,
deleteMessagesProps,
editHistoryMessages,
editNicknameAndNoteModalProps,
errorModalProps,
forwardMessagesProps,
messageRequestActionsConfirmationProps,
notePreviewModalProps,
isProfileEditorVisible,
isShortcutGuideModalVisible,
isSignalConnectionsVisible,
isStoriesSettingsVisible,
isWhatsNewVisible,
usernameOnboardingState,
safetyNumberChangedBlockingData,
safetyNumberModalContactId,
stickerPackPreviewId,
userNotFoundModalState,
} = useSelector(getGlobalModalsState);
const {
cancelAuthorizeArtCreator,
closeErrorModal,
confirmAuthorizeArtCreator,
hideUserNotFoundModal,
hideWhatsNewModal,
showFormattingWarningModal,
showSendEditWarningModal,
toggleSignalConnectionsModal,
} = useGlobalModalActions();
const {
closeErrorModal,
hideUserNotFoundModal,
hideWhatsNewModal,
toggleSignalConnectionsModal,
} = useGlobalModalActions();
const renderAddUserToAnotherGroup = useCallback(() => {
return (
<SmartAddUserToAnotherGroupModal
contactID={String(addUserToAnotherGroupModalContactId)}
/>
);
}, [addUserToAnotherGroupModalContactId]);
const renderSafetyNumber = useCallback(
() => (
<SmartSafetyNumberModal
contactID={String(safetyNumberModalContactId)}
/>
),
[safetyNumberModalContactId]
);
const renderStickerPreviewModal = useCallback(
() =>
stickerPackPreviewId ? (
<SmartStickerPreviewModal packId={stickerPackPreviewId} />
) : null,
[stickerPackPreviewId]
);
const renderErrorModal = useCallback(
({
buttonVariant,
description,
title,
}: {
buttonVariant?: ButtonVariant;
description?: string;
title?: string;
}) => (
<ErrorModal
buttonVariant={buttonVariant}
description={description}
title={title}
i18n={i18n}
onClose={closeErrorModal}
/>
),
[closeErrorModal, i18n]
);
const renderAddUserToAnotherGroup = useCallback(() => {
return (
<SmartAddUserToAnotherGroupModal
contactID={String(addUserToAnotherGroupModalContactId)}
<GlobalModalContainer
addUserToAnotherGroupModalContactId={
addUserToAnotherGroupModalContactId
}
callLinkAddNameModalRoomId={callLinkAddNameModalRoomId}
callLinkEditModalRoomId={callLinkEditModalRoomId}
confirmLeaveCallModalState={confirmLeaveCallModalState}
contactModalState={contactModalState}
editHistoryMessages={editHistoryMessages}
editNicknameAndNoteModalProps={editNicknameAndNoteModalProps}
errorModalProps={errorModalProps}
deleteMessagesProps={deleteMessagesProps}
forwardMessagesProps={forwardMessagesProps}
messageRequestActionsConfirmationProps={
messageRequestActionsConfirmationProps
}
notePreviewModalProps={notePreviewModalProps}
hasSafetyNumberChangeModal={hasSafetyNumberChangeModal}
hideUserNotFoundModal={hideUserNotFoundModal}
hideWhatsNewModal={hideWhatsNewModal}
i18n={i18n}
isAboutContactModalVisible={aboutContactModalContactId != null}
isProfileEditorVisible={isProfileEditorVisible}
isShortcutGuideModalVisible={isShortcutGuideModalVisible}
isSignalConnectionsVisible={isSignalConnectionsVisible}
isStoriesSettingsVisible={isStoriesSettingsVisible}
isWhatsNewVisible={isWhatsNewVisible}
renderAboutContactModal={renderAboutContactModal}
renderAddUserToAnotherGroup={renderAddUserToAnotherGroup}
renderCallLinkAddNameModal={renderCallLinkAddNameModal}
renderCallLinkEditModal={renderCallLinkEditModal}
renderConfirmLeaveCallModal={renderConfirmLeaveCallModal}
renderContactModal={renderContactModal}
renderEditHistoryMessagesModal={renderEditHistoryMessagesModal}
renderEditNicknameAndNoteModal={renderEditNicknameAndNoteModal}
renderErrorModal={renderErrorModal}
renderDeleteMessagesModal={renderDeleteMessagesModal}
renderForwardMessagesModal={renderForwardMessagesModal}
renderMessageRequestActionsConfirmation={
renderMessageRequestActionsConfirmation
}
renderNotePreviewModal={renderNotePreviewModal}
renderProfileEditor={renderProfileEditor}
renderUsernameOnboarding={renderUsernameOnboarding}
renderSafetyNumber={renderSafetyNumber}
renderSendAnywayDialog={renderSendAnywayDialog}
renderShortcutGuideModal={renderShortcutGuideModal}
renderStickerPreviewModal={renderStickerPreviewModal}
renderStoriesSettings={renderStoriesSettings}
safetyNumberChangedBlockingData={safetyNumberChangedBlockingData}
safetyNumberModalContactId={safetyNumberModalContactId}
stickerPackPreviewId={stickerPackPreviewId}
theme={theme}
toggleSignalConnectionsModal={toggleSignalConnectionsModal}
userNotFoundModalState={userNotFoundModalState}
usernameOnboardingState={usernameOnboardingState}
/>
);
}, [addUserToAnotherGroupModalContactId]);
const renderSafetyNumber = useCallback(
() => (
<SmartSafetyNumberModal contactID={String(safetyNumberModalContactId)} />
),
[safetyNumberModalContactId]
);
const renderStickerPreviewModal = useCallback(
() =>
stickerPackPreviewId ? (
<SmartStickerPreviewModal packId={stickerPackPreviewId} />
) : null,
[stickerPackPreviewId]
);
const renderErrorModal = useCallback(
({
buttonVariant,
description,
title,
}: {
buttonVariant?: ButtonVariant;
description?: string;
title?: string;
}) => (
<ErrorModal
buttonVariant={buttonVariant}
description={description}
title={title}
i18n={i18n}
onClose={closeErrorModal}
/>
),
[closeErrorModal, i18n]
);
return (
<GlobalModalContainer
addUserToAnotherGroupModalContactId={addUserToAnotherGroupModalContactId}
contactModalState={contactModalState}
editHistoryMessages={editHistoryMessages}
errorModalProps={errorModalProps}
deleteMessagesProps={deleteMessagesProps}
formattingWarningData={formattingWarningData}
forwardMessagesProps={forwardMessagesProps}
hasSafetyNumberChangeModal={hasSafetyNumberChangeModal}
hideUserNotFoundModal={hideUserNotFoundModal}
hideWhatsNewModal={hideWhatsNewModal}
i18n={i18n}
isAboutContactModalVisible={aboutContactModalContactId != null}
isProfileEditorVisible={isProfileEditorVisible}
isShortcutGuideModalVisible={isShortcutGuideModalVisible}
isSignalConnectionsVisible={isSignalConnectionsVisible}
isStoriesSettingsVisible={isStoriesSettingsVisible}
isWhatsNewVisible={isWhatsNewVisible}
renderAboutContactModal={renderAboutContactModal}
renderAddUserToAnotherGroup={renderAddUserToAnotherGroup}
renderContactModal={renderContactModal}
renderEditHistoryMessagesModal={renderEditHistoryMessagesModal}
renderErrorModal={renderErrorModal}
renderDeleteMessagesModal={renderDeleteMessagesModal}
renderForwardMessagesModal={renderForwardMessagesModal}
renderProfileEditor={renderProfileEditor}
renderUsernameOnboarding={renderUsernameOnboarding}
renderSafetyNumber={renderSafetyNumber}
renderSendAnywayDialog={renderSendAnywayDialog}
renderShortcutGuideModal={renderShortcutGuideModal}
renderStickerPreviewModal={renderStickerPreviewModal}
renderStoriesSettings={renderStoriesSettings}
safetyNumberChangedBlockingData={safetyNumberChangedBlockingData}
safetyNumberModalContactId={safetyNumberModalContactId}
sendEditWarningData={sendEditWarningData}
showFormattingWarningModal={showFormattingWarningModal}
showSendEditWarningModal={showSendEditWarningModal}
stickerPackPreviewId={stickerPackPreviewId}
theme={theme}
toggleSignalConnectionsModal={toggleSignalConnectionsModal}
userNotFoundModalState={userNotFoundModalState}
usernameOnboardingState={usernameOnboardingState}
isAuthorizingArtCreator={isAuthorizingArtCreator}
authArtCreatorData={authArtCreatorData}
cancelAuthorizeArtCreator={cancelAuthorizeArtCreator}
confirmAuthorizeArtCreator={confirmAuthorizeArtCreator}
/>
);
}
}
);

View file

@ -1,34 +1,38 @@
// Copyright 2020 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { connect } from 'react-redux';
import type { PropsDataType } from '../../components/conversation/conversation-details/GroupLinkManagement';
import type { StateType } from '../reducer';
import React, { memo } from 'react';
import { useSelector } from 'react-redux';
import { GroupLinkManagement } from '../../components/conversation/conversation-details/GroupLinkManagement';
import { getConversationSelector } from '../selectors/conversations';
import { getIntl } from '../selectors/user';
import { mapDispatchToProps } from '../actions';
import { useConversationsActions } from '../ducks/conversations';
export type SmartGroupLinkManagementProps = {
export type SmartGroupLinkManagementProps = Readonly<{
conversationId: string;
};
}>;
const mapStateToProps = (
state: StateType,
props: SmartGroupLinkManagementProps
): PropsDataType => {
const conversation = getConversationSelector(state)(props.conversationId);
const isAdmin = Boolean(conversation?.areWeAdmin);
return {
...props,
conversation,
i18n: getIntl(state),
isAdmin,
};
};
const smart = connect(mapStateToProps, mapDispatchToProps);
export const SmartGroupLinkManagement = smart(GroupLinkManagement);
export const SmartGroupLinkManagement = memo(function SmartGroupLinkManagement({
conversationId,
}: SmartGroupLinkManagementProps) {
const i18n = useSelector(getIntl);
const conversationSelector = useSelector(getConversationSelector);
const conversation = conversationSelector(conversationId);
const isAdmin = conversation?.areWeAdmin ?? false;
const {
changeHasGroupLink,
generateNewGroupLink,
setAccessControlAddFromInviteLinkSetting,
} = useConversationsActions();
return (
<GroupLinkManagement
i18n={i18n}
changeHasGroupLink={changeHasGroupLink}
conversation={conversation}
generateNewGroupLink={generateNewGroupLink}
isAdmin={isAdmin}
setAccessControlAddFromInviteLinkSetting={
setAccessControlAddFromInviteLinkSetting
}
/>
);
});

View file

@ -1,19 +1,18 @@
// Copyright 2020 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { connect } from 'react-redux';
import { mapDispatchToProps } from '../actions';
import React, { memo, useCallback, useMemo } from 'react';
import { useSelector } from 'react-redux';
import type { DataPropsType as GroupV1MigrationDialogPropsType } from '../../components/GroupV1MigrationDialog';
import { GroupV1MigrationDialog } from '../../components/GroupV1MigrationDialog';
import type { ConversationType } from '../ducks/conversations';
import type { StateType } from '../reducer';
import { useConversationsActions } from '../ducks/conversations';
import { getPreferredBadgeSelector } from '../selectors/badges';
import { getConversationSelector } from '../selectors/conversations';
import { getIntl, getTheme } from '../selectors/user';
import * as log from '../../logging/log';
import { useGlobalModalActions } from '../ducks/globalModals';
export type PropsType = {
readonly conversationId: string;
readonly droppedMemberIds: Array<string>;
readonly invitedMemberIds: Array<string>;
} & Omit<
@ -21,37 +20,64 @@ export type PropsType = {
'i18n' | 'droppedMembers' | 'invitedMembers' | 'theme' | 'getPreferredBadge'
>;
const mapStateToProps = (
state: StateType,
props: PropsType
): GroupV1MigrationDialogPropsType => {
const getConversation = getConversationSelector(state);
const { droppedMemberIds, invitedMemberIds } = props;
function isNonNullable<T>(value: T | null | undefined): value is T {
return value != null;
}
const droppedMembers = droppedMemberIds
.map(getConversation)
.filter(Boolean) as Array<ConversationType>;
if (droppedMembers.length !== droppedMemberIds.length) {
log.warn('smart/GroupV1MigrationDialog: droppedMembers length changed');
export const SmartGroupV1MigrationDialog = memo(
function SmartGroupV1MigrationDialog({
conversationId,
areWeInvited,
hasMigrated,
droppedMemberIds,
invitedMemberIds,
}: PropsType) {
const i18n = useSelector(getIntl);
const theme = useSelector(getTheme);
const getConversation = useSelector(getConversationSelector);
const getPreferredBadge = useSelector(getPreferredBadgeSelector);
const { initiateMigrationToGroupV2 } = useConversationsActions();
const { closeGV2MigrationDialog } = useGlobalModalActions();
const droppedMembers = useMemo(() => {
const result = droppedMemberIds
.map(getConversation)
.filter(isNonNullable);
if (result.length !== droppedMemberIds.length) {
log.warn('smart/GroupV1MigrationDialog: droppedMembers length changed');
}
return result;
}, [droppedMemberIds, getConversation]);
const invitedMembers = useMemo(() => {
const result = invitedMemberIds
.map(getConversation)
.filter(isNonNullable);
if (result.length !== invitedMemberIds.length) {
log.warn('smart/GroupV1MigrationDialog: invitedMembers length changed');
}
return result;
}, [invitedMemberIds, getConversation]);
const handleMigrate = useCallback(() => {
initiateMigrationToGroupV2(conversationId);
}, [initiateMigrationToGroupV2, conversationId]);
return (
<GroupV1MigrationDialog
i18n={i18n}
theme={theme}
areWeInvited={areWeInvited}
hasMigrated={hasMigrated}
getPreferredBadge={getPreferredBadge}
droppedMembers={droppedMembers}
droppedMemberCount={droppedMembers.length}
invitedMembers={invitedMembers}
invitedMemberCount={invitedMembers.length}
onMigrate={handleMigrate}
onClose={closeGV2MigrationDialog}
/>
);
}
const invitedMembers = invitedMemberIds
.map(getConversation)
.filter(Boolean) as Array<ConversationType>;
if (invitedMembers.length !== invitedMemberIds.length) {
log.warn('smart/GroupV1MigrationDialog: invitedMembers length changed');
}
return {
...props,
droppedMembers,
getPreferredBadge: getPreferredBadgeSelector(state),
invitedMembers,
i18n: getIntl(state),
theme: getTheme(state),
};
};
const smart = connect(mapStateToProps, mapDispatchToProps);
export const SmartGroupV1MigrationDialog = smart(GroupV1MigrationDialog);
);

View file

@ -1,34 +1,38 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { connect } from 'react-redux';
import { mapDispatchToProps } from '../actions';
import React, { memo } from 'react';
import { useSelector } from 'react-redux';
import type { PropsType as GroupV2JoinDialogPropsType } from '../../components/GroupV2JoinDialog';
import { GroupV2JoinDialog } from '../../components/GroupV2JoinDialog';
import type { StateType } from '../reducer';
import { getIntl } from '../selectors/user';
import { getPreJoinConversation } from '../selectors/conversations';
export type PropsType = Pick<GroupV2JoinDialogPropsType, 'join' | 'onClose'>;
export type SmartGroupV2JoinDialogProps = Pick<
GroupV2JoinDialogPropsType,
'join' | 'onClose'
>;
const mapStateToProps = (
state: StateType,
props: PropsType
): GroupV2JoinDialogPropsType => {
const preJoinConversation = getPreJoinConversation(state);
if (!preJoinConversation) {
export const SmartGroupV2JoinDialog = memo(function SmartGroupV2JoinDialog({
join,
onClose,
}: SmartGroupV2JoinDialogProps) {
const i18n = useSelector(getIntl);
const preJoinConversation = useSelector(getPreJoinConversation);
if (preJoinConversation == null) {
throw new Error('smart/GroupV2JoinDialog: No pre-join conversation!');
}
return {
...props,
...preJoinConversation,
i18n: getIntl(state),
};
};
const smart = connect(mapStateToProps, mapDispatchToProps);
export const SmartGroupV2JoinDialog = smart(GroupV2JoinDialog);
const { memberCount, title, groupDescription, approvalRequired, avatar } =
preJoinConversation;
return (
<GroupV2JoinDialog
approvalRequired={approvalRequired}
avatar={avatar}
groupDescription={groupDescription}
i18n={i18n}
join={join}
memberCount={memberCount}
onClose={onClose}
title={title}
/>
);
});

View file

@ -1,32 +1,35 @@
// Copyright 2020 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { connect } from 'react-redux';
import type { StateType } from '../reducer';
import type { PropsDataType } from '../../components/conversation/conversation-details/GroupV2Permissions';
import { mapDispatchToProps } from '../actions';
import { useSelector } from 'react-redux';
import React, { memo } from 'react';
import { GroupV2Permissions } from '../../components/conversation/conversation-details/GroupV2Permissions';
import { getConversationSelector } from '../selectors/conversations';
import { getIntl } from '../selectors/user';
import { useConversationsActions } from '../ducks/conversations';
export type SmartGroupV2PermissionsProps = {
conversationId: string;
};
const mapStateToProps = (
state: StateType,
props: SmartGroupV2PermissionsProps
): PropsDataType => {
const conversation = getConversationSelector(state)(props.conversationId);
return {
...props,
conversation,
i18n: getIntl(state),
};
};
const smart = connect(mapStateToProps, mapDispatchToProps);
export const SmartGroupV2Permissions = smart(GroupV2Permissions);
export const SmartGroupV2Permissions = memo(function SmartGroupV2Permissions({
conversationId,
}: SmartGroupV2PermissionsProps) {
const i18n = useSelector(getIntl);
const conversationSelector = useSelector(getConversationSelector);
const conversation = conversationSelector(conversationId);
const {
setAccessControlAttributesSetting,
setAccessControlMembersSetting,
setAnnouncementsOnly,
} = useConversationsActions();
return (
<GroupV2Permissions
i18n={i18n}
conversation={conversation}
setAccessControlAttributesSetting={setAccessControlAttributesSetting}
setAccessControlMembersSetting={setAccessControlMembersSetting}
setAnnouncementsOnly={setAnnouncementsOnly}
/>
);
});

View file

@ -1,41 +1,77 @@
// Copyright 2020 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { connect } from 'react-redux';
import { mapDispatchToProps } from '../actions';
import React, { memo } from 'react';
import { useSelector } from 'react-redux';
import { ConversationHero } from '../../components/conversation/ConversationHero';
import type { StateType } from '../reducer';
import { getPreferredBadgeSelector } from '../selectors/badges';
import { getIntl, getTheme } from '../selectors/user';
import { getHasStoriesSelector } from '../selectors/stories2';
import { isSignalConversation } from '../../util/isSignalConversation';
import { getConversationSelector } from '../selectors/conversations';
import { useConversationsActions } from '../ducks/conversations';
import { useGlobalModalActions } from '../ducks/globalModals';
import { useStoriesActions } from '../ducks/stories';
type ExternalProps = {
type SmartHeroRowProps = Readonly<{
id: string;
};
}>;
const mapStateToProps = (state: StateType, props: ExternalProps) => {
const { id } = props;
const conversation = state.conversations.conversationLookup[id];
if (!conversation) {
export const SmartHeroRow = memo(function SmartHeroRow({
id,
}: SmartHeroRowProps) {
const i18n = useSelector(getIntl);
const theme = useSelector(getTheme);
const getPreferredBadge = useSelector(getPreferredBadgeSelector);
const hasStoriesSelector = useSelector(getHasStoriesSelector);
const conversationSelector = useSelector(getConversationSelector);
const conversation = conversationSelector(id);
if (conversation == null) {
throw new Error(`Did not find conversation ${id} in state!`);
}
return {
i18n: getIntl(state),
...conversation,
conversationType: conversation.type,
hasStories: getHasStoriesSelector(state)(id),
badge: getPreferredBadgeSelector(state)(conversation.badges),
isSignalConversation: isSignalConversation(conversation),
theme: getTheme(state),
};
};
const smart = connect(mapStateToProps, mapDispatchToProps);
export const SmartHeroRow = smart(ConversationHero);
const badge = getPreferredBadge(conversation.badges);
const hasStories = hasStoriesSelector(id);
const isSignalConversationValue = isSignalConversation(conversation);
const { unblurAvatar, updateSharedGroups } = useConversationsActions();
const { toggleAboutContactModal } = useGlobalModalActions();
const { viewUserStories } = useStoriesActions();
const {
about,
acceptedMessageRequest,
avatarUrl,
groupDescription,
isMe,
membersCount,
phoneNumber,
profileName,
sharedGroupNames,
title,
type,
unblurredAvatarUrl,
} = conversation;
return (
<ConversationHero
about={about}
acceptedMessageRequest={acceptedMessageRequest}
avatarUrl={avatarUrl}
badge={badge}
conversationType={type}
groupDescription={groupDescription}
hasStories={hasStories}
i18n={i18n}
id={id}
isMe={isMe}
isSignalConversation={isSignalConversationValue}
membersCount={membersCount}
phoneNumber={phoneNumber}
profileName={profileName}
sharedGroupNames={sharedGroupNames}
theme={theme}
title={title}
toggleAboutContactModal={toggleAboutContactModal}
unblurAvatar={unblurAvatar}
unblurredAvatarUrl={unblurredAvatarUrl}
updateSharedGroups={updateSharedGroups}
viewUserStories={viewUserStories}
/>
);
});

View file

@ -1,11 +1,10 @@
// Copyright 2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React from 'react';
import React, { memo } from 'react';
import { useSelector } from 'react-redux';
import type { AppStateType } from '../ducks/app';
import type { StateType } from '../reducer';
import { Inbox } from '../../components/Inbox';
import { isAlpha } from '../../util/version';
import { getIntl } from '../selectors/user';
import { SmartCustomizingPreferredReactionsModal } from './CustomizingPreferredReactionsModal';
import { getIsCustomizingPreferredReactions } from '../selectors/preferredReactions';
@ -16,6 +15,11 @@ import { SmartCallsTab } from './CallsTab';
import { useItemsActions } from '../ducks/items';
import { getNavTabsCollapsed } from '../selectors/items';
import { SmartChatsTab } from './ChatsTab';
import { getHasInitialLoadCompleted } from '../selectors/app';
import {
getInboxEnvelopeTimestamp,
getInboxFirstEnvelopeTimestamp,
} from '../selectors/inbox';
function renderChatsTab() {
return <SmartChatsTab />;
@ -37,22 +41,16 @@ function renderStoriesTab() {
return <SmartStoriesTab />;
}
export function SmartInbox(): JSX.Element {
export const SmartInbox = memo(function SmartInbox(): JSX.Element {
const i18n = useSelector(getIntl);
const isCustomizingPreferredReactions = useSelector(
getIsCustomizingPreferredReactions
);
const envelopeTimestamp = useSelector<StateType, number | undefined>(
state => state.inbox.envelopeTimestamp
);
const firstEnvelopeTimestamp = useSelector<StateType, number | undefined>(
state => state.inbox.firstEnvelopeTimestamp
);
const { hasInitialLoadCompleted } = useSelector<StateType, AppStateType>(
state => state.app
);
const envelopeTimestamp = useSelector(getInboxEnvelopeTimestamp);
const firstEnvelopeTimestamp = useSelector(getInboxFirstEnvelopeTimestamp);
const hasInitialLoadCompleted = useSelector(getHasInitialLoadCompleted);
const navTabsCollapsed = useSelector(getNavTabsCollapsed);
const { toggleNavTabsCollapse } = useItemsActions();
return (
@ -61,6 +59,7 @@ export function SmartInbox(): JSX.Element {
firstEnvelopeTimestamp={firstEnvelopeTimestamp}
hasInitialLoadCompleted={hasInitialLoadCompleted}
i18n={i18n}
isAlpha={isAlpha(window.getVersion())}
isCustomizingPreferredReactions={isCustomizingPreferredReactions}
navTabsCollapsed={navTabsCollapsed}
onToggleNavTabsCollapse={toggleNavTabsCollapse}
@ -73,4 +72,4 @@ export function SmartInbox(): JSX.Element {
renderStoriesTab={renderStoriesTab}
/>
);
}
});

View file

@ -1,16 +1,14 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import type { ComponentProps, ReactElement } from 'react';
import React, { useCallback, useEffect, useRef, useState } from 'react';
import type { ComponentProps } from 'react';
import React, { memo, useCallback, useEffect, useRef, useState } from 'react';
import { useSelector } from 'react-redux';
import pTimeout, { TimeoutError } from 'p-timeout';
import { getIntl } from '../selectors/user';
import { getUpdatesState } from '../selectors/updates';
import { useUpdatesActions } from '../ducks/updates';
import { hasExpired as hasExpiredSelector } from '../selectors/expiration';
import * as log from '../../logging/log';
import type { Loadable } from '../../util/loadable';
import { LoadingState } from '../../util/loadable';
@ -23,10 +21,12 @@ import {
InstallScreenStep,
} from '../../components/InstallScreen';
import { InstallError } from '../../components/installScreen/InstallScreenErrorStep';
import { LoadError } from '../../components/installScreen/InstallScreenQrCodeNotScannedStep';
import { MAX_DEVICE_NAME_LENGTH } from '../../components/installScreen/InstallScreenChoosingDeviceNameStep';
import { WidthBreakpoint } from '../../components/_util';
import { HTTPError } from '../../textsecure/Errors';
import { isRecord } from '../../util/isRecord';
import type { ConfirmNumberResultType } from '../../textsecure/AccountManager';
import * as Errors from '../../types/errors';
import { normalizeDeviceName } from '../../util/normalizeDeviceName';
import OS from '../../util/os/osMain';
@ -34,6 +34,7 @@ import { SECOND } from '../../util/durations';
import { BackOff } from '../../util/BackOff';
import { drop } from '../../util/drop';
import { SmartToastManager } from './ToastManager';
import { fileToBytes } from '../../util/fileToBytes';
type PropsType = ComponentProps<typeof InstallScreen>;
@ -44,11 +45,12 @@ type StateType =
}
| {
step: InstallScreenStep.QrCodeNotScanned;
provisioningUrl: Loadable<string>;
provisioningUrl: Loadable<string, LoadError>;
}
| {
step: InstallScreenStep.ChoosingDeviceName;
deviceName: string;
backupFile?: File;
}
| {
step: InstallScreenStep.LinkInProgress;
@ -66,34 +68,44 @@ const qrCodeBackOff = new BackOff([
60 * SECOND,
]);
function getInstallError(err: unknown): InstallError {
function classifyError(
err: unknown
): { installError: InstallError } | { loadError: LoadError } {
if (err instanceof HTTPError) {
switch (err.code) {
case -1:
return InstallError.ConnectionFailed;
if (
isRecord(err.cause) &&
err.cause.code === 'SELF_SIGNED_CERT_IN_CHAIN'
) {
return { loadError: LoadError.NetworkIssue };
}
return { installError: InstallError.ConnectionFailed };
case 409:
return InstallError.TooOld;
return { installError: InstallError.TooOld };
case 411:
return InstallError.TooManyDevices;
return { installError: InstallError.TooManyDevices };
default:
return InstallError.UnknownError;
return { loadError: LoadError.Unknown };
}
}
// AccountManager.registerSecondDevice uses this specific "websocket closed" error
// message.
if (isRecord(err) && err.message === 'websocket closed') {
return InstallError.ConnectionFailed;
return { installError: InstallError.ConnectionFailed };
}
return InstallError.UnknownError;
return { loadError: LoadError.Unknown };
}
export function SmartInstallScreen(): ReactElement {
export const SmartInstallScreen = memo(function SmartInstallScreen() {
const i18n = useSelector(getIntl);
const updates = useSelector(getUpdatesState);
const { startUpdate } = useUpdatesActions();
const hasExpired = useSelector(hasExpiredSelector);
const chooseDeviceNamePromiseWrapperRef = useRef(explodePromise<string>());
const chooseBackupFilePromiseWrapperRef =
useRef(explodePromise<File | undefined>());
const [state, setState] = useState<StateType>(INITIAL_STATE);
const [retryCounter, setRetryCounter] = useState(0);
@ -148,6 +160,21 @@ export function SmartInstallScreen(): ReactElement {
[setState]
);
const setBackupFile = useCallback(
(backupFile: File) => {
setState(currentState => {
if (currentState.step !== InstallScreenStep.ChoosingDeviceName) {
return currentState;
}
return {
...currentState,
backupFile,
};
});
},
[setState]
);
const onSubmitDeviceName = useCallback(() => {
if (state.step !== InstallScreenStep.ChoosingDeviceName) {
return;
@ -163,6 +190,7 @@ export function SmartInstallScreen(): ReactElement {
deviceName = i18n('icu:Install__choose-device-name__placeholder');
}
chooseDeviceNamePromiseWrapperRef.current.resolve(deviceName);
chooseBackupFilePromiseWrapperRef.current.resolve(state.backupFile);
setState({ step: InstallScreenStep.LinkInProgress });
}, [state, i18n]);
@ -182,19 +210,23 @@ export function SmartInstallScreen(): ReactElement {
setProvisioningUrl(value);
};
const confirmNumber = async (): Promise<string> => {
const confirmNumber = async (): Promise<ConfirmNumberResultType> => {
if (hasCleanedUp) {
throw new Error('Cannot confirm number; the component was unmounted');
}
onQrCodeScanned();
let deviceName: string;
let backupFileData: Uint8Array | undefined;
if (window.SignalCI) {
chooseDeviceNamePromiseWrapperRef.current.resolve(
window.SignalCI.deviceName
);
}
({ deviceName, backupData: backupFileData } = window.SignalCI);
} else {
deviceName = await chooseDeviceNamePromiseWrapperRef.current.promise;
const backupFile =
await chooseBackupFilePromiseWrapperRef.current.promise;
const result = await chooseDeviceNamePromiseWrapperRef.current.promise;
backupFileData = backupFile ? await fileToBytes(backupFile) : undefined;
}
if (hasCleanedUp) {
throw new Error('Cannot confirm number; the component was unmounted');
@ -219,7 +251,7 @@ export function SmartInstallScreen(): ReactElement {
throw new Error('Cannot confirm number; the component was unmounted');
}
return result;
return { deviceName, backupFile: backupFileData };
};
async function getQRCode(): Promise<void> {
@ -231,8 +263,13 @@ export function SmartInstallScreen(): ReactElement {
);
const sleepMs = qrCodeBackOff.getAndIncrement();
log.info(`InstallScreen/getQRCode: race to ${sleepMs}ms`);
await pTimeout(qrCodeResolution.promise, sleepMs, sleepError);
await qrCodePromise;
await Promise.all([
pTimeout(qrCodeResolution.promise, sleepMs, sleepError),
// Note that `registerSecondDevice` resolves once the registration
// is fully complete and thus should not be subjected to a timeout.
qrCodePromise,
]);
window.IPC.removeSetupMenuItems();
} catch (error) {
@ -256,12 +293,26 @@ export function SmartInstallScreen(): ReactElement {
if (error === sleepError) {
setState({
step: InstallScreenStep.QrCodeNotScanned,
provisioningUrl: { loadingState: LoadingState.LoadFailed, error },
provisioningUrl: {
loadingState: LoadingState.LoadFailed,
error: LoadError.Timeout,
},
});
return;
}
const classifiedError = classifyError(error);
if ('installError' in classifiedError) {
setState({
step: InstallScreenStep.Error,
error: classifiedError.installError,
});
} else {
setState({
step: InstallScreenStep.Error,
error: getInstallError(error),
step: InstallScreenStep.QrCodeNotScanned,
provisioningUrl: {
loadingState: LoadingState.LoadFailed,
error: classifiedError.loadError,
},
});
}
}
@ -316,6 +367,7 @@ export function SmartInstallScreen(): ReactElement {
i18n,
deviceName: state.deviceName,
setDeviceName,
setBackupFile,
onSubmit: onSubmitDeviceName,
},
};
@ -339,4 +391,4 @@ export function SmartInstallScreen(): ReactElement {
/>
</>
);
}
});

View file

@ -1,23 +1,71 @@
// Copyright 2019 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React from 'react';
import { connect } from 'react-redux';
import { get } from 'lodash';
import { mapDispatchToProps } from '../actions';
import React, { memo } from 'react';
import { useSelector } from 'react-redux';
import type { PropsType as DialogExpiredBuildPropsType } from '../../components/DialogExpiredBuild';
import { DialogExpiredBuild } from '../../components/DialogExpiredBuild';
import type { PropsType as LeftPanePropsType } from '../../components/LeftPane';
import { LeftPane } from '../../components/LeftPane';
import { DialogExpiredBuild } from '../../components/DialogExpiredBuild';
import type { PropsType as DialogExpiredBuildPropsType } from '../../components/DialogExpiredBuild';
import type { StateType } from '../reducer';
import { missingCaseError } from '../../util/missingCaseError';
import { lookupConversationWithoutServiceId } from '../../util/lookupConversationWithoutServiceId';
import { isDone as isRegistrationDone } from '../../util/registration';
import { getCountryDataForLocale } from '../../util/getCountryData';
import { getUsernameFromSearch } from '../../util/Username';
import type { NavTabPanelProps } from '../../components/NavTabs';
import type { WidthBreakpoint } from '../../components/_util';
import {
getGroupSizeHardLimit,
getGroupSizeRecommendedLimit,
} from '../../groups/limits';
import { LeftPaneMode } from '../../types/leftPane';
import { getUsernameFromSearch } from '../../util/Username';
import { getCountryDataForLocale } from '../../util/getCountryData';
import { lookupConversationWithoutServiceId } from '../../util/lookupConversationWithoutServiceId';
import { missingCaseError } from '../../util/missingCaseError';
import { isDone as isRegistrationDone } from '../../util/registration';
import { useCallingActions } from '../ducks/calling';
import { useConversationsActions } from '../ducks/conversations';
import { ComposerStep, OneTimeModalState } from '../ducks/conversationsEnums';
import { useGlobalModalActions } from '../ducks/globalModals';
import { useItemsActions } from '../ducks/items';
import { useNetworkActions } from '../ducks/network';
import { useSearchActions } from '../ducks/search';
import { useUsernameActions } from '../ducks/username';
import type { StateType } from '../reducer';
import { getPreferredBadgeSelector } from '../selectors/badges';
import {
getComposeAvatarData,
getComposeGroupAvatar,
getComposeGroupExpireTimer,
getComposeGroupName,
getComposeSelectedContacts,
getComposerConversationSearchTerm,
getComposerSelectedRegion,
getComposerStep,
getComposerUUIDFetchState,
getFilteredCandidateContactsForNewGroup,
getFilteredComposeContacts,
getFilteredComposeGroups,
getLeftPaneLists,
getMaximumGroupSizeModalState,
getMe,
getRecommendedGroupSizeModalState,
getSelectedConversationId,
getShowArchived,
getTargetedMessage,
hasGroupCreationError,
isCreatingGroup,
isEditingAvatar,
} from '../selectors/conversations';
import { getCrashReportCount } from '../selectors/crashReports';
import { hasExpired } from '../selectors/expiration';
import {
getNavTabsCollapsed,
getPreferredLeftPaneWidth,
getUsernameCorrupted,
getUsernameLinkCorrupted,
} from '../selectors/items';
import {
getChallengeStatus,
hasNetworkDialog as getHasNetworkDialog,
} from '../selectors/network';
import {
getHasSearchQuery,
getIsSearching,
@ -27,65 +75,26 @@ import {
getSearchResults,
getStartSearchCounter,
} from '../selectors/search';
import {
isUpdateDownloaded as getIsUpdateDownloaded,
isOSUnsupported,
isUpdateDialogVisible,
} from '../selectors/updates';
import {
getIntl,
getIsMacOS,
getRegionCode,
getTheme,
getIsMacOS,
} from '../selectors/user';
import { hasExpired } from '../selectors/expiration';
import {
isUpdateDialogVisible,
isUpdateDownloaded,
isOSUnsupported,
} from '../selectors/updates';
import { getPreferredBadgeSelector } from '../selectors/badges';
import { hasNetworkDialog } from '../selectors/network';
import {
getPreferredLeftPaneWidth,
getUsernameCorrupted,
getUsernameLinkCorrupted,
getNavTabsCollapsed,
} from '../selectors/items';
import {
getComposeAvatarData,
getComposeGroupAvatar,
getComposeGroupExpireTimer,
getComposeGroupName,
getComposerConversationSearchTerm,
getComposerSelectedRegion,
getComposerStep,
getComposerUUIDFetchState,
getComposeSelectedContacts,
getFilteredCandidateContactsForNewGroup,
getFilteredComposeContacts,
getFilteredComposeGroups,
getLeftPaneLists,
getMaximumGroupSizeModalState,
getMe,
getRecommendedGroupSizeModalState,
getSelectedConversationId,
getTargetedMessage,
getShowArchived,
hasGroupCreationError,
isCreatingGroup,
isEditingAvatar,
} from '../selectors/conversations';
import type { WidthBreakpoint } from '../../components/_util';
import {
getGroupSizeRecommendedLimit,
getGroupSizeHardLimit,
} from '../../groups/limits';
import { SmartCaptchaDialog } from './CaptchaDialog';
import { SmartCrashReportDialog } from './CrashReportDialog';
import { SmartMessageSearchResult } from './MessageSearchResult';
import { SmartNetworkStatus } from './NetworkStatus';
import { SmartRelinkDialog } from './RelinkDialog';
import { SmartUnsupportedOSDialog } from './UnsupportedOSDialog';
import { SmartToastManager } from './ToastManager';
import type { PropsType as SmartUnsupportedOSDialogPropsType } from './UnsupportedOSDialog';
import { SmartUnsupportedOSDialog } from './UnsupportedOSDialog';
import { SmartUpdateDialog } from './UpdateDialog';
import { SmartCaptchaDialog } from './CaptchaDialog';
import { SmartCrashReportDialog } from './CrashReportDialog';
function renderMessageSearchResult(id: string): JSX.Element {
return <SmartMessageSearchResult id={id} />;
@ -121,7 +130,7 @@ function renderUnsupportedOSDialog(
): JSX.Element {
return <SmartUnsupportedOSDialog {...props} />;
}
function renderToastManager(props: {
function renderToastManagerWithMegaphone(props: {
containerWidthBreakpoint: WidthBreakpoint;
}): JSX.Element {
return <SmartToastManager {...props} />;
@ -247,15 +256,83 @@ const getModeSpecificProps = (
}
};
const mapStateToProps = (state: StateType) => {
const hasUpdateDialog = isUpdateDialogVisible(state);
const hasUnsupportedOS = isOSUnsupported(state);
const usernameCorrupted = getUsernameCorrupted(state);
const usernameLinkCorrupted = getUsernameLinkCorrupted(state);
export const SmartLeftPane = memo(function SmartLeftPane({
hasFailedStorySends,
hasPendingUpdate,
otherTabsUnreadStats,
}: NavTabPanelProps) {
const challengeStatus = useSelector(getChallengeStatus);
const composerStep = useSelector(getComposerStep);
const crashReportCount = useSelector(getCrashReportCount);
const getPreferredBadge = useSelector(getPreferredBadgeSelector);
const hasAppExpired = useSelector(hasExpired);
const hasNetworkDialog = useSelector(getHasNetworkDialog);
const hasSearchQuery = useSelector(getHasSearchQuery);
const hasUnsupportedOS = useSelector(isOSUnsupported);
const hasUpdateDialog = useSelector(isUpdateDialogVisible);
const i18n = useSelector(getIntl);
const isMacOS = useSelector(getIsMacOS);
const isUpdateDownloaded = useSelector(getIsUpdateDownloaded);
const modeSpecificProps = useSelector(getModeSpecificProps);
const navTabsCollapsed = useSelector(getNavTabsCollapsed);
const preferredWidthFromStorage = useSelector(getPreferredLeftPaneWidth);
const selectedConversationId = useSelector(getSelectedConversationId);
const showArchived = useSelector(getShowArchived);
const targetedMessage = useSelector(getTargetedMessage);
const theme = useSelector(getTheme);
const usernameCorrupted = useSelector(getUsernameCorrupted);
const usernameLinkCorrupted = useSelector(getUsernameLinkCorrupted);
const {
blockConversation,
clearGroupCreationError,
closeMaximumGroupSizeModal,
closeRecommendedGroupSizeModal,
composeDeleteAvatarFromDisk,
composeReplaceAvatar,
composeSaveAvatarToDisk,
createGroup,
removeConversation,
setComposeGroupAvatar,
setComposeGroupExpireTimer,
setComposeGroupName,
setComposeSearchTerm,
setComposeSelectedRegion,
setIsFetchingUUID,
showArchivedConversations,
showChooseGroupMembers,
showConversation,
showFindByPhoneNumber,
showFindByUsername,
showInbox,
startComposing,
startSettingGroupMetadata,
toggleComposeEditingAvatar,
toggleConversationInChooseMembers,
} = useConversationsActions();
const {
clearConversationSearch,
clearSearch,
endConversationSearch,
endSearch,
searchInConversation,
startSearch,
updateSearchTerm,
} = useSearchActions();
const {
onOutgoingAudioCallInConversation,
onOutgoingVideoCallInConversation,
} = useCallingActions();
const { openUsernameReservationModal } = useUsernameActions();
const { savePreferredLeftPaneWidth, toggleNavTabsCollapse } =
useItemsActions();
const { setChallengeStatus } = useNetworkActions();
const { showUserNotFoundModal, toggleProfileEditor } =
useGlobalModalActions();
let hasExpiredDialog = false;
let unsupportedOSDialogType: 'error' | 'warning' | undefined;
if (hasExpired(state)) {
if (hasAppExpired) {
if (hasUnsupportedOS) {
unsupportedOSDialogType = 'error';
} else {
@ -265,49 +342,89 @@ const mapStateToProps = (state: StateType) => {
unsupportedOSDialogType = 'warning';
}
const composerStep = getComposerStep(state);
const showArchived = getShowArchived(state);
const hasSearchQuery = getHasSearchQuery(state);
const hasRelinkDialog = !isRegistrationDone();
return {
hasNetworkDialog: hasNetworkDialog(state),
hasExpiredDialog,
hasRelinkDialog: !isRegistrationDone(),
hasUpdateDialog,
isUpdateDownloaded: isUpdateDownloaded(state),
unsupportedOSDialogType,
usernameCorrupted,
usernameLinkCorrupted,
const renderToastManager =
composerStep == null && !showArchived && !hasSearchQuery
? renderToastManagerWithMegaphone
: renderToastManagerWithoutMegaphone;
modeSpecificProps: getModeSpecificProps(state),
navTabsCollapsed: getNavTabsCollapsed(state),
preferredWidthFromStorage: getPreferredLeftPaneWidth(state),
selectedConversationId: getSelectedConversationId(state),
targetedMessageId: getTargetedMessage(state)?.id,
showArchived,
getPreferredBadge: getPreferredBadgeSelector(state),
i18n: getIntl(state),
isMacOS: getIsMacOS(state),
regionCode: getRegionCode(state),
challengeStatus: state.network.challengeStatus,
crashReportCount: state.crashReports.count,
renderMessageSearchResult,
renderNetworkStatus,
renderRelinkDialog,
renderUpdateDialog,
renderCaptchaDialog,
renderCrashReportDialog,
renderExpiredBuildDialog,
renderUnsupportedOSDialog,
renderToastManager:
composerStep == null && !showArchived && !hasSearchQuery
? renderToastManager
: renderToastManagerWithoutMegaphone,
lookupConversationWithoutServiceId,
theme: getTheme(state),
};
};
const targetedMessageId = targetedMessage?.id;
const smart = connect(mapStateToProps, mapDispatchToProps);
export const SmartLeftPane = smart(LeftPane);
return (
<LeftPane
blockConversation={blockConversation}
challengeStatus={challengeStatus}
clearConversationSearch={clearConversationSearch}
clearGroupCreationError={clearGroupCreationError}
clearSearch={clearSearch}
closeMaximumGroupSizeModal={closeMaximumGroupSizeModal}
closeRecommendedGroupSizeModal={closeRecommendedGroupSizeModal}
composeDeleteAvatarFromDisk={composeDeleteAvatarFromDisk}
composeReplaceAvatar={composeReplaceAvatar}
composeSaveAvatarToDisk={composeSaveAvatarToDisk}
crashReportCount={crashReportCount}
createGroup={createGroup}
endConversationSearch={endConversationSearch}
endSearch={endSearch}
getPreferredBadge={getPreferredBadge}
hasExpiredDialog={hasExpiredDialog}
hasFailedStorySends={hasFailedStorySends}
hasNetworkDialog={hasNetworkDialog}
hasPendingUpdate={hasPendingUpdate}
hasRelinkDialog={hasRelinkDialog}
hasUpdateDialog={hasUpdateDialog}
i18n={i18n}
isMacOS={isMacOS}
isUpdateDownloaded={isUpdateDownloaded}
lookupConversationWithoutServiceId={lookupConversationWithoutServiceId}
modeSpecificProps={modeSpecificProps}
navTabsCollapsed={navTabsCollapsed}
onOutgoingAudioCallInConversation={onOutgoingAudioCallInConversation}
onOutgoingVideoCallInConversation={onOutgoingVideoCallInConversation}
openUsernameReservationModal={openUsernameReservationModal}
otherTabsUnreadStats={otherTabsUnreadStats}
preferredWidthFromStorage={preferredWidthFromStorage}
removeConversation={removeConversation}
renderCaptchaDialog={renderCaptchaDialog}
renderCrashReportDialog={renderCrashReportDialog}
renderExpiredBuildDialog={renderExpiredBuildDialog}
renderMessageSearchResult={renderMessageSearchResult}
renderNetworkStatus={renderNetworkStatus}
renderRelinkDialog={renderRelinkDialog}
renderToastManager={renderToastManager}
renderUnsupportedOSDialog={renderUnsupportedOSDialog}
renderUpdateDialog={renderUpdateDialog}
savePreferredLeftPaneWidth={savePreferredLeftPaneWidth}
searchInConversation={searchInConversation}
selectedConversationId={selectedConversationId}
setChallengeStatus={setChallengeStatus}
setComposeGroupAvatar={setComposeGroupAvatar}
setComposeGroupExpireTimer={setComposeGroupExpireTimer}
setComposeGroupName={setComposeGroupName}
setComposeSearchTerm={setComposeSearchTerm}
setComposeSelectedRegion={setComposeSelectedRegion}
setIsFetchingUUID={setIsFetchingUUID}
showArchivedConversations={showArchivedConversations}
showChooseGroupMembers={showChooseGroupMembers}
showConversation={showConversation}
showFindByPhoneNumber={showFindByPhoneNumber}
showFindByUsername={showFindByUsername}
showInbox={showInbox}
showUserNotFoundModal={showUserNotFoundModal}
startComposing={startComposing}
startSearch={startSearch}
startSettingGroupMetadata={startSettingGroupMetadata}
targetedMessageId={targetedMessageId}
theme={theme}
toggleComposeEditingAvatar={toggleComposeEditingAvatar}
toggleConversationInChooseMembers={toggleConversationInChooseMembers}
toggleNavTabsCollapse={toggleNavTabsCollapse}
toggleProfileEditor={toggleProfileEditor}
unsupportedOSDialogType={unsupportedOSDialogType}
updateSearchTerm={updateSearchTerm}
usernameCorrupted={usernameCorrupted}
usernameLinkCorrupted={usernameLinkCorrupted}
/>
);
});

View file

@ -1,14 +1,8 @@
// Copyright 2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React, { useCallback } from 'react';
import React, { memo, useCallback } from 'react';
import { useSelector } from 'react-redux';
import type { ReadonlyDeep } from 'type-fest';
import type { GetConversationByIdType } from '../selectors/conversations';
import type { LocalizerType } from '../../types/Util';
import type { MediaItemType } from '../../types/MediaItem';
import type { StateType } from '../reducer';
import { Lightbox } from '../../components/Lightbox';
import { getConversationSelector } from '../selectors/conversations';
import { getIntl } from '../selectors/user';
@ -26,8 +20,8 @@ import {
shouldShowLightbox,
} from '../selectors/lightbox';
export function SmartLightbox(): JSX.Element | null {
const i18n = useSelector<StateType, LocalizerType>(getIntl);
export const SmartLightbox = memo(function SmartLightbox() {
const i18n = useSelector(getIntl);
const { saveAttachment } = useConversationsActions();
const {
closeLightbox,
@ -38,20 +32,15 @@ export function SmartLightbox(): JSX.Element | null {
const { toggleForwardMessagesModal } = useGlobalModalActions();
const { pauseVoiceNotePlayer } = useAudioPlayerActions();
const conversationSelector = useSelector<StateType, GetConversationByIdType>(
getConversationSelector
);
const conversationSelector = useSelector(getConversationSelector);
const isShowingLightbox = useSelector<StateType, boolean>(shouldShowLightbox);
const isViewOnce = useSelector<StateType, boolean>(getIsViewOnce);
const media = useSelector<
StateType,
ReadonlyArray<ReadonlyDeep<MediaItemType>>
>(getMedia);
const hasPrevMessage = useSelector<StateType, boolean>(getHasPrevMessage);
const hasNextMessage = useSelector<StateType, boolean>(getHasNextMessage);
const selectedIndex = useSelector<StateType, number>(getSelectedIndex);
const playbackDisabled = useSelector<StateType, boolean>(getPlaybackDisabled);
const isShowingLightbox = useSelector(shouldShowLightbox);
const isViewOnce = useSelector(getIsViewOnce);
const media = useSelector(getMedia);
const hasPrevMessage = useSelector(getHasPrevMessage);
const hasNextMessage = useSelector(getHasNextMessage);
const selectedIndex = useSelector(getSelectedIndex);
const playbackDisabled = useSelector(getPlaybackDisabled);
const onPrevAttachment = useCallback(() => {
if (selectedIndex <= 0) {
@ -107,4 +96,4 @@ export function SmartLightbox(): JSX.Element | null {
hasPrevMessage={hasPrevMessage}
/>
);
}
});

View file

@ -1,6 +1,6 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React, { useCallback } from 'react';
import React, { memo, useCallback } from 'react';
import { useSelector } from 'react-redux';
import { MessageAudio } from '../../components/conversation/MessageAudio';
@ -26,10 +26,10 @@ export type Props = Omit<MessageAudioOwnProps, 'active' | 'onPlayMessage'> & {
renderingContext: string;
};
export function SmartMessageAudio({
export const SmartMessageAudio = memo(function SmartMessageAudio({
renderingContext,
...props
}: Props): JSX.Element | null {
}: Props) {
const active = useSelector(selectAudioPlayerActive);
const { loadVoiceNoteAudio, setIsPlaying, setPlaybackRate, setPosition } =
useAudioPlayerActions();
@ -100,4 +100,4 @@ export function SmartMessageAudio({
{...props}
/>
);
}
});

View file

@ -1,7 +1,7 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React, { useEffect } from 'react';
import React, { memo, useEffect } from 'react';
import { useSelector } from 'react-redux';
import type { Props as MessageDetailProps } from '../../components/conversation/MessageDetail';
@ -28,89 +28,91 @@ export type OwnProps = Pick<
'contacts' | 'errors' | 'message' | 'receivedAt'
>;
export function SmartMessageDetail(): JSX.Element | null {
const getContactNameColor = useSelector(getContactNameColorSelector);
const getPreferredBadge = useSelector(getPreferredBadgeSelector);
const i18n = useSelector(getIntl);
const platform = useSelector(getPlatform);
const interactionMode = useSelector(getInteractionMode);
const messageDetails = useSelector(getMessageDetails);
const theme = useSelector(getTheme);
const { checkForAccount } = useAccountsActions();
const {
clearTargetedMessage: clearSelectedMessage,
doubleCheckMissingQuoteReference,
kickOffAttachmentDownload,
markAttachmentAsCorrupted,
messageExpanded,
openGiftBadge,
retryMessageSend,
popPanelForConversation,
pushPanelForConversation,
saveAttachment,
showConversation,
showExpiredIncomingTapToViewToast,
showExpiredOutgoingTapToViewToast,
showSpoiler,
startConversation,
} = useConversationsActions();
const { showContactModal, showEditHistoryModal, toggleSafetyNumberModal } =
useGlobalModalActions();
const { showLightbox, showLightboxForViewOnceMedia } = useLightboxActions();
const { viewStory } = useStoriesActions();
export const SmartMessageDetail = memo(
function SmartMessageDetail(): JSX.Element | null {
const getContactNameColor = useSelector(getContactNameColorSelector);
const getPreferredBadge = useSelector(getPreferredBadgeSelector);
const i18n = useSelector(getIntl);
const platform = useSelector(getPlatform);
const interactionMode = useSelector(getInteractionMode);
const messageDetails = useSelector(getMessageDetails);
const theme = useSelector(getTheme);
const { checkForAccount } = useAccountsActions();
const {
clearTargetedMessage: clearSelectedMessage,
doubleCheckMissingQuoteReference,
kickOffAttachmentDownload,
markAttachmentAsCorrupted,
messageExpanded,
openGiftBadge,
retryMessageSend,
popPanelForConversation,
pushPanelForConversation,
saveAttachment,
showConversation,
showExpiredIncomingTapToViewToast,
showExpiredOutgoingTapToViewToast,
showSpoiler,
startConversation,
} = useConversationsActions();
const { showContactModal, showEditHistoryModal, toggleSafetyNumberModal } =
useGlobalModalActions();
const { showLightbox, showLightboxForViewOnceMedia } = useLightboxActions();
const { viewStory } = useStoriesActions();
useEffect(() => {
if (!messageDetails) {
popPanelForConversation();
}
}, [messageDetails, popPanelForConversation]);
useEffect(() => {
if (!messageDetails) {
popPanelForConversation();
return null;
}
}, [messageDetails, popPanelForConversation]);
if (!messageDetails) {
return null;
const { contacts, errors, message, receivedAt } = messageDetails;
const contactNameColor =
message.conversationType === 'group'
? getContactNameColor(message.conversationId, message.author.id)
: undefined;
return (
<MessageDetail
checkForAccount={checkForAccount}
clearTargetedMessage={clearSelectedMessage}
contactNameColor={contactNameColor}
contacts={contacts}
doubleCheckMissingQuoteReference={doubleCheckMissingQuoteReference}
errors={errors}
getPreferredBadge={getPreferredBadge}
i18n={i18n}
platform={platform}
interactionMode={interactionMode}
kickOffAttachmentDownload={kickOffAttachmentDownload}
markAttachmentAsCorrupted={markAttachmentAsCorrupted}
message={message}
messageExpanded={messageExpanded}
openGiftBadge={openGiftBadge}
retryMessageSend={retryMessageSend}
pushPanelForConversation={pushPanelForConversation}
receivedAt={receivedAt}
renderAudioAttachment={renderAudioAttachment}
saveAttachment={saveAttachment}
sentAt={message.timestamp}
showContactModal={showContactModal}
showConversation={showConversation}
showEditHistoryModal={showEditHistoryModal}
showExpiredIncomingTapToViewToast={showExpiredIncomingTapToViewToast}
showExpiredOutgoingTapToViewToast={showExpiredOutgoingTapToViewToast}
showLightbox={showLightbox}
showLightboxForViewOnceMedia={showLightboxForViewOnceMedia}
showSpoiler={showSpoiler}
startConversation={startConversation}
theme={theme}
toggleSafetyNumberModal={toggleSafetyNumberModal}
viewStory={viewStory}
/>
);
}
const { contacts, errors, message, receivedAt } = messageDetails;
const contactNameColor =
message.conversationType === 'group'
? getContactNameColor(message.conversationId, message.author.id)
: undefined;
return (
<MessageDetail
checkForAccount={checkForAccount}
clearTargetedMessage={clearSelectedMessage}
contactNameColor={contactNameColor}
contacts={contacts}
doubleCheckMissingQuoteReference={doubleCheckMissingQuoteReference}
errors={errors}
getPreferredBadge={getPreferredBadge}
i18n={i18n}
platform={platform}
interactionMode={interactionMode}
kickOffAttachmentDownload={kickOffAttachmentDownload}
markAttachmentAsCorrupted={markAttachmentAsCorrupted}
message={message}
messageExpanded={messageExpanded}
openGiftBadge={openGiftBadge}
retryMessageSend={retryMessageSend}
pushPanelForConversation={pushPanelForConversation}
receivedAt={receivedAt}
renderAudioAttachment={renderAudioAttachment}
saveAttachment={saveAttachment}
sentAt={message.timestamp}
showContactModal={showContactModal}
showConversation={showConversation}
showEditHistoryModal={showEditHistoryModal}
showExpiredIncomingTapToViewToast={showExpiredIncomingTapToViewToast}
showExpiredOutgoingTapToViewToast={showExpiredOutgoingTapToViewToast}
showLightbox={showLightbox}
showLightboxForViewOnceMedia={showLightboxForViewOnceMedia}
showSpoiler={showSpoiler}
startConversation={startConversation}
theme={theme}
toggleSafetyNumberModal={toggleSafetyNumberModal}
viewStory={viewStory}
/>
);
}
);

View file

@ -0,0 +1,85 @@
// Copyright 2024 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React, { memo, useCallback, useMemo } from 'react';
import { useSelector } from 'react-redux';
import { getIntl } from '../selectors/user';
import { getGlobalModalsState } from '../selectors/globalModals';
import { getConversationSelector } from '../selectors/conversations';
import { useConversationsActions } from '../ducks/conversations';
import {
MessageRequestActionsConfirmation,
MessageRequestState,
} from '../../components/conversation/MessageRequestActionsConfirmation';
import { useContactNameData } from '../../components/conversation/ContactName';
import { getAddedByForOurPendingInvitation } from '../../util/getAddedByForOurPendingInvitation';
import { strictAssert } from '../../util/assert';
import { useGlobalModalActions } from '../ducks/globalModals';
export const SmartMessageRequestActionsConfirmation = memo(
function SmartMessageRequestActionsConfirmation() {
const i18n = useSelector(getIntl);
const globalModals = useSelector(getGlobalModalsState);
const { messageRequestActionsConfirmationProps } = globalModals;
strictAssert(
messageRequestActionsConfirmationProps,
'messageRequestActionsConfirmationProps are required'
);
const { conversationId, state } = messageRequestActionsConfirmationProps;
strictAssert(state !== MessageRequestState.default, 'state is required');
const getConversation = useSelector(getConversationSelector);
const conversation = getConversation(conversationId);
const addedBy = useMemo(() => {
if (conversation.type === 'group') {
return getAddedByForOurPendingInvitation(conversation);
}
return null;
}, [conversation]);
const conversationName = useContactNameData(conversation);
strictAssert(conversationName, 'conversationName is required');
const addedByName = useContactNameData(addedBy);
const {
acceptConversation,
blockConversation,
reportSpam,
blockAndReportSpam,
deleteConversation,
} = useConversationsActions();
const { toggleMessageRequestActionsConfirmation } = useGlobalModalActions();
const handleChangeState = useCallback(
(nextState: MessageRequestState) => {
if (nextState === MessageRequestState.default) {
toggleMessageRequestActionsConfirmation(null);
} else {
toggleMessageRequestActionsConfirmation({
conversationId,
state: nextState,
});
}
},
[conversationId, toggleMessageRequestActionsConfirmation]
);
return (
<MessageRequestActionsConfirmation
i18n={i18n}
conversationId={conversation.id}
conversationType={conversation.type}
conversationName={conversationName}
addedByName={addedByName}
isBlocked={conversation.isBlocked ?? false}
isReported={conversation.isReported ?? false}
acceptConversation={acceptConversation}
blockConversation={blockConversation}
reportSpam={reportSpam}
blockAndReportSpam={blockAndReportSpam}
deleteConversation={deleteConversation}
state={state}
onChangeState={handleChangeState}
/>
);
}
);

View file

@ -1,40 +1,51 @@
// Copyright 2019 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import type { CSSProperties } from 'react';
import { connect } from 'react-redux';
import { mapDispatchToProps } from '../actions';
import type { StateType } from '../reducer';
// SPDX-License-Identifier: AGPL-3.0-onlyå
import React, { memo } from 'react';
import { useSelector } from 'react-redux';
import { MessageSearchResult } from '../../components/conversationList/MessageSearchResult';
import { getPreferredBadgeSelector } from '../selectors/badges';
import { getIntl, getTheme } from '../selectors/user';
import { getMessageSearchResultSelector } from '../selectors/search';
import * as log from '../../logging/log';
import { useConversationsActions } from '../ducks/conversations';
type SmartProps = {
type SmartMessageSearchResultProps = {
id: string;
style?: CSSProperties;
};
function mapStateToProps(state: StateType, ourProps: SmartProps) {
const { id, style } = ourProps;
export const SmartMessageSearchResult = memo(function SmartMessageSearchResult({
id,
}: SmartMessageSearchResultProps) {
const i18n = useSelector(getIntl);
const theme = useSelector(getTheme);
const messageSearchResultSelector = useSelector(
getMessageSearchResultSelector
);
const getPreferredBadge = useSelector(getPreferredBadgeSelector);
const { showConversation } = useConversationsActions();
const props = getMessageSearchResultSelector(state)(id);
if (!props) {
const messageResult = messageSearchResultSelector(id);
if (messageResult == null) {
log.error('SmartMessageSearchResult: no message was found');
return null;
}
const { conversationId, snippet, body, bodyRanges, from, to, sentAt } =
messageResult;
return {
...props,
getPreferredBadge: getPreferredBadgeSelector(state),
i18n: getIntl(state),
style,
theme: getTheme(state),
};
}
const smart = connect(mapStateToProps, mapDispatchToProps);
export const SmartMessageSearchResult = smart(MessageSearchResult);
return (
<MessageSearchResult
i18n={i18n}
theme={theme}
getPreferredBadge={getPreferredBadge}
id={id}
conversationId={conversationId}
snippet={snippet}
body={body}
bodyRanges={bodyRanges}
from={from}
to={to}
showConversation={showConversation}
sentAt={sentAt}
/>
);
});

View file

@ -1,7 +1,7 @@
// Copyright 2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React, { useCallback } from 'react';
import React, { memo, useCallback } from 'react';
import { useSelector } from 'react-redux';
import { MiniPlayer, PlayerState } from '../../components/MiniPlayer';
import type { Props as DumbProps } from '../../components/MiniPlayer';
@ -23,7 +23,9 @@ type Props = Pick<DumbProps, 'shouldFlow'>;
* It also triggers side-effecting actions (actual playback) in response to changes in
* the state
*/
export function SmartMiniPlayer({ shouldFlow }: Props): JSX.Element | null {
export const SmartMiniPlayer = memo(function SmartMiniPlayer({
shouldFlow,
}: Props): JSX.Element | null {
const i18n = useSelector(getIntl);
const active = useSelector(selectAudioPlayerActive);
const getVoiceNoteTitle = useSelector(selectVoiceNoteTitle);
@ -47,14 +49,14 @@ export function SmartMiniPlayer({ shouldFlow }: Props): JSX.Element | null {
state = active.playing ? PlayerState.playing : PlayerState.paused;
}
const title = AudioPlayerContent.isDraft(content)
? i18n('icu:you')
: getVoiceNoteTitle(content.current);
return (
<MiniPlayer
i18n={i18n}
title={
AudioPlayerContent.isDraft(content)
? i18n('icu:you')
: getVoiceNoteTitle(content.current)
}
title={title}
onPlay={handlePlay}
onPause={handlePause}
onPlaybackRate={setPlaybackRate}
@ -66,4 +68,4 @@ export function SmartMiniPlayer({ shouldFlow }: Props): JSX.Element | null {
playbackRate={active.playbackRate}
/>
);
}
});

View file

@ -1,7 +1,7 @@
// Copyright 2023 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React, { useCallback } from 'react';
import React, { memo, useCallback } from 'react';
import { useSelector } from 'react-redux';
import type { NavTabPanelProps } from '../../components/NavTabs';
import { NavTabs } from '../../components/NavTabs';
@ -33,7 +33,7 @@ export type SmartNavTabsProps = Readonly<{
renderStoriesTab(props: NavTabPanelProps): JSX.Element;
}>;
export function SmartNavTabs({
export const SmartNavTabs = memo(function SmartNavTabs({
navTabsCollapsed,
onToggleNavTabsCollapse,
renderCallsTab,
@ -91,4 +91,4 @@ export function SmartNavTabs({
unreadStoriesCount={unreadStoriesCount}
/>
);
}
});

View file

@ -1,23 +1,37 @@
// Copyright 2020 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { connect } from 'react-redux';
import { mapDispatchToProps } from '../actions';
import React, { memo } from 'react';
import { useSelector } from 'react-redux';
import { DialogNetworkStatus } from '../../components/DialogNetworkStatus';
import type { StateType } from '../reducer';
import { getIntl } from '../selectors/user';
import type { WidthBreakpoint } from '../../components/_util';
import {
getNetworkIsOnline,
getNetworkIsOutage,
getNetworkSocketStatus,
} from '../selectors/network';
import { useUserActions } from '../ducks/user';
type PropsType = Readonly<{ containerWidthBreakpoint: WidthBreakpoint }>;
type SmartNetworkStatusProps = Readonly<{
containerWidthBreakpoint: WidthBreakpoint;
}>;
const mapStateToProps = (state: StateType, ownProps: PropsType) => {
return {
...state.network,
i18n: getIntl(state),
...ownProps,
};
};
const smart = connect(mapStateToProps, mapDispatchToProps);
export const SmartNetworkStatus = smart(DialogNetworkStatus);
export const SmartNetworkStatus = memo(function SmartNetworkStatus({
containerWidthBreakpoint,
}: SmartNetworkStatusProps) {
const i18n = useSelector(getIntl);
const isOnline = useSelector(getNetworkIsOnline);
const isOutage = useSelector(getNetworkIsOutage);
const socketStatus = useSelector(getNetworkSocketStatus);
const { manualReconnect } = useUserActions();
return (
<DialogNetworkStatus
containerWidthBreakpoint={containerWidthBreakpoint}
i18n={i18n}
isOnline={isOnline}
isOutage={isOutage}
socketStatus={socketStatus}
manualReconnect={manualReconnect}
/>
);
});

View file

@ -0,0 +1,41 @@
// Copyright 2024 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React, { memo, useCallback } from 'react';
import { useSelector } from 'react-redux';
import { NotePreviewModal } from '../../components/NotePreviewModal';
import { strictAssert } from '../../util/assert';
import { getConversationSelector } from '../selectors/conversations';
import { getNotePreviewModalProps } from '../selectors/globalModals';
import { getIntl } from '../selectors/user';
import { useGlobalModalActions } from '../ducks/globalModals';
export const SmartNotePreviewModal = memo(function SmartNotePreviewModal() {
const i18n = useSelector(getIntl);
const props = useSelector(getNotePreviewModalProps);
strictAssert(props != null, 'props is required');
const { conversationId } = props;
const conversationSelector = useSelector(getConversationSelector);
const conversation = conversationSelector(conversationId);
strictAssert(conversation != null, 'conversation is required');
const { toggleNotePreviewModal, toggleEditNicknameAndNoteModal } =
useGlobalModalActions();
const handleClose = useCallback(() => {
toggleNotePreviewModal(null);
}, [toggleNotePreviewModal]);
const handleEdit = useCallback(() => {
toggleEditNicknameAndNoteModal({ conversationId });
}, [toggleEditNicknameAndNoteModal, conversationId]);
return (
<NotePreviewModal
conversation={conversation}
i18n={i18n}
onClose={handleClose}
onEdit={handleEdit}
/>
);
});

View file

@ -1,12 +1,8 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { connect } from 'react-redux';
import { mapDispatchToProps } from '../actions';
import type { PropsDataType } from '../../components/conversation/conversation-details/PendingInvites';
import React, { memo } from 'react';
import { useSelector } from 'react-redux';
import { PendingInvites } from '../../components/conversation/conversation-details/PendingInvites';
import type { StateType } from '../reducer';
import { getIntl, getTheme } from '../selectors/user';
import { getPreferredBadgeSelector } from '../selectors/badges';
import {
@ -16,36 +12,48 @@ import {
import { getGroupMemberships } from '../../util/getGroupMemberships';
import { assertDev } from '../../util/assert';
import type { AciString } from '../../types/ServiceId';
import { useConversationsActions } from '../ducks/conversations';
export type SmartPendingInvitesProps = {
conversationId: string;
ourAci: AciString;
};
const mapStateToProps = (
state: StateType,
props: SmartPendingInvitesProps
): PropsDataType => {
const conversationSelector = getConversationByIdSelector(state);
const conversationByServiceIdSelector =
getConversationByServiceIdSelector(state);
const conversation = conversationSelector(props.conversationId);
export const SmartPendingInvites = memo(function SmartPendingInvites({
conversationId,
ourAci,
}: SmartPendingInvitesProps) {
const i18n = useSelector(getIntl);
const theme = useSelector(getTheme);
const getPreferredBadge = useSelector(getPreferredBadgeSelector);
const conversationSelector = useSelector(getConversationByIdSelector);
const conversationByServiceIdSelector = useSelector(
getConversationByServiceIdSelector
);
const conversation = conversationSelector(conversationId);
assertDev(
conversation,
'<SmartPendingInvites> expected a conversation to be found'
);
return {
...props,
...getGroupMemberships(conversation, conversationByServiceIdSelector),
const groupMemberships = getGroupMemberships(
conversation,
getPreferredBadge: getPreferredBadgeSelector(state),
i18n: getIntl(state),
theme: getTheme(state),
};
};
const smart = connect(mapStateToProps, mapDispatchToProps);
export const SmartPendingInvites = smart(PendingInvites);
conversationByServiceIdSelector
);
const {
approvePendingMembershipFromGroupV2,
revokePendingMembershipsFromGroupV2,
} = useConversationsActions();
return (
<PendingInvites
i18n={i18n}
theme={theme}
getPreferredBadge={getPreferredBadge}
conversation={conversation}
ourAci={ourAci}
pendingMemberships={groupMemberships.pendingMemberships}
pendingApprovalMemberships={groupMemberships.pendingApprovalMemberships}
approvePendingMembershipFromGroupV2={approvePendingMembershipFromGroupV2}
revokePendingMembershipsFromGroupV2={revokePendingMembershipsFromGroupV2}
/>
);
});

Some files were not shown because too many files have changed in this diff Show more