Merge branch 'unstable' into feat/ses-50/onboarding

pull/3056/head
William Grant 1 year ago
commit 0f6d80b847

@ -4,6 +4,7 @@ import { getShowScrollButton } from '../state/selectors/conversations';
import { useSelectedUnreadCount } from '../state/selectors/selectedConversation';
import { SessionIconButton } from './icon';
import { SessionUnreadCount } from './icon/SessionNotificationCount';
const SessionScrollButtonDiv = styled.div`
position: fixed;
@ -29,8 +30,9 @@ export const SessionScrollButton = (props: { onClickScrollBottom: () => void })
isHidden={!show}
onClick={props.onClickScrollBottom}
dataTestId="scroll-to-bottom-button"
unreadCount={unreadCount}
/>
>
{Boolean(unreadCount) && <SessionUnreadCount count={unreadCount} />}
</SessionIconButton>
</SessionScrollButtonDiv>
);
};

@ -63,15 +63,12 @@ export const SessionWrapperModal = (props: SessionWrapperModalType) => {
}
};
const fallbackFocusId = 'session-wrapper-modal';
return (
<FocusTrap focusTrapOptions={{ fallbackFocus: `#${fallbackFocusId}`, allowOutsideClick: true }}>
<FocusTrap focusTrapOptions={{ initialFocus: false, allowOutsideClick: true }}>
<div
className={classNames('loki-dialog modal', additionalClassName || null)}
onClick={handleClick}
role="dialog"
id={fallbackFocusId}
>
<div className="session-confirm-wrapper">
<div ref={modalRef} className="session-modal">

