From 6dd7f34e4d43d95462513844a985b797ae6c1e03 Mon Sep 17 00:00:00 2001 From: Warrick Corfe-Tan Date: Tue, 29 Jun 2021 10:52:55 +1000 Subject: [PATCH 01/21] Pin conversations WIP. Pinning functioning and persisting on conversation list. --- ts/components/ConversationListItem.tsx | 12 ++++++- .../session/LeftPaneMessageSection.tsx | 12 +++++-- ts/components/session/icon/Icons.tsx | 7 ++++ .../menu/ConversationListItemContextMenu.tsx | 2 ++ ts/components/session/menu/Menu.tsx | 32 +++++++++++++++++-- ts/models/conversation.ts | 17 ++++++---- ts/state/createStore.ts | 5 +-- ts/state/ducks/conversations.ts | 1 + 8 files changed, 73 insertions(+), 15 deletions(-) diff --git a/ts/components/ConversationListItem.tsx b/ts/components/ConversationListItem.tsx index 7c3594054..73b889201 100644 --- a/ts/components/ConversationListItem.tsx +++ b/ts/components/ConversationListItem.tsx @@ -22,6 +22,8 @@ import { OutgoingMessageStatus } from './conversation/message/OutgoingMessageSta import { DefaultTheme, withTheme } from 'styled-components'; import { PubKey } from '../session/types'; import { ConversationType, openConversationExternal } from '../state/ducks/conversations'; +import { SessionIcon, SessionIconSize, SessionIconType } from './session/icon'; +import { SessionButtonColor } from './session/SessionButton'; export interface ConversationListItemProps extends ConversationType { index?: number; // used to force a refresh when one conversation is removed on top of the list @@ -63,7 +65,7 @@ class ConversationListItem extends React.PureComponent { } public renderHeader() { - const { unreadCount, mentionedUs, activeAt } = this.props; + const { unreadCount, mentionedUs, activeAt, isPinned } = this.props; let atSymbol = null; let unreadCountDiv = null; @@ -81,7 +83,15 @@ class ConversationListItem extends React.PureComponent { )} > {this.renderUser()} + + {isPinned ? + + : null + } {unreadCountDiv} {atSymbol} { diff --git a/ts/components/session/LeftPaneMessageSection.tsx b/ts/components/session/LeftPaneMessageSection.tsx index 5dcdd7f3c..6fa34522b 100644 --- a/ts/components/session/LeftPaneMessageSection.tsx +++ b/ts/components/session/LeftPaneMessageSection.tsx @@ -1,5 +1,5 @@ import React from 'react'; - +import _ from 'lodash'; import { AutoSizer, List } from 'react-virtualized'; import { MainViewController } from '../MainViewController'; @@ -81,12 +81,17 @@ export class LeftPaneMessageSection extends React.Component { } public renderRow = ({ index, key, style }: RowRendererParamsType): JSX.Element => { - const { conversations, openConversationExternal } = this.props; + const { openConversationExternal } = this.props; + let { conversations } = this.props; if (!conversations) { throw new Error('renderRow: Tried to render without conversations'); } + conversations = _.sortBy([...conversations], (convo) => { + return convo.isPinned ? -1 : 1 + }); + const conversation = conversations[index]; return ( @@ -100,7 +105,8 @@ export class LeftPaneMessageSection extends React.Component { }; public renderList(): JSX.Element | Array { - const { conversations, openConversationExternal, searchResults } = this.props; + let { conversations } = this.props; + const { openConversationExternal, searchResults } = this.props; const contacts = searchResults?.contacts || []; if (searchResults) { diff --git a/ts/components/session/icon/Icons.tsx b/ts/components/session/icon/Icons.tsx index fe5b1b3d0..e0c9e7daa 100644 --- a/ts/components/session/icon/Icons.tsx +++ b/ts/components/session/icon/Icons.tsx @@ -25,6 +25,7 @@ export enum SessionIconType { Moon = 'moon', Pause = 'pause', Pencil = 'pencil', + Pin = 'pin', Play = 'play', Plus = 'plus', Reply = 'reply', @@ -220,6 +221,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', diff --git a/ts/components/session/menu/ConversationListItemContextMenu.tsx b/ts/components/session/menu/ConversationListItemContextMenu.tsx index b00546fdb..cbd36da60 100644 --- a/ts/components/session/menu/ConversationListItemContextMenu.tsx +++ b/ts/components/session/menu/ConversationListItemContextMenu.tsx @@ -12,6 +12,7 @@ import { getInviteContactMenuItem, getLeaveGroupMenuItem, getMarkAllReadMenuItem, + MenuItemPinConversation } from './Menu'; export type PropsContextConversationItem = { @@ -46,6 +47,7 @@ export const ConversationListItemContextMenu = (props: PropsContextConversationI return ( <> + {getBlockMenuItem(isMe, type === ConversationTypeEnum.PRIVATE, isBlocked, conversationId)} {getCopyMenuItem(isPublic, isGroup, conversationId)} {getMarkAllReadMenuItem(conversationId)} diff --git a/ts/components/session/menu/Menu.tsx b/ts/components/session/menu/Menu.tsx index 39b741a86..5b38e38e8 100644 --- a/ts/components/session/menu/Menu.tsx +++ b/ts/components/session/menu/Menu.tsx @@ -4,8 +4,8 @@ import { NotificationForConvoOption, TimerOption } from '../../conversation/Conv import { Item, Submenu } from 'react-contexify'; import { ConversationNotificationSettingType } from '../../../models/conversation'; import { useDispatch } from 'react-redux'; +import { actions as conversationActions } from '../../../state/ducks/conversations'; import { - adminLeaveClosedGroup, changeNickNameModal, updateConfirmModal, } from '../../../state/ducks/modalDialog'; @@ -25,6 +25,8 @@ import { showUpdateGroupNameByConvoId, unblockConvoById, } from '../../../interactions/conversationInteractions'; +import { purgeStoredState } from 'redux-persist'; +import { persistConfig, _purgedStoredState } from '../../../state/createStore'; function showTimerOptions( isPublic: boolean, @@ -125,6 +127,30 @@ export function getInviteContactMenuItem( return null; } +export interface PinConversationMenuItemProps { + conversationId: string; +} + +export const MenuItemPinConversation = (props: PinConversationMenuItemProps): JSX.Element | null => { + const { conversationId } = props; + const conversation = getConversationController().get(conversationId).getProps(); + const { isPinned } = conversation; + + const togglePinConversation = () => { + window.inboxStore?.dispatch(conversationActions.conversationChanged(conversationId, + { + ...conversation, + isPinned: !isPinned + })) + + if (isPinned) { + // purgeStoredState(persistConfig); + } + } + + return {(isPinned ? 'Unpin' : 'Pin') + ' Conversation'} +} + export function getDeleteContactMenuItem( isMe: boolean | undefined, isGroup: boolean | undefined, @@ -297,7 +323,7 @@ export function getDisappearingMenuItem( // Remove the && false to make context menu work with RTL support {(timerOptions || []).map(item => ( {(notificationForConvoOptions || []).map(item => { const disabled = item.value === currentNotificationSetting; diff --git a/ts/models/conversation.ts b/ts/models/conversation.ts index bae1f445a..4ecf6253a 100644 --- a/ts/models/conversation.ts +++ b/ts/models/conversation.ts @@ -408,9 +408,14 @@ export class ConversationModel extends Backbone.Model { left: !!this.get('left'), groupAdmins, members, + isPinned: this.getIsPinned() || false }; } + private getIsPinned() { + return window.inboxStore?.getState().conversations.conversationLookup[this.id].isPinned; + } + public async updateGroupAdmins(groupAdmins: Array) { const existingAdmins = _.uniq(_.sortBy(this.getGroupAdmins())); const newAdmins = _.uniq(_.sortBy(groupAdmins)); @@ -501,9 +506,9 @@ export class ConversationModel extends Backbone.Model { fileName: fileName || null, thumbnail: thumbnail ? { - ...(await loadAttachmentData(thumbnail)), - objectUrl: getAbsoluteAttachmentPath(thumbnail.path), - } + ...(await loadAttachmentData(thumbnail)), + objectUrl: getAbsoluteAttachmentPath(thumbnail.path), + } : null, }; }) @@ -526,9 +531,9 @@ export class ConversationModel extends Backbone.Model { fileName: null, thumbnail: image ? { - ...(await loadAttachmentData(image)), - objectUrl: getAbsoluteAttachmentPath(image.path), - } + ...(await loadAttachmentData(image)), + objectUrl: getAbsoluteAttachmentPath(image.path), + } : null, }; }) diff --git a/ts/state/createStore.ts b/ts/state/createStore.ts index 949fb1de2..3f25f8771 100644 --- a/ts/state/createStore.ts +++ b/ts/state/createStore.ts @@ -6,6 +6,7 @@ import { persistReducer } from 'redux-persist'; // tslint:disable-next-line: no-submodule-imports match-default-export-name import storage from 'redux-persist/lib/storage'; +import purgeStoredState from 'redux-persist/es/purgeStoredState'; // @ts-ignore const env = window.getEnvironment(); @@ -26,10 +27,10 @@ const logger = createLogger({ logger: directConsole, }); -const persistConfig = { +export const persistConfig = { key: 'root', storage, - whitelist: ['userConfig'], + whitelist: ['userConfig', 'conversations'], }; const persistedReducer = persistReducer(persistConfig, allReducers); diff --git a/ts/state/ducks/conversations.ts b/ts/state/ducks/conversations.ts index 48be614ad..6c1df156d 100644 --- a/ts/state/ducks/conversations.ts +++ b/ts/state/ducks/conversations.ts @@ -82,6 +82,7 @@ export interface ConversationType { avatarPath?: string; // absolute filepath to the avatar groupAdmins?: Array; // admins for closed groups and moderators for open groups members?: Array; // members for closed groups only + isPinned?: boolean } export type ConversationLookupType = { From 60b30328338be31e9ddc12913a88de0815e50634 Mon Sep 17 00:00:00 2001 From: Warrick Corfe-Tan Date: Tue, 29 Jun 2021 13:14:35 +1000 Subject: [PATCH 02/21] Exclude showing pin icon and menu item for contacts list. --- ts/components/ConversationListItem.tsx | 27 ++++++++++++------- .../session/LeftPaneContactSection.tsx | 3 ++- .../session/LeftPaneMessageSection.tsx | 2 ++ .../menu/ConversationListItemContextMenu.tsx | 9 +++++-- ts/components/session/menu/Menu.tsx | 6 ----- ts/util/accountManager.ts | 5 ++++ 6 files changed, 34 insertions(+), 18 deletions(-) diff --git a/ts/components/ConversationListItem.tsx b/ts/components/ConversationListItem.tsx index 73b889201..d08a0085a 100644 --- a/ts/components/ConversationListItem.tsx +++ b/ts/components/ConversationListItem.tsx @@ -23,7 +23,11 @@ import { DefaultTheme, withTheme } from 'styled-components'; import { PubKey } from '../session/types'; import { ConversationType, openConversationExternal } from '../state/ducks/conversations'; import { SessionIcon, SessionIconSize, SessionIconType } from './session/icon'; -import { SessionButtonColor } from './session/SessionButton'; + +export enum ConversationListItemType { + Conversation = 'conversation', + Contact = 'contact', +} export interface ConversationListItemProps extends ConversationType { index?: number; // used to force a refresh when one conversation is removed on top of the list @@ -33,6 +37,7 @@ export interface ConversationListItemProps extends ConversationType { type PropsHousekeeping = { style?: Object; theme: DefaultTheme; + conversationListItemType: ConversationListItemType; }; type Props = ConversationListItemProps & PropsHousekeeping; @@ -65,7 +70,7 @@ class ConversationListItem extends React.PureComponent { } public renderHeader() { - const { unreadCount, mentionedUs, activeAt, isPinned } = this.props; + const { unreadCount, mentionedUs, activeAt, isPinned, conversationListItemType } = this.props; let atSymbol = null; let unreadCountDiv = null; @@ -74,6 +79,15 @@ class ConversationListItem extends React.PureComponent { unreadCountDiv =

{unreadCount}

; } + const pinIcon = (conversationListItemType === ConversationListItemType.Conversation && isPinned) ? + + : + null; + + return (
{ {this.renderUser()}
- {isPinned ? - - : null - } + + {pinIcon} {unreadCountDiv} {atSymbol} { diff --git a/ts/components/session/LeftPaneContactSection.tsx b/ts/components/session/LeftPaneContactSection.tsx index 826db465c..d7db56a3e 100644 --- a/ts/components/session/LeftPaneContactSection.tsx +++ b/ts/components/session/LeftPaneContactSection.tsx @@ -1,6 +1,6 @@ import React from 'react'; -import { ConversationListItemWithDetails } from '../ConversationListItem'; +import { ConversationListItemType, ConversationListItemWithDetails } from '../ConversationListItem'; import { RowRendererParamsType } from '../LeftPane'; import { AutoSizer, List } from 'react-virtualized'; import { ConversationType as ReduxConversationType } from '../../state/ducks/conversations'; @@ -39,6 +39,7 @@ export class LeftPaneContactSection extends React.Component { return ( { return ( : null; + return ( <> - + {pinMenuItem} {getBlockMenuItem(isMe, type === ConversationTypeEnum.PRIVATE, isBlocked, conversationId)} {getCopyMenuItem(isPublic, isGroup, conversationId)} {getMarkAllReadMenuItem(conversationId)} diff --git a/ts/components/session/menu/Menu.tsx b/ts/components/session/menu/Menu.tsx index 5b38e38e8..9748b16a0 100644 --- a/ts/components/session/menu/Menu.tsx +++ b/ts/components/session/menu/Menu.tsx @@ -25,8 +25,6 @@ import { showUpdateGroupNameByConvoId, unblockConvoById, } from '../../../interactions/conversationInteractions'; -import { purgeStoredState } from 'redux-persist'; -import { persistConfig, _purgedStoredState } from '../../../state/createStore'; function showTimerOptions( isPublic: boolean, @@ -142,10 +140,6 @@ export const MenuItemPinConversation = (props: PinConversationMenuItemProps): JS ...conversation, isPinned: !isPinned })) - - if (isPinned) { - // purgeStoredState(persistConfig); - } } return {(isPinned ? 'Unpin' : 'Pin') + ' Conversation'} diff --git a/ts/util/accountManager.ts b/ts/util/accountManager.ts index 9f4e4286e..2151b15de 100644 --- a/ts/util/accountManager.ts +++ b/ts/util/accountManager.ts @@ -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 @@ -134,6 +135,10 @@ async function bouncyDeleteAccount(reason?: string) { await window.Signal.Data.removeOtherData(); // 'unlink' => toast will be shown on app restart window.localStorage.setItem('restart-reason', reason || ''); + if (window.inboxStore) { + // warrick: this part might be redundant due to localStorage getting cleared. + persistStore(window.inboxStore).purge(); + } }; try { window?.log?.info('DeleteAccount => Sending a last SyncConfiguration'); From 264f97663e6849f0720a52c5a2e104fa0d338bb9 Mon Sep 17 00:00:00 2001 From: Warrick Corfe-Tan Date: Tue, 29 Jun 2021 15:57:37 +1000 Subject: [PATCH 03/21] Persisting conversation pin statuses. --- _locales/en/messages.json | 4 +++- ts/components/session/menu/Menu.tsx | 4 ++-- ts/state/createStore.ts | 4 +--- ts/state/reducer.ts | 12 +++++++++++- 4 files changed, 17 insertions(+), 7 deletions(-) diff --git a/_locales/en/messages.json b/_locales/en/messages.json index e358968e0..121de6fb5 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -411,5 +411,7 @@ "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" } diff --git a/ts/components/session/menu/Menu.tsx b/ts/components/session/menu/Menu.tsx index 9748b16a0..12a91216f 100644 --- a/ts/components/session/menu/Menu.tsx +++ b/ts/components/session/menu/Menu.tsx @@ -141,8 +141,8 @@ export const MenuItemPinConversation = (props: PinConversationMenuItemProps): JS isPinned: !isPinned })) } - - return {(isPinned ? 'Unpin' : 'Pin') + ' Conversation'} + const menuText = isPinned ? window.i18n('unpinConversation'): window.i18n('pinConversation'); + return {menuText} } export function getDeleteContactMenuItem( diff --git a/ts/state/createStore.ts b/ts/state/createStore.ts index 3f25f8771..ed0ac4b38 100644 --- a/ts/state/createStore.ts +++ b/ts/state/createStore.ts @@ -6,7 +6,6 @@ import { persistReducer } from 'redux-persist'; // tslint:disable-next-line: no-submodule-imports match-default-export-name import storage from 'redux-persist/lib/storage'; -import purgeStoredState from 'redux-persist/es/purgeStoredState'; // @ts-ignore const env = window.getEnvironment(); @@ -30,7 +29,7 @@ const logger = createLogger({ export const persistConfig = { key: 'root', storage, - whitelist: ['userConfig', 'conversations'], + whitelist: ['userConfig'] }; const persistedReducer = persistReducer(persistConfig, allReducers); @@ -41,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), diff --git a/ts/state/reducer.ts b/ts/state/reducer.ts index e757789d8..65ee3cc3c 100644 --- a/ts/state/reducer.ts +++ b/ts/state/reducer.ts @@ -14,6 +14,10 @@ import { defaultOnionReducer as onionPaths, OnionState } from './ducks/onion'; import { modalReducer as modals, ModalState } from './ducks/modalDialog'; import { userConfigReducer as userConfig, UserConfigState } from './ducks/userConfig'; +// tslint:disable-next-line: no-submodule-imports match-default-export-name +import storage from 'redux-persist/lib/storage'; +import persistReducer from 'redux-persist/lib/persistReducer'; + export type StateType = { search: SearchStateType; user: UserStateType; @@ -27,9 +31,15 @@ export type StateType = { userConfig: UserConfigState; }; +const conversationsPersistConfig = { + key: 'conversations', + storage, + whitelist: ['conversationLookup'] +} + export const reducers = { search, - conversations, + conversations: persistReducer(conversationsPersistConfig, conversations), user, theme, section, From 49415e3bf08d5fe7ab49d8eef5b7584293340ede Mon Sep 17 00:00:00 2001 From: Warrick Corfe-Tan Date: Tue, 29 Jun 2021 16:21:45 +1000 Subject: [PATCH 04/21] Yarn formatting and linting. --- ts/components/ConversationListItem.tsx | 17 +++++----- .../session/LeftPaneMessageSection.tsx | 13 +++---- .../menu/ConversationListItemContextMenu.tsx | 6 ++-- ts/components/session/menu/Menu.tsx | 34 ++++++++++--------- ts/models/conversation.ts | 22 ++++++------ ts/state/createStore.ts | 2 +- ts/state/ducks/conversations.ts | 2 +- ts/state/reducer.ts | 6 ++-- ts/util/accountManager.ts | 2 +- 9 files changed, 52 insertions(+), 52 deletions(-) diff --git a/ts/components/ConversationListItem.tsx b/ts/components/ConversationListItem.tsx index d08a0085a..8919a6c9d 100644 --- a/ts/components/ConversationListItem.tsx +++ b/ts/components/ConversationListItem.tsx @@ -79,14 +79,14 @@ class ConversationListItem extends React.PureComponent { unreadCountDiv =

{unreadCount}

; } - const pinIcon = (conversationListItemType === ConversationListItemType.Conversation && isPinned) ? - - : - null; - + const pinIcon = + conversationListItemType === ConversationListItemType.Conversation && isPinned ? ( + + ) : null; return (
@@ -97,7 +97,6 @@ class ConversationListItem extends React.PureComponent { )} > {this.renderUser()} -
{pinIcon} diff --git a/ts/components/session/LeftPaneMessageSection.tsx b/ts/components/session/LeftPaneMessageSection.tsx index bbe691efb..4e06057a3 100644 --- a/ts/components/session/LeftPaneMessageSection.tsx +++ b/ts/components/session/LeftPaneMessageSection.tsx @@ -1,7 +1,5 @@ import React from 'react'; -import _ from 'lodash'; import { AutoSizer, List } from 'react-virtualized'; - import { MainViewController } from '../MainViewController'; import { ConversationListItemProps, @@ -11,7 +9,7 @@ import { import { ConversationType as 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'; @@ -31,8 +29,6 @@ import autoBind from 'auto-bind'; import { onsNameRegex } from '../../session/snode_api/SNodeAPI'; import { SNodeAPI } from '../../session/snode_api'; -import { createClosedGroup } from '../../receiver/closedGroups'; - export interface Props { searchTerm: string; @@ -89,8 +85,8 @@ export class LeftPaneMessageSection extends React.Component { throw new Error('renderRow: Tried to render without conversations'); } - conversations = _.sortBy([...conversations], (convo) => { - return convo.isPinned ? -1 : 1 + conversations = _.sortBy([...conversations], convo => { + return convo.isPinned ? -1 : 1; }); const conversation = conversations[index]; @@ -107,8 +103,7 @@ export class LeftPaneMessageSection extends React.Component { }; public renderList(): JSX.Element | Array { - let { conversations } = this.props; - const { openConversationExternal, searchResults } = this.props; + const { conversations, openConversationExternal, searchResults } = this.props; const contacts = searchResults?.contacts || []; if (searchResults) { diff --git a/ts/components/session/menu/ConversationListItemContextMenu.tsx b/ts/components/session/menu/ConversationListItemContextMenu.tsx index dc92a8d6e..0717b593d 100644 --- a/ts/components/session/menu/ConversationListItemContextMenu.tsx +++ b/ts/components/session/menu/ConversationListItemContextMenu.tsx @@ -13,7 +13,7 @@ import { getInviteContactMenuItem, getLeaveGroupMenuItem, getMarkAllReadMenuItem, - MenuItemPinConversation + MenuItemPinConversation, } from './Menu'; export type PropsContextConversationItem = { @@ -47,7 +47,9 @@ export const ConversationListItemContextMenu = (props: PropsContextConversationI const isGroup = type === 'group'; const isConversation = conversationListItemType === ConversationListItemType.Conversation; - const pinMenuItem = isConversation ? : null; + const pinMenuItem = isConversation ? ( + + ) : null; return ( <> diff --git a/ts/components/session/menu/Menu.tsx b/ts/components/session/menu/Menu.tsx index 12a91216f..42f94b18b 100644 --- a/ts/components/session/menu/Menu.tsx +++ b/ts/components/session/menu/Menu.tsx @@ -5,10 +5,7 @@ import { Item, Submenu } from 'react-contexify'; import { ConversationNotificationSettingType } from '../../../models/conversation'; import { useDispatch } from 'react-redux'; import { actions as conversationActions } from '../../../state/ducks/conversations'; -import { - changeNickNameModal, - updateConfirmModal, -} from '../../../state/ducks/modalDialog'; +import { changeNickNameModal, updateConfirmModal } from '../../../state/ducks/modalDialog'; import { getConversationController } from '../../../session/conversations'; import { blockConvoById, @@ -129,21 +126,26 @@ export interface PinConversationMenuItemProps { conversationId: string; } -export const MenuItemPinConversation = (props: PinConversationMenuItemProps): JSX.Element | null => { +export const MenuItemPinConversation = ( + props: PinConversationMenuItemProps +): JSX.Element | null => { const { conversationId } = props; - const conversation = getConversationController().get(conversationId).getProps(); + const conversation = getConversationController() + .get(conversationId) + .getProps(); const { isPinned } = conversation; const togglePinConversation = () => { - window.inboxStore?.dispatch(conversationActions.conversationChanged(conversationId, - { + window.inboxStore?.dispatch( + conversationActions.conversationChanged(conversationId, { ...conversation, - isPinned: !isPinned - })) - } - const menuText = isPinned ? window.i18n('unpinConversation'): window.i18n('pinConversation'); - return {menuText} -} + isPinned: !isPinned, + }) + ); + }; + const menuText = isPinned ? window.i18n('unpinConversation') : window.i18n('pinConversation'); + return {menuText}; +}; export function getDeleteContactMenuItem( isMe: boolean | undefined, @@ -317,7 +319,7 @@ export function getDisappearingMenuItem( // Remove the && false to make context menu work with RTL support {(timerOptions || []).map(item => ( {(notificationForConvoOptions || []).map(item => { const disabled = item.value === currentNotificationSetting; diff --git a/ts/models/conversation.ts b/ts/models/conversation.ts index 4ecf6253a..aa179de25 100644 --- a/ts/models/conversation.ts +++ b/ts/models/conversation.ts @@ -408,14 +408,10 @@ export class ConversationModel extends Backbone.Model { left: !!this.get('left'), groupAdmins, members, - isPinned: this.getIsPinned() || false + isPinned: this.getIsPinned() || false, }; } - private getIsPinned() { - return window.inboxStore?.getState().conversations.conversationLookup[this.id].isPinned; - } - public async updateGroupAdmins(groupAdmins: Array) { const existingAdmins = _.uniq(_.sortBy(this.getGroupAdmins())); const newAdmins = _.uniq(_.sortBy(groupAdmins)); @@ -506,9 +502,9 @@ export class ConversationModel extends Backbone.Model { fileName: fileName || null, thumbnail: thumbnail ? { - ...(await loadAttachmentData(thumbnail)), - objectUrl: getAbsoluteAttachmentPath(thumbnail.path), - } + ...(await loadAttachmentData(thumbnail)), + objectUrl: getAbsoluteAttachmentPath(thumbnail.path), + } : null, }; }) @@ -531,9 +527,9 @@ export class ConversationModel extends Backbone.Model { fileName: null, thumbnail: image ? { - ...(await loadAttachmentData(image)), - objectUrl: getAbsoluteAttachmentPath(image.path), - } + ...(await loadAttachmentData(image)), + objectUrl: getAbsoluteAttachmentPath(image.path), + } : null, }; }) @@ -1441,6 +1437,10 @@ export class ConversationModel extends Backbone.Model { return typeof expireTimer === 'number' && expireTimer > 0; } + + private getIsPinned() { + return window.inboxStore?.getState().conversations.conversationLookup[this.id].isPinned; + } } export class ConversationCollection extends Backbone.Collection { diff --git a/ts/state/createStore.ts b/ts/state/createStore.ts index ed0ac4b38..196ed439f 100644 --- a/ts/state/createStore.ts +++ b/ts/state/createStore.ts @@ -29,7 +29,7 @@ const logger = createLogger({ export const persistConfig = { key: 'root', storage, - whitelist: ['userConfig'] + whitelist: ['userConfig'], }; const persistedReducer = persistReducer(persistConfig, allReducers); diff --git a/ts/state/ducks/conversations.ts b/ts/state/ducks/conversations.ts index 6c1df156d..a5ecb17bb 100644 --- a/ts/state/ducks/conversations.ts +++ b/ts/state/ducks/conversations.ts @@ -82,7 +82,7 @@ export interface ConversationType { avatarPath?: string; // absolute filepath to the avatar groupAdmins?: Array; // admins for closed groups and moderators for open groups members?: Array; // members for closed groups only - isPinned?: boolean + isPinned?: boolean; } export type ConversationLookupType = { diff --git a/ts/state/reducer.ts b/ts/state/reducer.ts index 65ee3cc3c..1c2aaf731 100644 --- a/ts/state/reducer.ts +++ b/ts/state/reducer.ts @@ -16,6 +16,8 @@ import { userConfigReducer as userConfig, UserConfigState } from './ducks/userCo // tslint:disable-next-line: no-submodule-imports match-default-export-name import storage from 'redux-persist/lib/storage'; + +// tslint:disable-next-line: no-submodule-imports match-default-export-name import persistReducer from 'redux-persist/lib/persistReducer'; export type StateType = { @@ -34,8 +36,8 @@ export type StateType = { const conversationsPersistConfig = { key: 'conversations', storage, - whitelist: ['conversationLookup'] -} + whitelist: ['conversationLookup'], +}; export const reducers = { search, diff --git a/ts/util/accountManager.ts b/ts/util/accountManager.ts index 2151b15de..f02dd94f2 100644 --- a/ts/util/accountManager.ts +++ b/ts/util/accountManager.ts @@ -137,7 +137,7 @@ async function bouncyDeleteAccount(reason?: string) { window.localStorage.setItem('restart-reason', reason || ''); if (window.inboxStore) { // warrick: this part might be redundant due to localStorage getting cleared. - persistStore(window.inboxStore).purge(); + await persistStore(window.inboxStore).purge(); } }; try { From 36cf05db1bf13d2a7506bf476dc68024cbe6d707 Mon Sep 17 00:00:00 2001 From: Warrick Corfe-Tan Date: Wed, 30 Jun 2021 13:09:09 +1000 Subject: [PATCH 05/21] Switched conversation pin state. --- ts/components/ConversationListItem.tsx | 15 +++++----- .../session/LeftPaneContactSection.tsx | 3 +- .../session/LeftPaneMessageSection.tsx | 2 -- .../menu/ConversationListItemContextMenu.tsx | 11 ++++--- ts/components/session/menu/Menu.tsx | 17 ++++------- ts/models/conversation.ts | 29 ++++++++++++------- ts/state/reducer.ts | 14 +-------- ts/util/accountManager.ts | 4 --- 8 files changed, 38 insertions(+), 57 deletions(-) diff --git a/ts/components/ConversationListItem.tsx b/ts/components/ConversationListItem.tsx index 8919a6c9d..29e44d3e0 100644 --- a/ts/components/ConversationListItem.tsx +++ b/ts/components/ConversationListItem.tsx @@ -23,11 +23,8 @@ import { DefaultTheme, withTheme } from 'styled-components'; import { PubKey } from '../session/types'; import { ConversationType, openConversationExternal } from '../state/ducks/conversations'; import { SessionIcon, SessionIconSize, SessionIconType } from './session/icon'; - -export enum ConversationListItemType { - Conversation = 'conversation', - Contact = 'contact', -} +import { useSelector } from 'react-redux'; +import { SectionType } from './session/ActionsPanel'; export interface ConversationListItemProps extends ConversationType { index?: number; // used to force a refresh when one conversation is removed on top of the list @@ -37,7 +34,6 @@ export interface ConversationListItemProps extends ConversationType { type PropsHousekeeping = { style?: Object; theme: DefaultTheme; - conversationListItemType: ConversationListItemType; }; type Props = ConversationListItemProps & PropsHousekeeping; @@ -70,7 +66,7 @@ class ConversationListItem extends React.PureComponent { } public renderHeader() { - const { unreadCount, mentionedUs, activeAt, isPinned, conversationListItemType } = this.props; + const { unreadCount, mentionedUs, activeAt, isPinned } = this.props; let atSymbol = null; let unreadCountDiv = null; @@ -79,8 +75,11 @@ class ConversationListItem extends React.PureComponent { unreadCountDiv =

{unreadCount}

; } + const isMessagesSection = + window.inboxStore?.getState().section.focusedSection === SectionType.Message; + const pinIcon = - conversationListItemType === ConversationListItemType.Conversation && isPinned ? ( + isMessagesSection && isPinned ? ( { return ( { return ( ) : null; diff --git a/ts/components/session/menu/Menu.tsx b/ts/components/session/menu/Menu.tsx index 42f94b18b..871f1987b 100644 --- a/ts/components/session/menu/Menu.tsx +++ b/ts/components/session/menu/Menu.tsx @@ -130,18 +130,11 @@ export const MenuItemPinConversation = ( props: PinConversationMenuItemProps ): JSX.Element | null => { const { conversationId } = props; - const conversation = getConversationController() - .get(conversationId) - .getProps(); - const { isPinned } = conversation; - - const togglePinConversation = () => { - window.inboxStore?.dispatch( - conversationActions.conversationChanged(conversationId, { - ...conversation, - isPinned: !isPinned, - }) - ); + const conversation = getConversationController().get(conversationId); + let isPinned = conversation.getIsPinned(); + + const togglePinConversation = async () => { + await conversation.setIsPinned(!isPinned); }; const menuText = isPinned ? window.i18n('unpinConversation') : window.i18n('pinConversation'); return {menuText}; diff --git a/ts/models/conversation.ts b/ts/models/conversation.ts index 55b6544c8..53dd24d77 100644 --- a/ts/models/conversation.ts +++ b/ts/models/conversation.ts @@ -16,7 +16,6 @@ import { getMessagesByConversation, getUnreadByConversation, getUnreadCountByConversation, - removeAllMessagesInConversation, removeMessage as dataRemoveMessage, saveMessages, updateConversation, @@ -40,11 +39,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'; export enum ConversationTypeEnum { GROUP = 'group', @@ -96,6 +91,7 @@ export interface ConversationAttributes { accessKey?: any; triggerNotificationsFor: ConversationNotificationSettingType; isTrustedForAttachmentDownload: boolean; + isPinned?: boolean; } export interface ConversationAttributesOptionals { @@ -133,6 +129,7 @@ export interface ConversationAttributesOptionals { accessKey?: any; triggerNotificationsFor?: ConversationNotificationSettingType; isTrustedForAttachmentDownload?: boolean; + isPinned?: boolean; } /** @@ -162,6 +159,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, }); }; @@ -409,7 +407,7 @@ export class ConversationModel extends Backbone.Model { left: !!this.get('left'), groupAdmins, members, - isPinned: this.getIsPinned() || false, + isPinned: this.getIsPinned(), }; } @@ -1095,6 +1093,17 @@ export class ConversationModel extends Backbone.Model { await this.commit(); } } + + public async setIsPinned(value: boolean) { + if (value !== this.get('isPinned')) { + this.set({ + isPinned: value, + }); + await this.commit(); + console.log(this); + } + } + public async setGroupName(name: string) { const profileName = this.get('name'); if (profileName !== name) { @@ -1226,6 +1235,10 @@ export class ConversationModel extends Backbone.Model { return this.get('name') || window.i18n('unknown'); } + public getIsPinned() { + return this.get('isPinned'); + } + public getTitle() { if (this.isPrivate()) { const profileName = this.getProfileName(); @@ -1438,10 +1451,6 @@ export class ConversationModel extends Backbone.Model { return typeof expireTimer === 'number' && expireTimer > 0; } - - private getIsPinned() { - return window.inboxStore?.getState().conversations.conversationLookup[this.id].isPinned; - } } export class ConversationCollection extends Backbone.Collection { diff --git a/ts/state/reducer.ts b/ts/state/reducer.ts index 1c2aaf731..e757789d8 100644 --- a/ts/state/reducer.ts +++ b/ts/state/reducer.ts @@ -14,12 +14,6 @@ import { defaultOnionReducer as onionPaths, OnionState } from './ducks/onion'; import { modalReducer as modals, ModalState } from './ducks/modalDialog'; import { userConfigReducer as userConfig, UserConfigState } from './ducks/userConfig'; -// tslint:disable-next-line: no-submodule-imports match-default-export-name -import storage from 'redux-persist/lib/storage'; - -// tslint:disable-next-line: no-submodule-imports match-default-export-name -import persistReducer from 'redux-persist/lib/persistReducer'; - export type StateType = { search: SearchStateType; user: UserStateType; @@ -33,15 +27,9 @@ export type StateType = { userConfig: UserConfigState; }; -const conversationsPersistConfig = { - key: 'conversations', - storage, - whitelist: ['conversationLookup'], -}; - export const reducers = { search, - conversations: persistReducer(conversationsPersistConfig, conversations), + conversations, user, theme, section, diff --git a/ts/util/accountManager.ts b/ts/util/accountManager.ts index f02dd94f2..7661bda9c 100644 --- a/ts/util/accountManager.ts +++ b/ts/util/accountManager.ts @@ -135,10 +135,6 @@ async function bouncyDeleteAccount(reason?: string) { await window.Signal.Data.removeOtherData(); // 'unlink' => toast will be shown on app restart window.localStorage.setItem('restart-reason', reason || ''); - if (window.inboxStore) { - // warrick: this part might be redundant due to localStorage getting cleared. - await persistStore(window.inboxStore).purge(); - } }; try { window?.log?.info('DeleteAccount => Sending a last SyncConfiguration'); From 563dff64ae4e1b0248367f3bb1010d037e1c1b00 Mon Sep 17 00:00:00 2001 From: Warrick Corfe-Tan Date: Wed, 30 Jun 2021 13:12:25 +1000 Subject: [PATCH 06/21] yarn linting and formatting, --- ts/components/session/menu/Menu.tsx | 3 +-- ts/models/conversation.ts | 1 - 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/ts/components/session/menu/Menu.tsx b/ts/components/session/menu/Menu.tsx index 871f1987b..1fc0102d4 100644 --- a/ts/components/session/menu/Menu.tsx +++ b/ts/components/session/menu/Menu.tsx @@ -4,7 +4,6 @@ import { NotificationForConvoOption, TimerOption } from '../../conversation/Conv import { Item, Submenu } from 'react-contexify'; import { ConversationNotificationSettingType } from '../../../models/conversation'; import { useDispatch } from 'react-redux'; -import { actions as conversationActions } from '../../../state/ducks/conversations'; import { changeNickNameModal, updateConfirmModal } from '../../../state/ducks/modalDialog'; import { getConversationController } from '../../../session/conversations'; import { @@ -131,7 +130,7 @@ export const MenuItemPinConversation = ( ): JSX.Element | null => { const { conversationId } = props; const conversation = getConversationController().get(conversationId); - let isPinned = conversation.getIsPinned(); + const isPinned = conversation.getIsPinned(); const togglePinConversation = async () => { await conversation.setIsPinned(!isPinned); diff --git a/ts/models/conversation.ts b/ts/models/conversation.ts index 53dd24d77..848cfbc3a 100644 --- a/ts/models/conversation.ts +++ b/ts/models/conversation.ts @@ -1100,7 +1100,6 @@ export class ConversationModel extends Backbone.Model { isPinned: value, }); await this.commit(); - console.log(this); } } From cf6ac09300ddbab3c1da2b3869198ca6cddf0f28 Mon Sep 17 00:00:00 2001 From: Warrick Corfe-Tan Date: Wed, 30 Jun 2021 14:55:26 +1000 Subject: [PATCH 07/21] Refactor to functional components --- ts/components/ConversationListItem.tsx | 409 +++++++++++++++---------- ts/components/session/menu/Menu.tsx | 2 +- 2 files changed, 247 insertions(+), 164 deletions(-) diff --git a/ts/components/ConversationListItem.tsx b/ts/components/ConversationListItem.tsx index 29e44d3e0..398b699bb 100644 --- a/ts/components/ConversationListItem.tsx +++ b/ts/components/ConversationListItem.tsx @@ -19,12 +19,18 @@ import { } from './session/menu/ConversationListItemContextMenu'; import { createPortal } from 'react-dom'; import { OutgoingMessageStatus } from './conversation/message/OutgoingMessageStatus'; -import { DefaultTheme, withTheme } from 'styled-components'; +import { DefaultTheme, useTheme, withTheme } from 'styled-components'; import { PubKey } from '../session/types'; -import { ConversationType, openConversationExternal } from '../state/ducks/conversations'; +import { + ConversationType, + LastMessageType, + openConversationExternal, +} from '../state/ducks/conversations'; import { SessionIcon, SessionIconSize, SessionIconType } from './session/icon'; -import { useSelector } from 'react-redux'; +import { useDispatch, useSelector } from 'react-redux'; import { SectionType } from './session/ActionsPanel'; +import { getTheme } from '../state/selectors/theme'; +import { getFocusedSection } from '../state/selectors/section'; export interface ConversationListItemProps extends ConversationType { index?: number; // used to force a refresh when one conversation is removed on top of the list @@ -42,197 +48,274 @@ const Portal = ({ children }: { children: any }) => { return createPortal(children, document.querySelector('.inbox.index') as Element); }; -class ConversationListItem extends React.PureComponent { - public constructor(props: Props) { - super(props); - } +const ConversationListItem = (props: Props) => { + const { + phoneNumber, + unreadCount, + id, + isSelected, + isBlocked, + style, + mentionedUs, + avatarPath, + name, + profileName, + activeAt, + isMe, + isPinned, + isTyping, + lastMessage, + memberAvatars, + } = props; + const triggerId: string = `conversation-item-${phoneNumber}-ctxmenu`; + const key: string = `conversation-item-${phoneNumber}`; - public renderAvatar() { - const { avatarPath, name, phoneNumber, profileName, memberAvatars } = this.props; + const dispatch = useDispatch(); - const userName = name || profileName || phoneNumber; + const menuProps: PropsContextConversationItem = { + ...props, + triggerId, + }; - return ( -
- +
{ + dispatch(openConversationExternal(id)); + }} + onContextMenu={(e: any) => { + contextMenu.show({ + id: triggerId, + event: e, + }); + }} + style={style} + className={classNames( + 'module-conversation-list-item', + unreadCount > 0 ? 'module-conversation-list-item--has-unread' : null, + unreadCount > 0 && mentionedUs ? 'module-conversation-list-item--mentioned-us' : null, + isSelected ? 'module-conversation-list-item--is-selected' : null, + isBlocked ? 'module-conversation-list-item--is-blocked' : null + )} + > + +
+ + +
- ); - } + + + +
+ ); +}; - public renderHeader() { - const { unreadCount, mentionedUs, activeAt, isPinned } = this.props; - - let atSymbol = null; - let unreadCountDiv = null; - if (unreadCount > 0) { - atSymbol = mentionedUs ?

@

: null; - unreadCountDiv =

{unreadCount}

; - } - - const isMessagesSection = - window.inboxStore?.getState().section.focusedSection === SectionType.Message; - - const pinIcon = - isMessagesSection && isPinned ? ( - - ) : null; +export interface ConversationListItemAvatarProps { + avatarPath?: string; + name?: string; + profileName?: string; + phoneNumber?: string; + memberAvatars?: Array; +} - return ( -
-
0 ? 'module-conversation-list-item__header__name--with-unread' : null - )} - > - {this.renderUser()} -
+export const ConversationListItemAvatar = (props: ConversationListItemAvatarProps) => { + const { avatarPath, name, phoneNumber, profileName, memberAvatars } = props; + const userName = name || profileName || phoneNumber; - {pinIcon} - {unreadCountDiv} - {atSymbol} - { -
0 ? 'module-conversation-list-item__header__date--has-unread' : null - )} - > - { - - } -
- } -
- ); - } + return ( +
+ +
+ ); +}; - public renderMessage() { - const { lastMessage, isTyping, unreadCount } = this.props; +export interface ConversationListItemHeaderProps { + unreadCount: number; + mentionedUs: boolean; + activeAt?: number; + isPinned?: boolean; - if (!lastMessage && !isTyping) { - return null; - } - const text = lastMessage && lastMessage.text ? lastMessage.text : ''; + name?: string; + phoneNumber: string; + profileName?: string; + isMe: boolean; +} - if (isEmpty(text)) { - return null; - } +export const ConversationListItemHeader = (props: ConversationListItemHeaderProps) => { + const { + unreadCount, + mentionedUs, + activeAt, + isPinned, + name, + phoneNumber, + profileName, + isMe, + } = props; - return ( -
-
0 ? 'module-conversation-list-item__message__text--has-unread' : null - )} - > - {isTyping ? ( - - ) : ( - - )} -
- {lastMessage && lastMessage.status ? ( - - ) : null} -
- ); + const theme = useTheme(); + + let atSymbol = null; + let unreadCountDiv = null; + if (unreadCount > 0) { + atSymbol = mentionedUs ?

@

: null; + unreadCountDiv =

{unreadCount}

; } - public render() { - const { phoneNumber, unreadCount, id, isSelected, isBlocked, style, mentionedUs } = this.props; - const triggerId = `conversation-item-${phoneNumber}-ctxmenu`; - const key = `conversation-item-${phoneNumber}`; + const isMessagesSection = useSelector(getFocusedSection) === SectionType.Message; + + const pinIcon = + isMessagesSection && isPinned ? ( + + ) : null; - return ( -
+ return ( +
+
0 ? 'module-conversation-list-item__header__name--with-unread' : null + )} + > + +
+ + {pinIcon} + {unreadCountDiv} + {atSymbol} + {
{ - window.inboxStore?.dispatch(openConversationExternal(id)); - }} - onContextMenu={(e: any) => { - contextMenu.show({ - id: triggerId, - event: e, - }); - }} - style={style} className={classNames( - 'module-conversation-list-item', - unreadCount > 0 ? 'module-conversation-list-item--has-unread' : null, - unreadCount > 0 && mentionedUs ? 'module-conversation-list-item--mentioned-us' : null, - isSelected ? 'module-conversation-list-item--is-selected' : null, - isBlocked ? 'module-conversation-list-item--is-blocked' : null + 'module-conversation-list-item__header__date', + unreadCount > 0 ? 'module-conversation-list-item__header__date--has-unread' : null )} > - {this.renderAvatar()} -
- {this.renderHeader()} - {this.renderMessage()} -
+ { + + }
- - - -
- ); + } +
+ ); +}; + +export interface ConversationListMessageProps { + lastMessage: LastMessageType; + isTyping: boolean; + unreadCount: number; +} + +export const ConversationListItemMessage = (props: any) => { + const { lastMessage, isTyping, unreadCount } = props; + const theme = useTheme(); + + if (!lastMessage && !isTyping) { + return null; } + const text = lastMessage && lastMessage.text ? lastMessage.text : ''; - private getMenuProps(triggerId: string): PropsContextConversationItem { - return { - triggerId, - ...this.props, - }; + if (isEmpty(text)) { + return null; } - private renderUser() { - const { name, phoneNumber, profileName, isMe } = this.props; + return ( +
+
0 ? 'module-conversation-list-item__message__text--has-unread' : null + )} + > + {isTyping ? ( + + ) : ( + + )} +
+ {lastMessage && lastMessage.status ? ( + + ) : null} +
+ ); +}; - const shortenedPubkey = PubKey.shorten(phoneNumber); +export interface ConversationListItemUserProps { + name?: string; + phoneNumber: string; + profileName?: string; + isMe: boolean; +} - const displayedPubkey = profileName ? shortenedPubkey : phoneNumber; - const displayName = isMe ? window.i18n('noteToSelf') : profileName; +export const ConversationListItemUser = (props: ConversationListItemUserProps) => { + const { name, phoneNumber, profileName, isMe } = props; - let shouldShowPubkey = false; - if ((!name || name.length === 0) && (!displayName || displayName.length === 0)) { - shouldShowPubkey = true; - } + const shortenedPubkey = PubKey.shorten(phoneNumber); - return ( -
- -
- ); + const displayedPubkey = profileName ? shortenedPubkey : phoneNumber; + const displayName = isMe ? window.i18n('noteToSelf') : profileName; + + let shouldShowPubkey = false; + if ((!name || name.length === 0) && (!displayName || displayName.length === 0)) { + shouldShowPubkey = true; } -} + + return ( +
+ +
+ ); +}; export const ConversationListItemWithDetails = usingClosedConversationDetails( withTheme(ConversationListItem) diff --git a/ts/components/session/menu/Menu.tsx b/ts/components/session/menu/Menu.tsx index 1fc0102d4..f31dc6a52 100644 --- a/ts/components/session/menu/Menu.tsx +++ b/ts/components/session/menu/Menu.tsx @@ -3,7 +3,7 @@ import React from 'react'; import { NotificationForConvoOption, TimerOption } from '../../conversation/ConversationHeader'; import { Item, Submenu } from 'react-contexify'; import { ConversationNotificationSettingType } from '../../../models/conversation'; -import { useDispatch } from 'react-redux'; +import { useDispatch, useSelector } from 'react-redux'; import { changeNickNameModal, updateConfirmModal } from '../../../state/ducks/modalDialog'; import { getConversationController } from '../../../session/conversations'; import { From 535a66b87916f94c6933d1142bbec8048a299c1f Mon Sep 17 00:00:00 2001 From: Warrick Corfe-Tan Date: Wed, 30 Jun 2021 15:36:19 +1000 Subject: [PATCH 08/21] minor tidying --- ts/components/ConversationListItem.tsx | 8 ++------ .../session/menu/ConversationListItemContextMenu.tsx | 6 +++--- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/ts/components/ConversationListItem.tsx b/ts/components/ConversationListItem.tsx index 398b699bb..3a56e5997 100644 --- a/ts/components/ConversationListItem.tsx +++ b/ts/components/ConversationListItem.tsx @@ -64,6 +64,7 @@ const ConversationListItem = (props: Props) => { isMe, isPinned, isTyping, + type, lastMessage, memberAvatars, } = props; @@ -72,11 +73,6 @@ const ConversationListItem = (props: Props) => { const dispatch = useDispatch(); - const menuProps: PropsContextConversationItem = { - ...props, - triggerId, - }; - return (
{
- +
); diff --git a/ts/components/session/menu/ConversationListItemContextMenu.tsx b/ts/components/session/menu/ConversationListItemContextMenu.tsx index 542de934b..ac812f1a7 100644 --- a/ts/components/session/menu/ConversationListItemContextMenu.tsx +++ b/ts/components/session/menu/ConversationListItemContextMenu.tsx @@ -1,6 +1,8 @@ import React from 'react'; import { animation, Menu } from 'react-contexify'; +import { useSelector } from 'react-redux'; import { ConversationTypeEnum } from '../../../models/conversation'; +import { getFocusedSection } from '../../../state/selectors/section'; import { SectionType } from '../ActionsPanel'; import { @@ -43,9 +45,7 @@ export const ConversationListItemContextMenu = (props: PropsContextConversationI } = props; const isGroup = type === 'group'; - - const isMessagesSection = - window.inboxStore?.getState().section.focusedSection === SectionType.Message; + const isMessagesSection = useSelector(getFocusedSection) === SectionType.Message; const pinMenuItem = isMessagesSection ? ( ) : null; From 6b8a4ffc6cc95eb4962f192fa22803e7cb44b8b6 Mon Sep 17 00:00:00 2001 From: Brice-W Date: Fri, 2 Jul 2021 14:35:28 +1000 Subject: [PATCH 09/21] display link preview popup only time --- js/background.js | 5 ----- .../session/conversation/SessionCompositionBox.tsx | 11 ++++++++--- ts/components/session/settings/SessionSettings.tsx | 10 ++++++++-- ts/data/data.ts | 1 + ts/window.d.ts | 1 - 5 files changed, 17 insertions(+), 11 deletions(-) diff --git a/js/background.js b/js/background.js index c58986015..dcbf28b3f 100644 --- a/js/background.js +++ b/js/background.js @@ -340,11 +340,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); diff --git a/ts/components/session/conversation/SessionCompositionBox.tsx b/ts/components/session/conversation/SessionCompositionBox.tsx index 22df902de..3bcce1d94 100644 --- a/ts/components/session/conversation/SessionCompositionBox.tsx +++ b/ts/components/session/conversation/SessionCompositionBox.tsx @@ -37,6 +37,7 @@ import { getMentionsInput } from '../../../state/selectors/mentionsInput'; import { updateConfirmModal } from '../../../state/ducks/modalDialog'; import { SessionButtonColor } from '../SessionButton'; import { SessionConfirmDialogProps } from '../SessionConfirm'; +import { createOrUpdateItem, getItemById, hasLinkPreviewPopupBeenDisplayed } from '../../../data/data'; export interface ReplyingToMessageProps { convoId: string; @@ -218,7 +219,7 @@ export class SessionCompositionBox extends React.Component { imgBlob = item.getAsFile(); break; case 'text': - this.showLinkSharingConfirmationModalDialog(e); + void this.showLinkSharingConfirmationModalDialog(e); break; default: } @@ -237,18 +238,22 @@ export class SessionCompositionBox extends React.Component { * 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 }); + }, }) ); } diff --git a/ts/components/session/settings/SessionSettings.tsx b/ts/components/session/settings/SessionSettings.tsx index abcff8bb7..72c6c4dc6 100644 --- a/ts/components/session/settings/SessionSettings.tsx +++ b/ts/components/session/settings/SessionSettings.tsx @@ -9,7 +9,7 @@ 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 { getPasswordHash, createOrUpdateItem, hasLinkPreviewPopupBeenDisplayed } from '../../../../ts/data/data'; import { SpacerLG, SpacerXS } from '../../basic/Text'; import { shell } from 'electron'; import { SessionConfirmDialogProps } from '../SessionConfirm'; @@ -339,7 +339,13 @@ class SettingsViewInner extends React.Component { hidden: false, type: SessionSettingType.Toggle, category: SessionSettingCategory.Appearance, - setFn: window.toggleLinkPreview, + setFn: () => { + const newValue = !window.getSettingValue('link-preview-setting'); + window.setSettingValue('link-preview-setting', newValue); + if (!newValue) { + createOrUpdateItem({ id: hasLinkPreviewPopupBeenDisplayed, value: false }); + } + }, content: undefined, comparisonValue: undefined, onClick: undefined, diff --git a/ts/data/data.ts b/ts/data/data.ts index 2613be494..77ca3175a 100644 --- a/ts/data/data.ts +++ b/ts/data/data.ts @@ -64,6 +64,7 @@ export type ServerToken = { export const hasSyncedInitialConfigurationItem = 'hasSyncedInitialConfigurationItem'; export const lastAvatarUploadTimestamp = 'lastAvatarUploadTimestamp'; +export const hasLinkPreviewPopupBeenDisplayed = 'hasLinkPreviewPopupBeenDisplayed'; const channelsToMake = { shutdown, diff --git a/ts/window.d.ts b/ts/window.d.ts index d8a86a540..a74ab6c7e 100644 --- a/ts/window.d.ts +++ b/ts/window.d.ts @@ -59,7 +59,6 @@ declare global { showResetSessionIdDialog: any; storage: any; textsecure: LibTextsecure; - toggleLinkPreview: any; toggleMediaPermissions: any; toggleMenuBar: any; toggleSpellCheck: any; From dcd8da87ca9c12664fb9665884376de5104704c3 Mon Sep 17 00:00:00 2001 From: Brice-W Date: Fri, 2 Jul 2021 14:59:09 +1000 Subject: [PATCH 10/21] fix errors --- .../session/conversation/SessionCompositionBox.tsx | 12 +++++++++--- ts/components/session/settings/SessionSettings.tsx | 10 +++++++--- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/ts/components/session/conversation/SessionCompositionBox.tsx b/ts/components/session/conversation/SessionCompositionBox.tsx index 3bcce1d94..bbc505ef2 100644 --- a/ts/components/session/conversation/SessionCompositionBox.tsx +++ b/ts/components/session/conversation/SessionCompositionBox.tsx @@ -37,7 +37,11 @@ import { getMentionsInput } from '../../../state/selectors/mentionsInput'; import { updateConfirmModal } from '../../../state/ducks/modalDialog'; import { SessionButtonColor } from '../SessionButton'; import { SessionConfirmDialogProps } from '../SessionConfirm'; -import { createOrUpdateItem, getItemById, hasLinkPreviewPopupBeenDisplayed } from '../../../data/data'; +import { + createOrUpdateItem, + getItemById, + hasLinkPreviewPopupBeenDisplayed, +} from '../../../data/data'; export interface ReplyingToMessageProps { convoId: string; @@ -241,10 +245,12 @@ export class SessionCompositionBox extends React.Component { private async showLinkSharingConfirmationModalDialog(e: any) { const pastedText = e.clipboardData.getData('text'); if (this.isURL(pastedText)) { - const alreadyDisplayedPopup = (await getItemById(hasLinkPreviewPopupBeenDisplayed))?.value || false; + const alreadyDisplayedPopup = + (await getItemById(hasLinkPreviewPopupBeenDisplayed))?.value || false; window.inboxStore?.dispatch( updateConfirmModal({ - shouldShowConfirm: () => !window.getSettingValue('link-preview-setting') && !alreadyDisplayedPopup, + shouldShowConfirm: () => + !window.getSettingValue('link-preview-setting') && !alreadyDisplayedPopup, title: window.i18n('linkPreviewsTitle'), message: window.i18n('linkPreviewsConfirmMessage'), okTheme: SessionButtonColor.Danger, diff --git a/ts/components/session/settings/SessionSettings.tsx b/ts/components/session/settings/SessionSettings.tsx index 72c6c4dc6..2c86f3e8a 100644 --- a/ts/components/session/settings/SessionSettings.tsx +++ b/ts/components/session/settings/SessionSettings.tsx @@ -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, createOrUpdateItem, hasLinkPreviewPopupBeenDisplayed } 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,11 +343,11 @@ class SettingsViewInner extends React.Component { hidden: false, type: SessionSettingType.Toggle, category: SessionSettingCategory.Appearance, - setFn: () => { + setFn: async () => { const newValue = !window.getSettingValue('link-preview-setting'); window.setSettingValue('link-preview-setting', newValue); if (!newValue) { - createOrUpdateItem({ id: hasLinkPreviewPopupBeenDisplayed, value: false }); + await createOrUpdateItem({ id: hasLinkPreviewPopupBeenDisplayed, value: false }); } }, content: undefined, From 4ddc00c9b6b81d093eff8ad9e405d63fa93c372f Mon Sep 17 00:00:00 2001 From: Brice-W Date: Tue, 6 Jul 2021 13:34:39 +1000 Subject: [PATCH 11/21] set a limit numer to pinned conversations --- ts/components/session/SessionInboxView.tsx | 3 ++ ts/components/session/menu/Menu.tsx | 22 +++++++- ts/state/ducks/conversations.ts | 60 ++++++++++++++++++++++ ts/state/selectors/conversations.ts | 4 ++ 4 files changed, 88 insertions(+), 1 deletion(-) diff --git a/ts/components/session/SessionInboxView.tsx b/ts/components/session/SessionInboxView.tsx index 900fb9453..e5fc69a87 100644 --- a/ts/components/session/SessionInboxView.tsx +++ b/ts/components/session/SessionInboxView.tsx @@ -90,10 +90,13 @@ export class SessionInboxView extends React.Component { const fullFilledConversations = await Promise.all(filledConversations); + const pinnedConversations = conversations.filter( (conversation) => conversation.isPinned).length; + const initialState: StateType = { conversations: { conversationLookup: makeLookup(fullFilledConversations, 'id'), messages: [], + pinnedConversations: pinnedConversations, }, user: { ourNumber: UserUtils.getOurPubKeyStrFromCache(), diff --git a/ts/components/session/menu/Menu.tsx b/ts/components/session/menu/Menu.tsx index dd59e04a6..6931e8607 100644 --- a/ts/components/session/menu/Menu.tsx +++ b/ts/components/session/menu/Menu.tsx @@ -1,5 +1,7 @@ import React from 'react'; +import { getNumberOfPinnedConversations } from '../../../state/selectors/conversations'; +import { conversationPinned, conversationUnpinned } from '../../../state/ducks/conversations'; import { NotificationForConvoOption, TimerOption } from '../../conversation/ConversationHeader'; import { Item, Submenu } from 'react-contexify'; import { ConversationNotificationSettingType } from '../../../models/conversation'; @@ -22,6 +24,9 @@ import { unblockConvoById, } from '../../../interactions/conversationInteractions'; import { SessionButtonColor } from '../SessionButton'; +import { ToastUtils } from '../../../session/utils'; + +const maxNumberOfPinnedConversations = 5; function showTimerOptions( isPublic: boolean, @@ -132,10 +137,25 @@ export const MenuItemPinConversation = ( const { conversationId } = props; const conversation = getConversationController().get(conversationId); const isPinned = conversation.getIsPinned(); + const dispatch = useDispatch(); + const nbOfAlreadyPinnedConvos = useSelector(getNumberOfPinnedConversations); const togglePinConversation = async () => { - await conversation.setIsPinned(!isPinned); + if (!isPinned && nbOfAlreadyPinnedConvos < maxNumberOfPinnedConversations) { + await conversation.setIsPinned(!isPinned); + dispatch(conversationPinned()); + } else if (isPinned) { + await conversation.setIsPinned(!isPinned); + dispatch(conversationUnpinned()); + } else { + ToastUtils.pushToastWarning( + 'setPasswordSuccessToast', + window.i18n('removePasswordTitle'), + window.i18n('removePasswordToastDescription') + ); + } }; + const menuText = isPinned ? window.i18n('unpinConversation') : window.i18n('pinConversation'); return {menuText}; }; diff --git a/ts/state/ducks/conversations.ts b/ts/state/ducks/conversations.ts index a5ecb17bb..4dd63e3ae 100644 --- a/ts/state/ducks/conversations.ts +++ b/ts/state/ducks/conversations.ts @@ -93,6 +93,7 @@ export type ConversationsStateType = { conversationLookup: ConversationLookupType; selectedConversation?: string; messages: Array; + pinnedConversations: number; }; async function getMessages( @@ -243,6 +244,14 @@ export type SelectedConversationChangedActionType = { messageId?: string; }; }; +export type ConversationPinnedActionType = { + type: 'CONVERSATION_PINNED'; + payload: null; +}; +export type ConversationUnpinnedActionType = { + type: 'CONVERSATION_UNPINNED'; + payload: null; +}; export type FetchMessagesForConversationType = { type: 'messages/fetchByConversationKey/fulfilled'; @@ -265,6 +274,8 @@ export type ConversationActionType = | MessagesChangedActionType | SelectedConversationChangedActionType | SelectedConversationChangedActionType + | ConversationPinnedActionType + | ConversationUnpinnedActionType | FetchMessagesForConversationType; // Action Creators @@ -282,6 +293,8 @@ export const actions = { messagesChanged, fetchMessagesForConversation, openConversationExternal, + conversationPinned, + conversationUnpinned, }; function conversationAdded(id: string, data: ConversationType): ConversationAddedActionType { @@ -406,6 +419,20 @@ export function openConversationExternal( }; } +export function conversationPinned(): ConversationPinnedActionType { + return { + type: 'CONVERSATION_PINNED', + payload: null, + }; +} + +export function conversationUnpinned(): ConversationUnpinnedActionType { + return { + type: 'CONVERSATION_UNPINNED', + payload: null, + }; +} + // Reducer const toPickFromMessageModel = [ @@ -437,6 +464,7 @@ function getEmptyState(): ConversationsStateType { return { conversationLookup: {}, messages: [], + pinnedConversations: 0, }; } @@ -583,6 +611,30 @@ function handleConversationReset( return state; } +function handleConversationPinned( + state: ConversationsStateType, + action: ConversationPinnedActionType +) { + const { pinnedConversations } = state; + + return { + ...state, + pinnedConversations: pinnedConversations + 1, + }; +} + +function handleConversationUnpinned( + state: ConversationsStateType, + action: ConversationUnpinnedActionType +) { + const { pinnedConversations } = state; + + return { + ...state, + pinnedConversations: (pinnedConversations > 0) ? pinnedConversations - 1 : 0, + }; +} + // tslint:disable: cyclomatic-complexity // tslint:disable: max-func-body-length export function reducer( @@ -691,5 +743,13 @@ export function reducer( return handleConversationReset(state, action); } + if (action.type === 'CONVERSATION_PINNED') { + return handleConversationPinned(state, action); + } + + if (action.type === 'CONVERSATION_UNPINNED') { + return handleConversationUnpinned(state, action); + } + return state; } diff --git a/ts/state/selectors/conversations.ts b/ts/state/selectors/conversations.ts index b054f312a..d25b23e96 100644 --- a/ts/state/selectors/conversations.ts +++ b/ts/state/selectors/conversations.ts @@ -213,3 +213,7 @@ export const getMe = createSelector( export const getUnreadMessageCount = createSelector(getLeftPaneLists, (state): number => { return state.unreadCount; }); + +export const getNumberOfPinnedConversations = createSelector(getConversations, (state): number => { + return state.pinnedConversations; +}); From e3aa500ba0d29ebab5cb6c0a76c4353323b62b61 Mon Sep 17 00:00:00 2001 From: Brice-W Date: Tue, 6 Jul 2021 14:01:29 +1000 Subject: [PATCH 12/21] i18n labels for pin conversations limit --- _locales/en/messages.json | 4 +++- _locales/fr/messages.json | 6 +++++- ts/components/session/menu/Menu.tsx | 6 +++--- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 813778600..5db95684d 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -411,5 +411,7 @@ "trustThisContactDialogTitle": "Trust $name$?", "trustThisContactDialogDescription": "Are you sure you want to download media sent by $name$?", "pinConversation": "Pin Conversation", - "unpinConversation": "Unpin Conversation" + "unpinConversation": "Unpin Conversation", + "pinConversationLimitTitle": "Pinned conversations limit", + "pinConversationLimitToastDescription": "You can only pin $number$ conversations" } diff --git a/_locales/fr/messages.json b/_locales/fr/messages.json index ab77e6b18..07b9dc6c1 100644 --- a/_locales/fr/messages.json +++ b/_locales/fr/messages.json @@ -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" } diff --git a/ts/components/session/menu/Menu.tsx b/ts/components/session/menu/Menu.tsx index 6931e8607..9d0a54bf1 100644 --- a/ts/components/session/menu/Menu.tsx +++ b/ts/components/session/menu/Menu.tsx @@ -149,9 +149,9 @@ export const MenuItemPinConversation = ( dispatch(conversationUnpinned()); } else { ToastUtils.pushToastWarning( - 'setPasswordSuccessToast', - window.i18n('removePasswordTitle'), - window.i18n('removePasswordToastDescription') + 'pinConversationLimitToast', + window.i18n('pinConversationLimitTitle'), + window.i18n('pinConversationLimitToastDescription', maxNumberOfPinnedConversations) ); } }; From 75a20e5de71117d541dd51cdceb4427aa5d0706e Mon Sep 17 00:00:00 2001 From: Brice-W Date: Tue, 6 Jul 2021 15:02:29 +1000 Subject: [PATCH 13/21] simplification by using derived data --- ts/components/session/SessionInboxView.tsx | 3 -- ts/components/session/menu/Menu.tsx | 7 +-- ts/state/ducks/conversations.ts | 60 ---------------------- ts/state/selectors/conversations.ts | 3 +- 4 files changed, 3 insertions(+), 70 deletions(-) diff --git a/ts/components/session/SessionInboxView.tsx b/ts/components/session/SessionInboxView.tsx index e5fc69a87..900fb9453 100644 --- a/ts/components/session/SessionInboxView.tsx +++ b/ts/components/session/SessionInboxView.tsx @@ -90,13 +90,10 @@ export class SessionInboxView extends React.Component { const fullFilledConversations = await Promise.all(filledConversations); - const pinnedConversations = conversations.filter( (conversation) => conversation.isPinned).length; - const initialState: StateType = { conversations: { conversationLookup: makeLookup(fullFilledConversations, 'id'), messages: [], - pinnedConversations: pinnedConversations, }, user: { ourNumber: UserUtils.getOurPubKeyStrFromCache(), diff --git a/ts/components/session/menu/Menu.tsx b/ts/components/session/menu/Menu.tsx index 9d0a54bf1..1972b6b41 100644 --- a/ts/components/session/menu/Menu.tsx +++ b/ts/components/session/menu/Menu.tsx @@ -1,7 +1,6 @@ import React from 'react'; import { getNumberOfPinnedConversations } from '../../../state/selectors/conversations'; -import { conversationPinned, conversationUnpinned } from '../../../state/ducks/conversations'; import { NotificationForConvoOption, TimerOption } from '../../conversation/ConversationHeader'; import { Item, Submenu } from 'react-contexify'; import { ConversationNotificationSettingType } from '../../../models/conversation'; @@ -141,12 +140,8 @@ export const MenuItemPinConversation = ( const nbOfAlreadyPinnedConvos = useSelector(getNumberOfPinnedConversations); const togglePinConversation = async () => { - if (!isPinned && nbOfAlreadyPinnedConvos < maxNumberOfPinnedConversations) { + if ((!isPinned && nbOfAlreadyPinnedConvos < maxNumberOfPinnedConversations) || isPinned) { await conversation.setIsPinned(!isPinned); - dispatch(conversationPinned()); - } else if (isPinned) { - await conversation.setIsPinned(!isPinned); - dispatch(conversationUnpinned()); } else { ToastUtils.pushToastWarning( 'pinConversationLimitToast', diff --git a/ts/state/ducks/conversations.ts b/ts/state/ducks/conversations.ts index 4dd63e3ae..a5ecb17bb 100644 --- a/ts/state/ducks/conversations.ts +++ b/ts/state/ducks/conversations.ts @@ -93,7 +93,6 @@ export type ConversationsStateType = { conversationLookup: ConversationLookupType; selectedConversation?: string; messages: Array; - pinnedConversations: number; }; async function getMessages( @@ -244,14 +243,6 @@ export type SelectedConversationChangedActionType = { messageId?: string; }; }; -export type ConversationPinnedActionType = { - type: 'CONVERSATION_PINNED'; - payload: null; -}; -export type ConversationUnpinnedActionType = { - type: 'CONVERSATION_UNPINNED'; - payload: null; -}; export type FetchMessagesForConversationType = { type: 'messages/fetchByConversationKey/fulfilled'; @@ -274,8 +265,6 @@ export type ConversationActionType = | MessagesChangedActionType | SelectedConversationChangedActionType | SelectedConversationChangedActionType - | ConversationPinnedActionType - | ConversationUnpinnedActionType | FetchMessagesForConversationType; // Action Creators @@ -293,8 +282,6 @@ export const actions = { messagesChanged, fetchMessagesForConversation, openConversationExternal, - conversationPinned, - conversationUnpinned, }; function conversationAdded(id: string, data: ConversationType): ConversationAddedActionType { @@ -419,20 +406,6 @@ export function openConversationExternal( }; } -export function conversationPinned(): ConversationPinnedActionType { - return { - type: 'CONVERSATION_PINNED', - payload: null, - }; -} - -export function conversationUnpinned(): ConversationUnpinnedActionType { - return { - type: 'CONVERSATION_UNPINNED', - payload: null, - }; -} - // Reducer const toPickFromMessageModel = [ @@ -464,7 +437,6 @@ function getEmptyState(): ConversationsStateType { return { conversationLookup: {}, messages: [], - pinnedConversations: 0, }; } @@ -611,30 +583,6 @@ function handleConversationReset( return state; } -function handleConversationPinned( - state: ConversationsStateType, - action: ConversationPinnedActionType -) { - const { pinnedConversations } = state; - - return { - ...state, - pinnedConversations: pinnedConversations + 1, - }; -} - -function handleConversationUnpinned( - state: ConversationsStateType, - action: ConversationUnpinnedActionType -) { - const { pinnedConversations } = state; - - return { - ...state, - pinnedConversations: (pinnedConversations > 0) ? pinnedConversations - 1 : 0, - }; -} - // tslint:disable: cyclomatic-complexity // tslint:disable: max-func-body-length export function reducer( @@ -743,13 +691,5 @@ export function reducer( return handleConversationReset(state, action); } - if (action.type === 'CONVERSATION_PINNED') { - return handleConversationPinned(state, action); - } - - if (action.type === 'CONVERSATION_UNPINNED') { - return handleConversationUnpinned(state, action); - } - return state; } diff --git a/ts/state/selectors/conversations.ts b/ts/state/selectors/conversations.ts index d25b23e96..5ebea6395 100644 --- a/ts/state/selectors/conversations.ts +++ b/ts/state/selectors/conversations.ts @@ -215,5 +215,6 @@ export const getUnreadMessageCount = createSelector(getLeftPaneLists, (state): n }); export const getNumberOfPinnedConversations = createSelector(getConversations, (state): number => { - return state.pinnedConversations; + const values = Object.values(state.conversationLookup); + return values.filter(conversation => conversation.isPinned).length; }); From b2deeb39be1812587bf4eab7873105e5e7c7f800 Mon Sep 17 00:00:00 2001 From: Brice-W Date: Tue, 6 Jul 2021 15:05:04 +1000 Subject: [PATCH 14/21] clean --- ts/components/session/menu/Menu.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/ts/components/session/menu/Menu.tsx b/ts/components/session/menu/Menu.tsx index 1972b6b41..2216d478e 100644 --- a/ts/components/session/menu/Menu.tsx +++ b/ts/components/session/menu/Menu.tsx @@ -136,7 +136,6 @@ export const MenuItemPinConversation = ( const { conversationId } = props; const conversation = getConversationController().get(conversationId); const isPinned = conversation.getIsPinned(); - const dispatch = useDispatch(); const nbOfAlreadyPinnedConvos = useSelector(getNumberOfPinnedConversations); const togglePinConversation = async () => { From c3bf2a4e965ef58986c4073b04df8f997a151aeb Mon Sep 17 00:00:00 2001 From: Brice-W Date: Tue, 6 Jul 2021 17:16:05 +1000 Subject: [PATCH 15/21] fixes and tests --- ts/components/ConversationListItem.tsx | 2 +- .../session/LeftPaneMessageSection.tsx | 7 +- ts/components/session/menu/Menu.tsx | 2 +- ts/models/conversation.ts | 8 +- ts/state/ducks/conversations.ts | 2 +- ts/state/selectors/conversations.ts | 8 + .../unit/selectors/conversations_test.ts | 206 ++++++++++++++++++ ts/test/state/selectors/conversations_test.ts | 103 --------- ts/test/test-utils/utils/message.ts | 1 + 9 files changed, 223 insertions(+), 116 deletions(-) create mode 100644 ts/test/session/unit/selectors/conversations_test.ts delete mode 100644 ts/test/state/selectors/conversations_test.ts diff --git a/ts/components/ConversationListItem.tsx b/ts/components/ConversationListItem.tsx index 3a56e5997..545385274 100644 --- a/ts/components/ConversationListItem.tsx +++ b/ts/components/ConversationListItem.tsx @@ -155,7 +155,7 @@ export interface ConversationListItemHeaderProps { unreadCount: number; mentionedUs: boolean; activeAt?: number; - isPinned?: boolean; + isPinned: boolean; name?: string; phoneNumber: string; diff --git a/ts/components/session/LeftPaneMessageSection.tsx b/ts/components/session/LeftPaneMessageSection.tsx index d0603d6d8..665293eb3 100644 --- a/ts/components/session/LeftPaneMessageSection.tsx +++ b/ts/components/session/LeftPaneMessageSection.tsx @@ -77,17 +77,12 @@ export class LeftPaneMessageSection extends React.Component { } public renderRow = ({ index, key, style }: RowRendererParamsType): JSX.Element => { - const { openConversationExternal } = this.props; - let { conversations } = this.props; + const { conversations, openConversationExternal } = this.props; if (!conversations) { throw new Error('renderRow: Tried to render without conversations'); } - conversations = _.sortBy([...conversations], convo => { - return convo.isPinned ? -1 : 1; - }); - const conversation = conversations[index]; return ( diff --git a/ts/components/session/menu/Menu.tsx b/ts/components/session/menu/Menu.tsx index 2216d478e..ff03f5c89 100644 --- a/ts/components/session/menu/Menu.tsx +++ b/ts/components/session/menu/Menu.tsx @@ -135,7 +135,7 @@ export const MenuItemPinConversation = ( ): JSX.Element | null => { const { conversationId } = props; const conversation = getConversationController().get(conversationId); - const isPinned = conversation.getIsPinned(); + const isPinned = conversation.isPinned(); const nbOfAlreadyPinnedConvos = useSelector(getNumberOfPinnedConversations); const togglePinConversation = async () => { diff --git a/ts/models/conversation.ts b/ts/models/conversation.ts index 848cfbc3a..5dae6d3c5 100644 --- a/ts/models/conversation.ts +++ b/ts/models/conversation.ts @@ -91,7 +91,7 @@ export interface ConversationAttributes { accessKey?: any; triggerNotificationsFor: ConversationNotificationSettingType; isTrustedForAttachmentDownload: boolean; - isPinned?: boolean; + isPinned: boolean; } export interface ConversationAttributesOptionals { @@ -129,7 +129,7 @@ export interface ConversationAttributesOptionals { accessKey?: any; triggerNotificationsFor?: ConversationNotificationSettingType; isTrustedForAttachmentDownload?: boolean; - isPinned?: boolean; + isPinned: boolean; } /** @@ -407,7 +407,7 @@ export class ConversationModel extends Backbone.Model { left: !!this.get('left'), groupAdmins, members, - isPinned: this.getIsPinned(), + isPinned: this.isPinned(), }; } @@ -1234,7 +1234,7 @@ export class ConversationModel extends Backbone.Model { return this.get('name') || window.i18n('unknown'); } - public getIsPinned() { + public isPinned() { return this.get('isPinned'); } diff --git a/ts/state/ducks/conversations.ts b/ts/state/ducks/conversations.ts index a5ecb17bb..eb20669f9 100644 --- a/ts/state/ducks/conversations.ts +++ b/ts/state/ducks/conversations.ts @@ -82,7 +82,7 @@ export interface ConversationType { avatarPath?: string; // absolute filepath to the avatar groupAdmins?: Array; // admins for closed groups and moderators for open groups members?: Array; // members for closed groups only - isPinned?: boolean; + isPinned: boolean; } export type ConversationLookupType = { diff --git a/ts/state/selectors/conversations.ts b/ts/state/selectors/conversations.ts index 5ebea6395..f580406f3 100644 --- a/ts/state/selectors/conversations.ts +++ b/ts/state/selectors/conversations.ts @@ -64,6 +64,14 @@ const collator = new Intl.Collator(); export const _getConversationComparator = (testingi18n?: LocalizerType) => { return (left: ConversationType, right: ConversationType): 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) { diff --git a/ts/test/session/unit/selectors/conversations_test.ts b/ts/test/session/unit/selectors/conversations_test.ts new file mode 100644 index 000000000..06b494aa5 --- /dev/null +++ b/ts/test/session/unit/selectors/conversations_test.ts @@ -0,0 +1,206 @@ +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', () => { + 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, + 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, + 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, + 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, + 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, + 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', () => { + 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, + 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, + 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, + isPinned: true, + }, + 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, + isPinned: true, + }, + 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, + isPinned: 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'); + }); + }); +}); diff --git a/ts/test/state/selectors/conversations_test.ts b/ts/test/state/selectors/conversations_test.ts deleted file mode 100644 index 99810915e..000000000 --- a/ts/test/state/selectors/conversations_test.ts +++ /dev/null @@ -1,103 +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', () => { - 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, - }, - 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, - }, - 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, - }, - 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, - }, - 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, - }, - }; - 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'); - }); - }); -}); diff --git a/ts/test/test-utils/utils/message.ts b/ts/test/test-utils/utils/message.ts index bb658a5d9..4d0cdf6f2 100644 --- a/ts/test/test-utils/utils/message.ts +++ b/ts/test/test-utils/utils/message.ts @@ -88,6 +88,7 @@ export class MockConversation { zombies: [], triggerNotificationsFor: 'all', isTrustedForAttachmentDownload: false, + isPinned: false, }; } From 21b8b9f35b35d5ca689e142abbcd51e4876db138 Mon Sep 17 00:00:00 2001 From: Brice-W Date: Tue, 6 Jul 2021 17:21:43 +1000 Subject: [PATCH 16/21] memory optimization --- ts/state/selectors/conversations.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/ts/state/selectors/conversations.ts b/ts/state/selectors/conversations.ts index f580406f3..869be2742 100644 --- a/ts/state/selectors/conversations.ts +++ b/ts/state/selectors/conversations.ts @@ -223,6 +223,10 @@ export const getUnreadMessageCount = createSelector(getLeftPaneLists, (state): n }); export const getNumberOfPinnedConversations = createSelector(getConversations, (state): number => { - const values = Object.values(state.conversationLookup); - return values.filter(conversation => conversation.isPinned).length; + let count = 0; + for (let key in state.conversationLookup) { + let value = state.conversationLookup[key]; + if (value.isPinned) count++; + } + return count; }); From 03891a9438d183d23b8ef7c5a2a7d5980d3626d9 Mon Sep 17 00:00:00 2001 From: Brice-W Date: Wed, 7 Jul 2021 09:29:04 +1000 Subject: [PATCH 17/21] revert last commit --- ts/state/selectors/conversations.ts | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/ts/state/selectors/conversations.ts b/ts/state/selectors/conversations.ts index 869be2742..f580406f3 100644 --- a/ts/state/selectors/conversations.ts +++ b/ts/state/selectors/conversations.ts @@ -223,10 +223,6 @@ export const getUnreadMessageCount = createSelector(getLeftPaneLists, (state): n }); export const getNumberOfPinnedConversations = createSelector(getConversations, (state): number => { - let count = 0; - for (let key in state.conversationLookup) { - let value = state.conversationLookup[key]; - if (value.isPinned) count++; - } - return count; + const values = Object.values(state.conversationLookup); + return values.filter(conversation => conversation.isPinned).length; }); From ff7f201e046a188d78ee5edda81ee19c40a716ff Mon Sep 17 00:00:00 2001 From: Brice-W Date: Wed, 7 Jul 2021 10:19:35 +1000 Subject: [PATCH 18/21] add feature flag --- preload.js | 1 + .../session/menu/ConversationListItemContextMenu.tsx | 7 ++++--- ts/window.d.ts | 1 + 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/preload.js b/preload.js index 5023abca0..87b3d051b 100644 --- a/preload.js +++ b/preload.js @@ -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')) { diff --git a/ts/components/session/menu/ConversationListItemContextMenu.tsx b/ts/components/session/menu/ConversationListItemContextMenu.tsx index ac812f1a7..941fad8f8 100644 --- a/ts/components/session/menu/ConversationListItemContextMenu.tsx +++ b/ts/components/session/menu/ConversationListItemContextMenu.tsx @@ -46,9 +46,10 @@ export const ConversationListItemContextMenu = (props: PropsContextConversationI const isGroup = type === 'group'; const isMessagesSection = useSelector(getFocusedSection) === SectionType.Message; - const pinMenuItem = isMessagesSection ? ( - - ) : null; + const pinMenuItem = + isMessagesSection && window.lokiFeatureFlags.enablePinConversations ? ( + + ) : null; return ( <> diff --git a/ts/window.d.ts b/ts/window.d.ts index d8a86a540..9aa831a01 100644 --- a/ts/window.d.ts +++ b/ts/window.d.ts @@ -48,6 +48,7 @@ declare global { useFileOnionRequests: boolean; useFileOnionRequestsV2: boolean; padOutgoingAttachments: boolean; + enablePinConversations: boolean; }; lokiSnodeAPI: LokiSnodeAPI; onLogin: any; From 14cecb12163cf83e1dc8e564ce88ecae27c14ead Mon Sep 17 00:00:00 2001 From: Brice-W Date: Wed, 7 Jul 2021 10:37:35 +1000 Subject: [PATCH 19/21] menu item creation moved to getPinConversationMenuItem --- .../menu/ConversationListItemContextMenu.tsx | 8 +--- ts/components/session/menu/Menu.tsx | 44 ++++++++++--------- 2 files changed, 26 insertions(+), 26 deletions(-) diff --git a/ts/components/session/menu/ConversationListItemContextMenu.tsx b/ts/components/session/menu/ConversationListItemContextMenu.tsx index 941fad8f8..681a4a5cb 100644 --- a/ts/components/session/menu/ConversationListItemContextMenu.tsx +++ b/ts/components/session/menu/ConversationListItemContextMenu.tsx @@ -15,7 +15,7 @@ import { getInviteContactMenuItem, getLeaveGroupMenuItem, getMarkAllReadMenuItem, - MenuItemPinConversation, + getPinConversationMenuItem, } from './Menu'; export type PropsContextConversationItem = { @@ -46,15 +46,11 @@ export const ConversationListItemContextMenu = (props: PropsContextConversationI const isGroup = type === 'group'; const isMessagesSection = useSelector(getFocusedSection) === SectionType.Message; - const pinMenuItem = - isMessagesSection && window.lokiFeatureFlags.enablePinConversations ? ( - - ) : null; return ( <> - {pinMenuItem} + {getPinConversationMenuItem(isMessagesSection, conversationId)} {getBlockMenuItem(isMe, type === ConversationTypeEnum.PRIVATE, isBlocked, conversationId)} {getCopyMenuItem(isPublic, isGroup, conversationId)} {getMarkAllReadMenuItem(conversationId)} diff --git a/ts/components/session/menu/Menu.tsx b/ts/components/session/menu/Menu.tsx index ff03f5c89..d4ed5fc62 100644 --- a/ts/components/session/menu/Menu.tsx +++ b/ts/components/session/menu/Menu.tsx @@ -130,28 +130,32 @@ export interface PinConversationMenuItemProps { conversationId: string; } -export const MenuItemPinConversation = ( - props: PinConversationMenuItemProps +export const getPinConversationMenuItem = ( + isMessagesSection: boolean, + conversationId: string ): JSX.Element | null => { - const { conversationId } = props; - 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) - ); - } - }; + 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 {menuText}; + const menuText = isPinned ? window.i18n('unpinConversation') : window.i18n('pinConversation'); + return {menuText}; + } else { + return null; + } }; export function getDeleteContactMenuItem( From f1e4cbaabf8635c119b6184e01196e5b04f7c03d Mon Sep 17 00:00:00 2001 From: Brice-W Date: Wed, 7 Jul 2021 11:16:12 +1000 Subject: [PATCH 20/21] clean --- .../session/menu/ConversationListItemContextMenu.tsx | 6 +----- ts/components/session/menu/Menu.tsx | 7 ++++--- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/ts/components/session/menu/ConversationListItemContextMenu.tsx b/ts/components/session/menu/ConversationListItemContextMenu.tsx index 681a4a5cb..740eacfa1 100644 --- a/ts/components/session/menu/ConversationListItemContextMenu.tsx +++ b/ts/components/session/menu/ConversationListItemContextMenu.tsx @@ -1,9 +1,6 @@ import React from 'react'; import { animation, Menu } from 'react-contexify'; -import { useSelector } from 'react-redux'; import { ConversationTypeEnum } from '../../../models/conversation'; -import { getFocusedSection } from '../../../state/selectors/section'; -import { SectionType } from '../ActionsPanel'; import { getBlockMenuItem, @@ -45,12 +42,11 @@ export const ConversationListItemContextMenu = (props: PropsContextConversationI } = props; const isGroup = type === 'group'; - const isMessagesSection = useSelector(getFocusedSection) === SectionType.Message; return ( <> - {getPinConversationMenuItem(isMessagesSection, conversationId)} + {getPinConversationMenuItem(conversationId)} {getBlockMenuItem(isMe, type === ConversationTypeEnum.PRIVATE, isBlocked, conversationId)} {getCopyMenuItem(isPublic, isGroup, conversationId)} {getMarkAllReadMenuItem(conversationId)} diff --git a/ts/components/session/menu/Menu.tsx b/ts/components/session/menu/Menu.tsx index d4ed5fc62..04b90b254 100644 --- a/ts/components/session/menu/Menu.tsx +++ b/ts/components/session/menu/Menu.tsx @@ -1,6 +1,7 @@ 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'; @@ -23,6 +24,7 @@ import { unblockConvoById, } from '../../../interactions/conversationInteractions'; import { SessionButtonColor } from '../SessionButton'; +import { SectionType } from '../ActionsPanel'; import { ToastUtils } from '../../../session/utils'; const maxNumberOfPinnedConversations = 5; @@ -131,9 +133,9 @@ export interface PinConversationMenuItemProps { } export const getPinConversationMenuItem = ( - isMessagesSection: boolean, 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(); @@ -153,9 +155,8 @@ export const getPinConversationMenuItem = ( const menuText = isPinned ? window.i18n('unpinConversation') : window.i18n('pinConversation'); return {menuText}; - } else { - return null; } + return null; }; export function getDeleteContactMenuItem( From 5252dd40e32ea3db660c66a1a8c298414c5442c7 Mon Sep 17 00:00:00 2001 From: Brice-W Date: Wed, 7 Jul 2021 13:43:33 +1000 Subject: [PATCH 21/21] fixes --- js/background.js | 4 ---- js/views/app_view.js | 11 ---------- ts/components/ConversationListItem.tsx | 3 +-- ts/components/LeftPane.tsx | 3 ++- ts/components/session/ActionsPanel.tsx | 22 +------------------ .../conversation/SessionCompositionBox.tsx | 2 +- ts/components/session/menu/Menu.tsx | 6 ++--- ts/state/ducks/SessionTheme.tsx | 3 --- ts/state/ducks/section.tsx | 11 +++++++++- ts/state/selectors/onions.ts | 2 +- ts/state/selectors/section.ts | 3 +-- ts/window.d.ts | 3 --- 12 files changed, 19 insertions(+), 54 deletions(-) diff --git a/js/background.js b/js/background.js index c58986015..0a632e39a 100644 --- a/js/background.js +++ b/js/background.js @@ -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; diff --git a/js/views/app_view.js b/js/views/app_view.js index ba1432b43..126e920e9 100644 --- a/js/views/app_view.js +++ b/js/views/app_view.js @@ -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; - }, }); })(); diff --git a/ts/components/ConversationListItem.tsx b/ts/components/ConversationListItem.tsx index 545385274..ebdd01aee 100644 --- a/ts/components/ConversationListItem.tsx +++ b/ts/components/ConversationListItem.tsx @@ -28,8 +28,7 @@ import { } from '../state/ducks/conversations'; import { SessionIcon, SessionIconSize, SessionIconType } from './session/icon'; import { useDispatch, useSelector } from 'react-redux'; -import { SectionType } from './session/ActionsPanel'; -import { getTheme } from '../state/selectors/theme'; +import { SectionType } from '../state/ducks/section'; import { getFocusedSection } from '../state/selectors/section'; export interface ConversationListItemProps extends ConversationType { diff --git a/ts/components/LeftPane.tsx b/ts/components/LeftPane.tsx index da5a16129..812298819 100644 --- a/ts/components/LeftPane.tsx +++ b/ts/components/LeftPane.tsx @@ -1,6 +1,6 @@ import React, { useState } from 'react'; -import { ActionsPanel, SectionType } from './session/ActionsPanel'; +import { ActionsPanel } from './session/ActionsPanel'; import { LeftPaneMessageSection } from './session/LeftPaneMessageSection'; import { openConversationExternal } from '../state/ducks/conversations'; @@ -13,6 +13,7 @@ import { useDispatch, 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 { 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 diff --git a/ts/components/session/ActionsPanel.tsx b/ts/components/session/ActionsPanel.tsx index 6c32f113d..bae63749c 100644 --- a/ts/components/session/ActionsPanel.tsx +++ b/ts/components/session/ActionsPanel.tsx @@ -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 diff --git a/ts/components/session/conversation/SessionCompositionBox.tsx b/ts/components/session/conversation/SessionCompositionBox.tsx index 22df902de..c28e8cd80 100644 --- a/ts/components/session/conversation/SessionCompositionBox.tsx +++ b/ts/components/session/conversation/SessionCompositionBox.tsx @@ -31,10 +31,10 @@ import { getConversationController } from '../../../session/conversations'; import { ConversationType } 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 } from '../../../state/ducks/section'; import { SessionButtonColor } from '../SessionButton'; import { SessionConfirmDialogProps } from '../SessionConfirm'; diff --git a/ts/components/session/menu/Menu.tsx b/ts/components/session/menu/Menu.tsx index 04b90b254..9c4886aab 100644 --- a/ts/components/session/menu/Menu.tsx +++ b/ts/components/session/menu/Menu.tsx @@ -7,6 +7,7 @@ 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, @@ -24,7 +25,6 @@ import { unblockConvoById, } from '../../../interactions/conversationInteractions'; import { SessionButtonColor } from '../SessionButton'; -import { SectionType } from '../ActionsPanel'; import { ToastUtils } from '../../../session/utils'; const maxNumberOfPinnedConversations = 5; @@ -132,9 +132,7 @@ export interface PinConversationMenuItemProps { conversationId: string; } -export const getPinConversationMenuItem = ( - conversationId: string -): JSX.Element | null => { +export const getPinConversationMenuItem = (conversationId: string): JSX.Element | null => { const isMessagesSection = useSelector(getFocusedSection) === SectionType.Message; if (isMessagesSection && window.lokiFeatureFlags.enablePinConversations) { const conversation = getConversationController().get(conversationId); diff --git a/ts/state/ducks/SessionTheme.tsx b/ts/state/ducks/SessionTheme.tsx index 3432efc28..6a4c43f93 100644 --- a/ts/state/ducks/SessionTheme.tsx +++ b/ts/state/ducks/SessionTheme.tsx @@ -163,6 +163,3 @@ export const inversedTheme = (theme: DefaultTheme): DefaultTheme => { export const SessionTheme = ({ children, theme }: { children: any; theme: DefaultTheme }) => ( {children} ); - -window.lightTheme = lightTheme; -window.darkTheme = darkTheme; diff --git a/ts/state/ducks/section.tsx b/ts/state/ducks/section.tsx index 3c75a20b2..6c6092cc3 100644 --- a/ts/state/ducks/section.tsx +++ b/ts/state/ducks/section.tsx @@ -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; diff --git a/ts/state/selectors/onions.ts b/ts/state/selectors/onions.ts index 897e8b3ff..6bb0f9693 100644 --- a/ts/state/selectors/onions.ts +++ b/ts/state/selectors/onions.ts @@ -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; diff --git a/ts/state/selectors/section.ts b/ts/state/selectors/section.ts index 35f5df599..049ad4983 100644 --- a/ts/state/selectors/section.ts +++ b/ts/state/selectors/section.ts @@ -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; diff --git a/ts/window.d.ts b/ts/window.d.ts index 9aa831a01..bb39ad709 100644 --- a/ts/window.d.ts +++ b/ts/window.d.ts @@ -57,7 +57,6 @@ declare global { getSeedNodeList: () => Array | undefined; setPassword: any; setSettingValue: any; - showResetSessionIdDialog: any; storage: any; textsecure: LibTextsecure; toggleLinkPreview: any; @@ -80,8 +79,6 @@ declare global { expired: (boolean) => void; expiredStatus: () => boolean; }; - lightTheme: DefaultTheme; - darkTheme: DefaultTheme; LokiPushNotificationServer: any; globalOnlineStatus: boolean; confirmationDialog: any;