- {
- dispatch(updateLightBoxOptions(null));
- }}
- />
+
{onSave ?
: null}
diff --git a/ts/components/lightbox/LightboxGallery.tsx b/ts/components/lightbox/LightboxGallery.tsx
index e1ab69137..d3c4055bf 100644
--- a/ts/components/lightbox/LightboxGallery.tsx
+++ b/ts/components/lightbox/LightboxGallery.tsx
@@ -96,10 +96,10 @@ export const LightboxGallery = (props: Props) => {
useKey(
'Escape',
() => {
- dispatch(updateLightBoxOptions(null));
if (onClose) {
onClose();
}
+ dispatch(updateLightBoxOptions(null));
},
undefined,
[currentIndex, updateLightBoxOptions, dispatch, onClose]
@@ -122,6 +122,7 @@ export const LightboxGallery = (props: Props) => {
onPrevious={hasPrevious ? onPrevious : undefined}
onNext={hasNext ? onNext : undefined}
onSave={handleSave}
+ onClose={onClose}
objectURL={objectURL}
caption={caption}
contentType={selectedMedia.contentType}
diff --git a/ts/components/registration/components/BackButton.tsx b/ts/components/registration/components/BackButton.tsx
index 6f7f0388d..f1ec39f7b 100644
--- a/ts/components/registration/components/BackButton.tsx
+++ b/ts/components/registration/components/BackButton.tsx
@@ -28,12 +28,14 @@ export const BackButtonWithinContainer = ({
children,
margin,
callback,
+ onQuitVisible,
shouldQuitOnClick,
quitMessage,
}: {
children: ReactNode;
margin?: string;
callback?: () => void;
+ onQuitVisible?: () => void;
shouldQuitOnClick?: boolean;
quitMessage?: string;
}) => {
@@ -48,6 +50,7 @@ export const BackButtonWithinContainer = ({
@@ -59,10 +62,12 @@ export const BackButtonWithinContainer = ({
export const BackButton = ({
callback,
+ onQuitVisible,
shouldQuitOnClick,
quitMessage,
}: {
callback?: () => void;
+ onQuitVisible?: () => void;
shouldQuitOnClick?: boolean;
quitMessage?: string;
}) => {
@@ -81,6 +86,9 @@ export const BackButton = ({
padding={'0'}
onClick={() => {
if (shouldQuitOnClick && quitMessage) {
+ if (onQuitVisible) {
+ onQuitVisible();
+ }
dispatch(
updateQuitModal({
title: window.i18n('warning'),
diff --git a/ts/components/registration/stages/RestoreAccount.tsx b/ts/components/registration/stages/RestoreAccount.tsx
index af3e7850b..777efd254 100644
--- a/ts/components/registration/stages/RestoreAccount.tsx
+++ b/ts/components/registration/stages/RestoreAccount.tsx
@@ -117,6 +117,8 @@ async function signInWithNewDisplayName({
}
}
+let abortController = new AbortController();
+
export const RestoreAccount = () => {
const step = useOnboardAccountRestorationStep();
const recoveryPassword = useRecoveryPassword();
@@ -133,13 +135,15 @@ export const RestoreAccount = () => {
if (!(!!recoveryPassword && !recoveryPasswordError)) {
return;
}
+ const trimmedPassword = recoveryPassword.trim();
+ setRecoveryPassword(trimmedPassword);
- const abortController = new AbortController();
try {
+ abortController = new AbortController();
dispatch(setProgress(0));
dispatch(setAccountRestorationStep(AccountRestoration.Loading));
await signInAndFetchDisplayName({
- recoveryPassword,
+ recoveryPassword: trimmedPassword,
dispatch,
abortSignal: abortController.signal,
});
@@ -199,6 +203,23 @@ export const RestoreAccount = () => {
margin={'2px 0 0 -36px'}
shouldQuitOnClick={step !== AccountRestoration.RecoveryPassword}
quitMessage={window.i18n('onboardingBackLoadAccount')}
+ onQuitVisible={() => {
+ if (!abortController.signal.aborted) {
+ abortController.abort();
+ }
+ dispatch(setRecoveryPassword(''));
+ dispatch(setDisplayName(''));
+ dispatch(setProgress(0));
+ dispatch(setRecoveryPasswordError(undefined));
+ dispatch(setDisplayNameError(undefined));
+ if (
+ step === AccountRestoration.Loading ||
+ step === AccountRestoration.Finishing ||
+ step === AccountRestoration.Finished
+ ) {
+ dispatch(setAccountRestorationStep(AccountRestoration.RecoveryPassword));
+ }
+ }}
callback={() => {
dispatch(setRecoveryPassword(''));
dispatch(setDisplayName(''));
@@ -248,7 +269,6 @@ export const RestoreAccount = () => {
}}
onEnterPressed={recoverAndFetchDisplayName}
error={recoveryPasswordError}
- maxLength={LIBSESSION_CONSTANTS.CONTACT_MAX_NAME_LENGTH}
enableShowHideButton={true}
showHideButtonAriaLabels={{
hide: 'Hide recovery password toggle',
@@ -289,6 +309,7 @@ export const RestoreAccount = () => {
}}
onEnterPressed={recoverAndEnterDisplayName}
error={displayNameError}
+ maxLength={LIBSESSION_CONSTANTS.CONTACT_MAX_NAME_LENGTH}
inputDataTestId="display-name-input"
/>
diff --git a/ts/components/registration/utils/index.tsx b/ts/components/registration/utils/index.tsx
index 3ea00dfc6..4220c06d2 100644
--- a/ts/components/registration/utils/index.tsx
+++ b/ts/components/registration/utils/index.tsx
@@ -9,15 +9,14 @@ export function sanitizeDisplayNameOrToast(
) {
try {
const sanitizedName = sanitizeSessionUsername(displayName);
- const trimName = sanitizedName.trim();
- const errorString = !trimName ? window.i18n('displayNameEmpty') : undefined;
+ const errorString = !sanitizedName ? window.i18n('displayNameEmpty') : undefined;
if (dispatch) {
dispatch(onDisplayNameError(errorString));
} else {
onDisplayNameError(errorString); // this is is either calling dispatch in the caller or just `setDisplayNameError`
}
- return trimName;
+ return sanitizedName;
} catch (e) {
if (dispatch) {
dispatch(onDisplayNameError(window.i18n('displayNameErrorDescriptionShorter')));
diff --git a/ts/components/search/MessageSearchResults.tsx b/ts/components/search/MessageSearchResults.tsx
index 8fe2af7c9..7ea4e209a 100644
--- a/ts/components/search/MessageSearchResults.tsx
+++ b/ts/components/search/MessageSearchResults.tsx
@@ -15,6 +15,7 @@ import { Avatar, AvatarSize } from '../avatar/Avatar';
import { MessageBodyHighlight } from '../basic/MessageBodyHighlight';
import { ContactName } from '../conversation/ContactName';
import { Timestamp } from '../conversation/Timestamp';
+import { leftPaneListWidth } from '../leftpane/LeftPane';
const StyledConversationTitleResults = styled.div`
flex-grow: 1;
@@ -39,11 +40,9 @@ const StyledConversationFromUserInGroup = styled(StyledConversationTitleResults)
`;
const StyledSearchResults = styled.div`
- padding: 8px;
- padding-inline-start: 16px;
- padding-inline-end: 16px;
- min-height: 64px;
- max-width: 300px;
+ padding: var(--margins-sm) var(--margins-md);
+ height: var(--contact-row-height);
+ max-width: ${leftPaneListWidth}px;
display: flex;
flex-direction: row;
diff --git a/ts/components/search/SearchResults.tsx b/ts/components/search/SearchResults.tsx
index f86d21124..651d40ee9 100644
--- a/ts/components/search/SearchResults.tsx
+++ b/ts/components/search/SearchResults.tsx
@@ -12,6 +12,7 @@ import {
getSearchResultsList,
getSearchTerm,
} from '../../state/selectors/search';
+import { calcContactRowHeight } from '../leftpane/overlay/choose-action/ContactsListWithBreaks';
const StyledSeparatorSection = styled.div<{ isSubtitle: boolean }>`
height: 36px;
@@ -63,15 +64,16 @@ function isContact(item: SearchResultsMergedListItem): item is { contactConvoId:
const VirtualizedList = () => {
const searchResultList = useSelector(getSearchResultsList);
+
return (
{({ height, width }) => (
{
- return isString(searchResultList[rowPos.index]) ? 36 : 64;
- }}
+ rowHeight={params =>
+ calcContactRowHeight(searchResultList, params, { breakRowHeight: 36 })
+ }
rowRenderer={({ index, key, style }) => {
const row = searchResultList[index];
if (!row) {
diff --git a/ts/components/settings/section/CategoryRecoveryPassword.tsx b/ts/components/settings/section/CategoryRecoveryPassword.tsx
index d9abcb06d..c3f9f06e4 100644
--- a/ts/components/settings/section/CategoryRecoveryPassword.tsx
+++ b/ts/components/settings/section/CategoryRecoveryPassword.tsx
@@ -1,6 +1,6 @@
import { isEmpty } from 'lodash';
import { useState } from 'react';
-import { useDispatch } from 'react-redux';
+import { useDispatch, useSelector } from 'react-redux';
import useMount from 'react-use/lib/useMount';
import styled from 'styled-components';
import { useIconToImageURL } from '../../../hooks/useIconToImageURL';
@@ -24,6 +24,8 @@ import { SpacerMD, SpacerSM } from '../../basic/Text';
import { CopyToClipboardIcon } from '../../buttons/CopyToClipboardButton';
import { SessionIconButton } from '../../icon';
import { SessionSettingButtonItem, SessionSettingsItemWrapper } from '../SessionSettingListItem';
+import { useHotkey } from '../../../hooks/useHotkey';
+import { getIsModalVisble } from '../../../state/selectors/modal';
const StyledSettingsItemContainer = styled.div`
p {
@@ -62,6 +64,7 @@ export const SettingsCategoryRecoveryPassword = () => {
const [isQRVisible, setIsQRVisible] = useState(false);
const hideRecoveryPassword = useHideRecoveryPasswordEnabled();
+ const isModalVisible = useSelector(getIsModalVisble);
const isDarkTheme = useIsDarkTheme();
const { dataURL, iconSize, iconColor, backgroundColor, loading } = useIconToImageURL(qrLogoProps);
@@ -90,6 +93,16 @@ export const SettingsCategoryRecoveryPassword = () => {
}
});
+ useHotkey(
+ 'v',
+ () => {
+ if (!isModalVisible) {
+ setIsQRVisible(!isQRVisible);
+ }
+ },
+ (hasPassword && !passwordValid) || loadingSeed || hideRecoveryPassword
+ );
+
if ((hasPassword && !passwordValid) || loadingSeed || hideRecoveryPassword) {
return null;
}
diff --git a/ts/hooks/useParamSelector.ts b/ts/hooks/useParamSelector.ts
index e51be0c27..142a65471 100644
--- a/ts/hooks/useParamSelector.ts
+++ b/ts/hooks/useParamSelector.ts
@@ -199,6 +199,7 @@ export function useIsOutgoingRequest(convoId?: string) {
if (!convoProps) {
return false;
}
+
return Boolean(
convoProps &&
hasValidOutgoingRequestValues({
diff --git a/ts/models/conversation.ts b/ts/models/conversation.ts
index 2f512c9e2..03e2fcd68 100644
--- a/ts/models/conversation.ts
+++ b/ts/models/conversation.ts
@@ -2598,7 +2598,14 @@ export function hasValidOutgoingRequestValues({
}): boolean {
const isActive = activeAt && isFinite(activeAt) && activeAt > 0;
- return Boolean(!isMe && isApproved && isPrivate && !isBlocked && !didApproveMe && isActive);
+ // Started a new message, but haven't sent a message yet
+ const emptyConvo = !isMe && !isApproved && isPrivate && !isBlocked && !didApproveMe && !!isActive;
+
+ // Started a new message, and sent a message
+ const sentOutgoingRequest =
+ !isMe && isApproved && isPrivate && !isBlocked && !didApproveMe && !!isActive;
+
+ return emptyConvo || sentOutgoingRequest;
}
/**
diff --git a/ts/receiver/configMessage.ts b/ts/receiver/configMessage.ts
index abe4c49d2..2b680fa07 100644
--- a/ts/receiver/configMessage.ts
+++ b/ts/receiver/configMessage.ts
@@ -602,6 +602,8 @@ async function handleLegacyGroupUpdate(latestEnvelopeTimestamp: number) {
const members = fromWrapper.members.map(m => m.pubkeyHex);
const admins = fromWrapper.members.filter(m => m.isAdmin).map(m => m.pubkeyHex);
+
+ const creationTimestamp = fromWrapper.joinedAtSeconds ? fromWrapper.joinedAtSeconds * 1000 : 0;
// then for all the existing legacy group in the wrapper, we need to override the field of what we have in the DB with what is in the wrapper
// We only set group admins on group creation
const groupDetails: ClosedGroup.GroupInfo = {
@@ -610,10 +612,9 @@ async function handleLegacyGroupUpdate(latestEnvelopeTimestamp: number) {
members,
admins,
activeAt:
- !!legacyGroupConvo.get('active_at') &&
- legacyGroupConvo.get('active_at') < latestEnvelopeTimestamp
+ !!legacyGroupConvo.get('active_at') && legacyGroupConvo.get('active_at') > creationTimestamp
? legacyGroupConvo.get('active_at')
- : latestEnvelopeTimestamp,
+ : creationTimestamp,
};
await ClosedGroup.updateOrCreateClosedGroup(groupDetails);
diff --git a/ts/state/selectors/conversations.ts b/ts/state/selectors/conversations.ts
index 3efe592f6..5473c6e55 100644
--- a/ts/state/selectors/conversations.ts
+++ b/ts/state/selectors/conversations.ts
@@ -297,18 +297,12 @@ const _getLeftPaneConversationIds = (
.map(m => m.id);
};
-const _getPrivateFriendsConversations = (
+const _getContacts = (
sortedConversations: Array
): Array => {
return sortedConversations.filter(convo => {
- return (
- convo.isPrivate &&
- !convo.isMe &&
- !convo.isBlocked &&
- convo.isApproved &&
- convo.didApproveMe &&
- convo.activeAt !== undefined
- );
+ // a private conversation not approved is a message request. Include them in the list of contacts
+ return !convo.isBlocked && convo.isPrivate && !convo.isMe;
});
};
@@ -437,7 +431,6 @@ export const getUnreadConversationRequests = createSelector(
* - approved (or message requests are disabled)
* - active_at is set to something truthy
*/
-
export const getLeftPaneConversationIds = createSelector(
getSortedConversations,
_getLeftPaneConversationIds
@@ -450,10 +443,16 @@ export const getLeftPaneConversationIdsCount = createSelector(
}
);
-const getDirectContacts = createSelector(getSortedConversations, _getPrivateFriendsConversations);
+/**
+ * Returns all the conversation ids of contacts which are
+ * - private
+ * - not me
+ * - not blocked
+ */
+const getContacts = createSelector(getSortedConversations, _getContacts);
-export const getDirectContactsCount = createSelector(
- getDirectContacts,
+export const getContactsCount = createSelector(
+ getContacts,
(contacts: Array) => contacts.length
);
@@ -463,8 +462,8 @@ export type DirectContactsByNameType = {
};
// make sure that createSelector is called here so this function is memoized
-export const getDirectContactsByName = createSelector(
- getDirectContacts,
+export const getSortedContacts = createSelector(
+ getContacts,
(contacts: Array): Array => {
const us = UserUtils.getOurPubKeyStrFromCache();
const extractedContacts = contacts
@@ -475,20 +474,61 @@ export const getDirectContactsByName = createSelector(
displayName: m.nickname || m.displayNameInProfile,
};
});
- const extractedContactsNoDisplayName = sortBy(
- extractedContacts.filter(m => !m.displayName),
- 'id'
+
+ const contactsStartingWithANumber = sortBy(
+ extractedContacts.filter(
+ m => !m.displayName || (m.displayName && m.displayName[0].match(/^[0-9]+$/))
+ ),
+ m => m.displayName || m.id
);
- const extractedContactsWithDisplayName = sortBy(
- extractedContacts.filter(m => Boolean(m.displayName)),
+
+ const contactsWithDisplayName = sortBy(
+ extractedContacts.filter(m => !!m.displayName && !m.displayName[0].match(/^[0-9]+$/)),
m => m.displayName?.toLowerCase()
);
- return [...extractedContactsWithDisplayName, ...extractedContactsNoDisplayName];
+ return [...contactsWithDisplayName, ...contactsStartingWithANumber];
+ }
+);
+
+export const getSortedContactsWithBreaks = createSelector(
+ getSortedContacts,
+ (contacts: Array): Array => {
+ // add a break wherever needed
+ const unknownSection = 'unknown';
+ let currentChar = '';
+ // if the item is a string we consider it to be a break of that string
+ const contactsWithBreaks: Array = [];
+
+ contacts.forEach(m => {
+ if (
+ !!m.displayName &&
+ m.displayName[0].toLowerCase() !== currentChar &&
+ !m.displayName[0].match(/^[0-9]+$/)
+ ) {
+ currentChar = m.displayName[0].toLowerCase();
+ contactsWithBreaks.push(currentChar.toUpperCase());
+ } else if (
+ ((m.displayName && m.displayName[0].match(/^[0-9]+$/)) || !m.displayName) &&
+ currentChar !== unknownSection
+ ) {
+ currentChar = unknownSection;
+ contactsWithBreaks.push('#');
+ }
+
+ contactsWithBreaks.push(m);
+ });
+
+ contactsWithBreaks.unshift({
+ id: UserUtils.getOurPubKeyStrFromCache(),
+ displayName: window.i18n('noteToSelf'),
+ });
+
+ return contactsWithBreaks;
}
);
-export const getPrivateContactsPubkeys = createSelector(getDirectContactsByName, state =>
+export const getPrivateContactsPubkeys = createSelector(getSortedContacts, state =>
state.map(m => m.id)
);
diff --git a/ts/state/selectors/modal.ts b/ts/state/selectors/modal.ts
index 71ce6b581..04054402c 100644
--- a/ts/state/selectors/modal.ts
+++ b/ts/state/selectors/modal.ts
@@ -27,6 +27,17 @@ export const getModal = (state: StateType): ModalState => {
return state.modals;
};
+export const getIsModalVisble = createSelector(getModal, (state: ModalState): boolean => {
+ const modalValues = Object.values(state);
+ for (let i = 0; i < modalValues.length; i++) {
+ if (modalValues[i] !== null) {
+ return true;
+ }
+ }
+
+ return false;
+});
+
export const getConfirmModal = createSelector(
getModal,
(state: ModalState): ConfirmModalState => state.confirmModal
diff --git a/ts/state/selectors/search.ts b/ts/state/selectors/search.ts
index f19c5c130..2f3c5ce07 100644
--- a/ts/state/selectors/search.ts
+++ b/ts/state/selectors/search.ts
@@ -76,56 +76,41 @@ export type SearchResultsMergedListItem =
export const getSearchResultsList = createSelector([getSearchResults], searchState => {
const { contactsAndGroups, messages } = searchState;
- const builtList: Array = [];
+ const builtList = [];
if (contactsAndGroups.length) {
- const us = UserUtils.getOurPubKeyStrFromCache();
- let usIndex: number = -1;
-
- const idsWithNameAndType = contactsAndGroups.map(m => ({
+ const contactsWithNameAndType = contactsAndGroups.map(m => ({
contactConvoId: m.id,
displayName: m.nickname || m.displayNameInProfile,
type: m.type,
}));
const groupsAndCommunities = sortBy(
- remove(idsWithNameAndType, m => m.type === ConversationTypeEnum.GROUP),
+ remove(contactsWithNameAndType, m => m.type === ConversationTypeEnum.GROUP),
m => m.displayName?.toLowerCase()
);
- const idsWithNoDisplayNames = sortBy(
- remove(idsWithNameAndType, m => !m.displayName),
- m => m.contactConvoId
+ const contactsStartingWithANumber = sortBy(
+ remove(
+ contactsWithNameAndType,
+ m => !m.displayName || (m.displayName && m.displayName[0].match(/^[0-9]+$/))
+ ),
+ m => m.displayName || m.contactConvoId
);
- // add a break wherever needed
- let currentChar = '';
- for (let i = 0; i < idsWithNameAndType.length; i++) {
- const m = idsWithNameAndType[i];
- if (m.contactConvoId === us) {
- usIndex = i;
- continue;
- }
- if (
- idsWithNameAndType.length > 1 &&
- m.displayName &&
- m.displayName[0].toLowerCase() !== currentChar
- ) {
- currentChar = m.displayName[0].toLowerCase();
- builtList.push(currentChar.toUpperCase());
- }
- builtList.push(m);
- }
-
- builtList.unshift(...groupsAndCommunities);
+ builtList.push(
+ ...groupsAndCommunities,
+ ...contactsWithNameAndType,
+ ...contactsStartingWithANumber
+ );
- if (idsWithNoDisplayNames.length) {
- builtList.push('#', ...idsWithNoDisplayNames);
- }
+ const us = UserUtils.getOurPubKeyStrFromCache();
+ const hasUs = remove(builtList, m => m.contactConvoId === us);
- if (usIndex !== -1) {
+ if (hasUs.length) {
builtList.unshift({ contactConvoId: us, displayName: window.i18n('noteToSelf') });
}
+
builtList.unshift(window.i18n('sessionConversations'));
}
diff --git a/ts/themes/globals.tsx b/ts/themes/globals.tsx
index 57fc3ccb5..40c3e8f56 100644
--- a/ts/themes/globals.tsx
+++ b/ts/themes/globals.tsx
@@ -7,6 +7,14 @@ function setDuration(duration: number | string) {
return `${!isTestIntegration() ? duration : typeof duration === 'string' ? '0s' : '0'}`;
}
+export function pxValueToNumber(value: string) {
+ const numberValue = Number(value.replace('px', ''));
+ if (Number.isNaN(numberValue)) {
+ throw new Error('Invalid number value');
+ }
+ return numberValue;
+}
+
// These variables are independent of the current theme
type ThemeGlobals = {
/* Fonts */
@@ -117,6 +125,10 @@ type ThemeGlobals = {
'--right-panel-height': string;
'--right-panel-attachment-width': string;
'--right-panel-attachment-height': string;
+
+ /* Contact Row */
+ '--contact-row-height': string;
+ '--contact-row-break-height': string;
};
type Theme = ThemeGlobals | ThemeColorVariables;
@@ -226,4 +238,7 @@ export const THEME_GLOBALS: ThemeGlobals = {
'--right-panel-attachment-width': 'calc(var(--right-panel-width) - 2 * var(--margins-2xl) - 7px)',
'--right-panel-attachment-height':
'calc(var(--right-panel-height) - 2 * var(--margins-2xl) -7px)',
+
+ '--contact-row-height': '60px',
+ '--contact-row-break-height': '20px',
};