Skip to content

Commit

Permalink
Podcast support
Browse files Browse the repository at this point in the history
  • Loading branch information
markharding committed Nov 27, 2024
1 parent a640bc0 commit b0cbe5d
Show file tree
Hide file tree
Showing 44 changed files with 2,342 additions and 2,608 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ Clone locale\

## Building

- `yarn android` or `yarn ios`
- `npx expo run:android` or `npx expo run:ios`

## Testing

Expand Down
28 changes: 28 additions & 0 deletions __mocks__/react-native-track-player.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
export const useActiveTrack = jest.fn();
export const useIsPlaying = jest.fn(() => ({}));
export const usePlaybackState = jest.fn();
export const useProgress = jest.fn(() => ({}));

export const State = {
Paused: 'paused',
Ready: 'ready',
Ended: 'ended',
};

export default {
add: jest.fn(),
remove: jest.fn(),
load: jest.fn(),
skip: jest.fn(),
seekTo: jest.fn(),
seekBy: jest.fn(),
setupPlayer: jest.fn(() => Promise.resolve()),
destroy: jest.fn(),
reset: jest.fn(),
play: jest.fn(),
pause: jest.fn(),
stop: jest.fn(),
getQueue: jest.fn(() => Promise.resolve([])),
setQueue: jest.fn(() => Promise.resolve()),
getActiveTrackIndex: jest.fn(() => Promise.resolve(undefined)),
};
2 changes: 1 addition & 1 deletion __tests__/blogs/__snapshots__/BlogCard.js.snap
Original file line number Diff line number Diff line change
Expand Up @@ -337,7 +337,7 @@ exports[`blog card component should render correctly 1`] = `
]
}
>
Apr 27 2018 · 14:17
Apr 27 2018 · 07:17
</Text>
<View
style={
Expand Down
1 change: 1 addition & 0 deletions app.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,7 @@ export default ({ config }: ConfigContext): ExpoConfig => ({
'This lets you save photos to your camera roll',
NSCameraUsageDescription: cameraMessage,
NSMicrophoneUsageDescription: micMessage,
UIBackgroundModes: ['audio'],
},
splash: {
image: './assets/images/splash.png',
Expand Down
14 changes: 14 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { registerRootComponent } from 'expo';
import { LogBox } from 'react-native';
import App from './App';
import { enableFreeze } from 'react-native-screens';
import TrackPlayer, { Capability } from 'react-native-track-player';

LogBox.ignoreAllLogs();
LogBox.ignoreLogs([
Expand All @@ -34,3 +35,16 @@ enableFreeze(true);
// console.log(`module.exports = ${JSON.stringify(loadedModuleNames.sort())};`);

registerRootComponent(App);

TrackPlayer.registerPlaybackService(() => require('./service'));

TrackPlayer.setupPlayer().then(async () => {
await TrackPlayer.updateOptions({
capabilities: [
Capability.Play,
Capability.Pause,
Capability.SkipToNext,
Capability.SkipToPrevious,
],
});
});
3 changes: 2 additions & 1 deletion locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -1255,7 +1255,8 @@
"settings": "Settings",
"upgrade": "Upgrade",
"terms": "Terms",
"wallet": "Wallet"
"wallet": "Wallet",
"downloadedAudio": "Downloaded Audio"
},
"userTypeAhead": {
"placeholder": "Start typing a username to search...",
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@
"react-native-svg": "^15.6.0",
"react-native-system-setting": "^1.7.6",
"react-native-tab-view": "^3.5.2",
"react-native-track-player": "^4.1.1",
"react-native-url-polyfill": "^2.0.0",
"react-native-vision-camera": "^4.5.2",
"react-native-webview": "13.8.6",
Expand Down
10 changes: 10 additions & 0 deletions service.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import TrackPlayer from 'react-native-track-player';

module.exports = async function () {
TrackPlayer.addEventListener('remote-play', () => TrackPlayer.play());
TrackPlayer.addEventListener('remote-pause', () => TrackPlayer.pause());
TrackPlayer.addEventListener('remote-next', () => TrackPlayer.skipToNext());
TrackPlayer.addEventListener('remote-previous', () =>
TrackPlayer.skipToPrevious(),
);
};
7 changes: 7 additions & 0 deletions src/common/components/MediaView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { showNotification } from '../../../AppMessages';
import MediaViewMultiImage from './media-view/MediaViewMultiImage';
import { copyToClipboard } from '../helpers/copyToClipboard';
import sp from '~/services/serviceProvider';
import InlineAudioPlayer from '~/modules/audio-player/components/InlineAudioPlayer';

type PropsType = {
entity: ActivityModel | CommentModel;
Expand Down Expand Up @@ -105,6 +106,12 @@ export default class MediaView extends Component<PropsType> {
/>
</View>
);
case 'audio':
return (
<View style={[sp.styles.style.fullWidth]}>
<InlineAudioPlayer entity={this.props.entity} />
</View>
);
}

if (this.props.entity.perma_url) {
Expand Down
4 changes: 4 additions & 0 deletions src/common/components/explicit/ExplicitText.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,10 @@ export default class ExplicitText extends Component<PropsType, StateType> {
}
}

if (entity.custom_type === 'audio') {
title = '';
}

let body: React.ReactNode | null = null;
let moreLess: React.ReactNode | null = null;
let explicitToggle = null;
Expand Down
3 changes: 3 additions & 0 deletions src/common/services/audio.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
async function AudioService() {}

export default AudioService;
2 changes: 1 addition & 1 deletion src/common/services/storage/storages.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { MMKV } from 'react-native-mmkv';
/**
* Storage instance
*/
class Storage extends MMKV {
export class Storage extends MMKV {
getObject<T>(key: string): T | undefined {
const data = this.getString(key);
if (data) {
Expand Down
7 changes: 5 additions & 2 deletions src/common/ui/icons/Icon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ export function Icon({
export interface IIconNext extends UIBaseType {
color?: ColorsNameType;
name: IconMapNameType;
size?: UIIconSizeType;
size?: UIIconSizeType | number;
active?: boolean;
light?: boolean;
disabled?: boolean;
Expand Down Expand Up @@ -184,7 +184,10 @@ function IconNextComponent({
iconStyles.push(styles.shadow);
}

const sizeNumeric = ICON_SIZES[size] || ICON_SIZES[ICON_SIZE_DEFAULT];
const sizeNumeric =
typeof size === 'number'
? size
: ICON_SIZES[size] || ICON_SIZES[ICON_SIZE_DEFAULT];

const iconColor = getIconColor({
color,
Expand Down
36 changes: 36 additions & 0 deletions src/common/ui/icons/map.ts
Original file line number Diff line number Diff line change
Expand Up @@ -486,6 +486,42 @@ const ICON_MAP = {
font: 'MaterialIcons',
name: 'local-fire-department',
},
'pause-circle': {
font: 'MaterialIcons',
name: 'pause-circle',
},
'play-circle': {
font: 'MaterialIcons',
name: 'play-circle',
},
'playlist-add': {
font: 'MaterialIcons',
name: 'playlist-add',
},
'playlist-remove': {
font: 'MaterialIcons',
name: 'playlist-remove',
},
'replay-10': {
font: 'MaterialIcons',
name: 'replay-10',
},
'forward-10': {
font: 'MaterialIcons',
name: 'forward-10',
},
'download-for-offline': {
font: 'MaterialIcons',
name: 'download-for-offline',
},
'offline-pin': {
font: 'MaterialIcons',
name: 'offline-pin',
},
downloading: {
font: 'MaterialIcons',
name: 'downloading',
},
} as const;

export type IconNameType = keyof typeof ICON_MAP;
Expand Down
146 changes: 146 additions & 0 deletions src/modules/audio-player/components/AudioQueueItem.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
import { fireEvent, render, waitFor } from '@testing-library/react-native';
import sp from '~/services/serviceProvider';
import TrackPlayer, { Track, useIsPlaying } from 'react-native-track-player';
import { AudioQueueItem } from './AudioQueueItem';
import useIsTrackDownloaded from '../hooks/useIsTrackDownloaded';

jest.mock('~/services/serviceProvider');
sp.mockService('styles');
const audioPlayerMock = sp.mockService('audioPlayer');
sp.mockService('api');
sp.mockService('settings');

jest.mock('../hooks/useIsTrackDownloaded');

const mockTrack: Track = {
id: '123',
url: 'https://fake-track',
};

describe('AudioQueueItem', () => {
beforeEach(() => {
jest.clearAllMocks();
});

it('should render component', () => {
const comp = render(
<AudioQueueItem
trackIndex={0}
track={mockTrack}
onRemoveTrack={() => {}}
/>,
);
expect(comp).toBeTruthy();
});

it('should remove track from the queue', async () => {
const removedCallbackFn = jest.fn();

jest.spyOn(TrackPlayer, 'remove');

const comp = render(
<AudioQueueItem
trackIndex={0}
track={mockTrack}
onRemoveTrack={removedCallbackFn}
/>,
);
const rmvQueueTrackBtn = comp.getByTestId('remove-track');

fireEvent.press(rmvQueueTrackBtn);

await waitFor(() => {
expect(TrackPlayer.remove).toHaveBeenCalledWith(0);
expect(removedCallbackFn).toHaveBeenCalledWith(mockTrack);
});
});

it('should skip to, and play, track if not already playing', async () => {
jest.spyOn(TrackPlayer, 'skip');
jest.spyOn(TrackPlayer, 'play');

const comp = render(
<AudioQueueItem
trackIndex={1}
track={mockTrack}
onRemoveTrack={jest.fn()}
/>,
);
const playBtn = comp.getByTestId('play-track');

fireEvent.press(playBtn);

await waitFor(() => {
expect(TrackPlayer.skip).toHaveBeenCalledWith(1);
expect(TrackPlayer.play).toHaveBeenCalled();
});
});

it('should pause track if already playing', async () => {
jest.spyOn(TrackPlayer, 'skip');
jest.spyOn(TrackPlayer, 'play');
jest.spyOn(TrackPlayer, 'pause');

(TrackPlayer.getActiveTrackIndex as jest.Mock).mockResolvedValue(1);
(useIsPlaying as jest.Mock).mockReturnValue({ playing: true });

const comp = render(
<AudioQueueItem
trackIndex={1}
track={mockTrack}
onRemoveTrack={jest.fn()}
/>,
);
const playBtn = comp.getByTestId('play-track');

await waitFor(() => {});

fireEvent.press(playBtn);

await waitFor(() => {
expect(TrackPlayer.pause).toHaveBeenCalled();
expect(TrackPlayer.skip).not.toHaveBeenCalled();
expect(TrackPlayer.play).not.toHaveBeenCalled();
});
});

it('should download a track', async () => {
jest.spyOn(audioPlayerMock, 'downloadTrack');

const comp = render(
<AudioQueueItem
trackIndex={1}
track={mockTrack}
onRemoveTrack={jest.fn()}
/>,
);
const downloadBtn = comp.getByTestId('download-track');

fireEvent.press(downloadBtn);

await waitFor(() => {
expect(audioPlayerMock.downloadTrack).toHaveBeenCalledWith(mockTrack);
});
});

it('should delete a downloaded a track', async () => {
jest.spyOn(audioPlayerMock, 'deleteTrack');

(useIsTrackDownloaded as jest.Mock).mockReturnValue(true);

const comp = render(
<AudioQueueItem
trackIndex={1}
track={mockTrack}
onRemoveTrack={jest.fn()}
/>,
);
const downloadBtn = comp.getByTestId('download-track');

fireEvent.press(downloadBtn);

await waitFor(() => {
expect(audioPlayerMock.deleteTrack).toHaveBeenCalledWith(mockTrack);
});
});
});
Loading

0 comments on commit b0cbe5d

Please sign in to comment.