diff --git a/e2e/constants/testIds.ts b/e2e/constants/testIds.ts index 672a7fc..4006cb9 100644 --- a/e2e/constants/testIds.ts +++ b/e2e/constants/testIds.ts @@ -20,6 +20,7 @@ export const ITEM_LIST_OPTIONS_OPEN_CHAT = 'item-list-options-open-chat'; export const ITEM_LIST_OPTIONS_OPEN_MAP = 'item-list-options-open-map'; export const ITEM_LIST_OPTIONS_DELETE = 'item-list-options-delete'; export const ITEM_LIST_OPTIONS_SHARE = 'item-list-options-share'; +export const ITEM_LIST_OPTIONS_SHOW_QR_CODE = 'item-list-options-show-qr-code'; export const CONFIRM_EDIT_ITEM = 'confirm-edit-item'; export const CANCEL_EDIT_ITEM = 'cancel-edit-item'; export const EDIT_ITEM_NAME_INPUT = 'edit-item-name-input'; diff --git a/package.json b/package.json index 5c2a513..8dfc7b0 100644 --- a/package.json +++ b/package.json @@ -99,11 +99,12 @@ "react-native-root-siblings": "^4.1.1", "react-native-safe-area-context": "4.8.2", "react-native-screens": "~3.29.0", - "react-native-svg": "^14.1.0", + "react-native-svg": "14.1.0", "react-native-toast-message": "^2.1.7", "react-native-url-polyfill": "^2.0.0", "react-native-web": "~0.19.9", "react-native-webview": "13.6.4", + "react-qr-code": "^2.0.13", "uuid": "^9.0.1" }, "devDependencies": { diff --git a/src/components/FileImage.tsx b/src/components/FileImage.tsx index 9f7fbaa..7b13da7 100644 --- a/src/components/FileImage.tsx +++ b/src/components/FileImage.tsx @@ -57,6 +57,11 @@ const FileImage: FC = ({ useEffect(() => { if (!isPlayerView) { navigation.setOptions({ + headerTitleAlign: 'left', + // move to headerBackTitle: ' ' when we have only 2 buttons + // corresponds to the max space available for title on the left and 3 buttons on the right + headerTitleContainerStyle: { maxWidth: '50%' }, + headerBackTitleVisible: false, headerRight: () => ( diff --git a/src/components/FileVideo.tsx b/src/components/FileVideo.tsx index 43fb202..337e765 100644 --- a/src/components/FileVideo.tsx +++ b/src/components/FileVideo.tsx @@ -78,6 +78,11 @@ const FileVideo: FC = ({ useEffect(() => { if (!isPlayerView) { navigation.setOptions({ + headerTitleAlign: 'left', + // corresponds to the max space available for title on the left and 3 buttons on the right + // to remove when right content has at most 2 buttons + headerTitleContainerStyle: { maxWidth: '50%' }, + headerBackTitleVisible: false, headerRight: () => ( diff --git a/src/components/FolderItem.tsx b/src/components/FolderItem.tsx index 5f6882a..f0dc0b9 100644 --- a/src/components/FolderItem.tsx +++ b/src/components/FolderItem.tsx @@ -100,7 +100,7 @@ const styles = StyleSheet.create({ backgroundColor: '#fff', }, headerButtons: { - paddingRight: 20, + paddingRight: 10, flexDirection: 'row', alignItems: 'center', }, diff --git a/src/components/Item.tsx b/src/components/Item.tsx index 9bbabf9..7549e06 100644 --- a/src/components/Item.tsx +++ b/src/components/Item.tsx @@ -1,4 +1,3 @@ -import { FC } from 'react'; import { Pressable, View } from 'react-native'; import { ListItem } from 'react-native-elements'; @@ -23,7 +22,12 @@ interface ItemProps { refresh: ItemOptionsButtonProps['refresh']; } -const Item: FC = ({ item, showOptions = false, index, refresh }) => { +const Item = ({ + item, + showOptions = false, + index, + refresh, +}: ItemProps): JSX.Element => { const { id, name, type, extra } = item; const { navigate } = useNavigation['navigation']>(); @@ -34,41 +38,44 @@ const Item: FC = ({ item, showOptions = false, index, refresh }) => { }); } - function renderListItem() { - return ( - - - - {name} - - - ); - } - return ( - - handleItemPress()} style={{ flex: 2 }}> - {renderListItem()} - - {showOptions && ( - <> - {type === ItemType.FOLDER && ( - + + + + handleItemPress()} + style={{ flex: 2, flexDirection: 'row', gap: 10 }} + > + + {name} + + {showOptions && ( + <> + {type === ItemType.FOLDER && ( + + )} + + )} - - - )} - + + + ); }; diff --git a/src/components/ItemListOptions.tsx b/src/components/ItemListOptions.tsx index df3646f..7f60db6 100644 --- a/src/components/ItemListOptions.tsx +++ b/src/components/ItemListOptions.tsx @@ -1,8 +1,9 @@ import { FC, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { Share, StyleSheet } from 'react-native'; -import { Button, Divider, ListItem, Overlay } from 'react-native-elements'; +import { Divider, ListItem, Overlay, Text } from 'react-native-elements'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; +import QRCode from 'react-qr-code'; import { MaterialIcons } from '@expo/vector-icons'; @@ -18,12 +19,10 @@ import { ITEM_LIST_OPTIONS_MAP, ITEM_LIST_OPTIONS_OPEN_CHAT, ITEM_LIST_OPTIONS_SHARE, - SHARE_ITEM_BUILDER, - SHARE_ITEM_PLAYER, + ITEM_LIST_OPTIONS_SHOW_QR_CODE, } from '../../e2e/constants/testIds'; import { ANALYTICS_EVENTS, - PRIMARY_COLOR, SHARE_HOST, SHARE_OPTIONS, VIEWS, @@ -76,7 +75,8 @@ const ItemListOptions: FC = ({ isLoading: isLoadingCurrentMember, isError: isErrorCurrentMember, } = hooks.useCurrentMember(); - const [shareModalVisible, setShareModalVisible] = useState(false); + const [showQrCodeModalVisible, setShowQrCodeModalVisible] = + useState(false); const [editItemModalVisible, setEditItemModalVisible] = useState(false); const [deleteItemModalVisible, setDeleteItemModalVisible] = @@ -116,7 +116,11 @@ const ItemListOptions: FC = ({ }; const handleSharePress = () => { - setShareModalVisible(true); + onShare(item.id, SHARE_OPTIONS.BUILDER); + }; + + const handleShowQrCodePress = () => { + setShowQrCodeModalVisible(true); }; const handleOpenChat = async () => { @@ -133,7 +137,7 @@ const ItemListOptions: FC = ({ throw new Error('No itemId'); } const result = await Share.share({ - message: `Check out this on Graasp: ${ + message: `Check out ${item.name} on Graasp: ${ linkType === SHARE_OPTIONS.BUILDER ? `${SHARE_HOST.BUILDER}/${itemId}` : `${SHARE_HOST.PLAYER}/${itemId}` @@ -141,16 +145,16 @@ const ItemListOptions: FC = ({ }); if (result.action === Share.sharedAction) { if (result.activityType) { - setShareModalVisible(false); + setShowQrCodeModalVisible(false); } else { - setShareModalVisible(false); + setShowQrCodeModalVisible(false); } await customAnalyticsEvent(ANALYTICS_EVENTS.SHARE_GRAASP_LINK, { method: linkType === SHARE_OPTIONS.BUILDER ? VIEWS.BUILDER : VIEWS.PLAYER, }); } else if (result.action === Share.dismissedAction) { - //setModalVisible({ toggle: false, itemId: null }); + // do nothing } } catch (error: any) { alert(error.message); @@ -166,29 +170,12 @@ const ItemListOptions: FC = ({ <> setShareModalVisible(false)} + isVisible={showQrCodeModalVisible} + onBackdropPress={() => setShowQrCodeModalVisible(false)} > - + > + + ); }; diff --git a/src/config/constants/constants.ts b/src/config/constants/constants.ts index c7d6fa2..4cae590 100644 --- a/src/config/constants/constants.ts +++ b/src/config/constants/constants.ts @@ -43,6 +43,7 @@ export const DEFAULT_LOCALE = 'en-US'; export const ITEMS_TABLE_ROW_ICON_COLOR = '#333333'; export const USERNAME_MAX_LENGTH = 30; +export const HEADER_ITEM_NAME_MAX_LENGTH = 30; export const LOADING_CONTENT = '…'; diff --git a/src/langs/en.json b/src/langs/en.json index 509eb6c..a7c9942 100644 --- a/src/langs/en.json +++ b/src/langs/en.json @@ -96,5 +96,7 @@ "REQUEST_ITEM_SUPPORT_ALERT_MESSAGE": "The request has been successfully sent.", "Open Chatbox": "Open Chatbox", "Open Map": "Open Map", - "My Map": "My Map" + "My Map": "My Map", + "Show QR code": "Show QR code", + "FOLDER_CONTAINS_ONLY_FOLDERS": "This folder contains only folders.\nClick on the button below to navigate to the next content." } diff --git a/src/langs/fr.json b/src/langs/fr.json index efe306f..083de5e 100644 --- a/src/langs/fr.json +++ b/src/langs/fr.json @@ -96,5 +96,7 @@ "REQUEST_ITEM_SUPPORT_ALERT_MESSAGE": "La demande a bien été envoyée.", "Open Chatbox": "Ouvrir la discussion", "Open Map": "Ouvrir la Carte", - "My Map": "Ma Carte" + "My Map": "Ma Carte", + "Show QR code": "Montrer le code QR", + "FOLDER_CONTAINS_ONLY_FOLDERS": "Ce dossier ne contient que des dossiers.\nCliquez sur le bouton ci-dessous pour naviguer au contenu suivant." } diff --git a/src/navigation/BookmarksNavigator.tsx b/src/navigation/BookmarksNavigator.tsx index 14e8eec..25d22e6 100644 --- a/src/navigation/BookmarksNavigator.tsx +++ b/src/navigation/BookmarksNavigator.tsx @@ -2,7 +2,6 @@ import { useTranslation } from 'react-i18next'; import { createStackNavigator } from '@react-navigation/stack'; -import Header from '../components/common/Header'; import { defaultScreenOptions } from '../config/constants/navigation'; import BookmarksScreen from '../screens/bookmarks/BookmarksScreen'; import { BOOKMARKS_NAVIGATOR, BOOKMARKS_NAVIGATOR_BOOKMARKS } from './names'; @@ -22,9 +21,7 @@ const BookmarksStackNavigator = () => { name={BOOKMARKS_NAVIGATOR_BOOKMARKS} component={BookmarksScreen} options={{ - title: '', - headerLeft: () =>
, - headerLeftContainerStyle: { paddingLeft: 10 }, + title: t('Bookmarks'), headerBackTitleVisible: false, }} /> diff --git a/src/navigation/HomeStackNavigator.tsx b/src/navigation/HomeStackNavigator.tsx index 71caa7f..0deca8b 100644 --- a/src/navigation/HomeStackNavigator.tsx +++ b/src/navigation/HomeStackNavigator.tsx @@ -2,7 +2,6 @@ import { useTranslation } from 'react-i18next'; import { createStackNavigator } from '@react-navigation/stack'; -import Header from '../components/common/Header'; import { defaultScreenOptions } from '../config/constants/navigation'; import HomeScreen from '../screens/home/HomeScreen'; import { HOME_NAVIGATOR, HOME_NAVIGATOR_HOME } from './names'; @@ -23,9 +22,7 @@ const HomeStackNavigator = () => { name={HOME_NAVIGATOR_HOME} component={HomeScreen} options={{ - title: '', - headerLeft: () =>
, - headerLeftContainerStyle: { paddingLeft: 10 }, + title: t('Home'), headerBackTitleVisible: false, }} /> diff --git a/src/navigation/ItemStackNavigator.tsx b/src/navigation/ItemStackNavigator.tsx index 2695d67..f1eadf0 100644 --- a/src/navigation/ItemStackNavigator.tsx +++ b/src/navigation/ItemStackNavigator.tsx @@ -33,7 +33,8 @@ const ItemStackNavigator = () => { options={({ route: { params } }) => ({ title: params?.headerTitle, headerTitleAlign: 'center', - headerBackTitleVisible: false, + // trick to force title ellipsis + headerBackTitle: ' ', })} /> { options={({ route: { params } }) => ({ title: params?.headerTitle, headerTitleAlign: 'center', - headerBackTitleVisible: false, + // trick to force title ellipsis + headerBackTitle: ' ', })} /> { }) => ({ title: headerTitle, headerTitleAlign: 'center', - headerBackTitleVisible: false, + // trick to force title ellipsis + headerBackTitle: ' ', })} /> { component={DetailsScreen} options={{ title: '', - headerBackTitleVisible: false, }} /> { }) => ({ title: headerTitle, headerTitleAlign: 'center', + // trick to force title ellipsis + headerBackTitle: ' ', })} /> diff --git a/src/navigation/LibraryNavigator.tsx b/src/navigation/LibraryNavigator.tsx index 8747dca..556c70a 100644 --- a/src/navigation/LibraryNavigator.tsx +++ b/src/navigation/LibraryNavigator.tsx @@ -2,7 +2,6 @@ import { useTranslation } from 'react-i18next'; import { createStackNavigator } from '@react-navigation/stack'; -import Header from '../components/common/Header'; import { defaultScreenOptions } from '../config/constants/navigation'; import CollectionScreen from '../screens/library/CollectionScreen'; import LibraryScreen from '../screens/library/LibraryScreen'; @@ -27,9 +26,8 @@ const LibraryStackNavigator = () => { name={LIBRARY_NAVIGATOR_LIBRARY} component={LibraryScreen} options={{ - title: '', - headerLeft: () =>
, - headerTitleAlign: 'center', + title: t('Library'), + headerTitleAlign: 'left', headerBackTitleVisible: false, }} /> @@ -41,8 +39,9 @@ const LibraryStackNavigator = () => { }} options={() => ({ title: '', + // trick to enable ellipsis for 2 right buttons + headerBackTitle: ' ', headerTitleAlign: 'center', - headerBackTitleVisible: false, })} /> diff --git a/src/navigation/MyItemsStackNavigator.tsx b/src/navigation/MyItemsStackNavigator.tsx index 516f459..5d86ea3 100644 --- a/src/navigation/MyItemsStackNavigator.tsx +++ b/src/navigation/MyItemsStackNavigator.tsx @@ -3,7 +3,6 @@ import { useTranslation } from 'react-i18next'; import { createStackNavigator } from '@react-navigation/stack'; import { MY_ITEMS_MAP_BUTTON } from '../../e2e/constants/testIds'; -import Header from '../components/common/Header'; import MapButton from '../components/common/MapButton'; import { defaultScreenOptions } from '../config/constants/navigation'; import MyItemsScreen from '../screens/MyItemsScreen'; @@ -24,9 +23,7 @@ const MyItemsStackNavigator = () => { name={MY_ITEMS_NAVIGATOR_MY_ITEMS} component={MyItemsScreen} options={{ - title: '', - headerLeft: () =>
, - headerLeftContainerStyle: { paddingLeft: 10 }, + title: t('My Items'), headerRight: () => ( { name={PROFILE_NAVIGATOR_PROFILE} component={ProfileScreen} options={() => ({ - title: '', - headerLeft: () =>
, - headerLeftContainerStyle: { paddingLeft: 10 }, + title: t('Profile'), })} /> diff --git a/src/navigation/SharedStackNavigator.tsx b/src/navigation/SharedStackNavigator.tsx index 2f201ad..d5d5dac 100644 --- a/src/navigation/SharedStackNavigator.tsx +++ b/src/navigation/SharedStackNavigator.tsx @@ -3,7 +3,6 @@ import { useTranslation } from 'react-i18next'; import { createStackNavigator } from '@react-navigation/stack'; import { SHARED_ITEMS_MAP_BUTTON } from '../../e2e/constants/testIds'; -import Header from '../components/common/Header'; import MapButton from '../components/common/MapButton'; import { defaultScreenOptions } from '../config/constants/navigation'; import SharedScreen from '../screens/SharedScreen'; @@ -25,9 +24,7 @@ const SharedStackNavigator = () => { name={SHARED_NAVIGATOR_SHARED} component={SharedScreen} options={{ - title: '', - headerLeft: () =>
, - headerLeftContainerStyle: { paddingLeft: 10 }, + title: t('Shared Items'), headerRight: () => ( > = ({ route }) => { buttonStyle={{ backgroundColor: PRIMARY_COLOR }} icon={} onPress={() => refetchChat()} - > + /> ), }); }, []); diff --git a/src/screens/ItemScreen.tsx b/src/screens/ItemScreen.tsx index 679d2df..516ee16 100644 --- a/src/screens/ItemScreen.tsx +++ b/src/screens/ItemScreen.tsx @@ -7,7 +7,6 @@ import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { Context, ItemType } from '@graasp/sdk'; import { useNavigation } from '@react-navigation/native'; -import truncate from 'lodash.truncate'; import { ITEM_NAVIGATOR_ITEM_ERROR } from '../../e2e/constants/testIds'; import ActivityIndicator from '../components/ActivityIndicator'; @@ -32,7 +31,7 @@ const ItemScreen = ({ route }: ItemScreenProps<'ItemStackItem'>) => { useEffect(() => { if (item) { - setOptions({ title: truncate(item.name, { length: 20 }) }); + setOptions({ title: item.name }); } }, [item]); diff --git a/src/screens/PlayerFolderScreen.tsx b/src/screens/PlayerFolderScreen.tsx index f9bbcf7..76a5e49 100644 --- a/src/screens/PlayerFolderScreen.tsx +++ b/src/screens/PlayerFolderScreen.tsx @@ -73,7 +73,7 @@ const PlayerFolderScreen = (): JSX.Element | null => { screen: MY_ITEMS_NAVIGATOR_MY_ITEMS, }); }} - > + /> ), }); }, [navigation]); diff --git a/yarn.lock b/yarn.lock index fdf02e0..54fe919 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9230,11 +9230,12 @@ __metadata: react-native-root-siblings: "npm:^4.1.1" react-native-safe-area-context: "npm:4.8.2" react-native-screens: "npm:~3.29.0" - react-native-svg: "npm:^14.1.0" + react-native-svg: "npm:14.1.0" react-native-toast-message: "npm:^2.1.7" react-native-url-polyfill: "npm:^2.0.0" react-native-web: "npm:~0.19.9" react-native-webview: "npm:13.6.4" + react-qr-code: "npm:^2.0.13" react-test-renderer: "npm:18.2.0" ts-jest: "npm:^29.1.1" typescript: "npm:^5.3.0" @@ -13310,6 +13311,13 @@ __metadata: languageName: node linkType: hard +"qr.js@npm:0.0.0": + version: 0.0.0 + resolution: "qr.js@npm:0.0.0" + checksum: 10/a05943d13cbc478e48935de8669b72312fe02de05057c35ebfab59a574c084530a9afb9ee651d53c8c870a54f9ec93ea7b63231de202740ce16a71c797e37ce9 + languageName: node + linkType: hard + "qrcode-terminal@npm:0.11.0": version: 0.11.0 resolution: "qrcode-terminal@npm:0.11.0" @@ -13732,7 +13740,7 @@ __metadata: languageName: node linkType: hard -"react-native-svg@npm:^14.1.0": +"react-native-svg@npm:14.1.0": version: 14.1.0 resolution: "react-native-svg@npm:14.1.0" dependencies: @@ -13859,6 +13867,22 @@ __metadata: languageName: node linkType: hard +"react-qr-code@npm:^2.0.13": + version: 2.0.13 + resolution: "react-qr-code@npm:2.0.13" + dependencies: + prop-types: "npm:^15.8.1" + qr.js: "npm:0.0.0" + peerDependencies: + react: ^16.x || ^17.x || ^18.x || ^19.x + react-native-svg: "*" + peerDependenciesMeta: + react-native-svg: + optional: true + checksum: 10/cfadaf22883d90b00d01daa7539c993d1229b698df6aa46c8ce5be306d3e932a03e5426dcec7ddc774af9965e88c84c3568e39ab5960d4987396002405e6968e + languageName: node + linkType: hard + "react-query@npm:3.39.3": version: 3.39.3 resolution: "react-query@npm:3.39.3"