Merge branch 'main' into HEAD
This commit is contained in:
commit
fed6bbfc8b
1127 changed files with 263697 additions and 302446 deletions
|
@ -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 (
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
@ -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;
|
||||
};
|
||||
|
|
|
@ -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
|
@ -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 };
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
),
|
||||
},
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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 don’t 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,
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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 };
|
||||
|
|
|
@ -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: {},
|
||||
|
|
|
@ -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> = [];
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
14
ts/state/selectors/app.ts
Normal 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);
|
|
@ -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);
|
||||
|
||||
|
|
23
ts/state/selectors/audioRecorder.ts
Normal file
23
ts/state/selectors/audioRecorder.ts
Normal 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;
|
||||
}
|
||||
);
|
|
@ -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;
|
||||
}
|
||||
);
|
||||
|
|
|
@ -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 = (
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
18
ts/state/selectors/crashReports.ts
Normal file
18
ts/state/selectors/crashReports.ts
Normal 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
|
||||
);
|
|
@ -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
|
||||
);
|
||||
|
|
17
ts/state/selectors/inbox.ts
Normal file
17
ts/state/selectors/inbox.ts
Normal 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
|
||||
);
|
34
ts/state/selectors/items-extra.ts
Normal file
34
ts/state/selectors/items-extra.ts
Normal 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;
|
||||
}
|
||||
);
|
|
@ -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)
|
||||
);
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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];
|
||||
};
|
||||
}
|
||||
);
|
||||
|
|
|
@ -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
|
||||
);
|
||||
}
|
||||
);
|
||||
|
|
|
@ -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',
|
||||
|
|
11
ts/state/selectors/toast.ts
Normal file
11
ts/state/selectors/toast.ts
Normal 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);
|
|
@ -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 }) => {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
|
65
ts/state/smart/CallLinkAddNameModal.tsx
Normal file
65
ts/state/smart/CallLinkAddNameModal.tsx
Normal 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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
70
ts/state/smart/CallLinkDetails.tsx
Normal file
70
ts/state/smart/CallLinkDetails.tsx
Normal 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}
|
||||
/>
|
||||
);
|
||||
});
|
98
ts/state/smart/CallLinkEditModal.tsx
Normal file
98
ts/state/smart/CallLinkEditModal.tsx
Normal 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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
);
|
||||
|
|
|
@ -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} />;
|
||||
}
|
||||
});
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
);
|
||||
|
|
37
ts/state/smart/ConfirmLeaveCallModal.tsx
Normal file
37
ts/state/smart/ConfirmLeaveCallModal.tsx
Normal 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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
|
56
ts/state/smart/EditNicknameAndNoteModal.tsx
Normal file
56
ts/state/smart/EditNicknameAndNoteModal.tsx
Normal 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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
})
|
||||
);
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
|
|
@ -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
|
||||
}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
);
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
|
|
@ -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 {
|
|||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
|
85
ts/state/smart/MessageRequestActionsConfirmation.tsx
Normal file
85
ts/state/smart/MessageRequestActionsConfirmation.tsx
Normal 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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
|
41
ts/state/smart/NotePreviewModal.tsx
Normal file
41
ts/state/smart/NotePreviewModal.tsx
Normal 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}
|
||||
/>
|
||||
);
|
||||
});
|
|
@ -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
Loading…
Add table
Add a link
Reference in a new issue