From 1c9b87310a858d6ab73a1c581f065c9131968315 Mon Sep 17 00:00:00 2001 From: Alan Fernandez Date: Sun, 17 Nov 2024 13:46:34 +0100 Subject: [PATCH 01/15] fix: add missing redux mock store types --- package.json | 1 + yarn.lock | 28 ++++++++++++++++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/package.json b/package.json index 2cdf070..671e40a 100755 --- a/package.json +++ b/package.json @@ -44,6 +44,7 @@ "@types/react": "^18.3.5", "@types/react-native-vector-icons": "^6.4.18", "@types/react-test-renderer": "^18.3.0", + "@types/redux-mock-store": "^1.0.6", "babel-jest": "^29.7.0", "babel-plugin-module-resolver": "^5.0.2", "eslint": "^9.10.0", diff --git a/yarn.lock b/yarn.lock index 954c91f..841aa0d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1636,6 +1636,15 @@ __metadata: languageName: node linkType: hard +"@babel/runtime@npm:^7.9.2": + version: 7.26.0 + resolution: "@babel/runtime@npm:7.26.0" + dependencies: + regenerator-runtime: ^0.14.0 + checksum: c8e2c0504ab271b3467a261a8f119bf2603eb857a0d71e37791f4e3fae00f681365073cc79f141ddaa90c6077c60ba56448004ad5429d07ac73532be9f7cf28a + languageName: node + linkType: hard + "@babel/template@npm:^7.0.0, @babel/template@npm:^7.24.7, @babel/template@npm:^7.25.0, @babel/template@npm:^7.3.3": version: 7.25.0 resolution: "@babel/template@npm:7.25.0" @@ -3002,6 +3011,15 @@ __metadata: languageName: node linkType: hard +"@types/redux-mock-store@npm:^1.0.6": + version: 1.0.6 + resolution: "@types/redux-mock-store@npm:1.0.6" + dependencies: + redux: ^4.0.5 + checksum: 5c799d2fc5b3f0f84bfcd6243c56b1ac98be6707057f570b5e51e9d305f446978cc2958c7e0b629ebf73293f15a79765517e6d0a1df0371fd27551f7128325c7 + languageName: node + linkType: hard + "@types/semver@npm:^7.3.12": version: 7.5.8 resolution: "@types/semver@npm:7.5.8" @@ -7074,6 +7092,7 @@ __metadata: "@types/react": ^18.3.5 "@types/react-native-vector-icons": ^6.4.18 "@types/react-test-renderer": ^18.3.0 + "@types/redux-mock-store": ^1.0.6 "@types/styled-components": ^5.1.34 babel-jest: ^29.7.0 babel-plugin-module-resolver: ^5.0.2 @@ -8906,6 +8925,15 @@ __metadata: languageName: node linkType: hard +"redux@npm:^4.0.5": + version: 4.2.1 + resolution: "redux@npm:4.2.1" + dependencies: + "@babel/runtime": ^7.9.2 + checksum: f63b9060c3a1d930ae775252bb6e579b42415aee7a23c4114e21a0b4ba7ec12f0ec76936c00f546893f06e139819f0e2855e0d55ebfce34ca9c026241a6950dd + languageName: node + linkType: hard + "redux@npm:^5.0.1": version: 5.0.1 resolution: "redux@npm:5.0.1" From f51c0478e248e313240920a22f09834bbc607d01 Mon Sep 17 00:00:00 2001 From: Alan Fernandez Date: Sun, 17 Nov 2024 21:55:32 +0100 Subject: [PATCH 02/15] chore: update tests --- __tests__/FolderList.test.tsx | 4 +- __tests__/HomeScreen.test.tsx | 98 ++++++++++++++++--- __tests__/NoteDetailsScreen.test.tsx | 95 ++++++++++++------ jest.config.js | 2 +- .../components/FloatingButton/styles.tsx | 2 +- .../components/SaveButton.tsx | 2 +- 6 files changed, 155 insertions(+), 48 deletions(-) diff --git a/__tests__/FolderList.test.tsx b/__tests__/FolderList.test.tsx index fbbd6b9..ec6d564 100644 --- a/__tests__/FolderList.test.tsx +++ b/__tests__/FolderList.test.tsx @@ -24,7 +24,7 @@ describe('FolderList', () => { }); it('renders the correct number of folders', () => { - const { getAllByTestId } = render(); + const { getAllByTestId } = render(); expect(getAllByTestId('folder-component').length).toBe(mockFolders.length); }); @@ -34,7 +34,7 @@ describe('FolderList', () => { callback({ folders: { folders: [] } }), ); - const { queryByTestId } = render(); + const { queryByTestId } = render(); expect(queryByTestId('folder-component')).toBeNull(); }); diff --git a/__tests__/HomeScreen.test.tsx b/__tests__/HomeScreen.test.tsx index a70c7e3..00180f1 100644 --- a/__tests__/HomeScreen.test.tsx +++ b/__tests__/HomeScreen.test.tsx @@ -1,9 +1,61 @@ import React from 'react'; -import HomeScreen from '@screens/HomeScreen'; -import { render } from '@testing-library/react-native'; +import { fireEvent, render, screen } from '@testing-library/react-native'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; +import { Provider } from 'react-redux'; +import configureStore, { MockStoreEnhanced } from 'redux-mock-store'; +import { NavigationContainer } from '@react-navigation/native'; + +import { Folder } from '@store/foldersSlice'; +import { RootState } from '@store/index'; +import foldersMockData from '@store/mockData/folders.mockData'; +import { Note } from '@store/queries/notes'; -jest.mock('@screens/HomeScreen/components/FolderList', () => 'FolderList'); +import HomeScreen from '@screens/HomeScreen'; + +jest.mock('@screens/HomeScreen/components/FolderList', () => { + const { View } = require('react-native'); + return (props: { folders: Folder[] }) => ( + + {props.folders.map((folder: Folder) => ( + + ))} + + ); +}); +jest.mock('@screens/HomeScreen/components/SearchBox', () => { + const React = require('react'); + const { TextInput, View } = require('react-native'); + return ({ + searchQuery, + setSearchQuery, + }: { + searchQuery: string; + setSearchQuery: (query: string) => void; + }) => ( + + + + ); +}); +jest.mock('@screens/HomeScreen/components/NotesResultsList', () => { + const { View } = require('react-native'); + return (props: { notes: Note[] }) => ( + + {props.notes.map((note: Note) => ( + + ))} + + ); +}); +jest.mock( + '@screens/HomeScreen/components/FloatingButton', + () => 'FloatingButton', +); jest.mock('@components/BackgroundLayers', () => 'BackgroundLayers'); jest.mock('react-native-safe-area-context', () => ({ @@ -11,7 +63,17 @@ jest.mock('react-native-safe-area-context', () => ({ })); describe('HomeScreen', () => { + const mockStore = configureStore>([]); + const initialState = { + folders: { + folders: foldersMockData, + }, + }; + + let store: MockStoreEnhanced>; + beforeEach(() => { + store = mockStore(initialState); (useSafeAreaInsets as jest.Mock).mockReturnValue({ top: 20, bottom: 0, @@ -24,25 +86,37 @@ describe('HomeScreen', () => { jest.clearAllMocks(); }); - it('renders the background layers', () => { - const { getByTestId } = render(); + const renderWithProviders = (component: React.ReactNode) => + render( + + {component} + , + ); + it('renders the background layers', () => { + const { getByTestId } = renderWithProviders(); expect(getByTestId('background-layers')).toBeTruthy(); }); it('renders the container with correct insets', () => { - const { getByTestId } = render(); - + const { getByTestId } = renderWithProviders(); const container = getByTestId('container'); - expect(container.props.style.paddingTop).toBe(20); expect(container.props.style.paddingLeft).toBe(0); - expect(container.props.style.paddingRight).toBe(0); + expect(container.props.style.paddingRight).toBe(-2); }); - it('renders the folder list', () => { - const { getByTestId } = render(); - + it('renders the folder list when there is no search query', () => { + const { getByTestId } = renderWithProviders(); expect(getByTestId('folder-list')).toBeTruthy(); }); + + it('renders the notes results list when there is a search query', () => { + const { getByTestId } = renderWithProviders(); + + const searchInput = getByTestId('search-input'); + fireEvent.changeText(searchInput, 'Sample Note'); + + expect(getByTestId('notes-results')).toBeTruthy(); + }); }); diff --git a/__tests__/NoteDetailsScreen.test.tsx b/__tests__/NoteDetailsScreen.test.tsx index 843ce75..4fc2f55 100644 --- a/__tests__/NoteDetailsScreen.test.tsx +++ b/__tests__/NoteDetailsScreen.test.tsx @@ -1,18 +1,23 @@ import React from 'react'; -import { render, fireEvent, waitFor } from '@testing-library/react-native'; +import { + render, + fireEvent, + waitFor, + screen, +} from '@testing-library/react-native'; +import { NavigationContainer, RouteProp } from '@react-navigation/native'; +import { Provider } from 'react-redux'; +import { configureStore } from '@reduxjs/toolkit'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; + +import NoteDetailsScreen from '@screens/NoteDetailsScreen'; +import { RootStackParamList } from '@navigation/types'; +import foldersReducer from '@store/foldersSlice'; +import authReducer from '@store/authSlice'; import { useFetchNoteDetailsQuery, useUpdateNoteMutation, } from '@store/queries/notes'; -import NoteDetailsScreen from '@screens/NoteDetailsScreen'; -import { - NavigationContainer, - RouteProp, - useNavigation, -} from '@react-navigation/native'; -import { RootStackParamList } from '@navigation/types'; -import { formatTimestampToDateTime } from '@root/src/utils'; jest.mock('@store/queries/notes', () => ({ useFetchNoteDetailsQuery: jest.fn(), @@ -23,20 +28,25 @@ jest.mock('react-native-safe-area-context', () => ({ useSafeAreaInsets: jest.fn(), })); -jest.mock('@react-navigation/native', () => ({ - ...jest.requireActual('@react-navigation/native'), - useNavigation: jest.fn(), +jest.mock('@store/queries/notes', () => ({ + useFetchNoteDetailsQuery: jest.fn(), + useUpdateNoteMutation: jest.fn(() => [jest.fn()]), + useCreateNoteMutation: jest.fn(() => [jest.fn().mockResolvedValue({})]), })); -const mockNavigate = jest.fn(); -const mockNavigation = { navigate: mockNavigate }; +const mockNavigation = { + navigate: jest.fn(), + goBack: jest.fn(), + setParams: jest.fn(), +}; + const mockRoute: RouteProp = { key: 'NoteDetailKey', name: 'NoteDetails', - params: { noteId: 1 }, + params: { noteId: 1, isNew: false }, }; -describe('NoteDetailScreen', () => { +describe('NoteDetailsScreen', () => { const mockedDate = new Date('2024-10-13T11:34:00').getTime(); beforeEach(() => { @@ -48,27 +58,50 @@ describe('NoteDetailScreen', () => { left: 0, right: 0, }); - - (useNavigation as jest.Mock).mockReturnValue(mockNavigation); }); afterEach(() => { jest.restoreAllMocks(); }); + const renderWithProvider = (ui: React.ReactElement) => { + const mockStore = configureStore({ + reducer: { + folders: foldersReducer, + auth: authReducer, + }, + preloadedState: { + folders: { + folders: [ + { + id: 1, + folderName: 'Personal', + notes: [], + }, + ], + }, + auth: { userToken: 'mockToken', isLoading: false, isSignout: false }, + }, + }); + + return render( + + {ui} + , + ); + }; + it('renders loading state initially', () => { (useFetchNoteDetailsQuery as jest.Mock).mockReturnValue({ data: null, isLoading: true, }); - const { getByText } = render( - - - , + const { getByText } = renderWithProvider( + , ); expect(getByText('Loading...')).toBeTruthy(); @@ -79,13 +112,13 @@ describe('NoteDetailScreen', () => { data: { id: '1', title: 'Test Note', - modifiedDate: Date.now(), + modifiedDate: mockedDate, content: 'This is the content of the note', }, isLoading: false, }); - const { getByDisplayValue, getByText } = render( + const { getByDisplayValue, getByText } = renderWithProvider( { (useUpdateNoteMutation as jest.Mock).mockReturnValue([mockUpdateNote]); - const { getByDisplayValue, getByText } = render( + const { getByDisplayValue, getByText } = renderWithProvider( { getByDisplayValue('This is the content of the note'), 'Updated Note Content', ); - fireEvent.press(getByText('Save Changes')); + fireEvent.press(screen.getByTestId('save-button')); await waitFor(() => { expect(mockUpdateNote).toHaveBeenCalledWith({ @@ -144,13 +177,13 @@ describe('NoteDetailScreen', () => { isLoading: false, }); - const { getByText } = render( + const { getByText } = renderWithProvider( , ); - expect(getByText(formatTimestampToDateTime(mockedDate))).toBeTruthy(); + expect(getByText('Oct 13, 2024 at 11:34 AM')).toBeTruthy(); }); }); diff --git a/jest.config.js b/jest.config.js index 485f2ff..8641f3f 100755 --- a/jest.config.js +++ b/jest.config.js @@ -5,7 +5,7 @@ module.exports = { '^.+\\.(js|jsx|ts|tsx)$': 'babel-jest', }, transformIgnorePatterns: [ - 'node_modules/(?!(react-native|@react-native|react-navigation|@react-navigation|react-native-vector-icons|react-redux)/)', + 'node_modules/(?!(react-native|@react-native|react-navigation|@react-navigation|react-native-vector-icons|react-redux|react-native-toast-message)/)', ], moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'], }; diff --git a/src/screens/HomeScreen/components/FloatingButton/styles.tsx b/src/screens/HomeScreen/components/FloatingButton/styles.tsx index 1ff92b3..f3bd25e 100644 --- a/src/screens/HomeScreen/components/FloatingButton/styles.tsx +++ b/src/screens/HomeScreen/components/FloatingButton/styles.tsx @@ -16,7 +16,7 @@ const PlusIcon = ({ color }: { color: string }) => ( x2="15" y2="25" stroke={color} - strokeWidth="3 " + strokeWidth="3" strokeLinecap="round" /> = ({ handleSave }) => { return ( - + ); From 7cb198c2a18a330456dacb82dff23306b153ef22 Mon Sep 17 00:00:00 2001 From: Alan Fernandez Date: Sat, 23 Nov 2024 03:43:43 +0100 Subject: [PATCH 03/15] chore: renamed --- src/store/queries/notes/index.ts | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/store/queries/notes/index.ts b/src/store/queries/notes/index.ts index 92cdf3b..edac160 100644 --- a/src/store/queries/notes/index.ts +++ b/src/store/queries/notes/index.ts @@ -46,11 +46,9 @@ export const noteApi = createApi({ const newId = notesMockData.length ? Math.max(...notesMockData.map(n => n.id)) + 1 : 1; - const createdNote = { ...newNote, id: newId, modifiedDate: Date.now() }; - - notesMockData.push(createdNote); - - return { data: createdNote }; + const note = { ...newNote, id: newId, modifiedDate: Date.now() }; + notesMockData.push(note); + return { data: note }; }, }), updateNote: builder.mutation & { id?: number }>({ From 06741dbb7e872915a827bd089e9dbe36d113f999 Mon Sep 17 00:00:00 2001 From: Alan Fernandez Date: Sun, 17 Nov 2024 21:55:32 +0100 Subject: [PATCH 04/15] chore: update tests --- __tests__/HomeScreen.test.tsx | 27 ++++++++++++++----- __tests__/NoteDetailsScreen.test.tsx | 19 ++++++------- .../components/FloatingButton/styles.tsx | 2 +- 3 files changed, 32 insertions(+), 16 deletions(-) diff --git a/__tests__/HomeScreen.test.tsx b/__tests__/HomeScreen.test.tsx index bfa8e21..2175456 100644 --- a/__tests__/HomeScreen.test.tsx +++ b/__tests__/HomeScreen.test.tsx @@ -2,9 +2,11 @@ import React from 'react'; import { Provider } from 'react-redux'; import { NavigationContainer } from '@react-navigation/native'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; -import { render } from '@testing-library/react-native'; -import configureStore from 'redux-mock-store'; +import { fireEvent, render } from '@testing-library/react-native'; +import configureStore, { MockStoreEnhanced } from 'redux-mock-store'; import HomeScreen from '@screens/HomeScreen'; +import foldersMockData from '@store/mockData/folders.mockData'; +import { RootState } from '@store/index'; jest.mock('@screens/HomeScreen/components/FolderList', () => 'FolderList'); jest.mock('@screens/HomeScreen/components/SearchBox', () => 'SearchBox'); @@ -23,12 +25,18 @@ jest.mock('react-native-safe-area-context', () => ({ })); describe('HomeScreen', () => { - const mockStore = configureStore(); + const mockStore = configureStore>([]); const initialState = { - folders: { folders: [{ id: 1, name: 'Test Folder', notes: [] }] }, + folders: { + folders: foldersMockData, + }, }; + // eslint-disable-next-line @typescript-eslint/no-unused-vars + let store: MockStoreEnhanced>; + beforeEach(() => { + store = mockStore(initialState); (useSafeAreaInsets as jest.Mock).mockReturnValue({ top: 20, bottom: 0, @@ -58,9 +66,7 @@ describe('HomeScreen', () => { it('renders the container with correct insets', () => { const { getByTestId } = renderWithProviders(); - const container = getByTestId('container'); - expect(container.props.style.paddingTop).toBe(20); expect(container.props.style.paddingLeft).toBe(0); expect(container.props.style.paddingRight).toBe(-2); @@ -70,4 +76,13 @@ describe('HomeScreen', () => { const { getByTestId } = renderWithProviders(); expect(getByTestId('folder-list')).toBeTruthy(); }); + + it('renders the notes results list when there is a search query', () => { + const { getByTestId } = renderWithProviders(); + + const searchInput = getByTestId('search-input'); + fireEvent.changeText(searchInput, 'Sample Note'); + + expect(getByTestId('notes-results')).toBeTruthy(); + }); }); diff --git a/__tests__/NoteDetailsScreen.test.tsx b/__tests__/NoteDetailsScreen.test.tsx index 75ded66..a663e54 100644 --- a/__tests__/NoteDetailsScreen.test.tsx +++ b/__tests__/NoteDetailsScreen.test.tsx @@ -3,18 +3,18 @@ import { render, fireEvent, waitFor } from '@testing-library/react-native'; import configureStore from 'redux-mock-store'; import { Provider } from 'react-redux'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; + +import NoteDetailsScreen from '@screens/NoteDetailsScreen'; +import { RootStackParamList } from '@navigation/types'; import { useFetchNoteDetailsQuery, useUpdateNoteMutation, } from '@store/queries/notes'; -import NoteDetailsScreen from '@screens/NoteDetailsScreen'; import { NavigationContainer, RouteProp, useNavigation, } from '@react-navigation/native'; -import { RootStackParamList } from '@navigation/types'; -import { formatTimestampToDateTime } from '@root/src/utils'; jest.mock('@store/queries/notes', () => ({ useFetchNoteDetailsQuery: jest.fn(), @@ -26,9 +26,10 @@ jest.mock('react-native-safe-area-context', () => ({ useSafeAreaInsets: jest.fn(), })); -jest.mock('@react-navigation/native', () => ({ - ...jest.requireActual('@react-navigation/native'), - useNavigation: jest.fn(), +jest.mock('@store/queries/notes', () => ({ + useFetchNoteDetailsQuery: jest.fn(), + useUpdateNoteMutation: jest.fn(() => [jest.fn()]), + useCreateNoteMutation: jest.fn(() => [jest.fn().mockResolvedValue({})]), })); const mockStore = configureStore([]); @@ -40,7 +41,7 @@ const mockRoute: RouteProp = { params: { noteId: 1, isNew: false }, }; -describe('NoteDetailScreen', () => { +describe('NoteDetailsScreen', () => { const mockedDate = new Date('2024-10-13T11:34:00').getTime(); beforeEach(() => { @@ -96,7 +97,7 @@ describe('NoteDetailScreen', () => { data: { id: '1', title: 'Test Note', - modifiedDate: Date.now(), + modifiedDate: mockedDate, content: 'This is the content of the note', }, isLoading: false, @@ -209,6 +210,6 @@ describe('NoteDetailScreen', () => { , ); - expect(getByText(formatTimestampToDateTime(mockedDate))).toBeTruthy(); + expect(getByText('Oct 13, 2024 at 11:34 AM')).toBeTruthy(); }); }); diff --git a/src/screens/HomeScreen/components/FloatingButton/styles.tsx b/src/screens/HomeScreen/components/FloatingButton/styles.tsx index 7ebbb2b..8fcd0fe 100644 --- a/src/screens/HomeScreen/components/FloatingButton/styles.tsx +++ b/src/screens/HomeScreen/components/FloatingButton/styles.tsx @@ -17,7 +17,7 @@ const PlusIcon = ({ color }: { color: string }) => ( x2="15" y2="25" stroke={color} - strokeWidth="3 " + strokeWidth="3" strokeLinecap="round" /> Date: Sat, 23 Nov 2024 03:43:43 +0100 Subject: [PATCH 05/15] chore: renamed --- src/store/queries/notes/index.ts | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/store/queries/notes/index.ts b/src/store/queries/notes/index.ts index defd9e9..16d2ab2 100644 --- a/src/store/queries/notes/index.ts +++ b/src/store/queries/notes/index.ts @@ -34,11 +34,9 @@ export const noteApi = createApi({ const newId = notesMockData.length ? Math.max(...notesMockData.map(n => n.id)) + 1 : 1; - const createdNote = { ...newNote, id: newId, modifiedDate: Date.now() }; - - notesMockData.push(createdNote); - - return { data: createdNote }; + const note = { ...newNote, id: newId, modifiedDate: Date.now() }; + notesMockData.push(note); + return { data: note }; }, }), updateNote: builder.mutation & { id?: number }>({ From b0fed36140e4ff0b2ecdcd7a47fb1c473ad2d7f2 Mon Sep 17 00:00:00 2001 From: Alan Fernandez Date: Sat, 23 Nov 2024 11:35:19 +0100 Subject: [PATCH 06/15] chore: fix tests --- __tests__/HomeScreen.test.tsx | 1 - __tests__/NoteDetailsScreen.test.tsx | 8 ++++++++ src/screens/HomeScreen/components/SearchBox/index.tsx | 1 + 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/__tests__/HomeScreen.test.tsx b/__tests__/HomeScreen.test.tsx index 2175456..d3b44c5 100644 --- a/__tests__/HomeScreen.test.tsx +++ b/__tests__/HomeScreen.test.tsx @@ -9,7 +9,6 @@ import foldersMockData from '@store/mockData/folders.mockData'; import { RootState } from '@store/index'; jest.mock('@screens/HomeScreen/components/FolderList', () => 'FolderList'); -jest.mock('@screens/HomeScreen/components/SearchBox', () => 'SearchBox'); jest.mock( '@screens/HomeScreen/components/NotesResultsList', () => 'NotesResultsList', diff --git a/__tests__/NoteDetailsScreen.test.tsx b/__tests__/NoteDetailsScreen.test.tsx index a663e54..75f3bfb 100644 --- a/__tests__/NoteDetailsScreen.test.tsx +++ b/__tests__/NoteDetailsScreen.test.tsx @@ -32,6 +32,14 @@ jest.mock('@store/queries/notes', () => ({ useCreateNoteMutation: jest.fn(() => [jest.fn().mockResolvedValue({})]), })); +jest.mock('@react-navigation/native', () => { + const actual = jest.requireActual('@react-navigation/native'); + return { + ...actual, + useNavigation: jest.fn(), + }; +}); + const mockStore = configureStore([]); const mockNavigate = jest.fn(); const mockNavigation = { navigate: mockNavigate }; diff --git a/src/screens/HomeScreen/components/SearchBox/index.tsx b/src/screens/HomeScreen/components/SearchBox/index.tsx index d905182..750905b 100644 --- a/src/screens/HomeScreen/components/SearchBox/index.tsx +++ b/src/screens/HomeScreen/components/SearchBox/index.tsx @@ -25,6 +25,7 @@ const SearchBox: React.FC = ({ )} Date: Sat, 23 Nov 2024 11:56:26 +0100 Subject: [PATCH 07/15] chore: deprecate redux-mock-store --- __tests__/HomeScreen.test.tsx | 22 +++++++------ __tests__/NoteDetailsScreen.test.tsx | 37 ++++++++++++++-------- package.json | 2 -- yarn.lock | 47 ---------------------------- 4 files changed, 37 insertions(+), 71 deletions(-) diff --git a/__tests__/HomeScreen.test.tsx b/__tests__/HomeScreen.test.tsx index d3b44c5..9faa9fc 100644 --- a/__tests__/HomeScreen.test.tsx +++ b/__tests__/HomeScreen.test.tsx @@ -1,12 +1,13 @@ import React from 'react'; +import { configureStore } from '@reduxjs/toolkit'; import { Provider } from 'react-redux'; import { NavigationContainer } from '@react-navigation/native'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { fireEvent, render } from '@testing-library/react-native'; -import configureStore, { MockStoreEnhanced } from 'redux-mock-store'; -import HomeScreen from '@screens/HomeScreen'; + +import foldersReducer from '@store/foldersSlice'; import foldersMockData from '@store/mockData/folders.mockData'; -import { RootState } from '@store/index'; +import HomeScreen from '@screens/HomeScreen'; jest.mock('@screens/HomeScreen/components/FolderList', () => 'FolderList'); jest.mock( @@ -24,18 +25,13 @@ jest.mock('react-native-safe-area-context', () => ({ })); describe('HomeScreen', () => { - const mockStore = configureStore>([]); const initialState = { folders: { folders: foldersMockData, }, }; - // eslint-disable-next-line @typescript-eslint/no-unused-vars - let store: MockStoreEnhanced>; - beforeEach(() => { - store = mockStore(initialState); (useSafeAreaInsets as jest.Mock).mockReturnValue({ top: 20, bottom: 0, @@ -48,8 +44,16 @@ describe('HomeScreen', () => { jest.clearAllMocks(); }); + const createTestStore = (preloadedState = {}) => + configureStore({ + reducer: { + folders: foldersReducer, + }, + preloadedState, + }); + const renderWithProviders = (component: React.ReactNode) => { - const store = mockStore(initialState); + const store = createTestStore(initialState); return render( diff --git a/__tests__/NoteDetailsScreen.test.tsx b/__tests__/NoteDetailsScreen.test.tsx index 75f3bfb..3f3ab3d 100644 --- a/__tests__/NoteDetailsScreen.test.tsx +++ b/__tests__/NoteDetailsScreen.test.tsx @@ -1,21 +1,22 @@ import React from 'react'; import { render, fireEvent, waitFor } from '@testing-library/react-native'; -import configureStore from 'redux-mock-store'; import { Provider } from 'react-redux'; +import { configureStore } from '@reduxjs/toolkit'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; - -import NoteDetailsScreen from '@screens/NoteDetailsScreen'; -import { RootStackParamList } from '@navigation/types'; -import { - useFetchNoteDetailsQuery, - useUpdateNoteMutation, -} from '@store/queries/notes'; import { NavigationContainer, RouteProp, useNavigation, } from '@react-navigation/native'; +import foldersReducer from '@store/foldersSlice'; +import { + useFetchNoteDetailsQuery, + useUpdateNoteMutation, +} from '@store/queries/notes'; +import NoteDetailsScreen from '@screens/NoteDetailsScreen'; +import { RootStackParamList } from '@navigation/types'; + jest.mock('@store/queries/notes', () => ({ useFetchNoteDetailsQuery: jest.fn(), useUpdateNoteMutation: jest.fn(() => [jest.fn()]), @@ -40,7 +41,15 @@ jest.mock('@react-navigation/native', () => { }; }); -const mockStore = configureStore([]); +const createTestStore = (preloadedState = {}) => { + return configureStore({ + reducer: { + folders: foldersReducer, + }, + preloadedState, + }); +}; + const mockNavigate = jest.fn(); const mockNavigation = { navigate: mockNavigate }; const mockRoute: RouteProp = { @@ -75,7 +84,7 @@ describe('NoteDetailsScreen', () => { isLoading: true, }); - const store = mockStore({ + const store = createTestStore({ folders: { folders: [ { @@ -86,6 +95,7 @@ describe('NoteDetailsScreen', () => { ], }, }); + const { getByText } = render( @@ -111,7 +121,7 @@ describe('NoteDetailsScreen', () => { isLoading: false, }); - const store = mockStore({ + const store = createTestStore({ folders: { folders: [ { @@ -122,6 +132,7 @@ describe('NoteDetailsScreen', () => { ], }, }); + const { getByDisplayValue } = render( { (useUpdateNoteMutation as jest.Mock).mockReturnValue([mockUpdateNote]); - const store = mockStore({ + const store = createTestStore({ folders: { folders: [ { @@ -197,7 +208,7 @@ describe('NoteDetailsScreen', () => { isLoading: false, }); - const store = mockStore({ + const store = createTestStore({ folders: { folders: [ { diff --git a/package.json b/package.json index 0bab830..20f2aa8 100755 --- a/package.json +++ b/package.json @@ -47,7 +47,6 @@ "@types/react": "^18.3.5", "@types/react-native-vector-icons": "^6.4.18", "@types/react-test-renderer": "^18.3.0", - "@types/redux-mock-store": "^1.5.0", "@typescript-eslint/eslint-plugin": "canary", "@typescript-eslint/parser": "canary", "babel-jest": "^29.7.0", @@ -60,7 +59,6 @@ "metro-react-native-babel-preset": "^0.77.0", "prettier": "^3.3.3", "react-test-renderer": "^18.3.1", - "redux-mock-store": "^1.5.5", "typescript": "^5.6.2", "typescript-eslint": "^8.14.0" }, diff --git a/yarn.lock b/yarn.lock index e00b1e7..7980bd1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1636,15 +1636,6 @@ __metadata: languageName: node linkType: hard -"@babel/runtime@npm:^7.9.2": - version: 7.26.0 - resolution: "@babel/runtime@npm:7.26.0" - dependencies: - regenerator-runtime: ^0.14.0 - checksum: c8e2c0504ab271b3467a261a8f119bf2603eb857a0d71e37791f4e3fae00f681365073cc79f141ddaa90c6077c60ba56448004ad5429d07ac73532be9f7cf28a - languageName: node - linkType: hard - "@babel/template@npm:^7.0.0, @babel/template@npm:^7.24.7, @babel/template@npm:^7.25.0, @babel/template@npm:^7.3.3": version: 7.25.0 resolution: "@babel/template@npm:7.25.0" @@ -3009,15 +3000,6 @@ __metadata: languageName: node linkType: hard -"@types/redux-mock-store@npm:^1.5.0": - version: 1.5.0 - resolution: "@types/redux-mock-store@npm:1.5.0" - dependencies: - redux: ^4.0.5 - checksum: 82248c276fed1303c4c85b38cdb9604b6ea1118b5c9a6968e84dd9820ea8c08dd1d8d2fcbf940ac2f6b379d88d87aaa05d2787e6841b65ccfee09cdf23b4c32b - languageName: node - linkType: hard - "@types/semver@npm:^7.3.12": version: 7.5.8 resolution: "@types/semver@npm:7.5.8" @@ -7487,7 +7469,6 @@ __metadata: "@types/react": ^18.3.5 "@types/react-native-vector-icons": ^6.4.18 "@types/react-test-renderer": ^18.3.0 - "@types/redux-mock-store": ^1.5.0 "@types/styled-components": ^5.1.34 "@typescript-eslint/eslint-plugin": canary "@typescript-eslint/parser": canary @@ -7514,7 +7495,6 @@ __metadata: react-native-vector-icons: ^10.2.0 react-redux: ^9.1.2 react-test-renderer: ^18.3.1 - redux-mock-store: ^1.5.5 styled-components: ^6.1.13 typescript: ^5.6.2 typescript-eslint: ^8.14.0 @@ -7604,13 +7584,6 @@ __metadata: languageName: node linkType: hard -"lodash.isplainobject@npm:^4.0.6": - version: 4.0.6 - resolution: "lodash.isplainobject@npm:4.0.6" - checksum: 29c6351f281e0d9a1d58f1a4c8f4400924b4c79f18dfc4613624d7d54784df07efaff97c1ff2659f3e085ecf4fff493300adc4837553104cef2634110b0d5337 - languageName: node - linkType: hard - "lodash.merge@npm:^4.6.2": version: 4.6.2 resolution: "lodash.merge@npm:4.6.2" @@ -9318,17 +9291,6 @@ __metadata: languageName: node linkType: hard -"redux-mock-store@npm:^1.5.5": - version: 1.5.5 - resolution: "redux-mock-store@npm:1.5.5" - dependencies: - lodash.isplainobject: ^4.0.6 - peerDependencies: - redux: "*" - checksum: a6e1dda7abc96066344b7f9fa8726c40dff6ea31b8393ead26c0525663528af257bfef85c38af8beb4e549c189bd90fde5dcb5729b16057de7fffc0c43881ca6 - languageName: node - linkType: hard - "redux-thunk@npm:^3.1.0": version: 3.1.0 resolution: "redux-thunk@npm:3.1.0" @@ -9338,15 +9300,6 @@ __metadata: languageName: node linkType: hard -"redux@npm:^4.0.5": - version: 4.2.1 - resolution: "redux@npm:4.2.1" - dependencies: - "@babel/runtime": ^7.9.2 - checksum: f63b9060c3a1d930ae775252bb6e579b42415aee7a23c4114e21a0b4ba7ec12f0ec76936c00f546893f06e139819f0e2855e0d55ebfce34ca9c026241a6950dd - languageName: node - linkType: hard - "redux@npm:^5.0.1": version: 5.0.1 resolution: "redux@npm:5.0.1" From 09ef6de184f907e8f251983f621dd6a9b1396f5e Mon Sep 17 00:00:00 2001 From: Alan Fernandez Date: Sat, 23 Nov 2024 12:31:11 +0100 Subject: [PATCH 08/15] chore: improve folders data structure --- __tests__/HomeScreen.test.tsx | 6 +-- __tests__/NoteDetailsScreen.test.tsx | 64 +++++++++++-------------- src/screens/HomeScreen/index.tsx | 2 +- src/screens/NoteDetailsScreen/index.tsx | 2 +- src/store/foldersSlice.ts | 23 +++------ 5 files changed, 39 insertions(+), 58 deletions(-) diff --git a/__tests__/HomeScreen.test.tsx b/__tests__/HomeScreen.test.tsx index 9faa9fc..c831f98 100644 --- a/__tests__/HomeScreen.test.tsx +++ b/__tests__/HomeScreen.test.tsx @@ -26,9 +26,7 @@ jest.mock('react-native-safe-area-context', () => ({ describe('HomeScreen', () => { const initialState = { - folders: { - folders: foldersMockData, - }, + folders: foldersMockData, }; beforeEach(() => { @@ -44,7 +42,7 @@ describe('HomeScreen', () => { jest.clearAllMocks(); }); - const createTestStore = (preloadedState = {}) => + const createTestStore = (preloadedState = initialState) => configureStore({ reducer: { folders: foldersReducer, diff --git a/__tests__/NoteDetailsScreen.test.tsx b/__tests__/NoteDetailsScreen.test.tsx index 3f3ab3d..21d3f59 100644 --- a/__tests__/NoteDetailsScreen.test.tsx +++ b/__tests__/NoteDetailsScreen.test.tsx @@ -85,15 +85,13 @@ describe('NoteDetailsScreen', () => { }); const store = createTestStore({ - folders: { - folders: [ - { - id: 1, - folderName: 'Test Folder', - notes: [], - }, - ], - }, + folders: [ + { + id: 1, + folderName: 'Test Folder', + notes: [], + }, + ], }); const { getByText } = render( @@ -122,15 +120,13 @@ describe('NoteDetailsScreen', () => { }); const store = createTestStore({ - folders: { - folders: [ - { - id: 1, - folderName: 'Test Folder', - notes: [], - }, - ], - }, + folders: [ + { + id: 1, + folderName: 'Test Folder', + notes: [], + }, + ], }); const { getByDisplayValue } = render( @@ -161,15 +157,13 @@ describe('NoteDetailsScreen', () => { (useUpdateNoteMutation as jest.Mock).mockReturnValue([mockUpdateNote]); const store = createTestStore({ - folders: { - folders: [ - { - id: 1, - folderName: 'Test Folder', - notes: [], - }, - ], - }, + folders: [ + { + id: 1, + folderName: 'Test Folder', + notes: [], + }, + ], }); const { getByDisplayValue, getByTestId } = render( @@ -209,15 +203,13 @@ describe('NoteDetailsScreen', () => { }); const store = createTestStore({ - folders: { - folders: [ - { - id: 1, - folderName: 'Test Folder', - notes: [], - }, - ], - }, + folders: [ + { + id: 1, + folderName: 'Test Folder', + notes: [], + }, + ], }); const { getByText } = render( diff --git a/src/screens/HomeScreen/index.tsx b/src/screens/HomeScreen/index.tsx index a4929ed..9dae78b 100755 --- a/src/screens/HomeScreen/index.tsx +++ b/src/screens/HomeScreen/index.tsx @@ -13,7 +13,7 @@ import FloatingButton from './components/FloatingButton'; const HomeScreen: React.FC = () => { const insets = useSafeAreaInsets(); - const folders = useSelector((state: RootState) => state.folders.folders); + const folders = useSelector((state: RootState) => state.folders); const navigation = useNavigation(); const [searchQuery, setSearchQuery] = useState(''); diff --git a/src/screens/NoteDetailsScreen/index.tsx b/src/screens/NoteDetailsScreen/index.tsx index ad5b1cc..6efa14e 100644 --- a/src/screens/NoteDetailsScreen/index.tsx +++ b/src/screens/NoteDetailsScreen/index.tsx @@ -33,7 +33,7 @@ type NoteDetailsScreenProps = StackScreenProps< const NoteDetailsScreen: React.FC = ({ route }) => { const { noteId = 0, isNew } = route.params; const insets = useSafeAreaInsets(); - const folders = useSelector((state: RootState) => state.folders.folders); + const folders = useSelector((state: RootState) => state.folders); const dispatch = useDispatch(); const { data: note, isLoading } = useFetchNoteDetailsQuery(noteId, { diff --git a/src/store/foldersSlice.ts b/src/store/foldersSlice.ts index 6fdb6c0..2c9d208 100644 --- a/src/store/foldersSlice.ts +++ b/src/store/foldersSlice.ts @@ -7,39 +7,30 @@ export interface Folder { notes: NoteListItem[]; } -interface FoldersState { - folders: Folder[]; -} +type FoldersState = Folder[]; -const initialState: FoldersState = { - folders: [], -}; +const initialState: FoldersState = []; const foldersSlice = createSlice({ name: 'folders', initialState, reducers: { - setFolders: (state, action: PayloadAction) => { - state.folders = action.payload; - }, + setFolders: (state, action: PayloadAction) => action.payload, addFolder: (state, action: PayloadAction) => { - state.folders.push(action.payload); + state.push(action.payload); }, addNoteToFolder: ( state, action: PayloadAction<{ folderId: number; note: NoteListItem }>, ) => { const { folderId, note } = action.payload; - const folder = state.folders.find(f => f.id === folderId); + const folder = state.find(f => f.id === folderId); if (folder) { folder.notes.push(note); } }, - removeFolder: (state, action: PayloadAction) => { - state.folders = state.folders.filter( - folder => folder.id !== action.payload, - ); - }, + removeFolder: (state, action: PayloadAction) => + state.filter(folder => folder.id !== action.payload), }, }); From 8bde0ed3bc91db484cd035e921892cba960c0c09 Mon Sep 17 00:00:00 2001 From: Alan Fernandez Date: Mon, 25 Nov 2024 21:49:46 +0100 Subject: [PATCH 09/15] feat: wip - audio recorder hook --- .../FloatingButton/hooks/useAudioRecorder.ts | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 src/screens/HomeScreen/components/FloatingButton/hooks/useAudioRecorder.ts diff --git a/src/screens/HomeScreen/components/FloatingButton/hooks/useAudioRecorder.ts b/src/screens/HomeScreen/components/FloatingButton/hooks/useAudioRecorder.ts new file mode 100644 index 0000000..378deea --- /dev/null +++ b/src/screens/HomeScreen/components/FloatingButton/hooks/useAudioRecorder.ts @@ -0,0 +1,39 @@ +import { useState } from 'react'; + +export const useAudioRecorder = () => { + const recorder = {}; + const [isRecording, setIsRecording] = useState(false); + const [recordingPath, setRecordingPath] = useState(null); + + const startRecording = async () => { + try { + setIsRecording(true); + const path = 'audioRecord.m4a'; + console.log(path); + /** + * TODO: + * 1. build path + * 2. record audio + * 3. get uri? + */ + } catch (error) { + console.error('Failed to start recording:', error); + setIsRecording(false); + } + }; + + const stopRecording = async () => { + if (!isRecording) return; + + try { + const uri = 'uri'; + setIsRecording(false); + console.log('Audio saved at:', uri); + return uri; + } catch (error) { + console.error('Failed to stop recording:', error); + } + }; + + return { startRecording, stopRecording, isRecording, recordingPath }; +}; From da69bdaa14e18697b2216471e35737e1bca64a68 Mon Sep 17 00:00:00 2001 From: Alan Fernandez Date: Fri, 6 Dec 2024 10:40:51 +0100 Subject: [PATCH 10/15] chore: add rn audio recorder player --- package.json | 1 + yarn.lock | 22 ++++++++++++++++++++++ 2 files changed, 23 insertions(+) diff --git a/package.json b/package.json index 20f2aa8..c9df27f 100755 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "eslint-plugin-prettier": "^5.2.1", "react": "18.3.1", "react-native": "0.75.3", + "react-native-audio-recorder-player": "^3.6.12", "react-native-gesture-handler": "^2.19.0", "react-native-paper": "^5.12.5", "react-native-safe-area-context": "^4.11.0", diff --git a/yarn.lock b/yarn.lock index 7980bd1..d8de55c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6210,6 +6210,15 @@ __metadata: languageName: node linkType: hard +"hyochan-welcome@npm:^1.0.0": + version: 1.0.1 + resolution: "hyochan-welcome@npm:1.0.1" + bin: + hyochan-welcome: bin/hello.js + checksum: 2ea7093b54eefcde4c65e4a8ffd3f3926a55ff0bce20a3f5142fe1108e76c0dfcb98517b47714c536f9de8302b92ae02bf240891d3114d527ccd497e2ea9478e + languageName: node + linkType: hard + "iconv-lite@npm:^0.6.2": version: 0.6.3 resolution: "iconv-lite@npm:0.6.3" @@ -7485,6 +7494,7 @@ __metadata: prettier: ^3.3.3 react: 18.3.1 react-native: 0.75.3 + react-native-audio-recorder-player: ^3.6.12 react-native-gesture-handler: ^2.19.0 react-native-paper: ^5.12.5 react-native-safe-area-context: ^4.11.0 @@ -9008,6 +9018,18 @@ __metadata: languageName: node linkType: hard +"react-native-audio-recorder-player@npm:^3.6.12": + version: 3.6.12 + resolution: "react-native-audio-recorder-player@npm:3.6.12" + dependencies: + hyochan-welcome: ^1.0.0 + peerDependencies: + react: "*" + react-native: "*" + checksum: a0c74694fe54eacd8f705b38d77d8ee35ddb9e964243ac0a727483d001e7ad24178b5ed3edcb0b4ddccf87d65af3233a823d5dd73fc98099ddbe0602134586e7 + languageName: node + linkType: hard + "react-native-gesture-handler@npm:^2.19.0": version: 2.19.0 resolution: "react-native-gesture-handler@npm:2.19.0" From c404a3e59e3c5e4683e8b2f39ee8641048b386ce Mon Sep 17 00:00:00 2001 From: Alan Fernandez Date: Fri, 6 Dec 2024 10:55:38 +0100 Subject: [PATCH 11/15] chore: rn permissions config --- android/app/src/main/AndroidManifest.xml | 1 + ios/Podfile | 43 ++++++++++++++++--- ios/Podfile.lock | 14 +++++- .../xcshareddata/IDEWorkspaceChecks.plist | 8 ++++ ios/kiByte/Info.plist | 2 + package.json | 1 + src/screens/NoteDetailsScreen/index.tsx | 1 + yarn.lock | 15 +++++++ 8 files changed, 78 insertions(+), 7 deletions(-) create mode 100644 ios/kiByte.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index e189252..01f86c1 100755 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -1,6 +1,7 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/ios/kiByte/Info.plist b/ios/kiByte/Info.plist index 99691b7..365fe88 100755 --- a/ios/kiByte/Info.plist +++ b/ios/kiByte/Info.plist @@ -34,6 +34,8 @@ NSLocationWhenInUseUsageDescription + NSMicrophoneUsageDescription + Give $(PRODUCT_NAME) permission to use your microphone. Your record wont be shared without your permission. UILaunchStoryboardName LaunchScreen UIRequiredDeviceCapabilities diff --git a/package.json b/package.json index c9df27f..1a49bdb 100755 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ "react-native-audio-recorder-player": "^3.6.12", "react-native-gesture-handler": "^2.19.0", "react-native-paper": "^5.12.5", + "react-native-permissions": "^5.2.1", "react-native-safe-area-context": "^4.11.0", "react-native-safe-area-view": "^1.1.1", "react-native-screens": "^3.34.0", diff --git a/src/screens/NoteDetailsScreen/index.tsx b/src/screens/NoteDetailsScreen/index.tsx index 6efa14e..b35fa52 100644 --- a/src/screens/NoteDetailsScreen/index.tsx +++ b/src/screens/NoteDetailsScreen/index.tsx @@ -120,6 +120,7 @@ const NoteDetailsScreen: React.FC = ({ route }) => { } } }; + if (!isNew && isLoading) { return Loading...; } diff --git a/yarn.lock b/yarn.lock index d8de55c..149bfb8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7497,6 +7497,7 @@ __metadata: react-native-audio-recorder-player: ^3.6.12 react-native-gesture-handler: ^2.19.0 react-native-paper: ^5.12.5 + react-native-permissions: ^5.2.1 react-native-safe-area-context: ^4.11.0 react-native-safe-area-view: ^1.1.1 react-native-screens: ^3.34.0 @@ -9061,6 +9062,20 @@ __metadata: languageName: node linkType: hard +"react-native-permissions@npm:^5.2.1": + version: 5.2.1 + resolution: "react-native-permissions@npm:5.2.1" + peerDependencies: + react: ">=18.1.0" + react-native: ">=0.70.0" + react-native-windows: ">=0.70.0" + peerDependenciesMeta: + react-native-windows: + optional: true + checksum: a57b0a6c71c38bb126e48e76b8eef7e62bee8083d748ec1cf7e7b5699d46867b10e96a02837c45a045b77dab37e45a5c20b1deb3637ad5cee963c9e92c6ee05a + languageName: node + linkType: hard + "react-native-safe-area-context@npm:^4.11.0": version: 4.11.0 resolution: "react-native-safe-area-context@npm:4.11.0" From c4cf468645f1ed63e4889e7d847761941683184f Mon Sep 17 00:00:00 2001 From: Alan Fernandez Date: Fri, 6 Dec 2024 10:59:14 +0100 Subject: [PATCH 12/15] chore: permissions config to enable rn audio recorder features --- android/app/src/main/AndroidManifest.xml | 2 ++ android/build.gradle | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 01f86c1..bbb130a 100755 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -2,6 +2,8 @@ + + Date: Fri, 6 Dec 2024 11:01:48 +0100 Subject: [PATCH 13/15] feat: wip - audio recorder hook --- .../FloatingButton/hooks/useAudioRecorder.ts | 44 +++++++++++++++---- 1 file changed, 36 insertions(+), 8 deletions(-) diff --git a/src/screens/HomeScreen/components/FloatingButton/hooks/useAudioRecorder.ts b/src/screens/HomeScreen/components/FloatingButton/hooks/useAudioRecorder.ts index 378deea..24745d8 100644 --- a/src/screens/HomeScreen/components/FloatingButton/hooks/useAudioRecorder.ts +++ b/src/screens/HomeScreen/components/FloatingButton/hooks/useAudioRecorder.ts @@ -1,21 +1,49 @@ import { useState } from 'react'; +import { PermissionsAndroid, Platform } from 'react-native'; +import AudioRecorderPlayer from 'react-native-audio-recorder-player'; + +const audioRecorderPlayer = new AudioRecorderPlayer(); export const useAudioRecorder = () => { - const recorder = {}; const [isRecording, setIsRecording] = useState(false); const [recordingPath, setRecordingPath] = useState(null); const startRecording = async () => { try { + // TODO: move this to permissions helper + if (Platform.OS === 'android') { + try { + const grants = await PermissionsAndroid.requestMultiple([ + PermissionsAndroid.PERMISSIONS.WRITE_EXTERNAL_STORAGE, + PermissionsAndroid.PERMISSIONS.READ_EXTERNAL_STORAGE, + PermissionsAndroid.PERMISSIONS.RECORD_AUDIO, + ]); + + console.log('write external storage', grants); + + if ( + grants['android.permission.WRITE_EXTERNAL_STORAGE'] === + PermissionsAndroid.RESULTS.GRANTED && + grants['android.permission.READ_EXTERNAL_STORAGE'] === + PermissionsAndroid.RESULTS.GRANTED && + grants['android.permission.RECORD_AUDIO'] === + PermissionsAndroid.RESULTS.GRANTED + ) { + console.log('Permissions granted'); + } else { + console.log('All required permissions not granted'); + return; + } + } catch (err) { + console.warn(err); + return; + } + } + setIsRecording(true); const path = 'audioRecord.m4a'; - console.log(path); - /** - * TODO: - * 1. build path - * 2. record audio - * 3. get uri? - */ + const uri = await audioRecorderPlayer.startRecorder(path); + setRecordingPath(uri); } catch (error) { console.error('Failed to start recording:', error); setIsRecording(false); From bd5769df0744371f2e8bdfe355b8fa3fd7fdf20e Mon Sep 17 00:00:00 2001 From: Alan Fernandez Date: Fri, 6 Dec 2024 11:06:44 +0100 Subject: [PATCH 14/15] chore: permissions refactor --- .../FloatingButton/hooks/useAudioRecorder.ts | 36 ++++--------------- src/utils/hooks/usePermissions.ts | 33 +++++++++++++++++ src/utils/index.ts | 28 +-------------- src/utils/time.ts | 27 ++++++++++++++ 4 files changed, 68 insertions(+), 56 deletions(-) create mode 100644 src/utils/hooks/usePermissions.ts create mode 100644 src/utils/time.ts diff --git a/src/screens/HomeScreen/components/FloatingButton/hooks/useAudioRecorder.ts b/src/screens/HomeScreen/components/FloatingButton/hooks/useAudioRecorder.ts index 24745d8..7610705 100644 --- a/src/screens/HomeScreen/components/FloatingButton/hooks/useAudioRecorder.ts +++ b/src/screens/HomeScreen/components/FloatingButton/hooks/useAudioRecorder.ts @@ -1,43 +1,21 @@ import { useState } from 'react'; -import { PermissionsAndroid, Platform } from 'react-native'; import AudioRecorderPlayer from 'react-native-audio-recorder-player'; +import { usePermissions } from '@root/src/utils/hooks/usePermissions'; + const audioRecorderPlayer = new AudioRecorderPlayer(); export const useAudioRecorder = () => { const [isRecording, setIsRecording] = useState(false); const [recordingPath, setRecordingPath] = useState(null); + const { requestAudioPermissions } = usePermissions(); const startRecording = async () => { try { - // TODO: move this to permissions helper - if (Platform.OS === 'android') { - try { - const grants = await PermissionsAndroid.requestMultiple([ - PermissionsAndroid.PERMISSIONS.WRITE_EXTERNAL_STORAGE, - PermissionsAndroid.PERMISSIONS.READ_EXTERNAL_STORAGE, - PermissionsAndroid.PERMISSIONS.RECORD_AUDIO, - ]); - - console.log('write external storage', grants); - - if ( - grants['android.permission.WRITE_EXTERNAL_STORAGE'] === - PermissionsAndroid.RESULTS.GRANTED && - grants['android.permission.READ_EXTERNAL_STORAGE'] === - PermissionsAndroid.RESULTS.GRANTED && - grants['android.permission.RECORD_AUDIO'] === - PermissionsAndroid.RESULTS.GRANTED - ) { - console.log('Permissions granted'); - } else { - console.log('All required permissions not granted'); - return; - } - } catch (err) { - console.warn(err); - return; - } + const hasPermissions = await requestAudioPermissions(); + if (!hasPermissions) { + console.log('Permissions not granted'); + return; } setIsRecording(true); diff --git a/src/utils/hooks/usePermissions.ts b/src/utils/hooks/usePermissions.ts new file mode 100644 index 0000000..2f7feb5 --- /dev/null +++ b/src/utils/hooks/usePermissions.ts @@ -0,0 +1,33 @@ +import { PermissionsAndroid, Platform } from 'react-native'; + +export const usePermissions = () => { + const requestAudioPermissions = async (): Promise => { + if (Platform.OS === 'android') { + try { + const grants = await PermissionsAndroid.requestMultiple([ + PermissionsAndroid.PERMISSIONS.WRITE_EXTERNAL_STORAGE, + PermissionsAndroid.PERMISSIONS.READ_EXTERNAL_STORAGE, + PermissionsAndroid.PERMISSIONS.RECORD_AUDIO, + ]); + + console.log('Permissions status:', grants); + + return ( + grants['android.permission.WRITE_EXTERNAL_STORAGE'] === + PermissionsAndroid.RESULTS.GRANTED && + grants['android.permission.READ_EXTERNAL_STORAGE'] === + PermissionsAndroid.RESULTS.GRANTED && + grants['android.permission.RECORD_AUDIO'] === + PermissionsAndroid.RESULTS.GRANTED + ); + } catch (err) { + console.warn('Permission request failed:', err); + return false; + } + } + + return true; + }; + + return { requestAudioPermissions }; +}; diff --git a/src/utils/index.ts b/src/utils/index.ts index dc31a10..bd2fc55 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -1,27 +1 @@ -function formatTimestampToDate(timestamp: number): string { - const date = new Date(timestamp); - return new Intl.DateTimeFormat('en-US', { - month: 'short', // "Jan" - day: 'numeric', // "6" - year: 'numeric', // "2023" - }).format(date); -} - -function formatTimestampToDateTime(timestamp: number): string { - const date = new Date(timestamp); - const formattedDate = new Intl.DateTimeFormat('en-US', { - month: 'short', // "Jan" - day: 'numeric', // "6" - year: 'numeric', // "2023" - }).format(date); - - const formattedTime = new Intl.DateTimeFormat('en-US', { - hour: 'numeric', // "6" - minute: 'numeric', // "49" - hour12: true, // "AM/PM" - }).format(date); - - return `${formattedDate} at ${formattedTime}`; -} - -export { formatTimestampToDate, formatTimestampToDateTime }; +export { formatTimestampToDate, formatTimestampToDateTime } from './time'; diff --git a/src/utils/time.ts b/src/utils/time.ts new file mode 100644 index 0000000..dc31a10 --- /dev/null +++ b/src/utils/time.ts @@ -0,0 +1,27 @@ +function formatTimestampToDate(timestamp: number): string { + const date = new Date(timestamp); + return new Intl.DateTimeFormat('en-US', { + month: 'short', // "Jan" + day: 'numeric', // "6" + year: 'numeric', // "2023" + }).format(date); +} + +function formatTimestampToDateTime(timestamp: number): string { + const date = new Date(timestamp); + const formattedDate = new Intl.DateTimeFormat('en-US', { + month: 'short', // "Jan" + day: 'numeric', // "6" + year: 'numeric', // "2023" + }).format(date); + + const formattedTime = new Intl.DateTimeFormat('en-US', { + hour: 'numeric', // "6" + minute: 'numeric', // "49" + hour12: true, // "AM/PM" + }).format(date); + + return `${formattedDate} at ${formattedTime}`; +} + +export { formatTimestampToDate, formatTimestampToDateTime }; From 611ce157cbee51a3d487359ea40da98d0104a6aa Mon Sep 17 00:00:00 2001 From: Alan Fernandez Date: Fri, 6 Dec 2024 11:30:23 +0100 Subject: [PATCH 15/15] feat: wip - audio recorder hook --- .../FloatingButton/hooks/useAudioRecorder.ts | 34 +++++++++++++------ .../hooks/useFloatingButtonHandlers.ts | 15 +++++--- 2 files changed, 34 insertions(+), 15 deletions(-) diff --git a/src/screens/HomeScreen/components/FloatingButton/hooks/useAudioRecorder.ts b/src/screens/HomeScreen/components/FloatingButton/hooks/useAudioRecorder.ts index 7610705..8e4eefa 100644 --- a/src/screens/HomeScreen/components/FloatingButton/hooks/useAudioRecorder.ts +++ b/src/screens/HomeScreen/components/FloatingButton/hooks/useAudioRecorder.ts @@ -1,15 +1,20 @@ -import { useState } from 'react'; +import { useEffect, useMemo, useState } from 'react'; import AudioRecorderPlayer from 'react-native-audio-recorder-player'; import { usePermissions } from '@root/src/utils/hooks/usePermissions'; -const audioRecorderPlayer = new AudioRecorderPlayer(); - export const useAudioRecorder = () => { + const audioRecorderPlayer = useMemo(() => new AudioRecorderPlayer(), []); const [isRecording, setIsRecording] = useState(false); - const [recordingPath, setRecordingPath] = useState(null); + const [recordingResult, setRecordingResult] = useState(null); const { requestAudioPermissions } = usePermissions(); + useEffect(() => { + return () => { + audioRecorderPlayer.removeRecordBackListener(); + }; + }, [audioRecorderPlayer]); + const startRecording = async () => { try { const hasPermissions = await requestAudioPermissions(); @@ -19,9 +24,14 @@ export const useAudioRecorder = () => { } setIsRecording(true); - const path = 'audioRecord.m4a'; - const uri = await audioRecorderPlayer.startRecorder(path); - setRecordingPath(uri); + const result = await audioRecorderPlayer.startRecorder(); + + audioRecorderPlayer.addRecordBackListener(e => { + console.log(e); + return; + }); + + console.log('🚀 ~ startRecording ~ result:', result); } catch (error) { console.error('Failed to start recording:', error); setIsRecording(false); @@ -32,14 +42,16 @@ export const useAudioRecorder = () => { if (!isRecording) return; try { - const uri = 'uri'; + const result = await audioRecorderPlayer.stopRecorder(); + audioRecorderPlayer.removeRecordBackListener(); + setIsRecording(false); - console.log('Audio saved at:', uri); - return uri; + setRecordingResult(result); + console.log('🚀 ~ stopRecording ~ result:', result); } catch (error) { console.error('Failed to stop recording:', error); } }; - return { startRecording, stopRecording, isRecording, recordingPath }; + return { startRecording, stopRecording, isRecording, recordingResult }; }; diff --git a/src/screens/HomeScreen/components/FloatingButton/hooks/useFloatingButtonHandlers.ts b/src/screens/HomeScreen/components/FloatingButton/hooks/useFloatingButtonHandlers.ts index 15fa96b..7e6b410 100644 --- a/src/screens/HomeScreen/components/FloatingButton/hooks/useFloatingButtonHandlers.ts +++ b/src/screens/HomeScreen/components/FloatingButton/hooks/useFloatingButtonHandlers.ts @@ -1,5 +1,6 @@ import { useRef, useState } from 'react'; import { Animated } from 'react-native'; +import { useAudioRecorder } from './useAudioRecorder'; const pressAnimationDuration = 190; const releaseAnimationDuration = 150; @@ -13,6 +14,8 @@ export const useFloatingButtonHandlers = ({ onPress, onLongPress, }: UseFloatingButtonHandlersProps) => { + const { startRecording, stopRecording, isRecording } = useAudioRecorder(); + const [isLongPressed, setIsLongPressed] = useState(false); const bottomPositionAnim = useRef(new Animated.Value(25)).current; @@ -28,7 +31,7 @@ export const useFloatingButtonHandlers = ({ }).start(); }; - const handleLongPress = () => { + const handleLongPress = async () => { setIsLongPressed(true); Animated.parallel([ Animated.timing(bottomPositionAnim, { @@ -53,11 +56,14 @@ export const useFloatingButtonHandlers = ({ }), ]).start(); - onLongPress(); + if (onLongPress) onLongPress(); + await startRecording(); }; - const handlePressOut = () => { - if (!isLongPressed) onPress(); + const handlePressOut = async () => { + if (isLongPressed) { + await stopRecording(); + } else onPress(); Animated.parallel([ Animated.timing(bottomPositionAnim, { @@ -94,6 +100,7 @@ export const useFloatingButtonHandlers = ({ return { isLongPressed, + isRecording, animatedStyles, handlePressIn, handlePressOut,