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