diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 139bc81a9..820d929f2 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -784,7 +784,7 @@ "description": "Shown to separate the types of search results" }, "messagesHeader": { - "message": "Session", + "message": "Conversations", "description": "Shown to separate the types of search results" }, "settingsHeader": { diff --git a/js/background.js b/js/background.js index 41f636982..d88f34cc1 100644 --- a/js/background.js +++ b/js/background.js @@ -957,10 +957,6 @@ window.setSettingValue('link-preview-setting', false); } - // Render onboarding message from LeftPaneMessageSection - // unless user turns it off during their session - window.setSettingValue('render-message-onboarding', true); - // Generates useful random ID for various purposes window.generateID = () => Math.random() @@ -1013,20 +1009,6 @@ return toastID; }; - window.getFriendsFromContacts = contacts => { - // To call from TypeScript, input / output are both - // of type Array - let friendList = contacts; - if (friendList !== undefined) { - friendList = friendList.filter( - friend => - (friend.type === 'direct' && !friend.isMe) || - (friend.type === 'group' && !friend.isPublic && !friend.isRss) - ); - } - return friendList; - }; - // Get memberlist. This function is not accurate >> // window.getMemberList = window.lokiPublicChatAPI.getListOfMembers(); diff --git a/package.json b/package.json index 4603dedd2..ea074269f 100644 --- a/package.json +++ b/package.json @@ -20,10 +20,8 @@ "start-multi2": "cross-env NODE_APP_INSTANCE=2 electron .", "start-prod": "cross-env NODE_ENV=production NODE_APP_INSTANCE=devprod electron .", "start-prod-multi": "cross-env NODE_ENV=production NODE_APP_INSTANCE=devprod1 electron .", - "start-prod-multi-2": "cross-env NODE_ENV=production NODE_APP_INSTANCE=devprod2 electron .", "start-swarm-test": "cross-env NODE_ENV=swarm-testing NODE_APP_INSTANCE=1 electron .", "start-swarm-test-2": "cross-env NODE_ENV=swarm-testing NODE_APP_INSTANCE=2 electron .", - "start-swarm-test-3": "cross-env NODE_ENV=swarm-testing NODE_APP_INSTANCE=3 electron .", "grunt": "grunt", "icon-gen": "electron-icon-maker --input=images/icon_1024.png --output=./build", "generate": "yarn icon-gen && yarn grunt", diff --git a/stylesheets/_session.scss b/stylesheets/_session.scss index 7af79716b..db6061a4b 100644 --- a/stylesheets/_session.scss +++ b/stylesheets/_session.scss @@ -91,6 +91,7 @@ textarea { width: auto; display: flex; justify-content: center; + align-items: center; font-weight: 700; user-select: none; white-space: nowrap; diff --git a/ts/components/LeftPane.tsx b/ts/components/LeftPane.tsx index 6d677da68..88fe9b5da 100644 --- a/ts/components/LeftPane.tsx +++ b/ts/components/LeftPane.tsx @@ -11,7 +11,7 @@ import { LeftPaneSectionHeader } from './session/LeftPaneSectionHeader'; import { ConversationType } from '../state/ducks/conversations'; import { LeftPaneContactSection } from './session/LeftPaneContactSection'; import { LeftPaneSettingSection } from './session/LeftPaneSettingSection'; -import { LeftPaneChannelSection } from './session/LeftPaneChannelSection'; +import { SessionIconType } from './session/icon'; // from https://github.com/bvaughn/react-virtualized/blob/fb3484ed5dcc41bffae8eab029126c0fb8f7abc0/source/List/types.js#L5 export type RowRendererParamsType = { @@ -59,6 +59,7 @@ export class LeftPane extends React.Component { labels: Array, onTabSelected?: any, buttonLabel?: string, + buttonIcon?: SessionIconType, buttonClicked?: any, notificationCount?: number ): JSX.Element { @@ -68,6 +69,7 @@ export class LeftPane extends React.Component { selectedTab={0} labels={labels} buttonLabel={buttonLabel} + buttonIcon={buttonIcon} buttonClicked={buttonClicked} notificationCount={notificationCount} /> @@ -100,8 +102,6 @@ export class LeftPane extends React.Component { return this.renderMessageSection(); case SectionType.Contact: return this.renderContactSection(); - case SectionType.Channel: - return this.renderChannelSection(); case SectionType.Settings: return this.renderSettingSection(); case SectionType.Moon: @@ -176,30 +176,4 @@ export class LeftPane extends React.Component { return ; } - - private renderChannelSection() { - const { - openConversationInternal, - conversations, - searchResults, - searchTerm, - isSecondaryDevice, - updateSearchTerm, - search, - clearSearch, - } = this.props; - - return ( - - ); - } } diff --git a/ts/components/MainViewController.tsx b/ts/components/MainViewController.tsx index e344920c2..fccd0e85a 100644 --- a/ts/components/MainViewController.tsx +++ b/ts/components/MainViewController.tsx @@ -7,29 +7,14 @@ import { } from './session/settings/SessionSettings'; export const MainViewController = { - renderMessageView: () => { - if (document.getElementById('main-view')) { - ReactDOM.render(, document.getElementById('main-view')); - } - }, - - renderSettingsView: (category: SessionSettingCategory) => { - // tslint:disable-next-line: no-backbone-get-set-outside-model - const isSecondaryDevice = !!window.textsecure.storage.get( - 'isSecondaryDevice' - ); - if (document.getElementById('main-view')) { - ReactDOM.render( - , - document.getElementById('main-view') - ); - } - }, + joinChannelStateManager, + createClosedGroup, + renderMessageView, + renderSettingsView, }; +import { ContactType } from './session/SessionMemberListItem'; + export class MessageView extends React.Component { public render() { return ( @@ -50,3 +35,147 @@ export class MessageView extends React.Component { ); } } + +// ///////////////////////////////////// +// //////////// Management ///////////// +// ///////////////////////////////////// + +function joinChannelStateManager( + thisRef: any, + serverURL: string, + onSuccess?: any +) { + // Any component that uses this function MUST have the keys [loading, connectSuccess] + // in their State + + // TODO: Make this not hard coded + const channelId = 1; + thisRef.setState({ loading: true }); + const connectionResult = window.attemptConnection(serverURL, channelId); + + // Give 5s maximum for promise to revole. Else, throw error. + const connectionTimeout = setTimeout(() => { + if (!thisRef.state.connectSuccess) { + thisRef.setState({ loading: false }); + window.pushToast({ + title: window.i18n('connectToServerFail'), + type: 'error', + id: 'connectToServerFail', + }); + + return; + } + }, window.CONSTANTS.MAX_CONNECTION_DURATION); + + connectionResult + .then(() => { + clearTimeout(connectionTimeout); + + if (thisRef.state.loading) { + thisRef.setState({ + connectSuccess: true, + loading: false, + }); + window.pushToast({ + title: window.i18n('connectToServerSuccess'), + id: 'connectToServerSuccess', + type: 'success', + }); + + if (onSuccess) { + onSuccess(); + } + } + }) + .catch((connectionError: string) => { + clearTimeout(connectionTimeout); + thisRef.setState({ + connectSuccess: true, + loading: false, + }); + window.pushToast({ + title: connectionError, + id: 'connectToServerFail', + type: 'error', + }); + + return false; + }); + + return true; +} + +async function createClosedGroup( + groupName: string, + groupMembers: Array, + onSuccess: any +) { + // Validate groupName and groupMembers length + if ( + groupName.length === 0 || + groupName.length > window.CONSTANTS.MAX_GROUP_NAME_LENGTH + ) { + window.pushToast({ + title: window.i18n( + 'invalidGroupName', + window.CONSTANTS.MAX_GROUP_NAME_LENGTH + ), + type: 'error', + id: 'invalidGroupName', + }); + + return; + } + + // >= because we add ourself as a member after this. so a 10 group is already invalid as it will be 11 with ourself + if ( + groupMembers.length === 0 || + groupMembers.length >= window.CONSTANTS.SMALL_GROUP_SIZE_LIMIT + ) { + window.pushToast({ + title: window.i18n( + 'invalidGroupSize', + window.CONSTANTS.SMALL_GROUP_SIZE_LIMIT + ), + type: 'error', + id: 'invalidGroupSize', + }); + + return; + } + + const groupMemberIds = groupMembers.map(m => m.id); + await window.doCreateGroup(groupName, groupMemberIds); + + if (onSuccess) { + onSuccess(); + } + + return true; +} + +// ///////////////////////////////////// +// ///////////// Rendering ///////////// +// ///////////////////////////////////// + +function renderMessageView() { + if (document.getElementById('main-view')) { + ReactDOM.render(, document.getElementById('main-view')); + } +} + +function renderSettingsView(category: SessionSettingCategory) { + // tslint:disable-next-line: no-backbone-get-set-outside-model + const isSecondaryDevice = !!window.textsecure.storage.get( + 'isSecondaryDevice' + ); + if (document.getElementById('main-view')) { + ReactDOM.render( + , + document.getElementById('main-view') + ); + } +} diff --git a/ts/components/session/ActionsPanel.tsx b/ts/components/session/ActionsPanel.tsx index 2f8f9c10d..215560583 100644 --- a/ts/components/session/ActionsPanel.tsx +++ b/ts/components/session/ActionsPanel.tsx @@ -136,7 +136,6 @@ export class ActionsPanel extends React.Component { const isProfilePageSelected = selectedSection === SectionType.Profile; const isMessagePageSelected = selectedSection === SectionType.Message; const isContactPageSelected = selectedSection === SectionType.Contact; - const isChannelPageSelected = selectedSection === SectionType.Channel; const isSettingsPageSelected = selectedSection === SectionType.Settings; const isMoonPageSelected = selectedSection === SectionType.Moon; @@ -154,11 +153,6 @@ export class ActionsPanel extends React.Component { onSelect={this.handleSectionSelect} notificationCount={unreadMessageCount} /> - ; - - searchResults?: SearchResultsProps; - - updateSearchTerm: (searchTerm: string) => void; - search: (query: string, options: SearchOptions) => void; - openConversationInternal: (id: string, messageId?: string) => void; - clearSearch: () => void; -} - -export enum SessionGroupType { - Open = 'open-group', - Closed = 'closed-group', -} - -interface State { - channelUrlPasted: string; - loading: boolean; - connectSuccess: boolean; - // The type of group that is being added. Undefined in default view. - groupAddType: SessionGroupType | undefined; -} - -export class LeftPaneChannelSection extends React.Component { - private readonly updateSearchBound: (searchedString: string) => void; - private readonly debouncedSearch: (searchTerm: string) => void; - - public constructor(props: Props) { - super(props); - this.state = { - channelUrlPasted: '', - loading: false, - connectSuccess: false, - groupAddType: undefined, - }; - - this.handleOnPasteUrl = this.handleOnPasteUrl.bind(this); - this.handleJoinChannelButtonClick = this.handleJoinChannelButtonClick.bind( - this - ); - this.handleToggleOverlay = this.handleToggleOverlay.bind(this); - this.updateSearchBound = this.updateSearch.bind(this); - this.debouncedSearch = debounce(this.search.bind(this), 20); - } - - public componentWillUnmount() { - this.updateSearch(''); - } - - public getCurrentConversations(): - | Array - | undefined { - const { conversations } = this.props; - - let conversationList = conversations; - if (conversationList !== undefined) { - conversationList = conversationList.filter( - // a channel is either a public group or a rss group - conversation => conversation && conversation.type === 'group' - ); - } - - return conversationList; - } - - public renderRow = ({ - index, - key, - style, - }: RowRendererParamsType): JSX.Element => { - const { openConversationInternal } = this.props; - - const conversations = this.getCurrentConversations(); - - if (!conversations) { - throw new Error('renderRow: Tried to render without conversations'); - } - - const conversation = conversations[index]; - - return ( - - ); - }; - - public renderList(): JSX.Element | Array { - const { openConversationInternal, searchResults } = this.props; - - if (searchResults) { - return ( - - ); - } - - const conversations = this.getCurrentConversations(); - - if (!conversations) { - throw new Error( - 'render: must provided conversations if no search results are provided' - ); - } - - const length = conversations.length; - - // Note: conversations is not a known prop for List, but it is required to ensure that - // it re-renders when our conversation data changes. Otherwise it would just render - // on startup and scroll. - const list = ( -
- - {({ height, width }) => ( - - )} - -
- ); - - return [list]; - } - - public renderHeader(): JSX.Element { - const labels = [window.i18n('groups')]; - - return LeftPane.RENDER_HEADER(labels, null); - } - - public componentDidMount() { - MainViewController.renderMessageView(); - } - - public componentDidUpdate() { - MainViewController.renderMessageView(); - } - - public render(): JSX.Element { - return ( -
- {this.renderHeader()} - {this.state.groupAddType - ? this.renderClosableOverlay(this.state.groupAddType) - : this.renderGroups()} -
- ); - } - - public renderGroups() { - return ( -
- - {this.renderList()} - {this.renderBottomButtons()} -
- ); - } - - public updateSearch(searchTerm: string) { - const { updateSearchTerm, clearSearch } = this.props; - - if (!searchTerm) { - clearSearch(); - - return; - } - - this.setState({ channelUrlPasted: '' }); - - if (updateSearchTerm) { - updateSearchTerm(searchTerm); - } - - if (searchTerm.length < 2) { - return; - } - - const cleanedTerm = cleanSearchTerm(searchTerm); - if (!cleanedTerm) { - return; - } - - this.debouncedSearch(cleanedTerm); - } - - public clearSearch() { - this.props.clearSearch(); - } - - public search() { - const { search } = this.props; - const { searchTerm, isSecondaryDevice } = this.props; - - if (search) { - search(searchTerm, { - noteToSelf: window.i18n('noteToSelf').toLowerCase(), - ourNumber: window.textsecure.storage.user.getNumber(), - regionCode: '', - isSecondaryDevice, - }); - } - } - - private handleToggleOverlay(groupType?: SessionGroupType) { - // If no groupType, return to default view. - // Close the overlay with handleToggleOverlay(undefined) - - switch (groupType) { - case SessionGroupType.Open: - this.setState({ - groupAddType: SessionGroupType.Open, - }); - break; - case SessionGroupType.Closed: - this.setState({ - groupAddType: SessionGroupType.Closed, - }); - break; - default: - // Exit overlay - this.setState({ - groupAddType: undefined, - }); - } - } - - private renderClosableOverlay(groupType: SessionGroupType) { - const { searchTerm } = this.props; - const { loading } = this.state; - - const openGroupElement = ( - { - this.handleToggleOverlay(undefined); - }} - onButtonClick={this.handleJoinChannelButtonClick} - searchTerm={searchTerm} - updateSearch={this.updateSearchBound} - showSpinner={loading} - /> - ); - - const closedGroupElement = ( - { - this.handleToggleOverlay(undefined); - }} - onButtonClick={async ( - groupName: string, - groupMembers: Array - ) => this.onCreateClosedGroup(groupName, groupMembers)} - searchTerm={searchTerm} - updateSearch={this.updateSearchBound} - showSpinner={loading} - /> - ); - - return groupType === SessionGroupType.Open - ? openGroupElement - : closedGroupElement; - } - - private renderBottomButtons(): JSX.Element { - const edit = window.i18n('edit'); - const joinOpenGroup = window.i18n('joinOpenGroup'); - const createClosedGroup = window.i18n('createClosedGroup'); - const showEditButton = false; - - return ( -
- {showEditButton && ( - - )} - - { - this.handleToggleOverlay(SessionGroupType.Open); - }} - /> - { - this.handleToggleOverlay(SessionGroupType.Closed); - }} - /> -
- ); - } - - private handleOnPasteUrl(value: string) { - this.setState({ channelUrlPasted: value }); - } - - private handleJoinChannelButtonClick(groupUrl: string) { - const { loading } = this.state; - - if (loading) { - return false; - } - - // longest TLD is now (20/02/06) 24 characters per https://jasontucker.blog/8945/what-is-the-longest-tld-you-can-get-for-a-domain-name - const regexURL = /(http:\/\/|https:\/\/)?[a-z0-9]+([\-\.]{1}[a-z0-9]+)*\.[a-z]{2,24}(:[0-9]{1,5})?(\/.*)?/; - - if (groupUrl.length <= 0) { - window.pushToast({ - title: window.i18n('noServerURL'), - type: 'error', - id: 'connectToServerFail', - }); - - return false; - } - - if (!regexURL.test(groupUrl)) { - window.pushToast({ - title: window.i18n('noServerURL'), - type: 'error', - id: 'connectToServerFail', - }); - - return false; - } - - joinChannelStateManager(this, groupUrl, () => { - this.handleToggleOverlay(undefined); - }); - - return true; - } - - private async onCreateClosedGroup( - groupName: string, - groupMembers: Array - ) { - // Validate groupName and groupMembers length - if ( - groupName.length === 0 || - groupName.length > window.CONSTANTS.MAX_GROUP_NAME_LENGTH - ) { - window.pushToast({ - title: window.i18n( - 'invalidGroupName', - window.CONSTANTS.MAX_GROUP_NAME_LENGTH - ), - type: 'error', - id: 'invalidGroupName', - }); - - return; - } - - // >= because we add ourself as a member after this. so a 10 group is already invalid as it will be 11 with ourself - if ( - groupMembers.length === 0 || - groupMembers.length >= window.CONSTANTS.SMALL_GROUP_SIZE_LIMIT - ) { - window.pushToast({ - title: window.i18n( - 'invalidGroupSize', - window.CONSTANTS.SMALL_GROUP_SIZE_LIMIT - ), - type: 'error', - id: 'invalidGroupSize', - }); - - return; - } - - const groupMemberIds = groupMembers.map(m => m.id); - await window.doCreateGroup(groupName, groupMemberIds); - this.handleToggleOverlay(undefined); - - window.pushToast({ - title: window.i18n('closedGroupCreatedToastTitle'), - type: 'success', - }); - - return true; - } -} - -export function joinChannelStateManager( - thisRef: any, - serverURL: string, - onSuccess?: any -) { - // Any component that uses this function MUST have the keys [loading, connectSuccess] - // in their State - - // TODO: Make this not hard coded - const channelId = 1; - thisRef.setState({ loading: true }); - const connectionResult = window.attemptConnection(serverURL, channelId); - - // Give 5s maximum for promise to revole. Else, throw error. - const connectionTimeout = setTimeout(() => { - if (!thisRef.state.connectSuccess) { - thisRef.setState({ loading: false }); - window.pushToast({ - title: window.i18n('connectToServerFail'), - type: 'error', - id: 'connectToServerFail', - }); - - return; - } - }, window.CONSTANTS.MAX_CONNECTION_DURATION); - - connectionResult - .then(() => { - clearTimeout(connectionTimeout); - - if (thisRef.state.loading) { - thisRef.setState({ - connectSuccess: true, - loading: false, - }); - window.pushToast({ - title: window.i18n('connectToServerSuccess'), - id: 'connectToServerSuccess', - type: 'success', - }); - - if (onSuccess) { - onSuccess(); - } - } - }) - .catch((connectionError: string) => { - clearTimeout(connectionTimeout); - thisRef.setState({ - connectSuccess: true, - loading: false, - }); - window.pushToast({ - title: connectionError, - id: 'connectToServerFail', - type: 'error', - }); - - return false; - }); - - return true; -} diff --git a/ts/components/session/LeftPaneContactSection.tsx b/ts/components/session/LeftPaneContactSection.tsx index 75f5423f1..ec78352ba 100644 --- a/ts/components/session/LeftPaneContactSection.tsx +++ b/ts/components/session/LeftPaneContactSection.tsx @@ -17,7 +17,10 @@ import { import { AutoSizer, List } from 'react-virtualized'; import { validateNumber } from '../../types/PhoneNumber'; import { ConversationType } from '../../state/ducks/conversations'; -import { SessionClosableOverlay } from './SessionClosableOverlay'; +import { + SessionClosableOverlay, + SessionClosableOverlayType, +} from './SessionClosableOverlay'; import { MainViewController } from '../MainViewController'; export interface Props { @@ -89,6 +92,7 @@ export class LeftPaneContactSection extends React.Component { labels, this.handleTabSelected, undefined, + undefined, this.handleToggleFriendRequestPopup, receivedFriendRequestCount ); @@ -140,7 +144,8 @@ export class LeftPaneContactSection extends React.Component { style, }: RowRendererParamsType): JSX.Element | undefined => { const { sentFriendsRequest } = this.props; - const friends = window.getFriendsFromContacts(this.props.friends); + const contacts = this.props.friends.filter(f => f.type === 'direct'); + const friends = contacts.filter(c => c.isFriend); const combined = [...sentFriendsRequest, ...friends]; const item = combined[index]; @@ -203,7 +208,7 @@ export class LeftPaneContactSection extends React.Component { private renderClosableOverlay() { return ( { private renderList() { const { sentFriendsRequest } = this.props; - const friends = window.getFriendsFromContacts(this.props.friends); + const contacts = this.props.friends.filter(f => f.type === 'direct'); + const friends = contacts.filter(c => c.isFriend); const length = Number(sentFriendsRequest.length) + Number(friends.length); const combined = [...sentFriendsRequest, ...friends]; diff --git a/ts/components/session/LeftPaneMessageSection.tsx b/ts/components/session/LeftPaneMessageSection.tsx index 36601aa75..d1be7da0b 100644 --- a/ts/components/session/LeftPaneMessageSection.tsx +++ b/ts/components/session/LeftPaneMessageSection.tsx @@ -17,22 +17,23 @@ import { cleanSearchTerm } from '../../util/cleanSearchTerm'; import { SearchOptions } from '../../types/Search'; import { validateNumber } from '../../types/PhoneNumber'; import { LeftPane, RowRendererParamsType } from '../LeftPane'; -import { SessionClosableOverlay } from './SessionClosableOverlay'; -import { SessionIconButton, SessionIconSize, SessionIconType } from './icon'; +import { + SessionClosableOverlay, + SessionClosableOverlayType, +} from './SessionClosableOverlay'; +import { SessionIconType } from './icon'; +import { ContactType } from './SessionMemberListItem'; import { SessionButton, SessionButtonColor, SessionButtonType, } from './SessionButton'; -import { SessionSpinner } from './SessionSpinner'; -import { joinChannelStateManager } from './LeftPaneChannelSection'; export interface Props { searchTerm: string; isSecondaryDevice: boolean; conversations?: Array; - searchResults?: SearchResultsProps; updateSearchTerm: (searchTerm: string) => void; @@ -41,17 +42,40 @@ export interface Props { clearSearch: () => void; } -export class LeftPaneMessageSection extends React.Component { +export enum SessionComposeToType { + Message = 'message', + OpenGroup = 'open-group', + ClosedGroup = 'closed-group', +} + +export const SessionGroupType = { + OpenGroup: SessionComposeToType.OpenGroup, + ClosedGroup: SessionComposeToType.ClosedGroup, +}; +export type SessionGroupType = SessionComposeToType; + +interface State { + loading: boolean; + overlay: false | SessionComposeToType; + valuePasted: string; + connectSuccess: boolean; +} + +export class LeftPaneMessageSection extends React.Component { private readonly updateSearchBound: (searchedString: string) => void; private readonly debouncedSearch: (searchTerm: string) => void; public constructor(props: Props) { super(props); + this.state = { + loading: false, + overlay: false, + valuePasted: '', + connectSuccess: false, + }; + const conversations = this.getCurrentConversations(); - const renderOnboardingSetting = window.getSettingValue( - 'render-message-onboarding' - ); const realConversations: Array = []; if (conversations) { @@ -64,23 +88,21 @@ export class LeftPaneMessageSection extends React.Component { }); } - const length = realConversations.length; - - this.state = { - showComposeView: false, - pubKeyPasted: '', - shouldRenderMessageOnboarding: - length === 0 && renderOnboardingSetting && false, - connectSuccess: false, - loading: false, - }; - this.updateSearchBound = this.updateSearch.bind(this); + + this.handleOnPaste = this.handleOnPaste.bind(this); this.handleToggleOverlay = this.handleToggleOverlay.bind(this); - this.handleCloseOnboarding = this.handleCloseOnboarding.bind(this); - this.handleJoinPublicChat = this.handleJoinPublicChat.bind(this); - this.handleOnPasteSessionID = this.handleOnPasteSessionID.bind(this); this.handleMessageButtonClick = this.handleMessageButtonClick.bind(this); + + this.handleNewSessionButtonClick = this.handleNewSessionButtonClick.bind( + this + ); + this.handleJoinChannelButtonClick = this.handleJoinChannelButtonClick.bind( + this + ); + this.onCreateClosedGroup = this.onCreateClosedGroup.bind(this); + + this.renderClosableOverlay = this.renderClosableOverlay.bind(this); this.debouncedSearch = debounce(this.search.bind(this), 20); } @@ -197,17 +219,20 @@ export class LeftPaneMessageSection extends React.Component { return LeftPane.RENDER_HEADER( labels, null, - window.i18n('newSession'), - this.handleToggleOverlay + undefined, + SessionIconType.Plus, + this.handleNewSessionButtonClick ); } public render(): JSX.Element { + const { overlay } = this.state; + return (
{this.renderHeader()} - {this.state.showComposeView - ? this.renderClosableOverlay() + {overlay + ? this.renderClosableOverlay(overlay) : this.renderConversations()}
); @@ -216,90 +241,17 @@ export class LeftPaneMessageSection extends React.Component { public renderConversations() { return (
- {this.state.shouldRenderMessageOnboarding ? ( - <>{this.renderMessageOnboarding()} - ) : ( - <> - - {this.renderList()} - - )} -
- ); - } - - public renderMessageOnboarding() { - return ( -
-
- -
- -
-
-

{window.i18n('welcomeToSession')}

-
- -
- -
- -
-
- {window.i18n('noMessagesTitle')} -
-
- {window.i18n('noMessagesSubtitle')} -
-
- - <> - {this.state.loading ? ( -
- -
- ) : ( -
- - -
- )} - -
+ + {this.renderList()} + {this.renderBottomButtons()}
); } - public handleCloseOnboarding() { - window.setSettingValue('render-message-onboarding', false); - - this.setState({ - shouldRenderMessageOnboarding: false, - }); - } - public updateSearch(searchTerm: string) { const { updateSearchTerm, clearSearch } = this.props; @@ -308,8 +260,9 @@ export class LeftPaneMessageSection extends React.Component { return; } + // reset our pubKeyPasted, we can either have a pasted sessionID or a sessionID got from a search - this.setState({ pubKeyPasted: '' }); + this.setState({ valuePasted: '' }); if (updateSearchTerm) { updateSearchTerm(searchTerm); @@ -345,41 +298,125 @@ export class LeftPaneMessageSection extends React.Component { } } - private renderClosableOverlay() { + private renderClosableOverlay(overlay: SessionComposeToType) { const { searchTerm, searchResults } = this.props; + const { loading } = this.state; - return ( + const openGroupElement = ( + { + this.handleToggleOverlay(undefined); + }} + onButtonClick={this.handleJoinChannelButtonClick} + searchTerm={searchTerm} + updateSearch={this.updateSearchBound} + showSpinner={loading} + /> + ); + + const closedGroupElement = ( + { + this.handleToggleOverlay(undefined); + }} + onButtonClick={async ( + groupName: string, + groupMembers: Array + ) => this.onCreateClosedGroup(groupName, groupMembers)} + searchTerm={searchTerm} + updateSearch={this.updateSearchBound} + showSpinner={loading} + /> + ); + + const messageElement = ( { + this.handleToggleOverlay(undefined); + }} onButtonClick={this.handleMessageButtonClick} searchTerm={searchTerm} searchResults={searchResults} updateSearch={this.updateSearchBound} /> ); + + let overlayElement; + switch (overlay) { + case SessionComposeToType.OpenGroup: + overlayElement = openGroupElement; + break; + case SessionComposeToType.ClosedGroup: + overlayElement = closedGroupElement; + break; + default: + overlayElement = messageElement; + } + + return overlayElement; } - private handleToggleOverlay() { - this.setState((state: any) => { - return { showComposeView: !state.showComposeView }; - }); - // empty our generalized searchedString (one for the whole app) - this.updateSearch(''); + private renderBottomButtons(): JSX.Element { + const edit = window.i18n('edit'); + const joinOpenGroup = window.i18n('joinOpenGroup'); + const createClosedGroup = window.i18n('createClosedGroup'); + const showEditButton = false; + + return ( +
+ {showEditButton && ( + + )} + + { + this.handleToggleOverlay(SessionComposeToType.OpenGroup); + }} + /> + { + this.handleToggleOverlay(SessionComposeToType.ClosedGroup); + }} + /> +
+ ); } - private handleOnPasteSessionID(value: string) { - // reset our search, we can either have a pasted sessionID or a sessionID got from a search + private handleToggleOverlay(conversationType?: SessionComposeToType) { + const { overlay } = this.state; + + const overlayState = overlay ? false : conversationType || false; + + this.setState({ overlay: overlayState }); + + // empty our generalized searchedString (one for the whole app) this.updateSearch(''); + } - this.setState({ pubKeyPasted: value }); + private handleOnPaste(value: string) { + this.setState({ valuePasted: value }); } private handleMessageButtonClick() { const { openConversationInternal } = this.props; - if (!this.state.pubKeyPasted && !this.props.searchTerm) { + if (!this.state.valuePasted && !this.props.searchTerm) { window.pushToast({ title: window.i18n('invalidNumberError'), type: 'error', @@ -389,7 +426,7 @@ export class LeftPaneMessageSection extends React.Component { return; } let pubkey: string; - pubkey = this.state.pubKeyPasted || this.props.searchTerm; + pubkey = this.state.valuePasted || this.props.searchTerm; pubkey = pubkey.trim(); const error = validateNumber(pubkey); @@ -404,8 +441,58 @@ export class LeftPaneMessageSection extends React.Component { } } - private handleJoinPublicChat() { - const serverURL = window.CONSTANTS.DEFAULT_PUBLIC_CHAT_URL; - joinChannelStateManager(this, serverURL, this.handleCloseOnboarding); + private handleJoinChannelButtonClick(groupUrl: string) { + const { loading } = this.state; + + if (loading) { + return false; + } + + // longest TLD is now (20/02/06) 24 characters per https://jasontucker.blog/8945/what-is-the-longest-tld-you-can-get-for-a-domain-name + const regexURL = /(http:\/\/|https:\/\/)?[a-z0-9]+([\-\.]{1}[a-z0-9]+)*\.[a-z]{2,24}(:[0-9]{1,5})?(\/.*)?/; + + if (groupUrl.length <= 0) { + window.pushToast({ + title: window.i18n('noServerURL'), + type: 'error', + id: 'connectToServerFail', + }); + + return false; + } + + if (!regexURL.test(groupUrl)) { + window.pushToast({ + title: window.i18n('noServerURL'), + type: 'error', + id: 'connectToServerFail', + }); + + return false; + } + + MainViewController.joinChannelStateManager(this, groupUrl, () => { + this.handleToggleOverlay(undefined); + }); + + return true; + } + + private async onCreateClosedGroup( + groupName: string, + groupMembers: Array + ) { + await MainViewController.createClosedGroup(groupName, groupMembers, () => { + this.handleToggleOverlay(undefined); + + window.pushToast({ + title: window.i18n('closedGroupCreatedToastTitle'), + type: 'success', + }); + }); + } + + private handleNewSessionButtonClick() { + this.handleToggleOverlay(SessionComposeToType.Message); } } diff --git a/ts/components/session/LeftPaneSectionHeader.tsx b/ts/components/session/LeftPaneSectionHeader.tsx index 6a8d60bd6..c9ed53aeb 100644 --- a/ts/components/session/LeftPaneSectionHeader.tsx +++ b/ts/components/session/LeftPaneSectionHeader.tsx @@ -1,6 +1,7 @@ import React from 'react'; import classNames from 'classnames'; import { SessionButton } from './SessionButton'; +import { SessionIcon, SessionIconSize, SessionIconType } from './icon'; import { NotificationCountSize, SessionNotificationCount, @@ -43,6 +44,7 @@ interface Props { labels: Array; notificationCount?: number; buttonLabel?: string; + buttonIcon?: SessionIconType; buttonClicked?: any; } @@ -65,10 +67,13 @@ export class LeftPaneSectionHeader extends React.Component { const { labels, buttonLabel, + buttonIcon, buttonClicked, notificationCount, } = this.props; + const hasButton = buttonLabel || buttonIcon; + const children = []; //loop to create children for (let i = 0; i < labels.length; i++) { @@ -83,15 +88,19 @@ export class LeftPaneSectionHeader extends React.Component { ); } - if (buttonLabel && !notificationCount) { - children.push( - + if (hasButton && !notificationCount) { + const buttonContent = buttonIcon ? ( + + ) : ( + buttonLabel ); + const button = ( + + {buttonContent} + + ); + + children.push(button); } else if (buttonLabel && notificationCount && notificationCount > 0) { children.push(
@@ -118,7 +127,7 @@ export class LeftPaneSectionHeader extends React.Component { ); } - //Create the parent and add the children + // Create the parent and add the children return
{children}
; } diff --git a/ts/components/session/LeftPaneSettingSection.tsx b/ts/components/session/LeftPaneSettingSection.tsx index eec1bae01..7d729e498 100644 --- a/ts/components/session/LeftPaneSettingSection.tsx +++ b/ts/components/session/LeftPaneSettingSection.tsx @@ -62,6 +62,7 @@ export class LeftPaneSettingSection extends React.Component { null, undefined, undefined, + undefined, undefined ); } diff --git a/ts/components/session/SessionClosableOverlay.tsx b/ts/components/session/SessionClosableOverlay.tsx index 6a8b34a77..d748c72ee 100644 --- a/ts/components/session/SessionClosableOverlay.tsx +++ b/ts/components/session/SessionClosableOverlay.tsx @@ -11,11 +11,17 @@ import { SessionButtonType, } from './SessionButton'; import { SessionSpinner } from './SessionSpinner'; -import { SessionGroupType } from './LeftPaneChannelSection'; import { PillDivider } from './PillDivider'; +export enum SessionClosableOverlayType { + Contact = 'contact', + Message = 'message', + OpenGroup = 'open-group', + ClosedGroup = 'closed-group', +} + interface Props { - overlayMode: 'message' | 'contact' | SessionGroupType; + overlayMode: SessionClosableOverlayType; onChangeSessionID: any; onCloseClick: any; onButtonClick: any; @@ -43,7 +49,10 @@ export class SessionClosableOverlay extends React.Component { }; this.inputRef = React.createRef(); + this.onKeyUp = this.onKeyUp.bind(this); this.onGroupNameChanged = this.onGroupNameChanged.bind(this); + + window.addEventListener('keyup', this.onKeyUp); } public componentDidMount() { @@ -97,11 +106,12 @@ export class SessionClosableOverlay extends React.Component { onButtonClick, } = this.props; - const isAddContactView = overlayMode === 'contact'; - const isMessageView = overlayMode === 'message'; - - const isOpenGroupView = overlayMode === SessionGroupType.Open; - const isClosedGroupView = overlayMode === SessionGroupType.Closed; + const isAddContactView = overlayMode === SessionClosableOverlayType.Contact; + const isMessageView = overlayMode === SessionClosableOverlayType.Message; + const isOpenGroupView = + overlayMode === SessionClosableOverlayType.OpenGroup; + const isClosedGroupView = + overlayMode === SessionClosableOverlayType.ClosedGroup; let title; let buttonText; @@ -144,8 +154,10 @@ export class SessionClosableOverlay extends React.Component { const ourSessionID = window.textsecure.storage.user.getNumber(); const contacts = this.getContacts(); + const noContactsForClosedGroup = - overlayMode === SessionGroupType.Closed && contacts.length === 0; + overlayMode === SessionClosableOverlayType.ClosedGroup && + contacts.length === 0; return (
@@ -193,7 +205,6 @@ export class SessionClosableOverlay extends React.Component { {isClosedGroupView && ( <>
-
{noContactsForClosedGroup ? (
@@ -201,7 +212,7 @@ export class SessionClosableOverlay extends React.Component {
) : (
- {this.renderMemberList()} + {this.renderMemberList(contacts)}
)}
@@ -245,9 +256,7 @@ export class SessionClosableOverlay extends React.Component { ); } - private renderMemberList() { - const members = this.getContacts(); - + private renderMemberList(members: any) { return members.map((member: ContactType) => ( { groupName: event, }); } + + private onKeyUp(event: any) { + if (event.key === 'Escape') { + window.removeEventListener('keyup', this.onKeyUp); + this.props.onCloseClick(); + } + } } diff --git a/ts/components/session/SessionIdEditable.tsx b/ts/components/session/SessionIdEditable.tsx index 6fca9f63e..e2d43024c 100644 --- a/ts/components/session/SessionIdEditable.tsx +++ b/ts/components/session/SessionIdEditable.tsx @@ -55,6 +55,7 @@ export class SessionIdEditable extends React.PureComponent { private handleChange(e: any) { const { editable, onChange } = this.props; + if (editable) { onChange(e.target.value); } diff --git a/ts/components/session/icon/Icons.tsx b/ts/components/session/icon/Icons.tsx index f5d447890..0a95571db 100644 --- a/ts/components/session/icon/Icons.tsx +++ b/ts/components/session/icon/Icons.tsx @@ -23,6 +23,7 @@ export enum SessionIconType { Microphone = 'microphone', Moon = 'moon', Pencil = 'pencil', + Plus = 'plus', Reply = 'reply', Search = 'search', Send = 'send', @@ -160,6 +161,11 @@ export const icons = { 'M4,16.4142136 L4,20 L7.58578644,20 L19.5857864,8 L16,4.41421356 L4,16.4142136 Z M16.7071068,2.29289322 L21.7071068,7.29289322 C22.0976311,7.68341751 22.0976311,8.31658249 21.7071068,8.70710678 L8.70710678,21.7071068 C8.5195704,21.8946432 8.26521649,22 8,22 L3,22 C2.44771525,22 2,21.5522847 2,21 L2,16 C2,15.7347835 2.10535684,15.4804296 2.29289322,15.2928932 L15.2928932,2.29289322 C15.6834175,1.90236893 16.3165825,1.90236893 16.7071068,2.29289322 Z', viewBox: '1 1 21 21', }, + [SessionIconType.Plus]: { + path: + 'm405.332031 192h-170.664062v-170.667969c0-11.773437-9.558594-21.332031-21.335938-21.332031-11.773437 0-21.332031 9.558594-21.332031 21.332031v170.667969h-170.667969c-11.773437 0-21.332031 9.558594-21.332031 21.332031 0 11.777344 9.558594 21.335938 21.332031 21.335938h170.667969v170.664062c0 11.777344 9.558594 21.335938 21.332031 21.335938 11.777344 0 21.335938-9.558594 21.335938-21.335938v-170.664062h170.664062c11.777344 0 21.335938-9.558594 21.335938-21.335938 0-11.773437-9.558594-21.332031-21.335938-21.332031zm0 0', + viewBox: '0 0 427 427', + }, [SessionIconType.Reply]: { path: 'M4,3 C4.55228475,3 5,3.44771525 5,4 L5,4 L5,11 C5,12.6568542 6.34314575,14 8,14 L8,14 L17.585,14 L14.2928932,10.7071068 C13.9324093,10.3466228 13.9046797,9.77939176 14.2097046,9.38710056 L14.2928932,9.29289322 C14.6834175,8.90236893 15.3165825,8.90236893 15.7071068,9.29289322 L15.7071068,9.29289322 L20.7071068,14.2928932 C20.7355731,14.3213595 20.7623312,14.3515341 20.787214,14.3832499 C20.788658,14.3849951 20.7902348,14.3870172 20.7918027,14.389044 C20.8140715,14.4179625 20.8348358,14.4480862 20.8539326,14.4793398 C20.8613931,14.4913869 20.8685012,14.5036056 20.8753288,14.5159379 C20.8862061,14.5357061 20.8966234,14.5561086 20.9063462,14.5769009 C20.914321,14.5939015 20.9218036,14.6112044 20.9287745,14.628664 C20.9366843,14.6484208 20.9438775,14.6682023 20.9504533,14.6882636 C20.9552713,14.7031487 20.9599023,14.7185367 20.9641549,14.734007 C20.9701664,14.7555635 20.9753602,14.7772539 20.9798348,14.7992059 C20.9832978,14.8166247 20.9863719,14.834051 20.9889822,14.8515331 C20.9962388,14.8996379 21,14.9493797 21,15 L20.9962979,14.9137692 C20.9978436,14.9317345 20.9989053,14.9497336 20.9994829,14.9677454 L21,15 C21,15.0112225 20.9998151,15.0224019 20.9994483,15.0335352 C20.9988772,15.050591 20.997855,15.0679231 20.996384,15.0852242 C20.994564,15.1070574 20.9920941,15.1281144 20.9889807,15.1489612 C20.9863719,15.165949 20.9832978,15.1833753 20.9797599,15.2007258 C20.9753602,15.2227461 20.9701664,15.2444365 20.964279,15.2658396 C20.9599023,15.2814633 20.9552713,15.2968513 20.9502619,15.3121425 C20.9438775,15.3317977 20.9366843,15.3515792 20.928896,15.3710585 C20.9218036,15.3887956 20.914321,15.4060985 20.9063266,15.4232215 C20.8974314,15.4421635 20.8879327,15.4609002 20.8778732,15.4792864 C20.8703855,15.4931447 20.862375,15.5070057 20.8540045,15.5207088 C20.8382813,15.546275 20.8215099,15.5711307 20.8036865,15.5951593 C20.774687,15.6343256 20.7425008,15.6717127 20.7071068,15.7071068 L20.787214,15.6167501 C20.7849289,15.6196628 20.7826279,15.6225624 20.7803112,15.625449 L20.7071068,15.7071068 L15.7071068,20.7071068 C15.3165825,21.0976311 14.6834175,21.0976311 14.2928932,20.7071068 C13.9023689,20.3165825 13.9023689,19.6834175 14.2928932,19.2928932 L14.2928932,19.2928932 L17.585,16 L8,16 C5.3112453,16 3.11818189,13.8776933 3.00461951,11.2168896 L3,11 L3,4 C3,3.44771525 3.44771525,3 4,3',