Merge remote-tracking branch 'upstream/clearnet' into perf-improv

pull/1783/head
Audric Ackermann 4 years ago
commit 5b0b165ba9
No known key found for this signature in database
GPG Key ID: 999F434D76324AD4

@ -409,5 +409,9 @@
"audioMessageAutoplayDescription": "Automatically play consecutively sent audio messages", "audioMessageAutoplayDescription": "Automatically play consecutively sent audio messages",
"clickToTrustContact": "Click to download media", "clickToTrustContact": "Click to download media",
"trustThisContactDialogTitle": "Trust $name$?", "trustThisContactDialogTitle": "Trust $name$?",
"trustThisContactDialogDescription": "Are you sure you want to download media sent by $name$?" "trustThisContactDialogDescription": "Are you sure you want to download media sent by $name$?",
"pinConversation": "Pin Conversation",
"unpinConversation": "Unpin Conversation",
"pinConversationLimitTitle": "Pinned conversations limit",
"pinConversationLimitToastDescription": "You can only pin $number$ conversations"
} }

@ -390,5 +390,9 @@
"errorHappenedWhileRemovingModeratorDesc": "Une erreur est survenue lors de la suppression de cet utilisateur de la liste des modérateurs.", "errorHappenedWhileRemovingModeratorDesc": "Une erreur est survenue lors de la suppression de cet utilisateur de la liste des modérateurs.",
"orJoinOneOfThese": "Ou rejoignez un de ceux-ci...", "orJoinOneOfThese": "Ou rejoignez un de ceux-ci...",
"helpUsTranslateSession": "Help us Translate Session", "helpUsTranslateSession": "Help us Translate Session",
"translation": "Translation" "translation": "Translation",
"pinConversation": "Épingler la conversation",
"unpinConversation": "Défaire la conversation",
"pinConversationLimitTitle": "Limite de conversations épinglées",
"pinConversationLimitToastDescription": "Vous ne pouvez pas épingler plus de $number$ conversations"
} }

@ -297,10 +297,6 @@
window.addEventListener('focus', () => Whisper.Notifications.clear()); window.addEventListener('focus', () => Whisper.Notifications.clear());
window.addEventListener('unload', () => Whisper.Notifications.fastClear()); window.addEventListener('unload', () => Whisper.Notifications.fastClear());
window.showResetSessionIdDialog = () => {
appView.showResetSessionIdDialog();
};
// Set user's launch count. // Set user's launch count.
const prevLaunchCount = window.getSettingValue('launch-count'); const prevLaunchCount = window.getSettingValue('launch-count');
const launchCount = !prevLaunchCount ? 1 : prevLaunchCount + 1; const launchCount = !prevLaunchCount ? 1 : prevLaunchCount + 1;
@ -340,11 +336,6 @@
window.libsession.Utils.ToastUtils.pushSpellCheckDirty(); window.libsession.Utils.ToastUtils.pushSpellCheckDirty();
}; };
window.toggleLinkPreview = () => {
const newValue = !window.getSettingValue('link-preview-setting');
window.setSettingValue('link-preview-setting', newValue);
};
window.toggleMediaPermissions = () => { window.toggleMediaPermissions = () => {
const value = window.getMediaPermissions(); const value = window.getMediaPermissions();
window.setMediaPermissions(!value); window.setMediaPermissions(!value);

@ -94,16 +94,5 @@
window.focus(); // FIXME window.focus(); // FIXME
return Promise.resolve(); return Promise.resolve();
}, },
showResetSessionIdDialog() {
const theme = this.getThemeObject();
const resetSessionIDDialog = new Whisper.SessionIDResetDialog({ theme });
this.el.prepend(resetSessionIDDialog.el);
},
getThemeObject() {
const themeSettings = storage.get('theme-setting') || 'light';
const theme = themeSettings === 'light' ? window.lightTheme : window.darkTheme;
return theme;
},
}); });
})(); })();

