Don't access RemoteConfig directly from 'dumb' components
This commit is contained in:
parent
e79380b37c
commit
0134990275
40 changed files with 352 additions and 353 deletions
|
@ -8,7 +8,7 @@ import {
|
|||
getStickersPath,
|
||||
getTempPath,
|
||||
getDraftPath,
|
||||
} from '../ts/util/attachments';
|
||||
} from './attachments';
|
||||
|
||||
let initialized = false;
|
||||
|
||||
|
|
|
@ -1,25 +1,77 @@
|
|||
// Copyright 2018-2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { join, relative } from 'path';
|
||||
|
||||
import { join, relative, normalize } from 'path';
|
||||
import fastGlob from 'fast-glob';
|
||||
import glob from 'glob';
|
||||
import pify from 'pify';
|
||||
import fse from 'fs-extra';
|
||||
import { map } from 'lodash';
|
||||
import { map, isString } from 'lodash';
|
||||
import normalizePath from 'normalize-path';
|
||||
import { isPathInside } from '../ts/util/isPathInside';
|
||||
|
||||
import {
|
||||
getPath,
|
||||
getStickersPath,
|
||||
getBadgesPath,
|
||||
getDraftPath,
|
||||
getTempPath,
|
||||
createDeleter,
|
||||
} from '../ts/util/attachments';
|
||||
const PATH = 'attachments.noindex';
|
||||
const AVATAR_PATH = 'avatars.noindex';
|
||||
const BADGES_PATH = 'badges.noindex';
|
||||
const STICKER_PATH = 'stickers.noindex';
|
||||
const TEMP_PATH = 'temp';
|
||||
const UPDATE_CACHE_PATH = 'update-cache';
|
||||
const DRAFT_PATH = 'drafts.noindex';
|
||||
|
||||
export * from '../ts/util/attachments';
|
||||
const CACHED_PATHS = new Map<string, string>();
|
||||
|
||||
const createPathGetter =
|
||||
(subpath: string) =>
|
||||
(userDataPath: string): string => {
|
||||
if (!isString(userDataPath)) {
|
||||
throw new TypeError("'userDataPath' must be a string");
|
||||
}
|
||||
|
||||
const naivePath = join(userDataPath, subpath);
|
||||
|
||||
const cached = CACHED_PATHS.get(naivePath);
|
||||
if (cached) {
|
||||
return cached;
|
||||
}
|
||||
|
||||
let result = naivePath;
|
||||
if (fse.pathExistsSync(naivePath)) {
|
||||
result = fse.realpathSync(naivePath);
|
||||
}
|
||||
|
||||
CACHED_PATHS.set(naivePath, result);
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
export const getAvatarsPath = createPathGetter(AVATAR_PATH);
|
||||
export const getBadgesPath = createPathGetter(BADGES_PATH);
|
||||
export const getDraftPath = createPathGetter(DRAFT_PATH);
|
||||
export const getPath = createPathGetter(PATH);
|
||||
export const getStickersPath = createPathGetter(STICKER_PATH);
|
||||
export const getTempPath = createPathGetter(TEMP_PATH);
|
||||
export const getUpdateCachePath = createPathGetter(UPDATE_CACHE_PATH);
|
||||
|
||||
export const createDeleter = (
|
||||
root: string
|
||||
): ((relativePath: string) => Promise<void>) => {
|
||||
if (!isString(root)) {
|
||||
throw new TypeError("'root' must be a path");
|
||||
}
|
||||
|
||||
return async (relativePath: string): Promise<void> => {
|
||||
if (!isString(relativePath)) {
|
||||
throw new TypeError("'relativePath' must be a string");
|
||||
}
|
||||
|
||||
const absolutePath = join(root, relativePath);
|
||||
const normalized = normalize(absolutePath);
|
||||
if (!isPathInside(normalized, root)) {
|
||||
throw new Error('Invalid relative path');
|
||||
}
|
||||
await fse.remove(absolutePath);
|
||||
};
|
||||
};
|
||||
|
||||
export const getAllAttachments = async (
|
||||
userDataPath: string
|
||||
|
|
|
@ -17,7 +17,7 @@ import {
|
|||
getStickersPath,
|
||||
getTempPath,
|
||||
getUpdateCachePath,
|
||||
} from '../ts/util/attachments';
|
||||
} from './attachments';
|
||||
|
||||
type CallbackType = (response: string | ProtocolResponse) => void;
|
||||
|
||||
|
|
|
@ -1,25 +0,0 @@
|
|||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { isEnabled } from '../RemoteConfig';
|
||||
import { getEnvironment, Environment } from '../environment';
|
||||
import { isBeta } from '../util/version';
|
||||
|
||||
export function shouldShowBadges(): boolean {
|
||||
if (
|
||||
isEnabled('desktop.showUserBadges2') ||
|
||||
isEnabled('desktop.internalUser') ||
|
||||
getEnvironment() === Environment.Staging ||
|
||||
getEnvironment() === Environment.Development ||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
Boolean((window as any).STORYBOOK_ENV)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (isEnabled('desktop.showUserBadges.beta') && isBeta(window.getVersion())) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
|
@ -25,7 +25,6 @@ import { getBadgeImageFileLocalPath } from '../badges/getBadgeImageFileLocalPath
|
|||
import { getInitials } from '../util/getInitials';
|
||||
import { isBadgeVisible } from '../badges/isBadgeVisible';
|
||||
import { shouldBlurAvatar } from '../util/shouldBlurAvatar';
|
||||
import { shouldShowBadges } from '../badges/shouldShowBadges';
|
||||
|
||||
export enum AvatarBlur {
|
||||
NoBlur,
|
||||
|
@ -248,14 +247,7 @@ export const Avatar: FunctionComponent<Props> = ({
|
|||
|
||||
let badgeNode: ReactNode;
|
||||
const badgeSize = _getBadgeSize(size);
|
||||
if (
|
||||
badge &&
|
||||
theme &&
|
||||
!noteToSelf &&
|
||||
badgeSize &&
|
||||
isBadgeVisible(badge) &&
|
||||
shouldShowBadges()
|
||||
) {
|
||||
if (badge && theme && !noteToSelf && badgeSize && isBadgeVisible(badge)) {
|
||||
const badgePlacement = _getBadgePlacement(size);
|
||||
const badgeTheme =
|
||||
theme === ThemeType.light ? BadgeImageTheme.Light : BadgeImageTheme.Dark;
|
||||
|
|
|
@ -151,6 +151,7 @@ export const OngoingGroupCall = (): JSX.Element => (
|
|||
joinState: GroupCallJoinState.Joined,
|
||||
maxDevices: 5,
|
||||
groupMembers: [],
|
||||
isConversationTooBigToRing: false,
|
||||
peekedParticipants: [],
|
||||
remoteParticipants: [],
|
||||
remoteAudioLevels: new Map<number, number>(),
|
||||
|
@ -234,6 +235,7 @@ export const GroupCallSafetyNumberChanged = (): JSX.Element => (
|
|||
joinState: GroupCallJoinState.Joined,
|
||||
maxDevices: 5,
|
||||
groupMembers: [],
|
||||
isConversationTooBigToRing: false,
|
||||
peekedParticipants: [],
|
||||
remoteParticipants: [],
|
||||
remoteAudioLevels: new Map<number, number>(),
|
||||
|
|
|
@ -197,6 +197,7 @@ const ActiveCallManager: React.FC<ActiveCallManagerPropsType> = ({
|
|||
let groupMembers:
|
||||
| undefined
|
||||
| Array<Pick<ConversationType, 'id' | 'firstName' | 'title'>>;
|
||||
let isConversationTooBigToRing = false;
|
||||
|
||||
switch (activeCall.callMode) {
|
||||
case CallMode.Direct: {
|
||||
|
@ -222,6 +223,7 @@ const ActiveCallManager: React.FC<ActiveCallManagerPropsType> = ({
|
|||
case CallMode.Group: {
|
||||
showCallLobby = activeCall.joinState !== GroupCallJoinState.Joined;
|
||||
isCallFull = activeCall.deviceCount >= activeCall.maxDevices;
|
||||
isConversationTooBigToRing = activeCall.isConversationTooBigToRing;
|
||||
({ groupMembers } = activeCall);
|
||||
break;
|
||||
}
|
||||
|
@ -242,6 +244,7 @@ const ActiveCallManager: React.FC<ActiveCallManagerPropsType> = ({
|
|||
isGroupCall={activeCall.callMode === CallMode.Group}
|
||||
isGroupCallOutboundRingEnabled={isGroupCallOutboundRingEnabled}
|
||||
isCallFull={isCallFull}
|
||||
isConversationTooBigToRing={isConversationTooBigToRing}
|
||||
me={me}
|
||||
onCallCanceled={cancelActiveCall}
|
||||
onJoinCall={joinActiveCall}
|
||||
|
|
|
@ -101,6 +101,7 @@ const createActiveGroupCallProp = (overrideProps: GroupCallOverrideProps) => ({
|
|||
groupMembers: overrideProps.remoteParticipants || [],
|
||||
// Because remote participants are a superset, we can use them in place of peeked
|
||||
// participants.
|
||||
isConversationTooBigToRing: false,
|
||||
peekedParticipants:
|
||||
overrideProps.peekedParticipants || overrideProps.remoteParticipants || [],
|
||||
remoteParticipants: overrideProps.remoteParticipants || [],
|
||||
|
|
|
@ -59,6 +59,7 @@ const createProps = (overrideProps: Partial<PropsType> = {}): PropsType => {
|
|||
i18n,
|
||||
isGroupCall,
|
||||
isGroupCallOutboundRingEnabled: true,
|
||||
isConversationTooBigToRing: false,
|
||||
isCallFull: boolean('isCallFull', overrideProps.isCallFull || false),
|
||||
me:
|
||||
overrideProps.me ||
|
||||
|
|
|
@ -23,7 +23,6 @@ import type { LocalizerType } from '../types/Util';
|
|||
import { useIsOnline } from '../hooks/useIsOnline';
|
||||
import * as KeyboardLayout from '../services/keyboardLayout';
|
||||
import type { ConversationType } from '../state/ducks/conversations';
|
||||
import { isConversationTooBigToRing } from '../conversations/isConversationTooBigToRing';
|
||||
|
||||
export type PropsType = {
|
||||
availableCameras: Array<MediaDeviceInfo>;
|
||||
|
@ -46,6 +45,7 @@ export type PropsType = {
|
|||
hasLocalAudio: boolean;
|
||||
hasLocalVideo: boolean;
|
||||
i18n: LocalizerType;
|
||||
isConversationTooBigToRing: boolean;
|
||||
isGroupCall: boolean;
|
||||
isGroupCallOutboundRingEnabled: boolean;
|
||||
isCallFull?: boolean;
|
||||
|
@ -73,6 +73,7 @@ export const CallingLobby = ({
|
|||
isGroupCall = false,
|
||||
isGroupCallOutboundRingEnabled,
|
||||
isCallFull = false,
|
||||
isConversationTooBigToRing,
|
||||
me,
|
||||
onCallCanceled,
|
||||
onJoinCall,
|
||||
|
@ -166,8 +167,6 @@ export const CallingLobby = ({
|
|||
? CallingButtonType.AUDIO_ON
|
||||
: CallingButtonType.AUDIO_OFF;
|
||||
|
||||
const isGroupTooLargeToRing = isConversationTooBigToRing(conversation);
|
||||
|
||||
const isRingButtonVisible: boolean =
|
||||
isGroupCall &&
|
||||
isGroupCallOutboundRingEnabled &&
|
||||
|
@ -177,7 +176,7 @@ export const CallingLobby = ({
|
|||
let preCallInfoRingMode: RingMode;
|
||||
if (isGroupCall) {
|
||||
preCallInfoRingMode =
|
||||
outgoingRing && !isGroupTooLargeToRing
|
||||
outgoingRing && !isConversationTooBigToRing
|
||||
? RingMode.WillRing
|
||||
: RingMode.WillNotRing;
|
||||
} else {
|
||||
|
@ -189,7 +188,7 @@ export const CallingLobby = ({
|
|||
| CallingButtonType.RING_ON
|
||||
| CallingButtonType.RING_OFF;
|
||||
if (isRingButtonVisible) {
|
||||
if (isGroupTooLargeToRing) {
|
||||
if (isConversationTooBigToRing) {
|
||||
ringButtonType = CallingButtonType.RING_DISABLED;
|
||||
} else if (outgoingRing) {
|
||||
ringButtonType = CallingButtonType.RING_ON;
|
||||
|
|
|
@ -130,6 +130,7 @@ export const GroupCall = (): JSX.Element => {
|
|||
connectionState: GroupCallConnectionState.Connected,
|
||||
conversationsWithSafetyNumberChanges: [],
|
||||
groupMembers: times(3, () => getDefaultConversation()),
|
||||
isConversationTooBigToRing: false,
|
||||
joinState: GroupCallJoinState.Joined,
|
||||
maxDevices: 5,
|
||||
deviceCount: 0,
|
||||
|
|
|
@ -54,6 +54,12 @@ export default {
|
|||
General: UsernameReservationError.General,
|
||||
},
|
||||
},
|
||||
maxUsername: {
|
||||
defaultValue: 20,
|
||||
},
|
||||
minUsername: {
|
||||
defaultValue: 3,
|
||||
},
|
||||
discriminator: {
|
||||
type: { name: 'string', required: false },
|
||||
defaultValue: undefined,
|
||||
|
|
|
@ -7,12 +7,7 @@ import classNames from 'classnames';
|
|||
import type { LocalizerType } from '../types/Util';
|
||||
import type { UsernameReservationType } from '../types/Username';
|
||||
import { missingCaseError } from '../util/missingCaseError';
|
||||
import {
|
||||
getNickname,
|
||||
getDiscriminator,
|
||||
getMinNickname,
|
||||
getMaxNickname,
|
||||
} from '../util/Username';
|
||||
import { getNickname, getDiscriminator } from '../types/Username';
|
||||
import {
|
||||
UsernameReservationState,
|
||||
UsernameReservationError,
|
||||
|
@ -30,6 +25,8 @@ export type PropsDataType = Readonly<{
|
|||
reservation?: UsernameReservationType;
|
||||
error?: UsernameReservationError;
|
||||
state: UsernameReservationState;
|
||||
minNickname: number;
|
||||
maxNickname: number;
|
||||
}>;
|
||||
|
||||
export type ActionPropsDataType = Readonly<{
|
||||
|
@ -53,6 +50,8 @@ export const EditUsernameModalBody = ({
|
|||
currentUsername,
|
||||
reserveUsername,
|
||||
confirmUsername,
|
||||
minNickname,
|
||||
maxNickname,
|
||||
reservation,
|
||||
setUsernameReservationError,
|
||||
error,
|
||||
|
@ -103,12 +102,12 @@ export const EditUsernameModalBody = ({
|
|||
}
|
||||
if (error === UsernameReservationError.NotEnoughCharacters) {
|
||||
return i18n('ProfileEditor--username--check-character-min', {
|
||||
min: getMinNickname(),
|
||||
min: minNickname,
|
||||
});
|
||||
}
|
||||
if (error === UsernameReservationError.TooManyCharacters) {
|
||||
return i18n('ProfileEditor--username--check-character-max', {
|
||||
max: getMaxNickname(),
|
||||
max: maxNickname,
|
||||
});
|
||||
}
|
||||
if (error === UsernameReservationError.CheckStartingCharacter) {
|
||||
|
@ -125,7 +124,7 @@ export const EditUsernameModalBody = ({
|
|||
return;
|
||||
}
|
||||
throw missingCaseError(error);
|
||||
}, [error, i18n]);
|
||||
}, [error, i18n, minNickname, maxNickname]);
|
||||
|
||||
useEffect(() => {
|
||||
// Initial effect run
|
||||
|
|
|
@ -881,6 +881,8 @@ export const ChooseGroupMembersPartialPhoneNumber = (): JSX.Element => (
|
|||
mode: LeftPaneMode.ChooseGroupMembers,
|
||||
uuidFetchState: {},
|
||||
candidateContacts: [],
|
||||
groupSizeRecommendedLimit: 151,
|
||||
groupSizeHardLimit: 1001,
|
||||
isShowingRecommendedGroupSizeModal: false,
|
||||
isShowingMaximumGroupSizeModal: false,
|
||||
isUsernamesEnabled: true,
|
||||
|
@ -903,6 +905,8 @@ export const ChooseGroupMembersValidPhoneNumber = (): JSX.Element => (
|
|||
mode: LeftPaneMode.ChooseGroupMembers,
|
||||
uuidFetchState: {},
|
||||
candidateContacts: [],
|
||||
groupSizeRecommendedLimit: 151,
|
||||
groupSizeHardLimit: 1001,
|
||||
isShowingRecommendedGroupSizeModal: false,
|
||||
isShowingMaximumGroupSizeModal: false,
|
||||
isUsernamesEnabled: true,
|
||||
|
@ -925,6 +929,8 @@ export const ChooseGroupMembersUsername = (): JSX.Element => (
|
|||
mode: LeftPaneMode.ChooseGroupMembers,
|
||||
uuidFetchState: {},
|
||||
candidateContacts: [],
|
||||
groupSizeRecommendedLimit: 151,
|
||||
groupSizeHardLimit: 1001,
|
||||
isShowingRecommendedGroupSizeModal: false,
|
||||
isShowingMaximumGroupSizeModal: false,
|
||||
isUsernamesEnabled: true,
|
||||
|
|
|
@ -92,6 +92,8 @@ function renderEditUsernameModalBody(props: {
|
|||
return (
|
||||
<EditUsernameModalBody
|
||||
i18n={i18n}
|
||||
minNickname={3}
|
||||
maxNickname={20}
|
||||
state={UsernameReservationState.Open}
|
||||
error={undefined}
|
||||
setUsernameReservationError={action('setUsernameReservationError')}
|
||||
|
|
|
@ -51,6 +51,8 @@ const createProps = (
|
|||
makeRequest: async (conversationIds: ReadonlyArray<string>) => {
|
||||
action('onMakeRequest')(conversationIds);
|
||||
},
|
||||
maxGroupSize: 1001,
|
||||
maxRecommendedGroupSize: 151,
|
||||
requestState: RequestState.Inactive,
|
||||
renderChooseGroupMembersModal: props => {
|
||||
const { selectedConversationIds } = props;
|
||||
|
|
|
@ -12,10 +12,6 @@ import {
|
|||
} from '../../AddGroupMemberErrorDialog';
|
||||
import type { SmartChooseGroupMembersModalPropsType } from '../../../state/smart/ChooseGroupMembersModal';
|
||||
import type { SmartConfirmAdditionsModalPropsType } from '../../../state/smart/ConfirmAdditionsModal';
|
||||
import {
|
||||
getGroupSizeRecommendedLimit,
|
||||
getGroupSizeHardLimit,
|
||||
} from '../../../groups/limits';
|
||||
import {
|
||||
toggleSelectedContactForGroupAddition,
|
||||
OneTimeModalState,
|
||||
|
@ -31,6 +27,8 @@ type PropsType = {
|
|||
makeRequest: (conversationIds: ReadonlyArray<string>) => Promise<void>;
|
||||
onClose: () => void;
|
||||
requestState: RequestState;
|
||||
maxGroupSize: number;
|
||||
maxRecommendedGroupSize: number;
|
||||
|
||||
renderChooseGroupMembersModal: (
|
||||
props: SmartChooseGroupMembersModalPropsType
|
||||
|
@ -46,6 +44,8 @@ enum Stage {
|
|||
}
|
||||
|
||||
type StateType = {
|
||||
maxGroupSize: number;
|
||||
maxRecommendedGroupSize: number;
|
||||
maximumGroupSizeModalState: OneTimeModalState;
|
||||
recommendedGroupSizeModalState: OneTimeModalState;
|
||||
searchTerm: string;
|
||||
|
@ -116,8 +116,8 @@ function reducer(
|
|||
return {
|
||||
...state,
|
||||
...toggleSelectedContactForGroupAddition(action.conversationId, {
|
||||
maxGroupSize: getMaximumNumberOfContacts(),
|
||||
maxRecommendedGroupSize: getRecommendedMaximumNumberOfContacts(),
|
||||
maxGroupSize: state.maxGroupSize,
|
||||
maxRecommendedGroupSize: state.maxRecommendedGroupSize,
|
||||
maximumGroupSizeModalState: state.maximumGroupSizeModalState,
|
||||
numberOfContactsAlreadyInGroup: action.numberOfContactsAlreadyInGroup,
|
||||
recommendedGroupSizeModalState: state.recommendedGroupSizeModalState,
|
||||
|
@ -141,13 +141,12 @@ export const AddGroupMembersModal: FunctionComponent<PropsType> = ({
|
|||
i18n,
|
||||
onClose,
|
||||
makeRequest,
|
||||
maxGroupSize,
|
||||
maxRecommendedGroupSize,
|
||||
requestState,
|
||||
renderChooseGroupMembersModal,
|
||||
renderConfirmAdditionsModal,
|
||||
}) => {
|
||||
const maxGroupSize = getMaximumNumberOfContacts();
|
||||
const maxRecommendedGroupSize = getRecommendedMaximumNumberOfContacts();
|
||||
|
||||
const numberOfContactsAlreadyInGroup = conversationIdsAlreadyInGroup.size;
|
||||
const isGroupAlreadyFull = numberOfContactsAlreadyInGroup >= maxGroupSize;
|
||||
const isGroupAlreadyOverRecommendedMaximum =
|
||||
|
@ -163,6 +162,8 @@ export const AddGroupMembersModal: FunctionComponent<PropsType> = ({
|
|||
},
|
||||
dispatch,
|
||||
] = useReducer(reducer, {
|
||||
maxGroupSize,
|
||||
maxRecommendedGroupSize,
|
||||
maximumGroupSizeModalState: isGroupAlreadyFull
|
||||
? OneTimeModalState.Showing
|
||||
: OneTimeModalState.NeverShown,
|
||||
|
@ -260,11 +261,3 @@ export const AddGroupMembersModal: FunctionComponent<PropsType> = ({
|
|||
throw missingCaseError(stage);
|
||||
}
|
||||
};
|
||||
|
||||
function getRecommendedMaximumNumberOfContacts(): number {
|
||||
return getGroupSizeRecommendedLimit(151);
|
||||
}
|
||||
|
||||
function getMaximumNumberOfContacts(): number {
|
||||
return getGroupSizeHardLimit(1001);
|
||||
}
|
||||
|
|
|
@ -14,7 +14,7 @@ import type { MeasuredComponentProps } from 'react-measure';
|
|||
import Measure from 'react-measure';
|
||||
|
||||
import type { LocalizerType, ThemeType } from '../../../../types/Util';
|
||||
import { getUsernameFromSearch } from '../../../../util/Username';
|
||||
import { getUsernameFromSearch } from '../../../../types/Username';
|
||||
import { refMerger } from '../../../../util/refMerger';
|
||||
import { useRestoreFocus } from '../../../../hooks/useRestoreFocus';
|
||||
import { missingCaseError } from '../../../../util/missingCaseError';
|
||||
|
|
|
@ -62,6 +62,8 @@ const createProps = (hasGroupLink = false, expireTimer?: number): Props => ({
|
|||
isMe: i === 2,
|
||||
}),
|
||||
})),
|
||||
maxGroupSize: 1001,
|
||||
maxRecommendedGroupSize: 151,
|
||||
pendingApprovalMemberships: times(8, () => ({
|
||||
member: getDefaultConversation(),
|
||||
})),
|
||||
|
|
|
@ -74,6 +74,8 @@ export type StateProps = {
|
|||
isGroup: boolean;
|
||||
loadRecentMediaItems: (limit: number) => void;
|
||||
groupsInCommon: Array<ConversationType>;
|
||||
maxGroupSize: number;
|
||||
maxRecommendedGroupSize: number;
|
||||
memberships: Array<GroupV2Membership>;
|
||||
pendingApprovalMemberships: ReadonlyArray<GroupV2RequestingMembership>;
|
||||
pendingMemberships: ReadonlyArray<GroupV2PendingMembership>;
|
||||
|
@ -141,6 +143,8 @@ export const ConversationDetails: React.ComponentType<Props> = ({
|
|||
isGroup,
|
||||
loadRecentMediaItems,
|
||||
memberships,
|
||||
maxGroupSize,
|
||||
maxRecommendedGroupSize,
|
||||
onBlock,
|
||||
onLeave,
|
||||
onOutgoingAudioCallInConversation,
|
||||
|
@ -272,6 +276,8 @@ export const ConversationDetails: React.ComponentType<Props> = ({
|
|||
setAddGroupMembersRequestState(RequestState.InactiveWithError);
|
||||
}
|
||||
}}
|
||||
maxGroupSize={maxGroupSize}
|
||||
maxRecommendedGroupSize={maxRecommendedGroupSize}
|
||||
onClose={() => {
|
||||
setModalState(ModalState.NothingOpen);
|
||||
setEditGroupAttributesRequestState(RequestState.Inactive);
|
||||
|
|
|
@ -7,7 +7,6 @@ import React, { useCallback } from 'react';
|
|||
import { BaseConversationListItem } from './BaseConversationListItem';
|
||||
|
||||
import type { LocalizerType } from '../../types/Util';
|
||||
import { lookupConversationWithoutUuid } from '../../util/lookupConversationWithoutUuid';
|
||||
import type { LookupConversationWithoutUuidActionsType } from '../../util/lookupConversationWithoutUuid';
|
||||
import type { ShowConversationType } from '../../state/ducks/conversations';
|
||||
|
||||
|
@ -26,6 +25,7 @@ export type Props = PropsData & PropsHousekeeping;
|
|||
export const UsernameSearchResultListItem: FunctionComponent<Props> = ({
|
||||
i18n,
|
||||
isFetchingUsername,
|
||||
lookupConversationWithoutUuid,
|
||||
username,
|
||||
showUserNotFoundModal,
|
||||
setIsFetchingUUID,
|
||||
|
@ -48,11 +48,12 @@ export const UsernameSearchResultListItem: FunctionComponent<Props> = ({
|
|||
showConversation({ conversationId });
|
||||
}
|
||||
}, [
|
||||
username,
|
||||
showUserNotFoundModal,
|
||||
isFetchingUsername,
|
||||
lookupConversationWithoutUuid,
|
||||
setIsFetchingUUID,
|
||||
showConversation,
|
||||
isFetchingUsername,
|
||||
showUserNotFoundModal,
|
||||
username,
|
||||
]);
|
||||
|
||||
return (
|
||||
|
|
|
@ -18,7 +18,7 @@ import {
|
|||
} from '../AddGroupMemberErrorDialog';
|
||||
import { Button } from '../Button';
|
||||
import type { LocalizerType } from '../../types/Util';
|
||||
import { getUsernameFromSearch } from '../../util/Username';
|
||||
import { getUsernameFromSearch } from '../../types/Username';
|
||||
import type { ParsedE164Type } from '../../util/libphonenumberInstance';
|
||||
import { parseAndFormatPhoneNumber } from '../../util/libphonenumberInstance';
|
||||
import type { UUIDFetchStateType } from '../../util/uuidFetchState';
|
||||
|
@ -26,14 +26,12 @@ import {
|
|||
isFetchingByUsername,
|
||||
isFetchingByE164,
|
||||
} from '../../util/uuidFetchState';
|
||||
import {
|
||||
getGroupSizeRecommendedLimit,
|
||||
getGroupSizeHardLimit,
|
||||
} from '../../groups/limits';
|
||||
|
||||
export type LeftPaneChooseGroupMembersPropsType = {
|
||||
uuidFetchState: UUIDFetchStateType;
|
||||
candidateContacts: ReadonlyArray<ConversationType>;
|
||||
groupSizeRecommendedLimit: number;
|
||||
groupSizeHardLimit: number;
|
||||
isShowingRecommendedGroupSizeModal: boolean;
|
||||
isShowingMaximumGroupSizeModal: boolean;
|
||||
isUsernamesEnabled: boolean;
|
||||
|
@ -53,6 +51,10 @@ export class LeftPaneChooseGroupMembersHelper extends LeftPaneHelper<LeftPaneCho
|
|||
|
||||
private readonly isShowingRecommendedGroupSizeModal: boolean;
|
||||
|
||||
private readonly groupSizeRecommendedLimit: number;
|
||||
|
||||
private readonly groupSizeHardLimit: number;
|
||||
|
||||
private readonly searchTerm: string;
|
||||
|
||||
private readonly phoneNumber: ParsedE164Type | undefined;
|
||||
|
@ -70,6 +72,8 @@ export class LeftPaneChooseGroupMembersHelper extends LeftPaneHelper<LeftPaneCho
|
|||
isShowingMaximumGroupSizeModal,
|
||||
isShowingRecommendedGroupSizeModal,
|
||||
isUsernamesEnabled,
|
||||
groupSizeRecommendedLimit,
|
||||
groupSizeHardLimit,
|
||||
searchTerm,
|
||||
regionCode,
|
||||
selectedContacts,
|
||||
|
@ -78,6 +82,8 @@ export class LeftPaneChooseGroupMembersHelper extends LeftPaneHelper<LeftPaneCho
|
|||
super();
|
||||
|
||||
this.uuidFetchState = uuidFetchState;
|
||||
this.groupSizeRecommendedLimit = groupSizeRecommendedLimit - 1;
|
||||
this.groupSizeHardLimit = groupSizeHardLimit - 1;
|
||||
|
||||
this.candidateContacts = candidateContacts;
|
||||
this.isShowingMaximumGroupSizeModal = isShowingMaximumGroupSizeModal;
|
||||
|
@ -194,7 +200,7 @@ export class LeftPaneChooseGroupMembersHelper extends LeftPaneHelper<LeftPaneCho
|
|||
modalNode = (
|
||||
<AddGroupMemberErrorDialog
|
||||
i18n={i18n}
|
||||
maximumNumberOfContacts={this.getMaximumNumberOfContacts()}
|
||||
maximumNumberOfContacts={this.groupSizeHardLimit}
|
||||
mode={AddGroupMemberErrorDialogMode.MaximumGroupSize}
|
||||
onClose={closeMaximumGroupSizeModal}
|
||||
/>
|
||||
|
@ -203,7 +209,7 @@ export class LeftPaneChooseGroupMembersHelper extends LeftPaneHelper<LeftPaneCho
|
|||
modalNode = (
|
||||
<AddGroupMemberErrorDialog
|
||||
i18n={i18n}
|
||||
recommendedMaximumNumberOfContacts={this.getRecommendedMaximumNumberOfContacts()}
|
||||
recommendedMaximumNumberOfContacts={this.groupSizeRecommendedLimit}
|
||||
mode={AddGroupMemberErrorDialogMode.RecommendedMaximumGroupSize}
|
||||
onClose={closeRecommendedGroupSizeModal}
|
||||
/>
|
||||
|
@ -393,20 +399,12 @@ export class LeftPaneChooseGroupMembersHelper extends LeftPaneHelper<LeftPaneCho
|
|||
}
|
||||
|
||||
private hasSelectedMaximumNumberOfContacts(): boolean {
|
||||
return this.selectedContacts.length >= this.getMaximumNumberOfContacts();
|
||||
return this.selectedContacts.length >= this.groupSizeHardLimit;
|
||||
}
|
||||
|
||||
private hasExceededMaximumNumberOfContacts(): boolean {
|
||||
// It should be impossible to reach this state. This is here as a failsafe.
|
||||
return this.selectedContacts.length > this.getMaximumNumberOfContacts();
|
||||
}
|
||||
|
||||
private getRecommendedMaximumNumberOfContacts(): number {
|
||||
return getGroupSizeRecommendedLimit(151) - 1;
|
||||
}
|
||||
|
||||
private getMaximumNumberOfContacts(): number {
|
||||
return getGroupSizeHardLimit(1001) - 1;
|
||||
return this.selectedContacts.length > this.groupSizeHardLimit;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -14,7 +14,7 @@ import type { LocalizerType } from '../../types/Util';
|
|||
import type { ParsedE164Type } from '../../util/libphonenumberInstance';
|
||||
import { parseAndFormatPhoneNumber } from '../../util/libphonenumberInstance';
|
||||
import { missingCaseError } from '../../util/missingCaseError';
|
||||
import { getUsernameFromSearch } from '../../util/Username';
|
||||
import { getUsernameFromSearch } from '../../types/Username';
|
||||
import type { UUIDFetchStateType } from '../../util/uuidFetchState';
|
||||
import {
|
||||
isFetchingByUsername,
|
||||
|
|
|
@ -36,6 +36,7 @@ import {
|
|||
} from '../../services/notifications';
|
||||
import * as log from '../../logging/log';
|
||||
import { getPreferredBadgeSelector } from '../selectors/badges';
|
||||
import { isConversationTooBigToRing } from '../../conversations/isConversationTooBigToRing';
|
||||
|
||||
function renderDeviceSelection(): JSX.Element {
|
||||
return <SmartCallingDeviceSelection />;
|
||||
|
@ -260,6 +261,7 @@ const mapStateToActiveCallProp = (
|
|||
conversationsWithSafetyNumberChanges,
|
||||
deviceCount: peekInfo.deviceCount,
|
||||
groupMembers,
|
||||
isConversationTooBigToRing: isConversationTooBigToRing(conversation),
|
||||
joinState: call.joinState,
|
||||
maxDevices: peekInfo.maxDevices,
|
||||
peekedParticipants,
|
||||
|
|
|
@ -30,6 +30,10 @@ import type { SmartChooseGroupMembersModalPropsType } from './ChooseGroupMembers
|
|||
import { SmartChooseGroupMembersModal } from './ChooseGroupMembersModal';
|
||||
import type { SmartConfirmAdditionsModalPropsType } from './ConfirmAdditionsModal';
|
||||
import { SmartConfirmAdditionsModal } from './ConfirmAdditionsModal';
|
||||
import {
|
||||
getGroupSizeRecommendedLimit,
|
||||
getGroupSizeHardLimit,
|
||||
} from '../../groups/limits';
|
||||
|
||||
export type SmartConversationDetailsProps = {
|
||||
addMembers: (conversationIds: ReadonlyArray<string>) => Promise<void>;
|
||||
|
@ -114,6 +118,9 @@ const mapStateToProps = (
|
|||
|
||||
const groupsInCommonSorted = sortBy(groupsInCommon, 'title');
|
||||
|
||||
const maxGroupSize = getGroupSizeHardLimit(1001);
|
||||
const maxRecommendedGroupSize = getGroupSizeRecommendedLimit(151);
|
||||
|
||||
return {
|
||||
...props,
|
||||
areWeASubscriber: getAreWeASubscriber(state),
|
||||
|
@ -129,6 +136,8 @@ const mapStateToProps = (
|
|||
i18n: getIntl(state),
|
||||
isAdmin,
|
||||
...groupMemberships,
|
||||
maxGroupSize,
|
||||
maxRecommendedGroupSize,
|
||||
userAvatarData: conversation.avatars || [],
|
||||
hasGroupLink,
|
||||
groupsInCommon: groupsInCommonSorted,
|
||||
|
|
|
@ -6,6 +6,7 @@ import { mapDispatchToProps } from '../actions';
|
|||
|
||||
import type { PropsDataType } from '../../components/EditUsernameModalBody';
|
||||
import { EditUsernameModalBody } from '../../components/EditUsernameModalBody';
|
||||
import { getMinNickname, getMaxNickname } from '../../util/Username';
|
||||
|
||||
import type { StateType } from '../reducer';
|
||||
import { getIntl } from '../selectors/user';
|
||||
|
@ -23,6 +24,8 @@ function mapStateToProps(state: StateType): PropsDataType {
|
|||
return {
|
||||
i18n,
|
||||
currentUsername: username,
|
||||
minNickname: getMinNickname(),
|
||||
maxNickname: getMaxNickname(),
|
||||
state: getUsernameReservationState(state),
|
||||
reservation: getUsernameReservationObject(state),
|
||||
error: getUsernameReservationError(state),
|
||||
|
|
|
@ -49,6 +49,10 @@ import {
|
|||
isEditingAvatar,
|
||||
} from '../selectors/conversations';
|
||||
import type { WidthBreakpoint } from '../../components/_util';
|
||||
import {
|
||||
getGroupSizeRecommendedLimit,
|
||||
getGroupSizeHardLimit,
|
||||
} from '../../groups/limits';
|
||||
|
||||
import { SmartExpiredBuildDialog } from './ExpiredBuildDialog';
|
||||
import { SmartMainHeader } from './MainHeader';
|
||||
|
@ -148,6 +152,8 @@ const getModeSpecificProps = (
|
|||
return {
|
||||
mode: LeftPaneMode.ChooseGroupMembers,
|
||||
candidateContacts: getFilteredCandidateContactsForNewGroup(state),
|
||||
groupSizeRecommendedLimit: getGroupSizeRecommendedLimit(),
|
||||
groupSizeHardLimit: getGroupSizeHardLimit(),
|
||||
isShowingRecommendedGroupSizeModal:
|
||||
getRecommendedGroupSizeModalState(state) ===
|
||||
OneTimeModalState.Showing,
|
||||
|
|
|
@ -9,7 +9,6 @@ import { ContactCheckboxDisabledReason } from '../../../components/conversationL
|
|||
import { getDefaultConversation } from '../../../test-both/helpers/getDefaultConversation';
|
||||
|
||||
import { LeftPaneChooseGroupMembersHelper } from '../../../components/leftPane/LeftPaneChooseGroupMembersHelper';
|
||||
import { updateRemoteConfig } from '../../../test-both/helpers/RemoteConfigStub';
|
||||
|
||||
describe('LeftPaneChooseGroupMembersHelper', () => {
|
||||
const defaults = {
|
||||
|
@ -18,22 +17,13 @@ describe('LeftPaneChooseGroupMembersHelper', () => {
|
|||
isShowingRecommendedGroupSizeModal: false,
|
||||
isShowingMaximumGroupSizeModal: false,
|
||||
isUsernamesEnabled: true,
|
||||
groupSizeRecommendedLimit: 22,
|
||||
groupSizeHardLimit: 33,
|
||||
searchTerm: '',
|
||||
regionCode: 'US',
|
||||
selectedContacts: [],
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
await updateRemoteConfig([
|
||||
{ name: 'global.groupsv2.maxGroupSize', value: '22', enabled: true },
|
||||
{
|
||||
name: 'global.groupsv2.groupSizeHardLimit',
|
||||
value: '33',
|
||||
enabled: true,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
describe('getBackAction', () => {
|
||||
it('returns the "show composer" action', () => {
|
||||
const startComposing = sinon.fake();
|
||||
|
|
54
ts/test-node/types/Username_test.ts
Normal file
54
ts/test-node/types/Username_test.ts
Normal file
|
@ -0,0 +1,54 @@
|
|||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { assert } from 'chai';
|
||||
|
||||
import * as Username from '../../types/Username';
|
||||
|
||||
describe('Username', () => {
|
||||
describe('getUsernameFromSearch', () => {
|
||||
const { getUsernameFromSearch } = Username;
|
||||
|
||||
it('matches invalid username searches', () => {
|
||||
assert.strictEqual(getUsernameFromSearch('use'), 'use');
|
||||
assert.strictEqual(
|
||||
getUsernameFromSearch('username9012345678901234567'),
|
||||
'username9012345678901234567'
|
||||
);
|
||||
});
|
||||
|
||||
it('matches valid username searches', () => {
|
||||
assert.strictEqual(getUsernameFromSearch('username_34'), 'username_34');
|
||||
assert.strictEqual(getUsernameFromSearch('u5ername'), 'u5ername');
|
||||
assert.strictEqual(getUsernameFromSearch('username.12'), 'username.12');
|
||||
assert.strictEqual(getUsernameFromSearch('user'), 'user');
|
||||
assert.strictEqual(
|
||||
getUsernameFromSearch('username901234567890123456'),
|
||||
'username901234567890123456'
|
||||
);
|
||||
});
|
||||
|
||||
it('matches valid and invalid usernames with @ prefix', () => {
|
||||
assert.strictEqual(getUsernameFromSearch('@username!'), 'username!');
|
||||
assert.strictEqual(getUsernameFromSearch('@1username'), '1username');
|
||||
assert.strictEqual(getUsernameFromSearch('@username_34'), 'username_34');
|
||||
assert.strictEqual(getUsernameFromSearch('@username.34'), 'username.34');
|
||||
assert.strictEqual(getUsernameFromSearch('@u5ername'), 'u5ername');
|
||||
});
|
||||
|
||||
it('matches valid and invalid usernames with @ suffix', () => {
|
||||
assert.strictEqual(getUsernameFromSearch('username!@'), 'username!');
|
||||
assert.strictEqual(getUsernameFromSearch('1username@'), '1username');
|
||||
assert.strictEqual(getUsernameFromSearch('username_34@'), 'username_34');
|
||||
assert.strictEqual(getUsernameFromSearch('username.34@'), 'username.34');
|
||||
assert.strictEqual(getUsernameFromSearch('u5ername@'), 'u5ername');
|
||||
});
|
||||
|
||||
it('does not match something that looks like a phone number', () => {
|
||||
assert.isUndefined(getUsernameFromSearch('+'));
|
||||
assert.isUndefined(getUsernameFromSearch('2223'));
|
||||
assert.isUndefined(getUsernameFromSearch('+3'));
|
||||
assert.isUndefined(getUsernameFromSearch('+234234234233'));
|
||||
});
|
||||
});
|
||||
});
|
|
@ -6,52 +6,6 @@ import { assert } from 'chai';
|
|||
import * as Username from '../../util/Username';
|
||||
|
||||
describe('Username', () => {
|
||||
describe('getUsernameFromSearch', () => {
|
||||
const { getUsernameFromSearch } = Username;
|
||||
|
||||
it('matches invalid username searches', () => {
|
||||
assert.strictEqual(getUsernameFromSearch('use'), 'use');
|
||||
assert.strictEqual(
|
||||
getUsernameFromSearch('username9012345678901234567'),
|
||||
'username9012345678901234567'
|
||||
);
|
||||
});
|
||||
|
||||
it('matches valid username searches', () => {
|
||||
assert.strictEqual(getUsernameFromSearch('username_34'), 'username_34');
|
||||
assert.strictEqual(getUsernameFromSearch('u5ername'), 'u5ername');
|
||||
assert.strictEqual(getUsernameFromSearch('username.12'), 'username.12');
|
||||
assert.strictEqual(getUsernameFromSearch('user'), 'user');
|
||||
assert.strictEqual(
|
||||
getUsernameFromSearch('username901234567890123456'),
|
||||
'username901234567890123456'
|
||||
);
|
||||
});
|
||||
|
||||
it('matches valid and invalid usernames with @ prefix', () => {
|
||||
assert.strictEqual(getUsernameFromSearch('@username!'), 'username!');
|
||||
assert.strictEqual(getUsernameFromSearch('@1username'), '1username');
|
||||
assert.strictEqual(getUsernameFromSearch('@username_34'), 'username_34');
|
||||
assert.strictEqual(getUsernameFromSearch('@username.34'), 'username.34');
|
||||
assert.strictEqual(getUsernameFromSearch('@u5ername'), 'u5ername');
|
||||
});
|
||||
|
||||
it('matches valid and invalid usernames with @ suffix', () => {
|
||||
assert.strictEqual(getUsernameFromSearch('username!@'), 'username!');
|
||||
assert.strictEqual(getUsernameFromSearch('1username@'), '1username');
|
||||
assert.strictEqual(getUsernameFromSearch('username_34@'), 'username_34');
|
||||
assert.strictEqual(getUsernameFromSearch('username.34@'), 'username.34');
|
||||
assert.strictEqual(getUsernameFromSearch('u5ername@'), 'u5ername');
|
||||
});
|
||||
|
||||
it('does not match something that looks like a phone number', () => {
|
||||
assert.isUndefined(getUsernameFromSearch('+'));
|
||||
assert.isUndefined(getUsernameFromSearch('2223'));
|
||||
assert.isUndefined(getUsernameFromSearch('+3'));
|
||||
assert.isUndefined(getUsernameFromSearch('+234234234233'));
|
||||
});
|
||||
});
|
||||
|
||||
describe('isValidUsername', () => {
|
||||
const { isValidUsername } = Username;
|
||||
|
||||
|
|
|
@ -15,7 +15,6 @@ import { blobToArrayBuffer } from 'blob-util';
|
|||
|
||||
import type { LoggerType } from './Logging';
|
||||
import * as MIME from './MIME';
|
||||
import * as log from '../logging/log';
|
||||
import { toLogFormat } from './errors';
|
||||
import { SignalService } from '../protobuf';
|
||||
import {
|
||||
|
@ -24,11 +23,7 @@ import {
|
|||
} from '../util/GoogleChrome';
|
||||
import type { LocalizerType } from './Util';
|
||||
import { ThemeType } from './Util';
|
||||
import { scaleImageToLevel } from '../util/scaleImageToLevel';
|
||||
import * as GoogleChrome from '../util/GoogleChrome';
|
||||
import { parseIntOrThrow } from '../util/parseIntOrThrow';
|
||||
import { getValue } from '../RemoteConfig';
|
||||
import { isRecord } from '../util/isRecord';
|
||||
import { ReadStatus } from '../messages/MessageReadStatus';
|
||||
import type { MessageStatusType } from '../components/conversation/Message';
|
||||
|
||||
|
@ -249,73 +244,6 @@ export function isValid(
|
|||
return true;
|
||||
}
|
||||
|
||||
// Upgrade steps
|
||||
// NOTE: This step strips all EXIF metadata from JPEG images as
|
||||
// part of re-encoding the image:
|
||||
export async function autoOrientJPEG(
|
||||
attachment: AttachmentType,
|
||||
{ logger }: { logger: LoggerType },
|
||||
{
|
||||
sendHQImages = false,
|
||||
isIncoming = false,
|
||||
}: {
|
||||
sendHQImages?: boolean;
|
||||
isIncoming?: boolean;
|
||||
} = {}
|
||||
): Promise<AttachmentType> {
|
||||
if (isIncoming && !MIME.isJPEG(attachment.contentType)) {
|
||||
return attachment;
|
||||
}
|
||||
|
||||
if (!canBeTranscoded(attachment)) {
|
||||
return attachment;
|
||||
}
|
||||
|
||||
// If we haven't downloaded the attachment yet, we won't have the data.
|
||||
// All images go through handleImageAttachment before being sent and thus have
|
||||
// already been scaled to level, oriented, stripped of exif data, and saved
|
||||
// in high quality format. If we want to send the image in HQ we can return
|
||||
// the attachment as-is. Otherwise we'll have to further scale it down.
|
||||
if (!attachment.data || sendHQImages) {
|
||||
return attachment;
|
||||
}
|
||||
|
||||
const dataBlob = new Blob([attachment.data], {
|
||||
type: attachment.contentType,
|
||||
});
|
||||
try {
|
||||
const { blob: xcodedDataBlob } = await scaleImageToLevel(
|
||||
dataBlob,
|
||||
attachment.contentType,
|
||||
isIncoming
|
||||
);
|
||||
const xcodedDataArrayBuffer = await blobToArrayBuffer(xcodedDataBlob);
|
||||
|
||||
// IMPORTANT: We overwrite the existing `data` `Uint8Array` losing the original
|
||||
// image data. Ideally, we’d preserve the original image data for users who want to
|
||||
// retain it but due to reports of data loss, we don’t want to overburden IndexedDB
|
||||
// by potentially doubling stored image data.
|
||||
// See: https://github.com/signalapp/Signal-Desktop/issues/1589
|
||||
const xcodedAttachment = {
|
||||
// `digest` is no longer valid for auto-oriented image data, so we discard it:
|
||||
...omit(attachment, 'digest'),
|
||||
data: new Uint8Array(xcodedDataArrayBuffer),
|
||||
size: xcodedDataArrayBuffer.byteLength,
|
||||
};
|
||||
|
||||
return xcodedAttachment;
|
||||
} catch (error: unknown) {
|
||||
const errorString =
|
||||
isRecord(error) && 'stack' in error ? error.stack : error;
|
||||
logger.error(
|
||||
'autoOrientJPEG: Failed to rotate/scale attachment',
|
||||
errorString
|
||||
);
|
||||
|
||||
return attachment;
|
||||
}
|
||||
}
|
||||
|
||||
const UNICODE_LEFT_TO_RIGHT_OVERRIDE = '\u202D';
|
||||
const UNICODE_RIGHT_TO_LEFT_OVERRIDE = '\u202E';
|
||||
const UNICODE_REPLACEMENT_CHARACTER = '\uFFFD';
|
||||
|
@ -1047,23 +975,6 @@ export const getFileExtension = (
|
|||
}
|
||||
};
|
||||
|
||||
const MEBIBYTE = 1024 * 1024;
|
||||
const DEFAULT_MAX = 100 * MEBIBYTE;
|
||||
|
||||
export const getMaximumAttachmentSize = (): number => {
|
||||
try {
|
||||
return parseIntOrThrow(
|
||||
getValue('global.attachments.maxBytes'),
|
||||
'preProcessAttachment/maxAttachmentSize'
|
||||
);
|
||||
} catch (error) {
|
||||
log.warn(
|
||||
'Failed to parse integer out of global.attachments.maxBytes feature flag'
|
||||
);
|
||||
return DEFAULT_MAX;
|
||||
}
|
||||
};
|
||||
|
||||
export const defaultBlurHash = (theme: ThemeType = ThemeType.light): string => {
|
||||
if (theme === ThemeType.dark) {
|
||||
return 'L05OQnoffQofoffQfQfQfQfQfQfQ';
|
||||
|
|
|
@ -72,6 +72,7 @@ type ActiveGroupCallType = ActiveCallBaseType & {
|
|||
maxDevices: number;
|
||||
deviceCount: number;
|
||||
groupMembers: Array<Pick<ConversationType, 'id' | 'firstName' | 'title'>>;
|
||||
isConversationTooBigToRing: boolean;
|
||||
peekedParticipants: Array<ConversationType>;
|
||||
remoteParticipants: Array<GroupCallRemoteParticipantType>;
|
||||
remoteAudioLevels: Map<number, number>;
|
||||
|
|
|
@ -5,8 +5,8 @@ import { isFunction, isObject, isString, omit } from 'lodash';
|
|||
|
||||
import * as Contact from './EmbeddedContact';
|
||||
import type { AttachmentType, AttachmentWithHydratedData } from './Attachment';
|
||||
import { autoOrientJPEG } from '../util/attachments';
|
||||
import {
|
||||
autoOrientJPEG,
|
||||
captureDimensionsAndScreenshot,
|
||||
hasData,
|
||||
migrateDataToFileSystem,
|
||||
|
|
|
@ -11,3 +11,41 @@ export enum ReserveUsernameError {
|
|||
Unprocessable = 'Unprocessable',
|
||||
Conflict = 'Conflict',
|
||||
}
|
||||
|
||||
export function getUsernameFromSearch(searchTerm: string): string | undefined {
|
||||
// Search term contains username if it:
|
||||
// - Is a valid username with or without a discriminator
|
||||
// - Starts with @
|
||||
// - Ends with @
|
||||
const match = searchTerm.match(
|
||||
/^(?:(?<valid>[a-z_][0-9a-z_]*(?:\.\d*)?)|@(?<start>.*?)@?|@?(?<end>.*?)?@)$/
|
||||
);
|
||||
if (!match) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const { groups } = match;
|
||||
if (!groups) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return (groups.valid || groups.start || groups.end) ?? undefined;
|
||||
}
|
||||
|
||||
export function getNickname(username: string): string | undefined {
|
||||
const match = username.match(/^(.*?)(?:\.|$)/);
|
||||
if (!match) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return match[1];
|
||||
}
|
||||
|
||||
export function getDiscriminator(username: string): string {
|
||||
const match = username.match(/(\..*)$/);
|
||||
if (!match) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return match[1];
|
||||
}
|
||||
|
|
|
@ -25,7 +25,7 @@ import type { BrowserWindow } from 'electron';
|
|||
import { app, ipcMain } from 'electron';
|
||||
|
||||
import * as durations from '../util/durations';
|
||||
import { getTempPath, getUpdateCachePath } from '../util/attachments';
|
||||
import { getTempPath, getUpdateCachePath } from '../../app/attachments';
|
||||
import { DialogType } from '../types/Dialogs';
|
||||
import * as Errors from '../types/errors';
|
||||
import { isAlpha, isBeta, isStaging } from '../util/version';
|
||||
|
|
|
@ -39,41 +39,3 @@ export function isValidUsername(username: string): boolean {
|
|||
const [, nickname] = match;
|
||||
return isValidNickname(nickname);
|
||||
}
|
||||
|
||||
export function getUsernameFromSearch(searchTerm: string): string | undefined {
|
||||
// Search term contains username if it:
|
||||
// - Is a valid username with or without a discriminator
|
||||
// - Starts with @
|
||||
// - Ends with @
|
||||
const match = searchTerm.match(
|
||||
/^(?:(?<valid>[a-z_][0-9a-z_]*(?:\.\d*)?)|@(?<start>.*?)@?|@?(?<end>.*?)?@)$/
|
||||
);
|
||||
if (!match) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const { groups } = match;
|
||||
if (!groups) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return (groups.valid || groups.start || groups.end) ?? undefined;
|
||||
}
|
||||
|
||||
export function getNickname(username: string): string | undefined {
|
||||
const match = username.match(/^(.*?)(?:\.|$)/);
|
||||
if (!match) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return match[1];
|
||||
}
|
||||
|
||||
export function getDiscriminator(username: string): string {
|
||||
const match = username.match(/(\..*)$/);
|
||||
if (!match) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return match[1];
|
||||
}
|
||||
|
|
|
@ -1,71 +1,99 @@
|
|||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { isString } from 'lodash';
|
||||
import { join, normalize } from 'path';
|
||||
import fse from 'fs-extra';
|
||||
import { omit } from 'lodash';
|
||||
import { blobToArrayBuffer } from 'blob-util';
|
||||
import * as log from '../logging/log';
|
||||
import { getValue } from '../RemoteConfig';
|
||||
|
||||
import { isPathInside } from './isPathInside';
|
||||
import { parseIntOrThrow } from './parseIntOrThrow';
|
||||
import { scaleImageToLevel } from './scaleImageToLevel';
|
||||
import { isRecord } from './isRecord';
|
||||
import type { AttachmentType } from '../types/Attachment';
|
||||
import { canBeTranscoded } from '../types/Attachment';
|
||||
import type { LoggerType } from '../types/Logging';
|
||||
import * as MIME from '../types/MIME';
|
||||
|
||||
const PATH = 'attachments.noindex';
|
||||
const AVATAR_PATH = 'avatars.noindex';
|
||||
const BADGES_PATH = 'badges.noindex';
|
||||
const STICKER_PATH = 'stickers.noindex';
|
||||
const TEMP_PATH = 'temp';
|
||||
const UPDATE_CACHE_PATH = 'update-cache';
|
||||
const DRAFT_PATH = 'drafts.noindex';
|
||||
const MEBIBYTE = 1024 * 1024;
|
||||
const DEFAULT_MAX = 100 * MEBIBYTE;
|
||||
|
||||
const CACHED_PATHS = new Map<string, string>();
|
||||
export const getMaximumAttachmentSize = (): number => {
|
||||
try {
|
||||
return parseIntOrThrow(
|
||||
getValue('global.attachments.maxBytes'),
|
||||
'preProcessAttachment/maxAttachmentSize'
|
||||
);
|
||||
} catch (error) {
|
||||
log.warn(
|
||||
'Failed to parse integer out of global.attachments.maxBytes feature flag'
|
||||
);
|
||||
return DEFAULT_MAX;
|
||||
}
|
||||
};
|
||||
|
||||
const createPathGetter =
|
||||
(subpath: string) =>
|
||||
(userDataPath: string): string => {
|
||||
if (!isString(userDataPath)) {
|
||||
throw new TypeError("'userDataPath' must be a string");
|
||||
}
|
||||
|
||||
const naivePath = join(userDataPath, subpath);
|
||||
|
||||
const cached = CACHED_PATHS.get(naivePath);
|
||||
if (cached) {
|
||||
return cached;
|
||||
}
|
||||
|
||||
let result = naivePath;
|
||||
if (fse.pathExistsSync(naivePath)) {
|
||||
result = fse.realpathSync(naivePath);
|
||||
}
|
||||
|
||||
CACHED_PATHS.set(naivePath, result);
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
export const getAvatarsPath = createPathGetter(AVATAR_PATH);
|
||||
export const getBadgesPath = createPathGetter(BADGES_PATH);
|
||||
export const getDraftPath = createPathGetter(DRAFT_PATH);
|
||||
export const getPath = createPathGetter(PATH);
|
||||
export const getStickersPath = createPathGetter(STICKER_PATH);
|
||||
export const getTempPath = createPathGetter(TEMP_PATH);
|
||||
export const getUpdateCachePath = createPathGetter(UPDATE_CACHE_PATH);
|
||||
|
||||
export const createDeleter = (
|
||||
root: string
|
||||
): ((relativePath: string) => Promise<void>) => {
|
||||
if (!isString(root)) {
|
||||
throw new TypeError("'root' must be a path");
|
||||
// Upgrade steps
|
||||
// NOTE: This step strips all EXIF metadata from JPEG images as
|
||||
// part of re-encoding the image:
|
||||
export async function autoOrientJPEG(
|
||||
attachment: AttachmentType,
|
||||
{ logger }: { logger: LoggerType },
|
||||
{
|
||||
sendHQImages = false,
|
||||
isIncoming = false,
|
||||
}: {
|
||||
sendHQImages?: boolean;
|
||||
isIncoming?: boolean;
|
||||
} = {}
|
||||
): Promise<AttachmentType> {
|
||||
if (isIncoming && !MIME.isJPEG(attachment.contentType)) {
|
||||
return attachment;
|
||||
}
|
||||
|
||||
return async (relativePath: string): Promise<void> => {
|
||||
if (!isString(relativePath)) {
|
||||
throw new TypeError("'relativePath' must be a string");
|
||||
}
|
||||
if (!canBeTranscoded(attachment)) {
|
||||
return attachment;
|
||||
}
|
||||
|
||||
const absolutePath = join(root, relativePath);
|
||||
const normalized = normalize(absolutePath);
|
||||
if (!isPathInside(normalized, root)) {
|
||||
throw new Error('Invalid relative path');
|
||||
}
|
||||
await fse.remove(absolutePath);
|
||||
};
|
||||
};
|
||||
// If we haven't downloaded the attachment yet, we won't have the data.
|
||||
// All images go through handleImageAttachment before being sent and thus have
|
||||
// already been scaled to level, oriented, stripped of exif data, and saved
|
||||
// in high quality format. If we want to send the image in HQ we can return
|
||||
// the attachment as-is. Otherwise we'll have to further scale it down.
|
||||
if (!attachment.data || sendHQImages) {
|
||||
return attachment;
|
||||
}
|
||||
|
||||
const dataBlob = new Blob([attachment.data], {
|
||||
type: attachment.contentType,
|
||||
});
|
||||
try {
|
||||
const { blob: xcodedDataBlob } = await scaleImageToLevel(
|
||||
dataBlob,
|
||||
attachment.contentType,
|
||||
isIncoming
|
||||
);
|
||||
const xcodedDataArrayBuffer = await blobToArrayBuffer(xcodedDataBlob);
|
||||
|
||||
// IMPORTANT: We overwrite the existing `data` `Uint8Array` losing the original
|
||||
// image data. Ideally, we’d preserve the original image data for users who want to
|
||||
// retain it but due to reports of data loss, we don’t want to overburden IndexedDB
|
||||
// by potentially doubling stored image data.
|
||||
// See: https://github.com/signalapp/Signal-Desktop/issues/1589
|
||||
const xcodedAttachment = {
|
||||
// `digest` is no longer valid for auto-oriented image data, so we discard it:
|
||||
...omit(attachment, 'digest'),
|
||||
data: new Uint8Array(xcodedDataArrayBuffer),
|
||||
size: xcodedDataArrayBuffer.byteLength,
|
||||
};
|
||||
|
||||
return xcodedAttachment;
|
||||
} catch (error: unknown) {
|
||||
const errorString =
|
||||
isRecord(error) && 'stack' in error ? error.stack : error;
|
||||
logger.error(
|
||||
'autoOrientJPEG: Failed to rotate/scale attachment',
|
||||
errorString
|
||||
);
|
||||
|
||||
return attachment;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import type { AttachmentType } from '../types/Attachment';
|
||||
import { getMaximumAttachmentSize } from '../types/Attachment';
|
||||
import { getMaximumAttachmentSize } from './attachments';
|
||||
import { showToast } from './showToast';
|
||||
import { ToastFileSize } from '../components/ToastFileSize';
|
||||
|
||||
|
|
|
@ -8,7 +8,7 @@ import type {
|
|||
AttachmentDraftType,
|
||||
InMemoryAttachmentDraftType,
|
||||
} from '../types/Attachment';
|
||||
import { getMaximumAttachmentSize } from '../types/Attachment';
|
||||
import { getMaximumAttachmentSize } from './attachments';
|
||||
import { AttachmentToastType } from '../types/AttachmentToastType';
|
||||
import { fileToBytes } from './fileToBytes';
|
||||
import { handleImageAttachment } from './handleImageAttachment';
|
||||
|
|
|
@ -14,7 +14,7 @@ import { isPathInside } from '../util/isPathInside';
|
|||
import { writeWindowsZoneIdentifier } from '../util/windowsZoneIdentifier';
|
||||
import { isWindows } from '../OS';
|
||||
|
||||
export * from '../util/attachments';
|
||||
export * from '../../app/attachments';
|
||||
|
||||
type FSAttrType = {
|
||||
set: (path: string, attribute: string, value: string) => Promise<void>;
|
||||
|
|
Loading…
Reference in a new issue