Skip to content

Commit

Permalink
feat: redesign join conversation page (#15584)
Browse files Browse the repository at this point in the history
* feat: redesign join conversation page

* chore: issue with login columns, removing OR text, adding translations

* feat: add already logged in page on conversation join

* chore: separate child components out for readability

* runfix: some errors with styling, incorrect import

---------

Co-authored-by: Timothy Le Bon <tlebon@gmail.com>
  • Loading branch information
aweiss-dev and tlebon authored Aug 21, 2023
1 parent fdde1e0 commit 4b69628
Show file tree
Hide file tree
Showing 6 changed files with 357 additions and 175 deletions.
12 changes: 9 additions & 3 deletions src/i18n/en-US.json
Original file line number Diff line number Diff line change
Expand Up @@ -372,14 +372,17 @@
"conversationGuestIndicator": "Guest",
"conversationImageAssetRestricted": "Receiving images is prohibited",
"conversationJoin.buttonExistentAccountOpen": "Open in {brandName}",
"conversationJoin.existentAccountHeadline": "{name}, you have been invited to join a conversation in {brandName}",
"conversationJoin.join": "Join",
"conversationJoin.joinWithOtherAccount": "Join with another account",
"conversationJoin.existentAccountJoinWithoutLink": "Join the conversation",
"conversationJoin.existentAccountJoinInBrowser": "Join in the browser",
"conversationJoin.existentAccountUserName": "You are logged in as {selfName}",
"conversationJoin.existentAccountJoinWithoutText": "{existentAccountJoinWithoutLink} without an account",
"conversationJoin.existentAccountSubhead": "Audio and video conferencing, text messaging, file sharing, and screen sharing, all end-to-end-encrypted.",
"conversationJoin.fullConversationHeadline": "Unable to join conversation",
"conversationJoin.fullConversationSubhead": "The maximum number of participants in this conversation has been reached.",
"conversationJoin.hasAccount": "Already have an account?",
"conversationJoin.headline": "You have been invited to join a conversation in {brandName}",
"conversationJoin.headline": "The conversation is hosted on {brandName}",
"conversationJoin.invalidCreateAccountLink": "Create an account",
"conversationJoin.invalidCreateAccountText": "for group messaging and conference calls.",
"conversationJoin.invalidHeadline": "Conversation not found",
Expand All @@ -389,6 +392,9 @@
"conversationJoin.subhead": "Join conversation as temporary guest (access expires after 24 hours)",
"conversationJoin.unsupportedJoinHeadline": "You have been invited to join a conversation in {brandName}",
"conversationJoin.unsupportedJoinMobileSubhead": "Open this link on your computer.",
"conversationJoin.joinButton": "Join as temporary user",
"conversationJoin.mainHeadline": "Join Conversation",
"conversationJoin.noAccountHead": "Dont have an account?",
"conversationJustNow": "Just now",
"conversationLabelFavorites": "Favorites",
"conversationLabelGroups": "Groups",
Expand Down Expand Up @@ -1367,4 +1373,4 @@
"wireMacos": "{{brandName}} for macOS",
"wireWindows": "{{brandName}} for Windows",
"wire_for_web": "{{brandName}} for Web"
}
}
202 changes: 60 additions & 142 deletions src/script/auth/page/ConversationJoin.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,48 +21,35 @@ import React, {useEffect, useState} from 'react';

import type {RegisterData} from '@wireapp/api-client/lib/auth';
import {BackendErrorLabel} from '@wireapp/api-client/lib/http';
import {FormattedMessage, useIntl} from 'react-intl';
import {useIntl} from 'react-intl';
import {connect} from 'react-redux';
import {Navigate} from 'react-router-dom';
import {AnyAction, Dispatch} from 'redux';

import {UrlUtil} from '@wireapp/commons';
import {
ArrowIcon,
Button,
ContainerXS,
Form,
H2,
Input,
InputBlock,
InputSubmitCombo,
Link,
RoundIconButton,
Small,
Text,
} from '@wireapp/react-ui-kit';
import {Column, Columns, H1, Muted} from '@wireapp/react-ui-kit';

import {noop} from 'Util/util';

import {GuestLoginColumn, IsLoggedInColumn, Separator} from './ConversationJoinComponents';
import {ConversationJoinFull, ConversationJoinInvalid} from './ConversationJoinInvalid';
import {EntropyContainer} from './EntropyContainer';
import {Login} from './Login';
import {Page} from './Page';

