From 5289d4c2aaf6935c0babcfd7f6618e5715f1f627 Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Thu, 29 Apr 2021 10:43:14 +1000 Subject: [PATCH] add joinable rooms on opengroupv2 joining screen --- ts/components/Lightbox.tsx | 2 +- ts/components/{session => basic}/Flex.tsx | 0 ts/components/basic/PillContainer.tsx | 29 ++++++++ ts/components/{ => basic}/Spinner.tsx | 0 ts/components/basic/Text.tsx | 23 ++++++ ts/components/conversation/Image.tsx | 2 +- ts/components/conversation/Message.tsx | 2 +- .../conversation/ModeratorsAddDialog.tsx | 2 +- .../conversation/ModeratorsRemoveDialog.tsx | 2 +- .../conversation/message/MessageMetadata.tsx | 2 +- ts/components/session/ActionsPanel.tsx | 6 +- .../session/SessionClosableOverlay.tsx | 2 + .../session/SessionJoinableDefaultRooms.tsx | 70 +++++++++++++++++++ ts/components/session/SessionRadio.tsx | 2 +- ts/components/session/SessionToast.tsx | 2 +- .../conversation/SessionCompositionBox.tsx | 2 +- .../conversation/SessionFileDropzone.tsx | 2 +- .../SessionQuotedMessageComposition.tsx | 2 +- .../session/registration/SignInTab.tsx | 2 +- ts/opengroup/opengroupV2/ApiUtil.ts | 62 ++++++++++++++++ ts/opengroup/opengroupV2/JoinOpenGroupV2.ts | 17 ++--- ts/opengroup/utils/OpenGroupUtils.ts | 10 ++- ts/session/utils/Toast.tsx | 12 ++-- ts/state/ducks/defaultRooms.tsx | 24 +++++++ ts/state/reducer.ts | 3 + 25 files changed, 253 insertions(+), 29 deletions(-) rename ts/components/{session => basic}/Flex.tsx (100%) create mode 100644 ts/components/basic/PillContainer.tsx rename ts/components/{ => basic}/Spinner.tsx (100%) create mode 100644 ts/components/basic/Text.tsx create mode 100644 ts/components/session/SessionJoinableDefaultRooms.tsx create mode 100644 ts/state/ducks/defaultRooms.tsx diff --git a/ts/components/Lightbox.tsx b/ts/components/Lightbox.tsx index 961626ae4..872601435 100644 --- a/ts/components/Lightbox.tsx +++ b/ts/components/Lightbox.tsx @@ -7,7 +7,7 @@ import is from '@sindresorhus/is'; import * as GoogleChrome from '../util/GoogleChrome'; import * as MIME from '../types/MIME'; import { SessionIconButton, SessionIconSize, SessionIconType } from './session/icon'; -import { Flex } from './session/Flex'; +import { Flex } from './basic/Flex'; import { DefaultTheme } from 'styled-components'; // useCss has some issues on our setup. so import it directly // tslint:disable-next-line: no-submodule-imports diff --git a/ts/components/session/Flex.tsx b/ts/components/basic/Flex.tsx similarity index 100% rename from ts/components/session/Flex.tsx rename to ts/components/basic/Flex.tsx diff --git a/ts/components/basic/PillContainer.tsx b/ts/components/basic/PillContainer.tsx new file mode 100644 index 000000000..9a9d4ab69 --- /dev/null +++ b/ts/components/basic/PillContainer.tsx @@ -0,0 +1,29 @@ +import React from 'react'; +import styled from 'styled-components'; + +type PillContainerProps = { + children: React.ReactNode; + margin?: string; + padding?: string; + onClick?: () => void; +}; + +const StyledPillContainer = styled.div` + display: flex; + background: none; + flex-direction: 'row'; + flex-grow: 1; + align-items: center; + padding: ${props => props.padding || ''}; + margin: ${props => props.margin || ''}; + border-radius: 300px; + border: 1px solid ${props => props.theme.colors.pillDividerColor}; + transition: ${props => props.theme.common.animations.defaultDuration}; + &:hover { + background: ${props => props.theme.colors.clickableHovered}; + } +`; + +export const PillContainer = (props: PillContainerProps) => { + return {props.children}; +}; diff --git a/ts/components/Spinner.tsx b/ts/components/basic/Spinner.tsx similarity index 100% rename from ts/components/Spinner.tsx rename to ts/components/basic/Spinner.tsx diff --git a/ts/components/basic/Text.tsx b/ts/components/basic/Text.tsx new file mode 100644 index 000000000..6cb4f1525 --- /dev/null +++ b/ts/components/basic/Text.tsx @@ -0,0 +1,23 @@ +import React from 'react'; +import styled from 'styled-components'; + +type TextProps = { + text: string; + subtle?: boolean; + opposite?: boolean; +}; + +const StyledDefaultText = styled.div` + transition: ${props => props.theme.common.animations.defaultDuration}; + font-family: ${props => props.theme.common.fonts.sessionFontDefault}; + color: ${props => + props.opposite + ? props.theme.colors.textColorOpposite + : props.subtle + ? props.theme.colors.textColorSubtle + : props.theme.colors.textColor}; +`; + +export const Text = (props: TextProps) => { + return {props.text}; +}; diff --git a/ts/components/conversation/Image.tsx b/ts/components/conversation/Image.tsx index 06875ff28..e9a31eac3 100644 --- a/ts/components/conversation/Image.tsx +++ b/ts/components/conversation/Image.tsx @@ -1,7 +1,7 @@ import React from 'react'; import classNames from 'classnames'; -import { Spinner } from '../Spinner'; +import { Spinner } from '../basic/Spinner'; import { LocalizerType } from '../../types/Util'; import { AttachmentType } from '../../types/Attachment'; import { useEncryptedFileFetch } from '../../hooks/useEncryptedFileFetch'; diff --git a/ts/components/conversation/Message.tsx b/ts/components/conversation/Message.tsx index 45c78243b..6223f6c43 100644 --- a/ts/components/conversation/Message.tsx +++ b/ts/components/conversation/Message.tsx @@ -2,7 +2,7 @@ import React from 'react'; import classNames from 'classnames'; import { Avatar, AvatarSize } from '../Avatar'; -import { Spinner } from '../Spinner'; +import { Spinner } from '../basic/Spinner'; import { MessageBody } from './MessageBody'; import { ImageGrid } from './ImageGrid'; import { Image } from './Image'; diff --git a/ts/components/conversation/ModeratorsAddDialog.tsx b/ts/components/conversation/ModeratorsAddDialog.tsx index 8bfa11c68..9a59f9dcb 100644 --- a/ts/components/conversation/ModeratorsAddDialog.tsx +++ b/ts/components/conversation/ModeratorsAddDialog.tsx @@ -5,7 +5,7 @@ import { ToastUtils } from '../../session/utils'; import { SessionModal } from '../session/SessionModal'; import { DefaultTheme } from 'styled-components'; import { SessionSpinner } from '../session/SessionSpinner'; -import { Flex } from '../session/Flex'; +import { Flex } from '../basic/Flex'; import { ConversationModel } from '../../models/conversation'; interface Props { convo: ConversationModel; diff --git a/ts/components/conversation/ModeratorsRemoveDialog.tsx b/ts/components/conversation/ModeratorsRemoveDialog.tsx index a8ce6d0d5..6e982650c 100644 --- a/ts/components/conversation/ModeratorsRemoveDialog.tsx +++ b/ts/components/conversation/ModeratorsRemoveDialog.tsx @@ -5,7 +5,7 @@ import { ApiV2 } from '../../opengroup/opengroupV2'; import { ConversationController } from '../../session/conversations'; import { PubKey } from '../../session/types'; import { ToastUtils } from '../../session/utils'; -import { Flex } from '../session/Flex'; +import { Flex } from '../basic/Flex'; import { SessionButton, SessionButtonColor, SessionButtonType } from '../session/SessionButton'; import { ContactType, SessionMemberListItem } from '../session/SessionMemberListItem'; import { SessionModal } from '../session/SessionModal'; diff --git a/ts/components/conversation/message/MessageMetadata.tsx b/ts/components/conversation/message/MessageMetadata.tsx index 0eca6b4e7..c5120a103 100644 --- a/ts/components/conversation/message/MessageMetadata.tsx +++ b/ts/components/conversation/message/MessageMetadata.tsx @@ -3,7 +3,7 @@ import classNames from 'classnames'; import { MessageSendingErrorText, MetadataSpacer } from './MetadataUtilComponent'; import { OutgoingMessageStatus } from './OutgoingMessageStatus'; -import { Spinner } from '../../Spinner'; +import { Spinner } from '../../basic/Spinner'; import { MetadataBadges } from './MetadataBadge'; import { Timestamp } from '../Timestamp'; import { ExpireTimer } from '../ExpireTimer'; diff --git a/ts/components/session/ActionsPanel.tsx b/ts/components/session/ActionsPanel.tsx index 0e5fb66e4..1cd601f16 100644 --- a/ts/components/session/ActionsPanel.tsx +++ b/ts/components/session/ActionsPanel.tsx @@ -31,6 +31,7 @@ import { showLeftPaneSection } from '../../state/ducks/section'; import { cleanUpOldDecryptedMedias } from '../../session/crypto/DecryptedAttachmentsManager'; import { OpenGroupManagerV2 } from '../../opengroup/opengroupV2/OpenGroupManagerV2'; +import { loadDefaultRoomsIfNeeded } from '../../opengroup/opengroupV2/ApiUtil'; // tslint:disable-next-line: no-import-side-effect no-submodule-imports export enum SectionType { @@ -175,11 +176,10 @@ export const ActionsPanel = () => { }; void generateAttachmentKeyIfEmpty(); // trigger a sync message if needed for our other devices - // 'http://sessionopengroup.com/main?public_key=658d29b91892a2389505596b135e76a53db6e11d613a51dbd3d0816adffb231b' - // 'https://sog.ibolpap.finance/main?public_key=b464aa186530c97d6bcf663a3a3b7465a5f782beaa67c83bee99468824b4aa10' - // 'https://opengroup.bilb.us/main?public_key=1352534ba73d4265973280431dbc72e097a3e43275d1ada984f9805b4943047d' void OpenGroupManagerV2.getInstance().startPolling(); void syncConfiguration(); + + void loadDefaultRoomsIfNeeded(); }, []); // wait for cleanUpMediasInterval and then start cleaning up medias diff --git a/ts/components/session/SessionClosableOverlay.tsx b/ts/components/session/SessionClosableOverlay.tsx index b3031453f..9fc9bf7f2 100644 --- a/ts/components/session/SessionClosableOverlay.tsx +++ b/ts/components/session/SessionClosableOverlay.tsx @@ -12,6 +12,7 @@ import { PillDivider } from './PillDivider'; import { DefaultTheme } from 'styled-components'; import { UserUtils } from '../../session/utils'; import { ConversationTypeEnum } from '../../models/conversation'; +import { SessionJoinableRooms } from './SessionJoinableDefaultRooms'; export enum SessionClosableOverlayType { Message = 'message', @@ -220,6 +221,7 @@ export class SessionClosableOverlay extends React.Component { {descriptionLong &&
{descriptionLong}
} {isMessageView && false &&

{window.i18n('or')}

} {/* FIXME enable back those two items when they are working */} + {isOpenGroupView && } {isMessageView && false && ( void; +}; + +const SessionJoinableRoomAvatar = (props: JoinableRoomProps) => { + return ( + props.onClick(props.completeUrl)} + /> + ); +}; + +const SessionJoinableRoomName = (props: JoinableRoomProps) => { + return {props.name}; +}; + +const SessionJoinableRoomRow = (props: JoinableRoomProps) => { + return ( + { + props.onClick(props.completeUrl); + }} + margin="5px" + padding="5px" + > + + + + ); +}; + +export const SessionJoinableRooms = () => { + const joinableRooms = useSelector((state: StateType) => state.defaultRooms); + + if (!joinableRooms?.length) { + console.warn('no default joinable rooms yet'); + return <>; + } + + return ( + + {joinableRooms.map(r => { + return ( + { + void joinOpenGroupV2WithUIEvents(completeUrl, true); + }} + /> + ); + })} + + ); +}; diff --git a/ts/components/session/SessionRadio.tsx b/ts/components/session/SessionRadio.tsx index 11b40c6bd..d858e4c94 100644 --- a/ts/components/session/SessionRadio.tsx +++ b/ts/components/session/SessionRadio.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { Flex } from './Flex'; +import { Flex } from '../basic/Flex'; // tslint:disable: react-unused-props-and-state interface Props { diff --git a/ts/components/session/SessionToast.tsx b/ts/components/session/SessionToast.tsx index dd2a70745..f5a9594ac 100644 --- a/ts/components/session/SessionToast.tsx +++ b/ts/components/session/SessionToast.tsx @@ -1,7 +1,7 @@ import React, { useContext } from 'react'; import { SessionIcon, SessionIconSize, SessionIconType } from './icon/'; -import { Flex } from './Flex'; +import { Flex } from '../basic/Flex'; import styled, { ThemeContext } from 'styled-components'; import { noop } from 'lodash'; diff --git a/ts/components/session/conversation/SessionCompositionBox.tsx b/ts/components/session/conversation/SessionCompositionBox.tsx index 56c0d62bd..6ae710e1c 100644 --- a/ts/components/session/conversation/SessionCompositionBox.tsx +++ b/ts/components/session/conversation/SessionCompositionBox.tsx @@ -13,7 +13,7 @@ import { SignalService } from '../../../protobuf'; import { Constants } from '../../../session'; import { toArray } from 'react-emoji-render'; -import { Flex } from '../Flex'; +import { Flex } from '../../basic/Flex'; import { AttachmentList } from '../../conversation/AttachmentList'; import { ToastUtils } from '../../../session/utils'; import { AttachmentUtil } from '../../../util'; diff --git a/ts/components/session/conversation/SessionFileDropzone.tsx b/ts/components/session/conversation/SessionFileDropzone.tsx index 55fefb3ef..f4197d913 100644 --- a/ts/components/session/conversation/SessionFileDropzone.tsx +++ b/ts/components/session/conversation/SessionFileDropzone.tsx @@ -1,6 +1,6 @@ import React, { useContext } from 'react'; import styled, { ThemeContext } from 'styled-components'; -import { Flex } from '../Flex'; +import { Flex } from '../../basic/Flex'; import { SessionIcon, SessionIconSize, SessionIconType } from '../icon'; // padding-inline-end: ${props => props.theme.common.margins.md}; diff --git a/ts/components/session/conversation/SessionQuotedMessageComposition.tsx b/ts/components/session/conversation/SessionQuotedMessageComposition.tsx index 8cedb7eb3..8a7a2a240 100644 --- a/ts/components/session/conversation/SessionQuotedMessageComposition.tsx +++ b/ts/components/session/conversation/SessionQuotedMessageComposition.tsx @@ -1,5 +1,5 @@ import React, { useContext } from 'react'; -import { Flex } from '../Flex'; +import { Flex } from '../../basic/Flex'; import { SessionIconButton, SessionIconSize, SessionIconType } from '../icon'; import { ReplyingToMessageProps } from './SessionCompositionBox'; import styled, { DefaultTheme, ThemeContext } from 'styled-components'; diff --git a/ts/components/session/registration/SignInTab.tsx b/ts/components/session/registration/SignInTab.tsx index 64c21b91c..bc9e6673c 100644 --- a/ts/components/session/registration/SignInTab.tsx +++ b/ts/components/session/registration/SignInTab.tsx @@ -1,5 +1,5 @@ import React, { useState } from 'react'; -import { Flex } from '../Flex'; +import { Flex } from '../../basic/Flex'; import { SessionButton, SessionButtonColor, SessionButtonType } from '../SessionButton'; import { SessionSpinner } from '../SessionSpinner'; import { signInWithLinking, signInWithRecovery, validatePassword } from './RegistrationTabs'; diff --git a/ts/opengroup/opengroupV2/ApiUtil.ts b/ts/opengroup/opengroupV2/ApiUtil.ts index 8625311e9..6e12b43c6 100644 --- a/ts/opengroup/opengroupV2/ApiUtil.ts +++ b/ts/opengroup/opengroupV2/ApiUtil.ts @@ -1,6 +1,11 @@ import _ from 'underscore'; import { PubKey } from '../../session/types'; +import { allowOnlyOneAtATime } from '../../session/utils/Promise'; import { fromBase64ToArrayBuffer, fromHex } from '../../session/utils/String'; +import { updateDefaultRooms } from '../../state/ducks/defaultRooms'; +import { getCompleteUrlFromRoom } from '../utils/OpenGroupUtils'; +import { parseOpenGroupV2 } from './JoinOpenGroupV2'; +import { getAllRoomInfos } from './OpenGroupAPIV2'; import { OpenGroupMessageV2 } from './OpenGroupMessageV2'; export const defaultServer = 'https://sessionopengroup.com'; @@ -36,6 +41,13 @@ export type OpenGroupV2Info = { imageId?: string; }; +export type OpenGroupV2InfoJoinable = { + id: string; + name: string; + completeUrl: string; + imageId?: string; +}; + /** * Try to build an full url and check it for validity. * @returns null if the check failed. the built URL otherwise @@ -97,3 +109,53 @@ export const parseMessages = async ( ); return _.compact(messages); }; + +// 'http://sessionopengroup.com/main?public_key=658d29b91892a2389505596b135e76a53db6e11d613a51dbd3d0816adffb231b' + +// FIXME audric change this to sessionopengroup.com once http is fixed +const defaultRoom = + 'https://opengroup.bilb.us/main?public_key=1352534ba73d4265973280431dbc72e097a3e43275d1ada984f9805b4943047d'; + +const loadDefaultRoomsSingle = () => + allowOnlyOneAtATime( + 'loadDefaultRoomsSingle', + async (): Promise> => { + const roomInfos = parseOpenGroupV2(defaultRoom); + if (roomInfos) { + try { + const roomsGot = await getAllRoomInfos(roomInfos); + + if (!roomsGot) { + return []; + } + + return roomsGot.map(room => { + return { + ...room, + completeUrl: getCompleteUrlFromRoom({ + serverUrl: roomInfos.serverUrl, + serverPublicKey: roomInfos.serverPublicKey, + roomId: room.id, + }), + }; + }); + } catch (e) { + window.log.warn('loadDefaultRoomsIfNeeded failed', e); + } + return []; + } + return []; + } + ); + +/** + * Load to the cache all the details of the room of the default opengroupv2 server + * This call will only run once at a time. + */ +export const loadDefaultRoomsIfNeeded = async () => { + // FIXME audric do the UI and refresh this list from time to time + const allRooms: Array = await loadDefaultRoomsSingle(); + if (allRooms !== undefined) { + window.inboxStore?.dispatch(updateDefaultRooms(allRooms)); + } +}; diff --git a/ts/opengroup/opengroupV2/JoinOpenGroupV2.ts b/ts/opengroup/opengroupV2/JoinOpenGroupV2.ts index 376b137e0..d9ee4cb36 100644 --- a/ts/opengroup/opengroupV2/JoinOpenGroupV2.ts +++ b/ts/opengroup/opengroupV2/JoinOpenGroupV2.ts @@ -27,6 +27,7 @@ export function parseOpenGroupV2(urlWithPubkey: string): OpenGroupV2Room | undef if (!openGroupV2CompleteURLRegex.test(lowerCased)) { throw new Error('regex fail'); } + // prefix the URL if it does not have a prefix const prefixedUrl = prefixify(lowerCased); // new URL fails if the protocol is not explicit @@ -44,7 +45,7 @@ export function parseOpenGroupV2(urlWithPubkey: string): OpenGroupV2Room | undef }; return room; } catch (e) { - window.log.error('Invalid Opengroup v2 join URL:', lowerCased); + window.log.error('Invalid Opengroup v2 join URL:', lowerCased, e); } return undefined; } @@ -127,14 +128,14 @@ export async function joinOpenGroupV2WithUIEvents( showToasts: boolean, uiCallback?: (loading: boolean) => void ): Promise { - const parsedRoom = parseOpenGroupV2(completeUrl); - if (!parsedRoom) { - if (showToasts) { - ToastUtils.pushToastError('connectToServer', window.i18n('invalidOpenGroupUrl')); - } - return false; - } try { + const parsedRoom = parseOpenGroupV2(completeUrl); + if (!parsedRoom) { + if (showToasts) { + ToastUtils.pushToastError('connectToServer', window.i18n('invalidOpenGroupUrl')); + } + return false; + } const conversationID = getOpenGroupV2ConversationId(parsedRoom.serverUrl, parsedRoom.roomId); if (ConversationController.getInstance().get(conversationID)) { if (showToasts) { diff --git a/ts/opengroup/utils/OpenGroupUtils.ts b/ts/opengroup/utils/OpenGroupUtils.ts index a50a801d2..f24eaf964 100644 --- a/ts/opengroup/utils/OpenGroupUtils.ts +++ b/ts/opengroup/utils/OpenGroupUtils.ts @@ -20,9 +20,15 @@ export const openGroupV2ServerUrlRegex = new RegExp( `${protocolRegex.source}${hostnameRegex.source}${portRegex}` ); +/** + * Regex to use to check if a string is a v2open completeURL with pubkey. + * Be aware that the /g flag is not set as .test() will otherwise return alternating result + * + * @see https://stackoverflow.com/a/9275499/1680951 + */ export const openGroupV2CompleteURLRegex = new RegExp( - `^${openGroupV2ServerUrlRegex.source}\/${roomIdV2Regex}${qMark}${publicKeyParam}${publicKeyRegex}$`, - 'gm' + `${openGroupV2ServerUrlRegex.source}\/${roomIdV2Regex}${qMark}${publicKeyParam}${publicKeyRegex}`, + 'm' ); /** diff --git a/ts/session/utils/Toast.tsx b/ts/session/utils/Toast.tsx index 26c8cfe78..505d15a2f 100644 --- a/ts/session/utils/Toast.tsx +++ b/ts/session/utils/Toast.tsx @@ -6,13 +6,15 @@ import { SessionToast, SessionToastType } from '../../components/session/Session // if you push a toast manually with toast...() be sure to set the type attribute of the SessionToast component export function pushToastError(id: string, title: string, description?: string) { toast.error( - + , + { toastId: id, updateId: id } ); } export function pushToastWarning(id: string, title: string, description?: string) { toast.warning( - + , + { toastId: id, updateId: id } ); } @@ -28,7 +30,8 @@ export function pushToastInfo( description={description} type={SessionToastType.Info} onToastClick={onToastClick} - /> + />, + { toastId: id, updateId: id } ); } @@ -44,7 +47,8 @@ export function pushToastSuccess( description={description} type={SessionToastType.Success} icon={icon} - /> + />, + { toastId: id, updateId: id } ); } diff --git a/ts/state/ducks/defaultRooms.tsx b/ts/state/ducks/defaultRooms.tsx new file mode 100644 index 000000000..ba669c2fd --- /dev/null +++ b/ts/state/ducks/defaultRooms.tsx @@ -0,0 +1,24 @@ +import { createSlice } from '@reduxjs/toolkit'; +import { OpenGroupV2InfoJoinable } from '../../opengroup/opengroupV2/ApiUtil'; + +export type DefaultRoomsState = Array; + +const initialState: DefaultRoomsState = []; + +/** + * This slice is the one holding the default joinable rooms fetched once in a while from the default opengroup v2 server. + */ +const defaultRoomsSlice = createSlice({ + name: 'defaultRooms', + initialState, + reducers: { + updateDefaultRooms(state, action) { + window.log.warn('updating default rooms', action.payload); + return action.payload as DefaultRoomsState; + }, + }, +}); + +const { actions, reducer } = defaultRoomsSlice; +export const { updateDefaultRooms } = actions; +export const defaultRoomReducer = reducer; diff --git a/ts/state/reducer.ts b/ts/state/reducer.ts index 27cc7842c..cb99c331d 100644 --- a/ts/state/reducer.ts +++ b/ts/state/reducer.ts @@ -5,6 +5,7 @@ import { ConversationsStateType, reducer as conversations } from './ducks/conver import { reducer as user, UserStateType } from './ducks/user'; import { reducer as theme, ThemeStateType } from './ducks/theme'; import { reducer as section, SectionStateType } from './ducks/section'; +import { defaultRoomReducer as defaultRooms, DefaultRoomsState } from './ducks/defaultRooms'; export type StateType = { search: SearchStateType; @@ -13,6 +14,7 @@ export type StateType = { conversations: ConversationsStateType; theme: ThemeStateType; section: SectionStateType; + defaultRooms: DefaultRoomsState; }; export const reducers = { @@ -24,6 +26,7 @@ export const reducers = { user, theme, section, + defaultRooms, }; // Making this work would require that our reducer signature supported AnyAction, not