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",
"clickToTrustContact": "Click to download media",
"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.",
"orJoinOneOfThese": "Ou rejoignez un de ceux-ci...",
"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('unload', () => Whisper.Notifications.fastClear());
window.showResetSessionIdDialog = () => {
appView.showResetSessionIdDialog();
};
// Set user's launch count.
const prevLaunchCount = window.getSettingValue('launch-count');
const launchCount = !prevLaunchCount ? 1 : prevLaunchCount + 1;
@ -340,11 +336,6 @@
window.libsession.Utils.ToastUtils.pushSpellCheckDirty();
};
window.toggleLinkPreview = () => {
const newValue = !window.getSettingValue('link-preview-setting');
window.setSettingValue('link-preview-setting', newValue);
};
window.toggleMediaPermissions = () => {
const value = window.getMediaPermissions();
window.setMediaPermissions(!value);

@ -94,16 +94,5 @@
window.focus(); // FIXME
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,
useFileOnionRequestsV2: true, // more compact encoding of files in response
padOutgoingAttachments: true,
enablePinConversations: false,
};
if (typeof process.env.NODE_ENV === 'string' && process.env.NODE_ENV.includes('test-integration')) {

@ -22,8 +22,12 @@ import {
} from '../state/ducks/conversations';
import _ from 'underscore';
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 {}
type PropsHousekeeping = {
@ -36,26 +40,73 @@ const Portal = ({ children }: { children: any }) => {
return createPortal(children, document.querySelector('.inbox.index') as Element);
};
const AvatarItem = (props: {
avatarPath?: string;
conversationId: string;
memberAvatars?: Array<ConversationAvatar>;
const HeaderItem = (props: {
unreadCount: number;
isMe: boolean;
mentionedUs: boolean;
activeAt?: number;
name?: 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 (
<div className="module-conversation-list-item__avatar-container">
<Avatar
avatarPath={avatarPath}
name={userName}
size={AvatarSize.S}
memberAvatars={memberAvatars}
pubkey={conversationId}
const isMessagesSection = useSelector(getFocusedSection) === SectionType.Message;
const pinIcon =
isMessagesSection && isPinned ? (
<SessionIcon
iconType={SessionIconType.Pin}
iconColor={theme.colors.textColorSubtle}
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>
);
};
@ -93,12 +144,11 @@ const UserItem = (props: {
};
const MessageItem = (props: {
isTyping: boolean;
lastMessage?: LastMessageType;
isTyping: boolean;
unreadCount: number;
}) => {
const { lastMessage, isTyping, unreadCount } = props;
const theme = useTheme();
if (!lastMessage && !isTyping) {
@ -134,51 +184,26 @@ const MessageItem = (props: {
);
};
const HeaderItem = (props: {
unreadCount: number;
isMe: boolean;
mentionedUs: boolean;
activeAt?: number;
const AvatarItem = (props: {
avatarPath?: string;
conversationId: string;
memberAvatars?: Array<ConversationAvatar>;
name?: string;
profileName?: string;
conversationId: string;
}) => {
const { unreadCount, mentionedUs, activeAt, isMe, conversationId, profileName, name } = props;
const { avatarPath, name, conversationId, profileName, memberAvatars } = props;
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>;
}
const userName = name || profileName || conversationId;
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>
{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 className="module-conversation-list-item__avatar-container">
<Avatar
avatarPath={avatarPath}
name={userName}
size={AvatarSize.S}
memberAvatars={memberAvatars}
pubkey={conversationId}
/>
</div>
);
};
@ -195,6 +220,7 @@ const ConversationListItem = (props: Props) => {
mentionedUs,
isMe,
name,
isPinned,
profileName,
isTyping,
lastMessage,
@ -247,6 +273,7 @@ const ConversationListItem = (props: Props) => {
unreadCount={unreadCount}
activeAt={activeAt}
isMe={isMe}
isPinned={isPinned}
conversationId={conversationId}
name={name}
profileName={profileName}

@ -1,6 +1,6 @@
import React from 'react';
import { ActionsPanel, SectionType } from './session/ActionsPanel';
import { ActionsPanel } from './session/ActionsPanel';
import { LeftPaneMessageSection } from './session/LeftPaneMessageSection';
import { LeftPaneContactSection } from './session/LeftPaneContactSection';
@ -8,11 +8,10 @@ import { LeftPaneSettingSection } from './session/LeftPaneSettingSection';
import { SessionTheme } from '../state/ducks/SessionTheme';
import { SessionExpiredWarning } from './session/network/SessionExpiredWarning';
import { getFocusedSection } from '../state/selectors/section';
import { useDispatch, useSelector } from 'react-redux';
import { useSelector } from 'react-redux';
import { getLeftPaneLists } from '../state/selectors/conversations';
import { getQuery, getSearchResults, isSearching } from '../state/selectors/search';
import { clearSearch, search, updateSearchTerm } from '../state/ducks/search';
import { useTheme } from 'styled-components';
import { SectionType } from '../state/ducks/section';
import { getTheme } from '../state/selectors/theme';
// 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 { useInterval } from '../../hooks/useInterval';
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 { getOpenGroupManager } from '../../opengroup/opengroupV2/OpenGroupManagerV2';
@ -47,16 +47,6 @@ import { ActionPanelOnionStatusLight } from '../OnionStatusPathDialog';
// 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 ourNumber = useSelector(getOurNumber);
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 setupTheme = () => {
@ -230,7 +211,6 @@ const doAppStartUp = () => {
void setupTheme();
// keep that one to make sure our users upgrade to new sessionIDS
void showResetSessionIDDialogIfNeeded();
void removeAllV1OpenGroups();
// this generates the key to encrypt attachments locally

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

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

@ -25,6 +25,7 @@ export enum SessionIconType {
Moon = 'moon',
Pause = 'pause',
Pencil = 'pencil',
Pin = 'pin',
Play = 'play',
Plus = 'plus',
Reply = 'reply',
@ -224,6 +225,12 @@ export const icons = {
viewBox: '1 1 21 21',
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]: {
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',

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

@ -1,10 +1,13 @@
import React from 'react';
import { getNumberOfPinnedConversations } from '../../../state/selectors/conversations';
import { getFocusedSection } from '../../../state/selectors/section';
import { NotificationForConvoOption, TimerOption } from '../../conversation/ConversationHeader';
import { Item, Submenu } from 'react-contexify';
import { ConversationNotificationSettingType } from '../../../models/conversation';
import { useDispatch, useSelector } from 'react-redux';
import { changeNickNameModal, updateConfirmModal } from '../../../state/ducks/modalDialog';
import { SectionType } from '../../../state/ducks/section';
import { getConversationController } from '../../../session/conversations';
import {
blockConvoById,
@ -23,6 +26,9 @@ import {
} from '../../../interactions/conversationInteractions';
import { SessionButtonColor } from '../SessionButton';
import { getTimerOptions } from '../../../state/selectors/timerOptions';
import { ToastUtils } from '../../../session/utils';
const maxNumberOfPinnedConversations = 5;
function showTimerOptions(
isPublic: boolean,
@ -119,6 +125,35 @@ export function getInviteContactMenuItem(
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(
isMe: boolean | undefined,
isGroup: boolean | undefined,

@ -9,7 +9,11 @@ import { StateType } from '../../../state/reducer';
import { getConversationController } from '../../../session/conversations';
import { getConversationLookup } from '../../../state/selectors/conversations';
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 { shell } from 'electron';
import { SessionConfirmDialogProps } from '../SessionConfirm';
@ -339,7 +343,13 @@ class SettingsViewInner extends React.Component<SettingsViewProps, State> {
hidden: false,
type: SessionSettingType.Toggle,
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,
comparisonValue: undefined,
onClick: undefined,

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

@ -16,7 +16,6 @@ import {
getMessagesByConversation,
getUnreadByConversation,
getUnreadCountByConversation,
removeAllMessagesInConversation,
removeMessage as dataRemoveMessage,
saveMessages,
updateConversation,
@ -41,11 +40,7 @@ import { ConversationInteraction } from '../interactions';
import { OpenGroupVisibleMessage } from '../session/messages/outgoing/visibleMessage/OpenGroupVisibleMessage';
import { OpenGroupRequestCommonType } from '../opengroup/opengroupV2/ApiUtil';
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 { DURATION, SWARM_POLLING_TIMEOUT } from '../session/constants';
import { perfEnd, perfStart } from '../session/utils/Performance';
export enum ConversationTypeEnum {
@ -98,6 +93,7 @@ export interface ConversationAttributes {
accessKey?: any;
triggerNotificationsFor: ConversationNotificationSettingType;
isTrustedForAttachmentDownload: boolean;
isPinned: boolean;
}
export interface ConversationAttributesOptionals {
@ -135,6 +131,7 @@ export interface ConversationAttributesOptionals {
accessKey?: any;
triggerNotificationsFor?: ConversationNotificationSettingType;
isTrustedForAttachmentDownload?: boolean;
isPinned: boolean;
}
/**
@ -164,6 +161,7 @@ export const fillConvoAttributesWithDefaults = (
active_at: 0,
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
isPinned: false,
});
};
@ -436,6 +434,7 @@ export class ConversationModel extends Backbone.Model<ConversationAttributes> {
members,
expireTimer: this.get('expireTimer') || 0,
subscriberCount: this.get('subscriberCount') || 0,
isPinned: this.isPinned(),
};
}
@ -1122,6 +1121,16 @@ export class ConversationModel extends Backbone.Model<ConversationAttributes> {
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) {
const profileName = this.get('name');
if (profileName !== name) {
@ -1253,6 +1262,10 @@ export class ConversationModel extends Backbone.Model<ConversationAttributes> {
return this.get('name') || window.i18n('unknown');
}
public isPinned() {
return this.get('isPinned');
}
public getTitle() {
if (this.isPrivate()) {
const profileName = this.getProfileName();

@ -26,7 +26,7 @@ const logger = createLogger({
logger: directConsole,
});
const persistConfig = {
export const persistConfig = {
key: 'root',
storage,
whitelist: ['userConfig'],
@ -40,7 +40,6 @@ const middlewareList = disableLogging ? [promise] : [promise, logger];
export const createStore = (initialState: any) =>
configureStore({
// reducer: allReducers,
reducer: persistedReducer,
preloadedState: initialState,
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 }) => (
<ThemeProvider theme={theme}>{children}</ThemeProvider>
);
window.lightTheme = lightTheme;
window.darkTheme = darkTheme;

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

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

@ -71,6 +71,14 @@ const collator = new Intl.Collator();
export const _getConversationComparator = (testingi18n?: LocalizerType) => {
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 rightActiveAt = right.activeAt;
if (leftActiveAt && !rightActiveAt) {
@ -244,3 +252,8 @@ export const getConversationHeaderProps = createSelector(getSelectedConversation
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 { StateType } from '../reducer';
import { SectionType } from '../../components/session/ActionsPanel';
import { OnionState } from '../ducks/onion';
import { Snode } from '../../data/data';
import { SectionType } from '../../state/ducks/section';
export const getOnionPaths = (state: StateType): OnionState => state.onionPaths;

@ -1,8 +1,7 @@
import { createSelector } from 'reselect';
import { StateType } from '../reducer';
import { SectionStateType } from '../ducks/section';
import { SectionType } from '../../components/session/ActionsPanel';
import { SectionStateType, SectionType } from '../ducks/section';
import { SessionSettingCategory } from '../../components/session/settings/SessionSettings';
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: [],
triggerNotificationsFor: 'all',
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 { ConversationTypeEnum } from '../models/conversation';
import _ from 'underscore';
import { persistStore } from 'redux-persist';
/**
* Might throw

5
ts/window.d.ts vendored

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

Loading…
Cancel
Save