import {Config} from '../../Config';
import {conversationJoinStrings} from '../../strings';
import {AppAlreadyOpen} from '../component/AppAlreadyOpen';
import {RouterLink} from '../component/RouterLink';
import {UnsupportedBrowser} from '../component/UnsupportedBrowser';
import {WirelessContainer} from '../component/WirelessContainer';
import {EXTERNAL_ROUTE} from '../externalRoute';
import {actionRoot as ROOT_ACTIONS} from '../module/action/';
import {actionRoot as ROOT_ACTIONS} from '../module/action';
import {ValidationError} from '../module/action/ValidationError';
import {bindActionCreators, RootState} from '../module/reducer';
import * as AuthSelector from '../module/selector/AuthSelector';
import * as ConversationSelector from '../module/selector/ConversationSelector';
import * as SelfSelector from '../module/selector/SelfSelector';
import {QUERY_KEY, ROUTE} from '../route';
import {QUERY_KEY} from '../route';
import * as AccentColor from '../util/AccentColor';
import {parseError, parseValidationErrors} from '../util/errorUtil';
import * as StringUtil from '../util/stringUtil';

type Props = React.HTMLProps<HTMLDivElement>;

Expand All @@ -73,21 +60,19 @@ const ConversationJoinComponent = ({
doRegisterWireless,
setLastEventDate,
doLogout,
isAuthenticated,
isTemporaryGuest,
selfName,
conversationError,
}: Props & ConnectedProps & DispatchProps) => {
const nameInput = React.useRef<HTMLInputElement>();
const nameInput = React.useRef<HTMLInputElement>(null);
const {formatMessage: _} = useIntl();

const [accentColor] = useState(AccentColor.random());
const [isLoggedIn, setIsLoggedIn] = useState(selfName !== null);
const [accentColor] = useState(AccentColor.STRONG_BLUE);
const [conversationCode, setConversationCode] = useState<string>();
const [conversationKey, setConversationKey] = useState<string>();
const [enteredName, setEnteredName] = useState<string>('');
const [error, setError] = useState<any>();
const [expiresIn, setExpiresIn] = useState<number>();
const [forceNewTemporaryGuestAccount, setForceNewTemporaryGuestAccount] = useState(false);
const [isValidLink, setIsValidLink] = useState(true);
const [isValidName, setIsValidName] = useState(true);
const [isSubmitingName, setIsSubmitingName] = useState(false);
Expand Down Expand Up @@ -201,132 +186,65 @@ const ConversationJoinComponent = ({
setEnteredName(event.target.value);
};

const isFullConversation =
conversationError && conversationError.label && conversationError.label === BackendErrorLabel.TOO_MANY_MEMBERS;
const renderTemporaryGuestAccountCreation = !isAuthenticated || isTemporaryGuest || forceNewTemporaryGuestAccount;
const handleLogout = async () => {
setIsLoggedIn(false);
await doLogout();
};

if (!isValidLink) {
return <Navigate to={ROUTE.CONVERSATION_JOIN_INVALID} replace />;
return <ConversationJoinInvalid />;
}

const isFullConversation =
conversationError && conversationError.label && conversationError.label === BackendErrorLabel.TOO_MANY_MEMBERS;
if (isFullConversation) {
return <ConversationJoinFull />;
}

return (
<UnsupportedBrowser isTemporaryGuest>
<WirelessContainer
showCookiePolicyBanner={showCookiePolicyBanner}
onCookiePolicyBannerClose={() => setShowCookiePolicyBanner(false)}
>
{isEntropyRequired && showEntropyForm ? (
<EntropyContainer onSetEntropy={handleSubmit} />
) : isFullConversation ? (
<ContainerXS style={{margin: 'auto 0'}}>
<H2 style={{fontWeight: 500, marginBottom: '10px', marginTop: '0'}} data-uie-name="status-full-headline">
<FormattedMessage {...conversationJoinStrings.fullConversationHeadline} />
</H2>
<Text style={{fontSize: '1rem', marginTop: '10px'}} data-uie-name="status-full-text">
{_(conversationJoinStrings.fullConversationSubhead)}
</Text>
</ContainerXS>
) : renderTemporaryGuestAccountCreation ? (
<div>
<ContainerXS style={{margin: 'auto 0'}}>
<AppAlreadyOpen />
<H2 style={{fontWeight: 500, marginBottom: '10px', marginTop: '0'}}>
<FormattedMessage
{...conversationJoinStrings.headline}
values={{
brandName: Config.getConfig().BRAND_NAME,
}}
<AppAlreadyOpen />
<div style={{display: 'flex', alignItems: 'center', flexDirection: 'column', marginBottom: '2rem'}}>
<H1 style={{fontWeight: 500, marginTop: '0', marginBottom: '1rem'}} data-uie-name="status-join-headline">
{_(conversationJoinStrings.mainHeadline)}
</H1>
<Muted data-uie-name="status-join-subhead">
{_(conversationJoinStrings.headline, {brandName: Config.getConfig().BRAND_NAME})}
</Muted>
</div>
<Columns style={{display: 'flex', gap: '2rem', alignSelf: 'center', maxWidth: '100%'}}>
<Column>
{isLoggedIn ? (
<IsLoggedInColumn selfName={selfName} handleLogout={handleLogout} handleSubmit={handleSubmit} />
) : (
<Login embedded />
)}
</Column>
<Separator />
<Column>
<Page>
{isEntropyRequired && showEntropyForm ? (
<EntropyContainer onSetEntropy={handleSubmit} />
) : (
<GuestLoginColumn
enteredName={enteredName}
nameInput={nameInput}
onNameChange={onNameChange}
checkNameValidity={checkNameValidity}
handleSubmit={handleSubmit}
isSubmitingName={isSubmitingName}
isValidName={isValidName}
conversationError={conversationError}
error={error}
/>
</H2>
<Text style={{fontSize: '1rem', marginTop: '10px'}}>
<FormattedMessage {...conversationJoinStrings.subhead} />
</Text>
<Form style={{marginTop: 30}}>
<InputBlock>
<InputSubmitCombo>
<Input
id="enter-name"
name="name"
autoComplete="username"
value={enteredName}
ref={nameInput}
onChange={onNameChange}
placeholder={_(conversationJoinStrings.namePlaceholder)}
maxLength={64}
minLength={2}
pattern=".{2,64}"
required
data-uie-name="enter-name"
/>
<RoundIconButton
disabled={!enteredName || !isValidName || isSubmitingName}
type="submit"
formNoValidate
onClick={checkNameValidity}
data-uie-name="do-next"
>
<ArrowIcon />
</RoundIconButton>
</InputSubmitCombo>
</InputBlock>
{error ? parseValidationErrors(error) : parseError(conversationError)}
</Form>
<Small block>
{`${_(conversationJoinStrings.hasAccount)} `}
<RouterLink
to={`${ROUTE.LOGIN}/${conversationKey}/${conversationCode}`}
textTransform={'none'}
data-uie-name="go-login"
>
{_(conversationJoinStrings.loginLink)}
</RouterLink>
</Small>
</ContainerXS>
</div>
) : (
<ContainerXS style={{margin: 'auto 0'}}>
<AppAlreadyOpen />
<H2 style={{fontWeight: 500, marginBottom: '10px', marginTop: '0'}} data-uie-name="status-join-headline">
{selfName
? _(conversationJoinStrings.existentAccountHeadline, {
brandName: Config.getConfig().BRAND_NAME,
name: StringUtil.capitalize(selfName),
})
: _(conversationJoinStrings.headline, {brandName: Config.getConfig().BRAND_NAME})}
</H2>
<Text block style={{fontSize: '1rem', marginTop: '10px'}}>
{_(conversationJoinStrings.existentAccountSubhead)}
</Text>
<Button
type="button"
style={{marginTop: 16}}
onClick={async () => {
try {
const conversationEvent = await doJoinConversationByCode(conversationKey, conversationCode);
routeToApp(conversationEvent.conversation, conversationEvent.qualified_conversation?.domain ?? '');
} catch (error) {
console.warn('Unable to join conversation with existing account', error);
}
}}
data-uie-name="do-open"
>
{_(conversationJoinStrings.existentAccountOpenButton, {brandName: Config.getConfig().BRAND_NAME})}
</Button>
{error ? parseValidationErrors(error) : parseError(conversationError)}
<Small block>
{_(conversationJoinStrings.existentAccountJoinWithoutText, {
existentAccountJoinWithoutLink: (
<Link
onClick={() => setForceNewTemporaryGuestAccount(true)}
textTransform={'none'}
data-uie-name="go-join"
>
{_(conversationJoinStrings.existentAccountJoinWithoutLink)}
</Link>
),
})}
</Small>
</ContainerXS>
)}
)}
</Page>
</Column>
</Columns>
</WirelessContainer>
</UnsupportedBrowser>
);
Expand Down
Loading

0 comments on commit 4b69628

Please sign in to comment.