@ -68,7 +68,7 @@ export const SelectionOverlay = () => {
const classNameAndId = 'message-selection-overlay';
return (
<FocusTrap focusTrapOptions={{ fallbackFocus: `#${classNameAndId}`, allowOutsideClick: true }}>
<FocusTrap focusTrapOptions={{ initialFocus: false, allowOutsideClick: true }}>
<div className={classNameAndId} id={classNameAndId}>
<div className="close-button">
<SessionIconButton iconType="exit" iconSize="medium" onClick={onCloseOverlay} />

@ -6,7 +6,7 @@ import { useMessageExpirationPropsById } from '../../../../hooks/useParamSelecto
import { useMessageStatus } from '../../../../state/selectors';
import { useIsDetailMessageView } from '../../../../contexts/isDetailViewContext';
import { getMostRecentMessageId } from '../../../../state/selectors/conversations';
import { getMostRecentOutgoingMessageId } from '../../../../state/selectors/conversations';
import { useSelectedIsGroupOrCommunity } from '../../../../state/selectors/selectedConversation';
import { SpacerXS } from '../../../basic/Text';
import { SessionIcon, SessionIconType } from '../../../icon';
@ -122,10 +122,9 @@ function useIsExpiring(messageId: string) {
);
}
function useIsMostRecentMessage(messageId: string) {
const mostRecentMessageId = useSelector(getMostRecentMessageId);
const isMostRecentMessage = mostRecentMessageId === messageId;
return isMostRecentMessage;
function useIsMostRecentOutgoingMessage(messageId: string) {
const mostRecentOutgoingMessageId = useSelector(getMostRecentOutgoingMessageId);
return mostRecentOutgoingMessageId === messageId;
}
function MessageStatusExpireTimer(props: Pick<Props, 'messageId'>) {
@ -180,11 +179,11 @@ function IconForExpiringMessageId({
const MessageStatusSent = ({ dataTestId, messageId }: Omit<Props, 'isDetailView'>) => {
const isExpiring = useIsExpiring(messageId);
const isMostRecentMessage = useIsMostRecentMessage(messageId);
const isMostRecentOutgoingMessage = useIsMostRecentOutgoingMessage(messageId);
const isGroup = useSelectedIsGroupOrCommunity();
// we hide a "sent" message status which is not expiring except for the most recent message
if (!isExpiring && !isMostRecentMessage) {
// we hide the "sent" message status for a non-expiring messages unless it's the most recent outgoing message
if (!isExpiring && !isMostRecentOutgoingMessage) {
return null;
}
return (
@ -208,10 +207,10 @@ const MessageStatusRead = ({
const isExpiring = useIsExpiring(messageId);
const isGroup = useSelectedIsGroupOrCommunity();
const isMostRecentMessage = useIsMostRecentMessage(messageId);
const isMostRecentOutgoingMessage = useIsMostRecentOutgoingMessage(messageId);
// we hide an outgoing "read" message status which is not expiring except for the most recent message
if (!isIncoming && !isExpiring && !isMostRecentMessage) {
if (!isIncoming && !isExpiring && !isMostRecentOutgoingMessage) {
return null;
}

@ -18,11 +18,22 @@ import { SessionRadioGroup } from '../basic/SessionRadioGroup';
const deleteDbLocally = async () => {
window?.log?.info('last message sent successfully. Deleting everything');
window.persistStore?.purge();
window?.log?.info('store purged');
await deleteAllLogs();
window?.log?.info('deleteAllLogs: done');
await Data.removeAll();
window?.log?.info('Data.removeAll: done');
await Data.close();
window?.log?.info('Data.close: done');
await Data.removeDB();
window?.log?.info('Data.removeDB: done');
await Data.removeOtherData();
window?.log?.info('Data.removeOtherData: done');
window.localStorage.setItem('restart-reason', 'delete-account');
};

@ -1,13 +1,11 @@
import classNames from 'classnames';
import _ from 'lodash';
import { KeyboardEvent, MouseEvent, forwardRef, memo } from 'react';
import { KeyboardEvent, MouseEvent, ReactNode, forwardRef, memo } from 'react';
import styled from 'styled-components';
import { SessionIcon, SessionIconProps } from '.';
import { SessionNotificationCount, SessionUnreadCount } from './SessionNotificationCount';
import { SessionIcon, SessionIconProps } from './SessionIcon';
interface SProps extends SessionIconProps {
onClick?: (e?: MouseEvent<HTMLButtonElement>) => void;
notificationCount?: number;
isSelected?: boolean;
isHidden?: boolean;
margin?: string;
@ -18,6 +16,7 @@ interface SProps extends SessionIconProps {
title?: string;
style?: object;
tabIndex?: number;
children?: ReactNode;
}
const StyledSessionIconButton = styled.button<{ color?: string; isSelected?: boolean }>`
@ -48,7 +47,6 @@ const SessionIconButtonInner = forwardRef<HTMLButtonElement, SProps>((props, ref
iconColor,
iconRotation,
isSelected,
notificationCount,
glowDuration,
glowStartDelay,
noScale,
@ -64,7 +62,7 @@ const SessionIconButtonInner = forwardRef<HTMLButtonElement, SProps>((props, ref
dataTestIdIcon,
style,
tabIndex,
unreadCount,
children,
} = props;
const clickHandler = (e: MouseEvent<HTMLButtonElement>) => {
if (props.onClick) {
@ -111,8 +109,7 @@ const SessionIconButtonInner = forwardRef<HTMLButtonElement, SProps>((props, ref
iconPadding={iconPadding}
dataTestId={dataTestIdIcon}
/>
{Boolean(notificationCount) && <SessionNotificationCount count={notificationCount} />}
{Boolean(unreadCount) && <SessionUnreadCount count={unreadCount} />}
{children}
</StyledSessionIconButton>
);
});

@ -7,23 +7,32 @@ type Props = {
count?: number;
};
const StyledCountContainer = styled.div<{ centeredOnTop: boolean }>`
background: var(--unread-messages-alert-background-color);
color: var(--unread-messages-alert-text-color);
text-align: center;
padding: ${props => (props.centeredOnTop ? '1px 3px 0' : '1px 4px')};
position: absolute;
font-size: 18px;
line-height: 1.2;
top: ${props => (props.centeredOnTop ? '-10px' : '27px')};
left: ${props => (props.centeredOnTop ? '50%' : '28px')};
transform: ${props => (props.centeredOnTop ? 'translateX(-50%)' : 'none')};
padding: ${props => (props.centeredOnTop ? '3px 3px' : '1px 4px')};
font-size: var(--font-size-xs);
font-family: var(--font-default);
font-weight: 700;
height: 16px;
min-width: 16px;
line-height: 16px;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
font-family: var(--font-default);
border-radius: 58px;
font-weight: 700;
background: var(--unread-messages-alert-background-color);
flex-shrink: 0;
transition: var(--default-duration);
text-align: center;
color: var(--unread-messages-alert-text-color);
transform: ${props => (props.centeredOnTop ? 'translateX(-50%)' : 'none')};
white-space: ${props => (props.centeredOnTop ? 'nowrap' : 'normal')};
`;

@ -33,7 +33,7 @@ import { getSwarmPollingInstance } from '../../session/apis/snode_api';
import { UserUtils } from '../../session/utils';
import { Avatar, AvatarSize } from '../avatar/Avatar';
import { ActionPanelOnionStatusLight } from '../dialog/OnionStatusPathDialog';
import { SessionIconButton } from '../icon';
import { SessionIconButton } from '../icon/SessionIconButton';
import { LeftPaneSectionContainer } from './LeftPaneSectionContainer';
import { SettingsKey } from '../../data/settings-key';
@ -48,6 +48,7 @@ import { ensureThemeConsistency } from '../../themes/SessionTheme';
import { switchThemeTo } from '../../themes/switchTheme';
import { ReleasedFeatures } from '../../util/releaseFeature';
import { getOppositeTheme } from '../../util/theme';
import { SessionNotificationCount } from '../icon/SessionNotificationCount';
const Section = (props: { type: SectionType }) => {
const ourNumber = useSelector(getOurNumber);
@ -103,10 +104,11 @@ const Section = (props: { type: SectionType }) => {
iconSize="medium"
dataTestId="message-section"
iconType={'chatBubble'}
notificationCount={unreadToShow}
onClick={handleClick}
isSelected={isSelected}
/>
>
{Boolean(unreadToShow) && <SessionNotificationCount count={unreadToShow} />}
</SessionIconButton>
);
case SectionType.Settings:
return (

@ -790,8 +790,20 @@ async function removeDB() {
try {
console.error('Remove DB: removing.', userDir);
userConfig.remove();
ephemeralConfig.remove();
try {
userConfig.remove();
} catch (e) {
if (e.code !== 'ENOENT') {
throw e;
}
}
try {
ephemeralConfig.remove();
} catch (e) {
if (e.code !== 'ENOENT') {
throw e;
}
}
} catch (e) {
console.error('Remove DB: Failed to remove configs.', e);
}

@ -1739,9 +1739,11 @@ export class ConversationModel extends Backbone.Model<ConversationAttributes> {
return;
}
const conversationId = this.id;
const isLegacyGroup = this.isClosedGroup() && this.id.startsWith('05');
let friendRequestText;
if (!this.isApproved()) {
// NOTE: legacy groups are never approved, so we should not cancel notifications
if (!this.isApproved() && !isLegacyGroup) {
window?.log?.info('notification cancelled for unapproved convo', this.idForLogging());
const hadNoRequestsPrior =
getConversationController()

@ -1,7 +1,7 @@
import { app, ipcMain } from 'electron';
import { sqlNode } from './sql'; // checked - only node
import { userConfig } from './config/user_config'; // checked - only node
import { ephemeralConfig } from './config/ephemeral_config'; // checked - only node
import { userConfig } from './config/user_config'; // checked - only node
import { sqlNode } from './sql'; // checked - only node
let initialized = false;
@ -31,8 +31,20 @@ export function initializeSqlChannel() {
ipcMain.on(ERASE_SQL_KEY, event => {
try {
userConfig.remove();
ephemeralConfig.remove();
try {
userConfig.remove();
} catch (e) {
if (e.code !== 'ENOENT') {
throw e;
}
}
try {
ephemeralConfig.remove();
} catch (e) {
if (e.code !== 'ENOENT') {
throw e;
}
}
event.sender.send(`${ERASE_SQL_KEY}-done`);
} catch (error) {
const errorForDisplay = error && error.stack ? error.stack : error;

@ -1,8 +1,8 @@
import path from 'path';
import { app, BrowserWindow, Menu, Tray } from 'electron';
import { LocaleMessagesType } from './locale';
import { getAppRootPath } from './getRootPath';
import { LocaleMessagesType } from './locale';
let trayContextMenu = null;
let tray: Tray | null = null;

@ -1,4 +1,4 @@
import { omit } from 'lodash';
import { isArray, omit } from 'lodash';
import { Snode } from '../../../data/data';
import { updateIsOnline } from '../../../state/ducks/onion';
import { doSnodeBatchRequest } from './batchRequest';
@ -7,8 +7,8 @@ import { SnodeNamespace, SnodeNamespaces } from './namespaces';
import { TTL_DEFAULT } from '../../constants';
import { UserUtils } from '../../utils';
import { sleepFor } from '../../utils/Promise';
import {
NotEmptyArrayOfBatchResults,
RetrieveLegacyClosedGroupSubRequestType,
RetrieveSubRequestType,
UpdateExpiryOnNodeSubRequest,
@ -103,40 +103,6 @@ async function buildRetrieveRequest(
return retrieveRequestsParams;
}
function verifyBatchRequestResults(
targetNode: Snode,
namespaces: Array<SnodeNamespaces>,
results: NotEmptyArrayOfBatchResults
) {
if (!results || !results.length) {
window?.log?.warn(
`_retrieveNextMessages - sessionRpc could not talk to ${targetNode.ip}:${targetNode.port}`
);
throw new Error(
`_retrieveNextMessages - sessionRpc could not talk to ${targetNode.ip}:${targetNode.port}`
);
}
// the +1 is to take care of the extra `expire` method added once user config is released
if (results.length !== namespaces.length && results.length !== namespaces.length + 1) {
throw new Error(
`We asked for updates about ${namespaces.length} messages but got results of length ${results.length}`
);
}
// do a basic check to know if we have something kind of looking right (status 200 should always be there for a retrieve)
const firstResult = results[0];
if (firstResult.code !== 200) {
window?.log?.warn(`_retrieveNextMessages result is not 200 but ${firstResult.code}`);
throw new Error(
`_retrieveNextMessages - retrieve result is not 200 with ${targetNode.ip}:${targetNode.port} but ${firstResult.code}`
);
}
return firstResult;
}
async function retrieveNextMessages(
targetNode: Snode,
lastHashes: Array<string>,
@ -158,17 +124,41 @@ async function retrieveNextMessages(
);
// let exceptions bubble up
// no retry for this one as this a call we do every few seconds while polling for messages
const timeOutMs = 4 * 1000;
const timeoutPromise = async () => sleepFor(timeOutMs);
const fetchPromise = async () =>
doSnodeBatchRequest(retrieveRequestsParams, targetNode, timeOutMs, associatedWith);
const results = await doSnodeBatchRequest(
retrieveRequestsParams,
targetNode,
4000,
associatedWith
);
// just to make sure that we don't hang for more than timeOutMs
const results = await Promise.race([timeoutPromise(), fetchPromise()]);
try {
if (!results || !isArray(results) || !results.length) {
window?.log?.warn(
`_retrieveNextMessages - sessionRpc could not talk to ${targetNode.ip}:${targetNode.port}`
);
throw new Error(
`_retrieveNextMessages - sessionRpc could not talk to ${targetNode.ip}:${targetNode.port}`
);
}
// the +1 is to take care of the extra `expire` method added once user config is released
if (results.length !== namespaces.length && results.length !== namespaces.length + 1) {
throw new Error(
`We asked for updates about ${namespaces.length} messages but got results of length ${results.length}`
);
}
// do a basic check to know if we have something kind of looking right (status 200 should always be there for a retrieve)
const firstResult = results[0];
if (firstResult.code !== 200) {
window?.log?.warn(`_retrieveNextMessages result is not 200 but ${firstResult.code}`);
throw new Error(
`_retrieveNextMessages - retrieve result is not 200 with ${targetNode.ip}:${targetNode.port} but ${firstResult.code}`
);
}
// we rely on the code of the first one to check for online status
const firstResult = verifyBatchRequestResults(targetNode, namespaces, results);
const bodyFirstResult = firstResult.body;
if (!window.inboxStore?.getState().onionPaths.isOnline) {
window.inboxStore?.dispatch(updateIsOnline(true));

@ -229,12 +229,21 @@ export class SwarmPolling {
let resultsFromAllNamespaces: RetrieveMessagesResultsBatched | null;
try {
// Note: always print something so we know if the polling is hanging
window.log.info(
`about to pollNodeForKey of ${ed25519Str(pubkey.key)} from snode: ${ed25519Str(toPollFrom.pubkey_ed25519)} namespaces: ${namespaces} `
);
resultsFromAllNamespaces = await this.pollNodeForKey(
toPollFrom,
pubkey,
namespaces,
!isGroup
);
// Note: always print something so we know if the polling is hanging
window.log.info(
`pollNodeForKey of ${ed25519Str(pubkey.key)} from snode: ${ed25519Str(toPollFrom.pubkey_ed25519)} namespaces: ${namespaces} returned: ${resultsFromAllNamespaces?.length}`
);
} catch (e) {
window.log.warn(
`pollNodeForKey of ${pubkey} namespaces: ${namespaces} failed with: ${e.message}`
@ -518,6 +527,9 @@ export class SwarmPolling {
return last(r.messages.messages);
});
window.log.info(
`updating last hashes for ${ed25519Str(pubkey.key)}: ${ed25519Str(snodeEdkey)} ${lastMessages.map(m => m?.hash || '')}`
);
await Promise.all(
lastMessages.map(async (lastMessage, index) => {
if (!lastMessage) {

@ -1,6 +1,5 @@
import { toast } from 'react-toastify';
import { SessionToast, SessionToastType } from '../../components/basic/SessionToast';
import { SessionIconType } from '../../components/icon';
import { SessionSettingCategory } from '../../components/settings/SessionSettings';
import { SectionType, showLeftPaneSection, showSettingsSection } from '../../state/ducks/section';
@ -37,19 +36,9 @@ export function pushToastInfo(
);
}
export function pushToastSuccess(
id: string,
title: string,
description?: string,
icon?: SessionIconType
) {
export function pushToastSuccess(id: string, title: string, description?: string) {
toast.success(
<SessionToast
title={title}
description={description}
type={SessionToastType.Success}
icon={icon}
/>,
<SessionToast title={title} description={description} type={SessionToastType.Success} />,
{ toastId: id, updateId: id }
);
}
@ -209,12 +198,7 @@ export function someDeletionsFailed() {
}
export function pushDeleted(messageCount: number) {
pushToastSuccess(
'deleted',
window.i18n('deleted', [messageCount.toString()]),
undefined,
'check'
);
pushToastSuccess('deleted', window.i18n('deleted', [messageCount.toString()]), undefined);
}
export function pushCannotRemoveCreatorFromGroup() {

@ -600,6 +600,13 @@ export const getMostRecentMessageId = (state: StateType): string | null => {
return state.conversations.mostRecentMessageId;
};
export const getMostRecentOutgoingMessageId = createSelector(
getSortedMessagesOfSelectedConversation,
(messages: Array<MessageModelPropsWithoutConvoProps>): string | undefined => {
return messages.find(m => m.propsForMessage.direction === 'outgoing')?.propsForMessage.id;
}
);
export const getOldestMessageId = createSelector(
getSortedMessagesOfSelectedConversation,
(messages: Array<MessageModelPropsWithoutConvoProps>): string | undefined => {

Loading…
Cancel
Save