diff --git a/js/background.js b/js/background.js index 24acc0536..2de2d427e 100644 --- a/js/background.js +++ b/js/background.js @@ -613,7 +613,7 @@ confirmDialog.render(); }; - window.showEditProfileDialog = async callback => { + window.showEditProfileDialog = async () => { const ourNumber = window.storage.get('primaryDevicePubKey'); const conversation = await ConversationController.getOrCreateAndWait( ourNumber, @@ -642,7 +642,6 @@ if (appView) { appView.showEditProfileDialog({ - callback, profileName: displayName, pubkey: ourNumber, avatarPath, @@ -706,26 +705,35 @@ url, }); newAvatarPath = upgraded.path; + // Replace our temporary image with the attachment pointer from the server: + conversation.set('avatar', null); + conversation.setLokiProfile({ + displayName: newName, + avatar: newAvatarPath, + }); + } else { + // do not update the avatar if it did not change + conversation.setLokiProfile({ + displayName: newName, + }); } - // Replace our temporary image with the attachment pointer from the server: - conversation.set('avatar', null); - conversation.setLokiProfile({ - displayName: newName, - avatar: newAvatarPath, - }); + conversation.commit(); // inform all your registered public servers // could put load on all the servers // if they just keep changing their names without sending messages // so we could disable this here // or least it enable for the quickest response window.lokiPublicChatAPI.setProfileName(newName); - window - .getConversations() - .filter(convo => convo.isPublic() && !convo.isRss()) - .forEach(convo => - convo.trigger('ourAvatarChanged', { url, profileKey }) - ); + + if (avatar) { + window + .getConversations() + .filter(convo => convo.isPublic() && !convo.isRss()) + .forEach(convo => + convo.trigger('ourAvatarChanged', { url, profileKey }) + ); + } }, }); } @@ -938,13 +946,6 @@ }); Whisper.events.on('onShowUserDetails', async ({ userPubKey }) => { - const isMe = userPubKey === textsecure.storage.user.getNumber(); - - if (isMe) { - Whisper.events.trigger('onEditProfile'); - return; - } - const conversation = await ConversationController.getOrCreateAndWait( userPubKey, 'private' diff --git a/js/models/conversations.js b/js/models/conversations.js index e38445dfe..16eab95e5 100644 --- a/js/models/conversations.js +++ b/js/models/conversations.js @@ -1895,8 +1895,11 @@ await this.commit(); } - // if set to null, it will show a placeholder with color and first letter - await this.setProfileAvatar({ path: newProfile.avatar }); + // a user cannot remove an avatar. Only change it + // if you change this behavior, double check all setLokiProfile calls (especially the one in EditProfileDialog) + if (newProfile.avatar) { + await this.setProfileAvatar({ path: newProfile.avatar }); + } await this.updateProfileName(); }, @@ -2386,7 +2389,6 @@ getAvatarPath() { const avatar = this.get('avatar') || this.get('profileAvatar'); - if (typeof avatar === 'string') { return avatar; } diff --git a/js/views/edit_profile_dialog_view.js b/js/views/edit_profile_dialog_view.js index cb1996753..c646a17ef 100644 --- a/js/views/edit_profile_dialog_view.js +++ b/js/views/edit_profile_dialog_view.js @@ -8,10 +8,9 @@ Whisper.EditProfileDialogView = Whisper.View.extend({ className: 'loki-dialog modal', - initialize({ profileName, avatarPath, pubkey, onOk, callback }) { + initialize({ profileName, avatarPath, pubkey, onOk }) { this.close = this.close.bind(this); - this.callback = callback; this.profileName = profileName; this.pubkey = pubkey; this.avatarPath = avatarPath; @@ -25,7 +24,6 @@ className: 'edit-profile-dialog', Component: window.Signal.Components.EditProfileDialog, props: { - callback: this.callback, onOk: this.onOk, onClose: this.close, profileName: this.profileName, diff --git a/ts/components/EditProfileDialog.tsx b/ts/components/EditProfileDialog.tsx index afeee4b0b..13b2afdad 100644 --- a/ts/components/EditProfileDialog.tsx +++ b/ts/components/EditProfileDialog.tsx @@ -26,7 +26,6 @@ declare global { } interface Props { - callback: any; i18n: any; profileName: string; avatarPath: string; @@ -318,17 +317,10 @@ export class EditProfileDialog extends React.Component { this.props.onOk(newName, avatar); - this.setState( - { - mode: 'default', - setProfileName: this.state.profileName, - }, - () => { - // Update settings in dialog complete; - // now callback to reloadactions panel avatar - this.props.callback(this.state.avatar); - } - ); + this.setState({ + mode: 'default', + setProfileName: this.state.profileName, + }); } private closeDialog() { diff --git a/ts/components/LeftPane.tsx b/ts/components/LeftPane.tsx index 54215dd4f..d360e197f 100644 --- a/ts/components/LeftPane.tsx +++ b/ts/components/LeftPane.tsx @@ -29,6 +29,7 @@ export type RowRendererParamsType = { }; interface Props { + ourPrimaryConversation: ConversationType; conversations: Array; contacts: Array; @@ -90,14 +91,15 @@ export class LeftPane extends React.Component { } public render(): JSX.Element { + const ourPrimaryConversation = this.props.ourPrimaryConversation; return (
{this.renderSection()}
diff --git a/ts/components/session/ActionsPanel.tsx b/ts/components/session/ActionsPanel.tsx index ec6c1e275..cdfe69e4f 100644 --- a/ts/components/session/ActionsPanel.tsx +++ b/ts/components/session/ActionsPanel.tsx @@ -1,12 +1,13 @@ import React from 'react'; -import { connect, useDispatch } from 'react-redux'; +import { connect } from 'react-redux'; import { SessionIconButton, SessionIconSize, SessionIconType } from './icon'; import { Avatar } from '../Avatar'; -import { PropsData as ConversationListItemPropsType } from '../ConversationListItem'; -import { createOrUpdateItem, getItemById } from '../../../js/modules/data'; -import { APPLY_THEME } from '../../state/ducks/theme'; +import { removeItemById } from '../../../js/modules/data'; import { darkTheme, lightTheme } from '../../state/ducks/SessionTheme'; import { SessionToastContainer } from './SessionToastContainer'; +import { mapDispatchToProps } from '../../state/actions'; +import { ConversationType } from '../../state/ducks/conversations'; +import { noop } from 'lodash'; // tslint:disable-next-line: no-import-side-effect no-submodule-imports export enum SectionType { @@ -18,104 +19,22 @@ export enum SectionType { Moon, } -interface State { - avatarPath: string; -} - interface Props { onSectionSelected: any; selectedSection: SectionType; - conversations: Array | undefined; unreadMessageCount: number; - dispatch?: any; + ourPrimaryConversation: ConversationType; + applyTheme?: any; } -class ActionsPanelPrivate extends React.Component { - private ourConversation: any; +class ActionsPanelPrivate extends React.Component { constructor(props: Props) { super(props); - this.state = { - avatarPath: '', - }; this.editProfileHandle = this.editProfileHandle.bind(this); - this.refreshAvatarCallback = this.refreshAvatarCallback.bind(this); - } - - public componentDidMount() { - // tslint:disable-next-line: no-backbone-get-set-outside-model - const ourNumber = window.storage.get('primaryDevicePubKey'); - - void window.ConversationController.getOrCreateAndWait( - ourNumber, - 'private' - ).then((conversation: any) => { - this.setState({ - avatarPath: conversation.getAvatarPath(), - }); - // When our primary device updates its avatar, we will need for a message sync to know about that. - // Once we get the avatar update, we need to refresh this react component. - // So we listen to changes on our profile avatar and use the updated avatarPath (done on message received). - this.ourConversation = conversation; - - this.ourConversation.on( - 'change', - () => { - this.refreshAvatarCallback(this.ourConversation); - }, - 'refreshAvatarCallback' - ); - - void this.showLightThemeDialogIfNeeded(); - }); - } - - public async showLightThemeDialogIfNeeded() { - const currentTheme = window.Events.getThemeSetting(); // defaults to light on new registration - if (currentTheme !== 'light') { - const message = 'Light Mode'; - const messageSub = - 'Whoops, who left the lights on?

\ - That’s right, Session has a spiffy new light mode! Take the fresh new color palette for a spin — it’s now the default mode.

\ - Want to go back to the dark side? Just tap the moon symbol in the lower left corner of the app to switch modes.'; - const hasSeenLightMode = await getItemById('hasSeenLightModeDialog'); - - if (hasSeenLightMode?.value === true) { - // if hasSeen is set and true, we have nothing to do - return; - } - // force light them right now, then ask for permission - await window.Events.setThemeSetting('light'); - window.confirmationDialog({ - message, - messageSub, - resolve: async () => { - const data = { - id: 'hasSeenLightModeDialog', - value: true, - }; - void createOrUpdateItem(data); - }, - okTheme: 'default primary', - hideCancel: true, - sessionIcon: SessionIconType.Sun, - iconSize: SessionIconSize.Max, - }); - } - } - - public refreshAvatarCallback(conversation: any) { - if (conversation.changed?.profileAvatar) { - this.setState({ - avatarPath: conversation.getAvatarPath(), - }); - } - } - - public componentWillUnmount() { - if (this.ourConversation) { - this.ourConversation.off('change', null, 'refreshAvatarCallback'); - } + // we consider people had the time to upgrade, so remove this id from the db + // it was used to display a dialog when we added the light mode auto-enabled + void removeItemById('hasSeenLightModeDialog'); } public Section = ({ @@ -143,10 +62,7 @@ class ActionsPanelPrivate extends React.Component { const newThemeObject = updatedTheme === 'dark' ? darkTheme : lightTheme; - this.props.dispatch({ - type: APPLY_THEME, - payload: newThemeObject, - }); + this.props.applyTheme(newThemeObject); } else { onSelect(type); } @@ -204,11 +120,7 @@ class ActionsPanelPrivate extends React.Component { }; public editProfileHandle() { - window.showEditProfileDialog((avatar: any) => { - this.setState({ - avatarPath: avatar, - }); - }); + window.showEditProfileDialog(noop); } public render(): JSX.Element { @@ -224,7 +136,7 @@ class ActionsPanelPrivate extends React.Component {
@@ -260,4 +172,6 @@ class ActionsPanelPrivate extends React.Component { }; } -export const ActionsPanel = connect()(ActionsPanelPrivate); +const smart = connect(null, mapDispatchToProps); + +export const ActionsPanel = smart(ActionsPanelPrivate); diff --git a/ts/components/session/SessionInboxView.tsx b/ts/components/session/SessionInboxView.tsx index 10bab024a..0fa30a653 100644 --- a/ts/components/session/SessionInboxView.tsx +++ b/ts/components/session/SessionInboxView.tsx @@ -15,7 +15,6 @@ import { // Workaround: A react component's required properties are filtering up through connect() // https://github.com/DefinitelyTyped/DefinitelyTyped/issues/31363 const FilteredLeftPane = SmartLeftPane as any; -const FilteredSessionConversation = SmartSessionConversation as any; type Props = { focusedSection: number; @@ -27,8 +26,6 @@ type State = { isExpired: boolean; }; -// tslint:disable: react-a11y-img-has-alt - export class SessionInboxView extends React.Component { private store: any; @@ -111,7 +108,7 @@ export class SessionInboxView extends React.Component { private renderSessionConversation() { return (
- +
); } diff --git a/ts/components/session/conversation/SessionMessagesList.tsx b/ts/components/session/conversation/SessionMessagesList.tsx index 057aef95a..45e44303c 100644 --- a/ts/components/session/conversation/SessionMessagesList.tsx +++ b/ts/components/session/conversation/SessionMessagesList.tsx @@ -43,10 +43,7 @@ interface Props { onDeleteSelectedMessages: () => Promise; } -export class SessionMessagesList extends React.Component< - Props, - State -> { +export class SessionMessagesList extends React.Component { private readonly messagesEndRef: React.RefObject; private readonly messageContainerRef: React.RefObject; diff --git a/ts/state/actions.ts b/ts/state/actions.ts index 99e715579..38f141d45 100644 --- a/ts/state/actions.ts +++ b/ts/state/actions.ts @@ -4,15 +4,19 @@ import { actions as search } from './ducks/search'; import { actions as conversations } from './ducks/conversations'; import { actions as user } from './ducks/user'; import { actions as sections } from './ducks/section'; - -const actions = { - ...search, - ...conversations, - ...user, - // ...messages, - ...sections, -}; +import { actions as theme } from './ducks/theme'; export function mapDispatchToProps(dispatch: Dispatch): Object { - return { ...bindActionCreators(actions, dispatch) }; + return { + ...bindActionCreators( + { + ...search, + ...conversations, + ...user, + ...theme, + ...sections, + }, + dispatch + ), + }; } diff --git a/ts/state/ducks/conversations.ts b/ts/state/ducks/conversations.ts index 9f4e98e52..160d8b668 100644 --- a/ts/state/ducks/conversations.ts +++ b/ts/state/ducks/conversations.ts @@ -75,6 +75,7 @@ export type ConversationType = { isBlocked: boolean; isKickedFromGroup: boolean; leftGroup: boolean; + avatarPath?: string; // absolute filepath to the avatar }; export type ConversationLookupType = { [key: string]: ConversationType; diff --git a/ts/state/ducks/search.ts b/ts/state/ducks/search.ts index 657249f1d..357ff03fd 100644 --- a/ts/state/ducks/search.ts +++ b/ts/state/ducks/search.ts @@ -2,7 +2,6 @@ import { omit, reject } from 'lodash'; import { normalize } from '../../types/PhoneNumber'; import { AdvancedSearchOptions, SearchOptions } from '../../types/Search'; -import { trigger } from '../../shims/events'; import { getMessageModel } from '../../shims/Whisper'; import { cleanSearchTerm } from '../../util/cleanSearchTerm'; import { searchConversations, searchMessages } from '../../../js/modules/data'; diff --git a/ts/state/ducks/section.tsx b/ts/state/ducks/section.tsx index 6dd0fd911..28c458493 100644 --- a/ts/state/ducks/section.tsx +++ b/ts/state/ducks/section.tsx @@ -2,12 +2,17 @@ import { SectionType } from '../../components/session/ActionsPanel'; export const FOCUS_SECTION = 'FOCUS_SECTION'; -const focusSection = (section: SectionType) => { +type FocusSectionActionType = { + type: 'FOCUS_SECTION'; + payload: SectionType; +}; + +function focusSection(section: SectionType): FocusSectionActionType { return { type: FOCUS_SECTION, payload: section, }; -}; +} export const actions = { focusSection, diff --git a/ts/state/ducks/theme.tsx b/ts/state/ducks/theme.tsx index 55a899cd6..611020353 100644 --- a/ts/state/ducks/theme.tsx +++ b/ts/state/ducks/theme.tsx @@ -29,3 +29,7 @@ export const reducer = ( return state; } }; + +export const actions = { + applyTheme, +}; diff --git a/ts/state/selectors/conversations.ts b/ts/state/selectors/conversations.ts index 2f7a620b3..ecf4281da 100644 --- a/ts/state/selectors/conversations.ts +++ b/ts/state/selectors/conversations.ts @@ -30,6 +30,12 @@ export const getSelectedConversation = createSelector( } ); +export const getOurPrimaryConversation = createSelector( + getConversations, + (state: ConversationsStateType): ConversationType => + state.conversationLookup[window.storage.get('primaryDevicePubKey')] +); + function getConversationTitle( conversation: ConversationType, options: { i18n: LocalizerType; ourRegionCode: string } diff --git a/ts/state/smart/LeftPane.tsx b/ts/state/smart/LeftPane.tsx index a6480be24..3b00d4634 100644 --- a/ts/state/smart/LeftPane.tsx +++ b/ts/state/smart/LeftPane.tsx @@ -1,5 +1,4 @@ import { connect } from 'react-redux'; -import { mapDispatchToProps } from '../actions'; import { LeftPane } from '../../components/LeftPane'; import { StateType } from '../reducer'; @@ -10,7 +9,11 @@ import { getRegionCode, getUserNumber, } from '../selectors/user'; -import { getLeftPaneLists } from '../selectors/conversations'; +import { + getLeftPaneLists, + getOurPrimaryConversation, +} from '../selectors/conversations'; +import { mapDispatchToProps } from '../actions'; // Workaround: A react component's required properties are filtering up through connect() // https://github.com/DefinitelyTyped/DefinitelyTyped/issues/31363 @@ -23,6 +26,7 @@ const mapStateToProps = (state: StateType) => { const searchResults = showSearch ? getSearchResults(state) : undefined; return { ...lists, + ourPrimaryConversation: getOurPrimaryConversation(state), // used in actionPanel searchTerm: getQuery(state), regionCode: getRegionCode(state), ourNumber: getUserNumber(state),