@ -56,6 +56,7 @@ window.lokiFeatureFlags = {
useFileOnionRequests: true, useFileOnionRequests: true,
useFileOnionRequestsV2: true, // more compact encoding of files in response useFileOnionRequestsV2: true, // more compact encoding of files in response
padOutgoingAttachments: true, padOutgoingAttachments: true,
enablePinConversations: false,
}; };
if (typeof process.env.NODE_ENV === 'string' && process.env.NODE_ENV.includes('test-integration')) { if (typeof process.env.NODE_ENV === 'string' && process.env.NODE_ENV.includes('test-integration')) {

@ -22,8 +22,12 @@ import {
} from '../state/ducks/conversations'; } from '../state/ducks/conversations';
import _ from 'underscore'; import _ from 'underscore';
import { useMembersAvatars } from '../hooks/useMembersAvatar'; import { useMembersAvatars } from '../hooks/useMembersAvatar';
import { useDispatch } from 'react-redux'; import { SessionIcon, SessionIconSize, SessionIconType } from './session/icon';
import { useDispatch, useSelector } from 'react-redux';
import { SectionType } from '../state/ducks/section';
import { getFocusedSection } from '../state/selectors/section';
// tslint:disable-next-line: no-empty-interface
export interface ConversationListItemProps extends ReduxConversationType {} export interface ConversationListItemProps extends ReduxConversationType {}
type PropsHousekeeping = { type PropsHousekeeping = {
@ -36,26 +40,73 @@ const Portal = ({ children }: { children: any }) => {
return createPortal(children, document.querySelector('.inbox.index') as Element); return createPortal(children, document.querySelector('.inbox.index') as Element);
}; };
const AvatarItem = (props: { const HeaderItem = (props: {
avatarPath?: string; unreadCount: number;
conversationId: string; isMe: boolean;
memberAvatars?: Array<ConversationAvatar>; mentionedUs: boolean;
activeAt?: number;
name?: string; name?: string;
profileName?: string; profileName?: string;
conversationId: string;
isPinned: boolean;
}) => { }) => {
const { avatarPath, name, conversationId, profileName, memberAvatars } = props; const {
unreadCount,
mentionedUs,
activeAt,
isMe,
isPinned,
conversationId,
profileName,
name,
} = props;
const theme = useTheme();
const userName = name || profileName || conversationId; let atSymbol = null;
let unreadCountDiv = null;
if (unreadCount > 0) {
atSymbol = mentionedUs ? <p className="at-symbol">@</p> : null;
unreadCountDiv = <p className="module-conversation-list-item__unread-count">{unreadCount}</p>;
}
return ( const isMessagesSection = useSelector(getFocusedSection) === SectionType.Message;
<div className="module-conversation-list-item__avatar-container">
<Avatar const pinIcon =
avatarPath={avatarPath} isMessagesSection && isPinned ? (
name={userName} <SessionIcon
size={AvatarSize.S} iconType={SessionIconType.Pin}
memberAvatars={memberAvatars} iconColor={theme.colors.textColorSubtle}
pubkey={conversationId} iconSize={SessionIconSize.Tiny}
/> />
) : null;
return (
<div className="module-conversation-list-item__header">
<div
className={classNames(
'module-conversation-list-item__header__name',
unreadCount > 0 ? 'module-conversation-list-item__header__name--with-unread' : null
)}
>
<UserItem
isMe={isMe}
conversationId={conversationId}
name={name}
profileName={profileName}
/>
</div>
{pinIcon}
{unreadCountDiv}
{atSymbol}
{
<div
className={classNames(
'module-conversation-list-item__header__date',
unreadCount > 0 ? 'module-conversation-list-item__header__date--has-unread' : null
)}
>
{<Timestamp timestamp={activeAt} extended={false} isConversationListItem={true} />}
</div>
}
</div> </div>
); );
}; };
@ -93,12 +144,11 @@ const UserItem = (props: {
}; };
const MessageItem = (props: { const MessageItem = (props: {
isTyping: boolean;
lastMessage?: LastMessageType; lastMessage?: LastMessageType;
isTyping: boolean;
unreadCount: number; unreadCount: number;
}) => { }) => {
const { lastMessage, isTyping, unreadCount } = props; const { lastMessage, isTyping, unreadCount } = props;
const theme = useTheme(); const theme = useTheme();
if (!lastMessage && !isTyping) { if (!lastMessage && !isTyping) {
@ -134,51 +184,26 @@ const MessageItem = (props: {
); );
}; };
const HeaderItem = (props: { const AvatarItem = (props: {
unreadCount: number; avatarPath?: string;
isMe: boolean; conversationId: string;
mentionedUs: boolean; memberAvatars?: Array<ConversationAvatar>;
activeAt?: number;
name?: string; name?: string;
profileName?: string; profileName?: string;
conversationId: string;
}) => { }) => {
const { unreadCount, mentionedUs, activeAt, isMe, conversationId, profileName, name } = props; const { avatarPath, name, conversationId, profileName, memberAvatars } = props;
let atSymbol = null; const userName = name || profileName || conversationId;
let unreadCountDiv = null;
if (unreadCount > 0) {
atSymbol = mentionedUs ? <p className="at-symbol">@</p> : null;
unreadCountDiv = <p className="module-conversation-list-item__unread-count">{unreadCount}</p>;
}
return ( return (
<div className="module-conversation-list-item__header"> <div className="module-conversation-list-item__avatar-container">
<div <Avatar
className={classNames( avatarPath={avatarPath}
'module-conversation-list-item__header__name', name={userName}
unreadCount > 0 ? 'module-conversation-list-item__header__name--with-unread' : null size={AvatarSize.S}
)} memberAvatars={memberAvatars}
> pubkey={conversationId}
<UserItem />
isMe={isMe}
conversationId={conversationId}
name={name}
profileName={profileName}
/>
</div>
{unreadCountDiv}
{atSymbol}
{
<div
className={classNames(
'module-conversation-list-item__header__date',
unreadCount > 0 ? 'module-conversation-list-item__header__date--has-unread' : null
)}
>
{<Timestamp timestamp={activeAt} extended={false} isConversationListItem={true} />}
</div>
}
</div> </div>
); );
}; };
@ -195,6 +220,7 @@ const ConversationListItem = (props: Props) => {
mentionedUs, mentionedUs,
isMe, isMe,
name, name,
isPinned,
profileName, profileName,
isTyping, isTyping,
lastMessage, lastMessage,
@ -247,6 +273,7 @@ const ConversationListItem = (props: Props) => {
unreadCount={unreadCount} unreadCount={unreadCount}
activeAt={activeAt} activeAt={activeAt}
isMe={isMe} isMe={isMe}
isPinned={isPinned}
conversationId={conversationId} conversationId={conversationId}
name={name} name={name}
profileName={profileName} profileName={profileName}

@ -1,6 +1,6 @@
import React from 'react'; import React from 'react';
import { ActionsPanel, SectionType } from './session/ActionsPanel'; import { ActionsPanel } from './session/ActionsPanel';
import { LeftPaneMessageSection } from './session/LeftPaneMessageSection'; import { LeftPaneMessageSection } from './session/LeftPaneMessageSection';
import { LeftPaneContactSection } from './session/LeftPaneContactSection'; import { LeftPaneContactSection } from './session/LeftPaneContactSection';
@ -8,11 +8,10 @@ import { LeftPaneSettingSection } from './session/LeftPaneSettingSection';
import { SessionTheme } from '../state/ducks/SessionTheme'; import { SessionTheme } from '../state/ducks/SessionTheme';
import { SessionExpiredWarning } from './session/network/SessionExpiredWarning'; import { SessionExpiredWarning } from './session/network/SessionExpiredWarning';
import { getFocusedSection } from '../state/selectors/section'; import { getFocusedSection } from '../state/selectors/section';
import { useDispatch, useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import { getLeftPaneLists } from '../state/selectors/conversations'; import { getLeftPaneLists } from '../state/selectors/conversations';
import { getQuery, getSearchResults, isSearching } from '../state/selectors/search'; import { getQuery, getSearchResults, isSearching } from '../state/selectors/search';
import { clearSearch, search, updateSearchTerm } from '../state/ducks/search'; import { SectionType } from '../state/ducks/section';
import { useTheme } from 'styled-components';
import { getTheme } from '../state/selectors/theme'; import { getTheme } from '../state/selectors/theme';
// from https://github.com/bvaughn/react-virtualized/blob/fb3484ed5dcc41bffae8eab029126c0fb8f7abc0/source/List/types.js#L5 // from https://github.com/bvaughn/react-virtualized/blob/fb3484ed5dcc41bffae8eab029126c0fb8f7abc0/source/List/types.js#L5

@ -30,7 +30,7 @@ import { applyTheme } from '../../state/ducks/theme';
import { getFocusedSection } from '../../state/selectors/section'; import { getFocusedSection } from '../../state/selectors/section';
import { useInterval } from '../../hooks/useInterval'; import { useInterval } from '../../hooks/useInterval';
import { clearSearch } from '../../state/ducks/search'; import { clearSearch } from '../../state/ducks/search';
import { showLeftPaneSection } from '../../state/ducks/section'; import { SectionType, showLeftPaneSection } from '../../state/ducks/section';
import { cleanUpOldDecryptedMedias } from '../../session/crypto/DecryptedAttachmentsManager'; import { cleanUpOldDecryptedMedias } from '../../session/crypto/DecryptedAttachmentsManager';
import { getOpenGroupManager } from '../../opengroup/opengroupV2/OpenGroupManagerV2'; import { getOpenGroupManager } from '../../opengroup/opengroupV2/OpenGroupManagerV2';
@ -47,16 +47,6 @@ import { ActionPanelOnionStatusLight } from '../OnionStatusPathDialog';
// tslint:disable-next-line: no-import-side-effect no-submodule-imports // tslint:disable-next-line: no-import-side-effect no-submodule-imports
export enum SectionType {
Profile,
Message,
Contact,
Channel,
Settings,
Moon,
PathIndicator,
}
const Section = (props: { type: SectionType; avatarPath?: string }) => { const Section = (props: { type: SectionType; avatarPath?: string }) => {
const ourNumber = useSelector(getOurNumber); const ourNumber = useSelector(getOurNumber);
const unreadMessageCount = useSelector(getUnreadMessageCount); const unreadMessageCount = useSelector(getUnreadMessageCount);
@ -143,15 +133,6 @@ const Section = (props: { type: SectionType; avatarPath?: string }) => {
); );
}; };
const showResetSessionIDDialogIfNeeded = async () => {
const userED25519KeyPairHex = await UserUtils.getUserED25519KeyPair();
if (userED25519KeyPairHex) {
return;
}
window.showResetSessionIdDialog();
};
const cleanUpMediasInterval = DURATION.MINUTES * 30; const cleanUpMediasInterval = DURATION.MINUTES * 30;
const setupTheme = () => { const setupTheme = () => {
@ -230,7 +211,6 @@ const doAppStartUp = () => {
void setupTheme(); void setupTheme();
// keep that one to make sure our users upgrade to new sessionIDS // keep that one to make sure our users upgrade to new sessionIDS
void showResetSessionIDDialogIfNeeded();
void removeAllV1OpenGroups(); void removeAllV1OpenGroups();
// this generates the key to encrypt attachments locally // this generates the key to encrypt attachments locally

@ -1,7 +1,5 @@
import React from 'react'; import React from 'react';
import { AutoSizer, List } from 'react-virtualized'; import { AutoSizer, List } from 'react-virtualized';
import { MainViewController } from '../MainViewController'; import { MainViewController } from '../MainViewController';
import { import {
ConversationListItemProps, ConversationListItemProps,
@ -10,9 +8,8 @@ import {
import { openConversationExternal, ReduxConversationType } from '../../state/ducks/conversations'; import { openConversationExternal, ReduxConversationType } from '../../state/ducks/conversations';
import { SearchResults, SearchResultsProps } from '../SearchResults'; import { SearchResults, SearchResultsProps } from '../SearchResults';
import { SessionSearchInput } from './SessionSearchInput'; import { SessionSearchInput } from './SessionSearchInput';
import { debounce } from 'lodash'; import _, { debounce } from 'lodash';
import { cleanSearchTerm } from '../../util/cleanSearchTerm'; import { cleanSearchTerm } from '../../util/cleanSearchTerm';
import { SearchOptions } from '../../types/Search';
import { RowRendererParamsType } from '../LeftPane'; import { RowRendererParamsType } from '../LeftPane';
import { SessionClosableOverlay, SessionClosableOverlayType } from './SessionClosableOverlay'; import { SessionClosableOverlay, SessionClosableOverlayType } from './SessionClosableOverlay';
import { SessionIconType } from './icon'; import { SessionIconType } from './icon';
@ -20,7 +17,6 @@ import { ContactType } from './SessionMemberListItem';
import { SessionButton, SessionButtonColor, SessionButtonType } from './SessionButton'; import { SessionButton, SessionButtonColor, SessionButtonType } from './SessionButton';
import { PubKey } from '../../session/types'; import { PubKey } from '../../session/types';
import { ToastUtils, UserUtils } from '../../session/utils'; import { ToastUtils, UserUtils } from '../../session/utils';
import { DefaultTheme } from 'styled-components';
import { LeftPaneSectionHeader } from './LeftPaneSectionHeader'; import { LeftPaneSectionHeader } from './LeftPaneSectionHeader';
import { getConversationController } from '../../session/conversations'; import { getConversationController } from '../../session/conversations';
import { ConversationTypeEnum } from '../../models/conversation'; import { ConversationTypeEnum } from '../../models/conversation';

@ -31,14 +31,20 @@ import { getConversationController } from '../../../session/conversations';
import { ReduxConversationType } from '../../../state/ducks/conversations'; import { ReduxConversationType } from '../../../state/ducks/conversations';
import { SessionMemberListItem } from '../SessionMemberListItem'; import { SessionMemberListItem } from '../SessionMemberListItem';
import autoBind from 'auto-bind'; import autoBind from 'auto-bind';
import { SectionType } from '../ActionsPanel';
import { SessionSettingCategory } from '../settings/SessionSettings'; import { SessionSettingCategory } from '../settings/SessionSettings';
import { getMentionsInput } from '../../../state/selectors/mentionsInput'; import { getMentionsInput } from '../../../state/selectors/mentionsInput';
import { updateConfirmModal } from '../../../state/ducks/modalDialog'; import { updateConfirmModal } from '../../../state/ducks/modalDialog';
import {
SectionType,
showLeftPaneSection,
showSettingsSection,
} from '../../../state/ducks/section';
import { SessionButtonColor } from '../SessionButton'; import { SessionButtonColor } from '../SessionButton';
import { SessionConfirmDialogProps } from '../SessionConfirm'; import {
import { showLeftPaneSection, showSettingsSection } from '../../../state/ducks/section'; createOrUpdateItem,
import { pushAudioPermissionNeeded } from '../../../session/utils/Toast'; getItemById,
hasLinkPreviewPopupBeenDisplayed,
} from '../../../data/data';
export interface ReplyingToMessageProps { export interface ReplyingToMessageProps {
convoId: string; convoId: string;
@ -215,7 +221,7 @@ export class SessionCompositionBox extends React.Component<Props, State> {
imgBlob = item.getAsFile(); imgBlob = item.getAsFile();
break; break;
case 'text': case 'text':
this.showLinkSharingConfirmationModalDialog(e); void this.showLinkSharingConfirmationModalDialog(e);
break; break;
default: default:
} }
@ -234,18 +240,24 @@ export class SessionCompositionBox extends React.Component<Props, State> {
* Check if what is pasted is a URL and prompt confirmation for a setting change * Check if what is pasted is a URL and prompt confirmation for a setting change
* @param e paste event * @param e paste event
*/ */
private showLinkSharingConfirmationModalDialog(e: any) { private async showLinkSharingConfirmationModalDialog(e: any) {
const pastedText = e.clipboardData.getData('text'); const pastedText = e.clipboardData.getData('text');
if (this.isURL(pastedText)) { if (this.isURL(pastedText)) {
const alreadyDisplayedPopup =
(await getItemById(hasLinkPreviewPopupBeenDisplayed))?.value || false;
window.inboxStore?.dispatch( window.inboxStore?.dispatch(
updateConfirmModal({ updateConfirmModal({
shouldShowConfirm: !window.getSettingValue('link-preview-setting'), shouldShowConfirm:
!window.getSettingValue('link-preview-setting') && !alreadyDisplayedPopup,
title: window.i18n('linkPreviewsTitle'), title: window.i18n('linkPreviewsTitle'),
message: window.i18n('linkPreviewsConfirmMessage'), message: window.i18n('linkPreviewsConfirmMessage'),
okTheme: SessionButtonColor.Danger, okTheme: SessionButtonColor.Danger,
onClickOk: () => { onClickOk: () => {
window.setSettingValue('link-preview-setting', true); window.setSettingValue('link-preview-setting', true);
}, },
onClickClose: async () => {
await createOrUpdateItem({ id: hasLinkPreviewPopupBeenDisplayed, value: true });
},
}) })
); );
} }

@ -25,6 +25,7 @@ export enum SessionIconType {
Moon = 'moon', Moon = 'moon',
Pause = 'pause', Pause = 'pause',
Pencil = 'pencil', Pencil = 'pencil',
Pin = 'pin',
Play = 'play', Play = 'play',
Plus = 'plus', Plus = 'plus',
Reply = 'reply', Reply = 'reply',
@ -224,6 +225,12 @@ export const icons = {
viewBox: '1 1 21 21', viewBox: '1 1 21 21',
ratio: 1, ratio: 1,
}, },
[SessionIconType.Pin]: {
path:
'M83.88.451L122.427 39c.603.601.603 1.585 0 2.188l-13.128 13.125c-.602.604-1.586.604-2.187 0l-3.732-3.73-17.303 17.3c3.882 14.621.095 30.857-11.37 42.32-.266.268-.535.529-.808.787-1.004.955-.843.949-1.813-.021L47.597 86.48 0 122.867l36.399-47.584L11.874 50.76c-.978-.98-.896-.826.066-1.837.24-.251.485-.503.734-.753C24.137 36.707 40.376 32.917 54.996 36.8l17.301-17.3-3.733-3.732c-.601-.601-.601-1.585 0-2.188L81.691.451c.604-.601 1.588-.601 2.189 0z',
viewBox: '0 0 122.879 122.867',
ratio: 1,
},
[SessionIconType.Play]: { [SessionIconType.Play]: {
path: path:
'M29.462,15.707c0,1.061-0.562,2.043-1.474,2.583L6.479,30.999c-0.47,0.275-0.998,0.417-1.526,0.417 c-0.513,0-1.026-0.131-1.487-0.396c-0.936-0.534-1.513-1.527-1.513-2.604V2.998c0-1.077,0.578-2.07,1.513-2.605 C4.402-0.139,5.553-0.13,6.479,0.415l21.509,12.709C28.903,13.664,29.462,14.646,29.462,15.707z', 'M29.462,15.707c0,1.061-0.562,2.043-1.474,2.583L6.479,30.999c-0.47,0.275-0.998,0.417-1.526,0.417 c-0.513,0-1.026-0.131-1.487-0.396c-0.936-0.534-1.513-1.527-1.513-2.604V2.998c0-1.077,0.578-2.07,1.513-2.605 C4.402-0.139,5.553-0.13,6.479,0.415l21.509,12.709C28.903,13.664,29.462,14.646,29.462,15.707z',

@ -13,6 +13,7 @@ import {
getInviteContactMenuItem, getInviteContactMenuItem,
getLeaveGroupMenuItem, getLeaveGroupMenuItem,
getMarkAllReadMenuItem, getMarkAllReadMenuItem,
getPinConversationMenuItem,
} from './Menu'; } from './Menu';
export type PropsContextConversationItem = { export type PropsContextConversationItem = {
@ -43,12 +44,12 @@ const ConversationListItemContextMenu = (props: PropsContextConversationItem) =>
const isGroup = type === 'group'; const isGroup = type === 'group';
return ( return (
<Menu id={triggerId} animation={animation.fade}> <Menu id={triggerId} animation={animation.fade}>
{getPinConversationMenuItem(conversationId)}
{getBlockMenuItem(isMe, type === ConversationTypeEnum.PRIVATE, isBlocked, conversationId)} {getBlockMenuItem(isMe, type === ConversationTypeEnum.PRIVATE, isBlocked, conversationId)}
{getCopyMenuItem(isPublic, isGroup, conversationId)} {getCopyMenuItem(isPublic, isGroup, conversationId)}
{getMarkAllReadMenuItem(conversationId)} {getMarkAllReadMenuItem(conversationId)}
{getChangeNicknameMenuItem(isMe, isGroup, conversationId)} {getChangeNicknameMenuItem(isMe, isGroup, conversationId)}
{getClearNicknameMenuItem(isMe, hasNickname, isGroup, conversationId)} {getClearNicknameMenuItem(isMe, hasNickname, isGroup, conversationId)}
{getDeleteMessagesMenuItem(isPublic, conversationId)} {getDeleteMessagesMenuItem(isPublic, conversationId)}
{getInviteContactMenuItem(isGroup, isPublic, conversationId)} {getInviteContactMenuItem(isGroup, isPublic, conversationId)}
{getDeleteContactMenuItem(isMe, isGroup, isPublic, left, isKickedFromGroup, conversationId)} {getDeleteContactMenuItem(isMe, isGroup, isPublic, left, isKickedFromGroup, conversationId)}

@ -1,10 +1,13 @@
import React from 'react'; import React from 'react';
import { getNumberOfPinnedConversations } from '../../../state/selectors/conversations';
import { getFocusedSection } from '../../../state/selectors/section';
import { NotificationForConvoOption, TimerOption } from '../../conversation/ConversationHeader'; import { NotificationForConvoOption, TimerOption } from '../../conversation/ConversationHeader';
import { Item, Submenu } from 'react-contexify'; import { Item, Submenu } from 'react-contexify';
import { ConversationNotificationSettingType } from '../../../models/conversation'; import { ConversationNotificationSettingType } from '../../../models/conversation';
import { useDispatch, useSelector } from 'react-redux'; import { useDispatch, useSelector } from 'react-redux';
import { changeNickNameModal, updateConfirmModal } from '../../../state/ducks/modalDialog'; import { changeNickNameModal, updateConfirmModal } from '../../../state/ducks/modalDialog';
import { SectionType } from '../../../state/ducks/section';
import { getConversationController } from '../../../session/conversations'; import { getConversationController } from '../../../session/conversations';
import { import {
blockConvoById, blockConvoById,
@ -23,6 +26,9 @@ import {
} from '../../../interactions/conversationInteractions'; } from '../../../interactions/conversationInteractions';
import { SessionButtonColor } from '../SessionButton'; import { SessionButtonColor } from '../SessionButton';
import { getTimerOptions } from '../../../state/selectors/timerOptions'; import { getTimerOptions } from '../../../state/selectors/timerOptions';
import { ToastUtils } from '../../../session/utils';
const maxNumberOfPinnedConversations = 5;
function showTimerOptions( function showTimerOptions(
isPublic: boolean, isPublic: boolean,
@ -119,6 +125,35 @@ export function getInviteContactMenuItem(
return null; return null;
} }
export interface PinConversationMenuItemProps {
conversationId: string;
}
export const getPinConversationMenuItem = (conversationId: string): JSX.Element | null => {
const isMessagesSection = useSelector(getFocusedSection) === SectionType.Message;
if (isMessagesSection && window.lokiFeatureFlags.enablePinConversations) {
const conversation = getConversationController().get(conversationId);
const isPinned = conversation.isPinned();
const nbOfAlreadyPinnedConvos = useSelector(getNumberOfPinnedConversations);
const togglePinConversation = async () => {
if ((!isPinned && nbOfAlreadyPinnedConvos < maxNumberOfPinnedConversations) || isPinned) {
await conversation.setIsPinned(!isPinned);
} else {
ToastUtils.pushToastWarning(
'pinConversationLimitToast',
window.i18n('pinConversationLimitTitle'),
window.i18n('pinConversationLimitToastDescription', maxNumberOfPinnedConversations)
);
}
};
const menuText = isPinned ? window.i18n('unpinConversation') : window.i18n('pinConversation');
return <Item onClick={togglePinConversation}>{menuText}</Item>;
}
return null;
};
export function getDeleteContactMenuItem( export function getDeleteContactMenuItem(
isMe: boolean | undefined, isMe: boolean | undefined,
isGroup: boolean | undefined, isGroup: boolean | undefined,

@ -9,7 +9,11 @@ import { StateType } from '../../../state/reducer';
import { getConversationController } from '../../../session/conversations'; import { getConversationController } from '../../../session/conversations';
import { getConversationLookup } from '../../../state/selectors/conversations'; import { getConversationLookup } from '../../../state/selectors/conversations';
import { connect, useSelector } from 'react-redux'; import { connect, useSelector } from 'react-redux';
import { getPasswordHash } from '../../../../ts/data/data'; import {
createOrUpdateItem,
getPasswordHash,
hasLinkPreviewPopupBeenDisplayed,
} from '../../../../ts/data/data';
import { SpacerLG, SpacerXS } from '../../basic/Text'; import { SpacerLG, SpacerXS } from '../../basic/Text';
import { shell } from 'electron'; import { shell } from 'electron';
import { SessionConfirmDialogProps } from '../SessionConfirm'; import { SessionConfirmDialogProps } from '../SessionConfirm';
@ -339,7 +343,13 @@ class SettingsViewInner extends React.Component<SettingsViewProps, State> {
hidden: false, hidden: false,
type: SessionSettingType.Toggle, type: SessionSettingType.Toggle,
category: SessionSettingCategory.Appearance, category: SessionSettingCategory.Appearance,
setFn: window.toggleLinkPreview, setFn: async () => {
const newValue = !window.getSettingValue('link-preview-setting');
window.setSettingValue('link-preview-setting', newValue);
if (!newValue) {
await createOrUpdateItem({ id: hasLinkPreviewPopupBeenDisplayed, value: false });
}
},
content: undefined, content: undefined,
comparisonValue: undefined, comparisonValue: undefined,
onClick: undefined, onClick: undefined,

@ -64,6 +64,7 @@ export type ServerToken = {
export const hasSyncedInitialConfigurationItem = 'hasSyncedInitialConfigurationItem'; export const hasSyncedInitialConfigurationItem = 'hasSyncedInitialConfigurationItem';
export const lastAvatarUploadTimestamp = 'lastAvatarUploadTimestamp'; export const lastAvatarUploadTimestamp = 'lastAvatarUploadTimestamp';
export const hasLinkPreviewPopupBeenDisplayed = 'hasLinkPreviewPopupBeenDisplayed';
const channelsToMake = { const channelsToMake = {
shutdown, shutdown,

@ -16,7 +16,6 @@ import {
getMessagesByConversation, getMessagesByConversation,
getUnreadByConversation, getUnreadByConversation,
getUnreadCountByConversation, getUnreadCountByConversation,
removeAllMessagesInConversation,
removeMessage as dataRemoveMessage, removeMessage as dataRemoveMessage,
saveMessages, saveMessages,
updateConversation, updateConversation,
@ -41,11 +40,7 @@ import { ConversationInteraction } from '../interactions';
import { OpenGroupVisibleMessage } from '../session/messages/outgoing/visibleMessage/OpenGroupVisibleMessage'; import { OpenGroupVisibleMessage } from '../session/messages/outgoing/visibleMessage/OpenGroupVisibleMessage';
import { OpenGroupRequestCommonType } from '../opengroup/opengroupV2/ApiUtil'; import { OpenGroupRequestCommonType } from '../opengroup/opengroupV2/ApiUtil';
import { getOpenGroupV2FromConversationId } from '../opengroup/utils/OpenGroupUtils'; import { getOpenGroupV2FromConversationId } from '../opengroup/utils/OpenGroupUtils';
import { NotificationForConvoOption } from '../components/conversation/ConversationHeader';
import { useDispatch } from 'react-redux';
import { updateConfirmModal } from '../state/ducks/modalDialog';
import { createTaskWithTimeout } from '../session/utils/TaskWithTimeout'; import { createTaskWithTimeout } from '../session/utils/TaskWithTimeout';
import { DURATION, SWARM_POLLING_TIMEOUT } from '../session/constants';
import { perfEnd, perfStart } from '../session/utils/Performance'; import { perfEnd, perfStart } from '../session/utils/Performance';
export enum ConversationTypeEnum { export enum ConversationTypeEnum {
@ -98,6 +93,7 @@ export interface ConversationAttributes {
accessKey?: any; accessKey?: any;
triggerNotificationsFor: ConversationNotificationSettingType; triggerNotificationsFor: ConversationNotificationSettingType;
isTrustedForAttachmentDownload: boolean; isTrustedForAttachmentDownload: boolean;
isPinned: boolean;
} }
export interface ConversationAttributesOptionals { export interface ConversationAttributesOptionals {
@ -135,6 +131,7 @@ export interface ConversationAttributesOptionals {
accessKey?: any; accessKey?: any;
triggerNotificationsFor?: ConversationNotificationSettingType; triggerNotificationsFor?: ConversationNotificationSettingType;
isTrustedForAttachmentDownload?: boolean; isTrustedForAttachmentDownload?: boolean;
isPinned: boolean;
} }
/** /**
@ -164,6 +161,7 @@ export const fillConvoAttributesWithDefaults = (
active_at: 0, active_at: 0,
triggerNotificationsFor: 'all', // if the settings is not set in the db, this is the default triggerNotificationsFor: 'all', // if the settings is not set in the db, this is the default
isTrustedForAttachmentDownload: false, // we don't trust a contact until we say so isTrustedForAttachmentDownload: false, // we don't trust a contact until we say so
isPinned: false,
}); });
}; };
@ -436,6 +434,7 @@ export class ConversationModel extends Backbone.Model<ConversationAttributes> {
members, members,
expireTimer: this.get('expireTimer') || 0, expireTimer: this.get('expireTimer') || 0,
subscriberCount: this.get('subscriberCount') || 0, subscriberCount: this.get('subscriberCount') || 0,
isPinned: this.isPinned(),
}; };
} }
@ -1122,6 +1121,16 @@ export class ConversationModel extends Backbone.Model<ConversationAttributes> {
await this.commit(); await this.commit();
} }
} }
public async setIsPinned(value: boolean) {
if (value !== this.get('isPinned')) {
this.set({
isPinned: value,
});
await this.commit();
}
}
public async setGroupName(name: string) { public async setGroupName(name: string) {
const profileName = this.get('name'); const profileName = this.get('name');
if (profileName !== name) { if (profileName !== name) {
@ -1253,6 +1262,10 @@ export class ConversationModel extends Backbone.Model<ConversationAttributes> {
return this.get('name') || window.i18n('unknown'); return this.get('name') || window.i18n('unknown');
} }
public isPinned() {
return this.get('isPinned');
}
public getTitle() { public getTitle() {
if (this.isPrivate()) { if (this.isPrivate()) {
const profileName = this.getProfileName(); const profileName = this.getProfileName();

@ -26,7 +26,7 @@ const logger = createLogger({
logger: directConsole, logger: directConsole,
}); });
const persistConfig = { export const persistConfig = {
key: 'root', key: 'root',
storage, storage,
whitelist: ['userConfig'], whitelist: ['userConfig'],
@ -40,7 +40,6 @@ const middlewareList = disableLogging ? [promise] : [promise, logger];
export const createStore = (initialState: any) => export const createStore = (initialState: any) =>
configureStore({ configureStore({
// reducer: allReducers,
reducer: persistedReducer, reducer: persistedReducer,
preloadedState: initialState, preloadedState: initialState,
middleware: (getDefaultMiddleware: any) => getDefaultMiddleware().concat(middlewareList), middleware: (getDefaultMiddleware: any) => getDefaultMiddleware().concat(middlewareList),

@ -163,6 +163,3 @@ export const inversedTheme = (theme: DefaultTheme): DefaultTheme => {
export const SessionTheme = ({ children, theme }: { children: any; theme: DefaultTheme }) => ( export const SessionTheme = ({ children, theme }: { children: any; theme: DefaultTheme }) => (
<ThemeProvider theme={theme}>{children}</ThemeProvider> <ThemeProvider theme={theme}>{children}</ThemeProvider>
); );
window.lightTheme = lightTheme;
window.darkTheme = darkTheme;

@ -200,6 +200,8 @@ export interface ReduxConversationType {
currentNotificationSetting: ConversationNotificationSettingType; currentNotificationSetting: ConversationNotificationSettingType;
notificationForConvo: Array<NotificationForConvoOption>; notificationForConvo: Array<NotificationForConvoOption>;
isPinned: boolean;
} }
export type ConversationLookupType = { export type ConversationLookupType = {

@ -1,9 +1,18 @@
import { SectionType } from '../../components/session/ActionsPanel';
import { SessionSettingCategory } from '../../components/session/settings/SessionSettings'; import { SessionSettingCategory } from '../../components/session/settings/SessionSettings';
export const FOCUS_SECTION = 'FOCUS_SECTION'; export const FOCUS_SECTION = 'FOCUS_SECTION';
export const FOCUS_SETTINGS_SECTION = 'FOCUS_SETTINGS_SECTION'; export const FOCUS_SETTINGS_SECTION = 'FOCUS_SETTINGS_SECTION';
export enum SectionType {
Profile,
Message,
Contact,
Channel,
Settings,
Moon,
PathIndicator,
}
type FocusSectionActionType = { type FocusSectionActionType = {
type: 'FOCUS_SECTION'; type: 'FOCUS_SECTION';
payload: SectionType; payload: SectionType;

@ -71,6 +71,14 @@ const collator = new Intl.Collator();
export const _getConversationComparator = (testingi18n?: LocalizerType) => { export const _getConversationComparator = (testingi18n?: LocalizerType) => {
return (left: ReduxConversationType, right: ReduxConversationType): number => { return (left: ReduxConversationType, right: ReduxConversationType): number => {
// Pin is the first criteria to check
if (left.isPinned && !right.isPinned) {
return -1;
}
if (!left.isPinned && right.isPinned) {
return 1;
}
// Then if none is pinned, check other criteria
const leftActiveAt = left.activeAt; const leftActiveAt = left.activeAt;
const rightActiveAt = right.activeAt; const rightActiveAt = right.activeAt;
if (leftActiveAt && !rightActiveAt) { if (leftActiveAt && !rightActiveAt) {
@ -244,3 +252,8 @@ export const getConversationHeaderProps = createSelector(getSelectedConversation
isGroup: state.isGroup, isGroup: state.isGroup,
}; };
}); });
export const getNumberOfPinnedConversations = createSelector(getConversations, (state): number => {
const values = Object.values(state.conversationLookup);
return values.filter(conversation => conversation.isPinned).length;
});

@ -1,9 +1,9 @@
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import { StateType } from '../reducer'; import { StateType } from '../reducer';
import { SectionType } from '../../components/session/ActionsPanel';
import { OnionState } from '../ducks/onion'; import { OnionState } from '../ducks/onion';
import { Snode } from '../../data/data'; import { Snode } from '../../data/data';
import { SectionType } from '../../state/ducks/section';
export const getOnionPaths = (state: StateType): OnionState => state.onionPaths; export const getOnionPaths = (state: StateType): OnionState => state.onionPaths;

@ -1,8 +1,7 @@
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import { StateType } from '../reducer'; import { StateType } from '../reducer';
import { SectionStateType } from '../ducks/section'; import { SectionStateType, SectionType } from '../ducks/section';
import { SectionType } from '../../components/session/ActionsPanel';
import { SessionSettingCategory } from '../../components/session/settings/SessionSettings'; import { SessionSettingCategory } from '../../components/session/settings/SessionSettings';
export const getSection = (state: StateType): SectionStateType => state.section; export const getSection = (state: StateType): SectionStateType => state.section;

@ -0,0 +1,349 @@
import { assert } from 'chai';
import { ConversationTypeEnum } from '../../../../models/conversation';
import { ConversationLookupType } from '../../../../state/ducks/conversations';
import {
_getConversationComparator,
_getLeftPaneLists,
} from '../../../../state/selectors/conversations';
describe('state/selectors/conversations', () => {
describe('#getLeftPaneList', () => {
// tslint:disable-next-line: max-func-body-length
it('sorts conversations based on timestamp then by intl-friendly title', () => {
const i18n = (key: string) => key;
const data: ConversationLookupType = {
id1: {
id: 'id1',
activeAt: 0,
name: 'No timestamp',
phoneNumber: 'notused',
type: ConversationTypeEnum.PRIVATE,
isMe: false,
unreadCount: 1,
mentionedUs: false,
isSelected: false,
isTyping: false,
isBlocked: false,
isKickedFromGroup: false,
left: false,
hasNickname: false,
isPublic: false,
subscriberCount: 0,
currentNotificationSetting: 'all',
weAreAdmin: false,
isGroup: false,
isPrivate: false,
notificationForConvo: [{ value: 'all', name: 'all' }],
avatarPath: '',
groupAdmins: [],
lastMessage: undefined,
members: [],
profileName: 'df',
expireTimer: 0,
isPinned: false,
},
id2: {
id: 'id2',
activeAt: 20,
name: 'B',
phoneNumber: 'notused',
type: ConversationTypeEnum.PRIVATE,
isMe: false,
unreadCount: 1,
mentionedUs: false,
isSelected: false,
isTyping: false,
isBlocked: false,
isKickedFromGroup: false,
left: false,
hasNickname: false,
isPublic: false,
subscriberCount: 0,
currentNotificationSetting: 'all',
weAreAdmin: false,
isGroup: false,
isPrivate: false,
notificationForConvo: [{ value: 'all', name: 'all' }],
avatarPath: '',
groupAdmins: [],
lastMessage: undefined,
members: [],
profileName: 'df',
expireTimer: 0,
isPinned: false,
},
id3: {
id: 'id3',
activeAt: 20,
name: 'C',
phoneNumber: 'notused',
type: ConversationTypeEnum.PRIVATE,
isMe: false,
unreadCount: 1,
mentionedUs: false,
isSelected: false,
isTyping: false,
isBlocked: false,
isKickedFromGroup: false,
left: false,
hasNickname: false,
isPublic: false,
subscriberCount: 0,
currentNotificationSetting: 'all',
weAreAdmin: false,
isGroup: false,
isPrivate: false,
notificationForConvo: [{ value: 'all', name: 'all' }],
avatarPath: '',
groupAdmins: [],
lastMessage: undefined,
members: [],
profileName: 'df',
expireTimer: 0,
isPinned: false,
},
id4: {
id: 'id4',
activeAt: 20,
name: 'Á',
phoneNumber: 'notused',
type: ConversationTypeEnum.PRIVATE,
isMe: false,
unreadCount: 1,
mentionedUs: false,
isSelected: false,
isTyping: false,
isBlocked: false,
isKickedFromGroup: false,
left: false,
hasNickname: false,
isPublic: false,
subscriberCount: 0,
currentNotificationSetting: 'all',
weAreAdmin: false,
isGroup: false,
isPrivate: false,
notificationForConvo: [{ value: 'all', name: 'all' }],
avatarPath: '',
groupAdmins: [],
expireTimer: 0,
lastMessage: undefined,
members: [],
profileName: 'df',
isPinned: false,
},
id5: {
id: 'id5',
activeAt: 30,
name: 'First!',
phoneNumber: 'notused',
type: ConversationTypeEnum.PRIVATE,
isMe: false,
unreadCount: 1,
mentionedUs: false,
isSelected: false,
isTyping: false,
isBlocked: false,
isKickedFromGroup: false,
left: false,
hasNickname: false,
isPublic: false,
subscriberCount: 0,
expireTimer: 0,
currentNotificationSetting: 'all',
weAreAdmin: false,
isGroup: false,
isPrivate: false,
notificationForConvo: [{ value: 'all', name: 'all' }],
avatarPath: '',
groupAdmins: [],
lastMessage: undefined,
members: [],
profileName: 'df',
isPinned: false,
},
};
const comparator = _getConversationComparator(i18n);
const { conversations } = _getLeftPaneLists(data, comparator);
assert.strictEqual(conversations[0].name, 'First!');
assert.strictEqual(conversations[1].name, 'Á');
assert.strictEqual(conversations[2].name, 'B');
assert.strictEqual(conversations[3].name, 'C');
});
});
describe('#getLeftPaneListWithPinned', () => {
// tslint:disable-next-line: max-func-body-length
it('sorts conversations based on pin, timestamp then by intl-friendly title', () => {
const i18n = (key: string) => key;
const data: ConversationLookupType = {
id1: {
id: 'id1',
activeAt: 0,
name: 'No timestamp',
phoneNumber: 'notused',
type: ConversationTypeEnum.PRIVATE,
isMe: false,
unreadCount: 1,
mentionedUs: false,
isSelected: false,
isTyping: false,
isBlocked: false,
isKickedFromGroup: false,
left: false,
subscriberCount: 0,
expireTimer: 0,
currentNotificationSetting: 'all',
weAreAdmin: false,
isGroup: false,
isPrivate: false,
notificationForConvo: [{ value: 'all', name: 'all' }],
avatarPath: '',
groupAdmins: [],
lastMessage: undefined,
members: [],
profileName: 'df',
isPinned: false,
hasNickname: false,
isPublic: false,
},
id2: {
id: 'id2',
activeAt: 20,
name: 'B',
phoneNumber: 'notused',
type: ConversationTypeEnum.PRIVATE,
isMe: false,
unreadCount: 1,
mentionedUs: false,
isSelected: false,
isTyping: false,
isBlocked: false,
isKickedFromGroup: false,
left: false,
subscriberCount: 0,
expireTimer: 0,
currentNotificationSetting: 'all',
weAreAdmin: false,
isGroup: false,
isPrivate: false,
notificationForConvo: [{ value: 'all', name: 'all' }],
avatarPath: '',
groupAdmins: [],
lastMessage: undefined,
members: [],
profileName: 'df',
isPinned: false,
hasNickname: false,
isPublic: false,
},
id3: {
id: 'id3',
activeAt: 20,
name: 'C',
phoneNumber: 'notused',
type: ConversationTypeEnum.PRIVATE,
isMe: false,
unreadCount: 1,
mentionedUs: false,
isSelected: false,
isTyping: false,
isBlocked: false,
isKickedFromGroup: false,
left: false,
subscriberCount: 0,
expireTimer: 0,
currentNotificationSetting: 'all',
weAreAdmin: false,
isGroup: false,
isPrivate: false,
notificationForConvo: [{ value: 'all', name: 'all' }],
avatarPath: '',
groupAdmins: [],
lastMessage: undefined,
members: [],
profileName: 'df',
isPinned: true,
hasNickname: false,
isPublic: false,
},
id4: {
id: 'id4',
activeAt: 20,
name: 'Á',
phoneNumber: 'notused',
type: ConversationTypeEnum.PRIVATE,
isMe: false,
unreadCount: 1,
mentionedUs: false,
isSelected: false,
isTyping: false,
isBlocked: false,
isKickedFromGroup: false,
left: false,
subscriberCount: 0,
expireTimer: 0,
currentNotificationSetting: 'all',
weAreAdmin: false,
isGroup: false,
isPrivate: false,
notificationForConvo: [{ value: 'all', name: 'all' }],
avatarPath: '',
groupAdmins: [],
lastMessage: undefined,
members: [],
profileName: 'df',
isPinned: true,
hasNickname: false,
isPublic: false,
},
id5: {
id: 'id5',
activeAt: 30,
name: 'First!',
phoneNumber: 'notused',
type: ConversationTypeEnum.PRIVATE,
isMe: false,
unreadCount: 1,
mentionedUs: false,
isSelected: false,
isTyping: false,
isBlocked: false,
isKickedFromGroup: false,
left: false,
subscriberCount: 0,
expireTimer: 0,
currentNotificationSetting: 'all',
weAreAdmin: false,
isGroup: false,
isPrivate: false,
notificationForConvo: [{ value: 'all', name: 'all' }],
avatarPath: '',
groupAdmins: [],
lastMessage: undefined,
members: [],
profileName: 'df',
isPinned: false,
hasNickname: false,
isPublic: false,
},
};
const comparator = _getConversationComparator(i18n);
const { conversations } = _getLeftPaneLists(data, comparator);
assert.strictEqual(conversations[0].name, 'Á');
assert.strictEqual(conversations[1].name, 'C');
assert.strictEqual(conversations[2].name, 'First!');
assert.strictEqual(conversations[3].name, 'B');
});
});
});

@ -1,174 +0,0 @@
import { assert } from 'chai';
import { ConversationTypeEnum } from '../../../models/conversation';
import { ConversationLookupType } from '../../../state/ducks/conversations';
import {
_getConversationComparator,
_getLeftPaneLists,
} from '../../../state/selectors/conversations';
describe('state/selectors/conversations', () => {
describe('#getLeftPaneList', () => {
// tslint:disable-next-line: max-func-body-length
it('sorts conversations based on timestamp then by intl-friendly title', () => {
const i18n = (key: string) => key;
const data: ConversationLookupType = {
id1: {
id: 'id1',
activeAt: 0,
name: 'No timestamp',
phoneNumber: 'notused',
type: ConversationTypeEnum.PRIVATE,
isMe: false,
unreadCount: 1,
mentionedUs: false,
isSelected: false,
isTyping: false,
isBlocked: false,
isKickedFromGroup: false,
left: false,
hasNickname: false,
isPublic: false,
subscriberCount: 0,
currentNotificationSetting: 'all',
weAreAdmin: false,
isGroup: false,
isPrivate: false,
notificationForConvo: [{ value: 'all', name: 'all' }],
avatarPath: '',
groupAdmins: [],
lastMessage: undefined,
members: [],
profileName: 'df',
expireTimer: 0,
},
id2: {
id: 'id2',
activeAt: 20,
name: 'B',
phoneNumber: 'notused',
type: ConversationTypeEnum.PRIVATE,
isMe: false,
unreadCount: 1,
mentionedUs: false,
isSelected: false,
isTyping: false,
isBlocked: false,
isKickedFromGroup: false,
left: false,
hasNickname: false,
isPublic: false,
subscriberCount: 0,
currentNotificationSetting: 'all',
weAreAdmin: false,
isGroup: false,
isPrivate: false,
notificationForConvo: [{ value: 'all', name: 'all' }],
avatarPath: '',
groupAdmins: [],
lastMessage: undefined,
members: [],
profileName: 'df',
expireTimer: 0,
},
id3: {
id: 'id3',
activeAt: 20,
name: 'C',
phoneNumber: 'notused',
type: ConversationTypeEnum.PRIVATE,
isMe: false,
unreadCount: 1,
mentionedUs: false,
isSelected: false,
isTyping: false,
isBlocked: false,
isKickedFromGroup: false,
left: false,
hasNickname: false,
isPublic: false,
subscriberCount: 0,
currentNotificationSetting: 'all',
weAreAdmin: false,
isGroup: false,
isPrivate: false,
notificationForConvo: [{ value: 'all', name: 'all' }],
avatarPath: '',
groupAdmins: [],
lastMessage: undefined,
members: [],
profileName: 'df',
expireTimer: 0,
},
id4: {
id: 'id4',
activeAt: 20,
name: 'Á',
phoneNumber: 'notused',
type: ConversationTypeEnum.PRIVATE,
isMe: false,
unreadCount: 1,
mentionedUs: false,
isSelected: false,
isTyping: false,
isBlocked: false,
isKickedFromGroup: false,
left: false,
hasNickname: false,
isPublic: false,
subscriberCount: 0,
currentNotificationSetting: 'all',
weAreAdmin: false,
isGroup: false,
isPrivate: false,
notificationForConvo: [{ value: 'all', name: 'all' }],
avatarPath: '',
groupAdmins: [],
expireTimer: 0,
lastMessage: undefined,
members: [],
profileName: 'df',
},
id5: {
id: 'id5',
activeAt: 30,
name: 'First!',
phoneNumber: 'notused',
type: ConversationTypeEnum.PRIVATE,
isMe: false,
unreadCount: 1,
mentionedUs: false,
isSelected: false,
isTyping: false,
isBlocked: false,
isKickedFromGroup: false,
left: false,
hasNickname: false,
isPublic: false,
subscriberCount: 0,
expireTimer: 0,
currentNotificationSetting: 'all',
weAreAdmin: false,
isGroup: false,
isPrivate: false,
notificationForConvo: [{ value: 'all', name: 'all' }],
avatarPath: '',
groupAdmins: [],
lastMessage: undefined,
members: [],
profileName: 'df',
},
};
const comparator = _getConversationComparator(i18n);
const { conversations } = _getLeftPaneLists(data, comparator);
assert.strictEqual(conversations[0].name, 'First!');
assert.strictEqual(conversations[1].name, 'Á');
assert.strictEqual(conversations[2].name, 'B');
assert.strictEqual(conversations[3].name, 'C');
});
});
});

@ -88,6 +88,7 @@ export class MockConversation {
zombies: [], zombies: [],
triggerNotificationsFor: 'all', triggerNotificationsFor: 'all',
isTrustedForAttachmentDownload: false, isTrustedForAttachmentDownload: false,
isPinned: false,
}; };
} }

@ -9,6 +9,7 @@ import { actions as userActions } from '../state/ducks/user';
import { mn_decode, mn_encode } from '../session/crypto/mnemonic'; import { mn_decode, mn_encode } from '../session/crypto/mnemonic';
import { ConversationTypeEnum } from '../models/conversation'; import { ConversationTypeEnum } from '../models/conversation';
import _ from 'underscore'; import _ from 'underscore';
import { persistStore } from 'redux-persist';
/** /**
* Might throw * Might throw

5
ts/window.d.ts vendored

@ -48,6 +48,7 @@ declare global {
useFileOnionRequests: boolean; useFileOnionRequests: boolean;
useFileOnionRequestsV2: boolean; useFileOnionRequestsV2: boolean;
padOutgoingAttachments: boolean; padOutgoingAttachments: boolean;
enablePinConversations: boolean;
}; };
lokiSnodeAPI: LokiSnodeAPI; lokiSnodeAPI: LokiSnodeAPI;
onLogin: any; onLogin: any;
@ -56,10 +57,8 @@ declare global {
getSeedNodeList: () => Array<any> | undefined; getSeedNodeList: () => Array<any> | undefined;
setPassword: any; setPassword: any;
setSettingValue: any; setSettingValue: any;
showResetSessionIdDialog: any;
storage: any; storage: any;
textsecure: LibTextsecure; textsecure: LibTextsecure;
toggleLinkPreview: any;
toggleMediaPermissions: any; toggleMediaPermissions: any;
toggleMenuBar: any; toggleMenuBar: any;
toggleSpellCheck: any; toggleSpellCheck: any;
@ -79,8 +78,6 @@ declare global {
expired: (boolean) => void; expired: (boolean) => void;
expiredStatus: () => boolean; expiredStatus: () => boolean;
}; };
lightTheme: DefaultTheme;
darkTheme: DefaultTheme;
LokiPushNotificationServer: any; LokiPushNotificationServer: any;
globalOnlineStatus: boolean; globalOnlineStatus: boolean;
confirmationDialog: any; confirmationDialog: any;

Loading…
Cancel
Save