Skip to content

Commit

Permalink
Merge pull request #320 from e-picsa/feat/farmer-testimonial-videos
Browse files Browse the repository at this point in the history
Feat(farmer): testimonial videos
  • Loading branch information
chrismclarke authored Aug 12, 2024
2 parents c3e05da + 4cef03d commit c8f7733
Show file tree
Hide file tree
Showing 27 changed files with 278 additions and 97 deletions.
4 changes: 2 additions & 2 deletions apps/picsa-apps/extension-app-native/android/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ android {
applicationId "io.picsa.extension"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 3043004
versionName "3.43.4"
versionCode 3044000
versionName "3.44.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
aaptOptions {
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
Expand Down
Original file line number Diff line number Diff line change
@@ -1 +1,8 @@
<resource-item-file [resource]="videoResource()"></resource-item-file>
@if(videoData()){
<!-- Ignore case where video entry exists but ranking empty (e.g. playlist with multi-country videos) -->
@if(videoResource(); as resource){
<resource-item-file [resource]="resource"></resource-item-file>

} } @else {
<p>Video not found</p>
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import { CommonModule } from '@angular/common';
import { Component, computed, input } from '@angular/core';
import { ConfigurationService } from '@picsa/configuration/src';
import { ILocaleCode } from '@picsa/data';
import { IPicsaVideo, IPicsaVideoData } from '@picsa/data/resources';
import { PICSA_FARMER_VIDEO_RESOURCES_HASHMAP } from '@picsa/data/resources';
import { RESOURCE_VIDEO_HASHMAP } from '@picsa/data/resources';
import { ResourcesComponentsModule } from '@picsa/resources/src/app/components/components.module';

/**
Expand All @@ -25,25 +24,51 @@ export class FarmerStepVideoComponent {
videoData = input.required<IPicsaVideoData>();

videoResource = computed(() => {
const { language_code } = this.configurationService.userSettings();
// HACK - when identifying video to show user cannot rely solely on language_code as
// that populates 'global_en' when different country used (should be zm_en)
// So instead use country_code specified and language part of localeCode
const { country_code: userCountry, language_code } = this.configurationService.userSettings();
const [_, userLanguage] = language_code.split('_');
const availableVideos = this.videoData().children;
const video = this.selectDefaultVideo(language_code, availableVideos);
// HACK - lookup resource entry which should be given by same id
const resource = PICSA_FARMER_VIDEO_RESOURCES_HASHMAP[video.id];
return resource;
// HACK - select best video recommendation. TODO - show toggle options in future
const [video] = this.filterAvailableVideos(userCountry, userLanguage, availableVideos);
if (video) {
// HACK - lookup resource entry which should be given by same id
const resource = RESOURCE_VIDEO_HASHMAP[video.id];
return resource;
}
return undefined;
});

constructor(private configurationService: ConfigurationService) {}

private selectDefaultVideo(locale_code: ILocaleCode, videos: IPicsaVideo[]) {
// prioritise video in same locale
const localeVideo = videos.find((v) => v.locale_code === locale_code);
if (localeVideo) return localeVideo;

// TODO - fallback video to same language different locale
// TODO - track preference for video size (when supported in future, currently all 360p)
private filterAvailableVideos(userCountry: string, userLanguage: string, videos: IPicsaVideo[] = []) {
const rankedVideos = videos
.map((v) => ({ ...v, _rank: getVideoRank(userCountry, userLanguage, v) }))
.filter(({ _rank }) => _rank > 0)
.sort((a, b) => a._rank - b._rank);

// default fallback to first video entry
return videos[0];
return rankedVideos;
}
}

function getVideoRank(userCountry: string, userLanguage: string, video: IPicsaVideo) {
const [audio, subtitle] = video.locale_codes;
const [audioCountry, audioLanguage] = audio.split('_');
const subtitleLanguage = subtitle?.split('_')[1];

// 1 - same country and audio
if (audioCountry === userCountry && audioLanguage === userLanguage) return 1;
// 2 - same country and user language subtitle
if (audioCountry === userCountry && subtitleLanguage === userLanguage) return 2;
// 3 - global video with user language audio
if (audioCountry === 'global' && audioLanguage === userLanguage) return 3;
// 4 - global video with user language subtitle
if (audioCountry === 'global' && subtitleLanguage === userLanguage) return 4;
// 5 - return all videos for global users
if (userCountry === 'global') return 5;
// 6 - return global fallback
if (audioCountry === 'global' && audioLanguage === 'en') return 6;
return -1;
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@
<h2 class="step-title">{{ step.title | translate }}</h2>
<div class="tags-container">
@for(tag of step.tags; track tag){
<span class="tag step-tag">{{ tag.label | translate }}</span>
<span class="tag" [attr.data-color]="tag.color">{{ tag.label | translate }}</span>
} @for(tool of step.tools; track tool){
<span class="tag tool-tag">{{ tool.label | translate }}</span>
<span class="tag">{{ tool.label | translate }}</span>
}
</div>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,12 +79,10 @@ button.mat-mdc-icon-button.nav-button {
background: var(--tag-background, black);
padding: 8px;
color: white;
.step-tag {
}
}
.tag.tool-tag {
.tag {
--tag-background: var(--color-primary);
}
.tag.step-tag {
.tag[data-color='secondary'] {
--tag-background: var(--color-secondary);
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,15 +28,26 @@ <h2 class="title">{{ content.title | translate }}</h2>
<!-- video -->
@case ("video") {
<ng-template mat-tab-label>
<mat-icon class="tab-icon">slideshow</mat-icon>
<mat-icon class="tab-icon">{{ step.tabMatIcon || 'slideshow' }}</mat-icon>
{{ step.tabLabel || 'video' | translate }}
</ng-template>
<div class="tab-content">
@if(step.video){
<farmer-step-video [videoData]="step.video"></farmer-step-video>
}
</div>

}
<!-- video playlist -->
@case ("video_playlist") {
<ng-template mat-tab-label>
<mat-icon class="tab-icon">{{ step.tabMatIcon || 'slideshow' }}</mat-icon>
{{ step.tabLabel || 'video' | translate }}
</ng-template>
<div class="tab-content">
@for(video of step.videos; track video.id){
<farmer-step-video [videoData]="video"></farmer-step-video>
}
</div>
} }
</mat-tab>
}
Expand All @@ -55,7 +66,7 @@ <h2 class="title">{{ content.title | translate }}</h2>
</mat-tab>
}
<!-- User photos -->
@if(photoAlbum(); as album){
@if(content.showReviewSection){ @if(photoAlbum(); as album){
<mat-tab>
<ng-template mat-tab-label>
<mat-icon class="tab-icon">perm_media</mat-icon>
Expand All @@ -67,7 +78,7 @@ <h2 class="title">{{ content.title | translate }}</h2>
</div>
</mat-tab>

}
} }
<!-- @if(tools()[1]; as tool_1){
<mat-tab>
<ng-template mat-tab-label>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<!-- Ready -->
@if(downloadStatus==='ready'){
@if(downloadStatus==='ready' || downloadStatus==='error'){
<button mat-icon-button (click)="downloadResource()" [attr.data-style-variant]="styleVariant">
<div class="download-button-inner">
<mat-icon style="font-size: 30px">download</mat-icon>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,9 @@ export class ResourceItemFileComponent implements OnInit, OnDestroy {

async ngOnDestroy() {
// ensure any created file attachment uris disposed of
this.service.revokeFileAttachmentURIs([this.dbDoc.filename]);
if (this.dbDoc) {
this.service.revokeFileAttachmentURIs([this.dbDoc.filename]);
}
}

/** When attachment state changed attempt to get URI to downloaded file resource */
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,16 @@
import { PICSA_FARMER_VIDEO_RESOURCES_HASHMAP } from '@picsa/data/resources';
import { RESOURCE_VIDEO_HASHMAP } from '@picsa/data/resources';

import { IResourceCollection } from '../../schemas';

/**************************************************************************
* Legacy Resource Format
* Support legacy resources system where each resource child has own db entry
*
* TODO - migrate all resources to use modern format so code below can be removed
***************************************************************************/

const files = Object.keys(RESOURCE_VIDEO_HASHMAP);

/**
* Create a collection to store all farmer videos populated to hardcoded data
*/
Expand All @@ -11,8 +20,11 @@ const picsa_videos_farmer: IResourceCollection = {
type: 'collection',
title: 'Farmer Videos',
description: 'Training videos to support PICSA',
childResources: { collections: [], files: Object.keys(PICSA_FARMER_VIDEO_RESOURCES_HASHMAP), links: [] },
childResources: { collections: [], files, links: [] },
parentCollection: 'picsa_videos',
};

export default { ...PICSA_FARMER_VIDEO_RESOURCES_HASHMAP, picsa_videos_farmer };
export default {
...RESOURCE_VIDEO_HASHMAP,
picsa_videos_farmer,
};
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { Browser } from '@capacitor/browser';
import { Capacitor } from '@capacitor/core';
import { Share } from '@capacitor/share';
import { ConfigurationService } from '@picsa/configuration/src';
import { APP_VERSION } from '@picsa/environments/src';
import { APP_VERSION, ENVIRONMENT } from '@picsa/environments/src';
import { PicsaAsyncService } from '@picsa/shared/services/asyncService.service';
import { AnalyticsService } from '@picsa/shared/services/core/analytics.service';
import { PicsaDatabase_V2_Service, PicsaDatabaseAttachmentService } from '@picsa/shared/services/core/db_v2';
Expand Down Expand Up @@ -162,9 +162,9 @@ export class ResourcesToolService extends PicsaAsyncService {
// TODO - process after cache check
await this.deleteRemovedResources();

// Use caching system to only populate once per app version launch
// Use caching system to only populate once per app version launch in production
const assetsCacheVersion = this.getAssetResourcesVersion();
if (assetsCacheVersion === APP_VERSION.number) {
if (ENVIRONMENT.production && assetsCacheVersion === APP_VERSION.number) {
return;
}
// Update DB with hardcoded entries
Expand Down
14 changes: 11 additions & 3 deletions libs/data/farmer_content/data/content/0_intro.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,23 @@
import { marker as translateMarker } from '@biesbjerg/ngx-translate-extract-marker';

import { IFarmerContent, IFarmerContentStep } from '../../types';
import { PICSA_FARMER_VIDEOS_HASHMAP } from '@picsa/data/resources';
import { PICSA_FARMER_VIDEOS_HASHMAP, PICSA_VIDEO_TESTIMONIAL_HASHMAP } from '@picsa/data/resources';

const steps: IFarmerContentStep[] = [{ type: 'video', video: PICSA_FARMER_VIDEOS_HASHMAP.intro }];
const steps: IFarmerContentStep[] = [
{ type: 'video', video: PICSA_FARMER_VIDEOS_HASHMAP.intro, tabLabel: translateMarker('Intro') },
{
type: 'video_playlist',
videos: Object.values(PICSA_VIDEO_TESTIMONIAL_HASHMAP),
tabLabel: translateMarker('Testimonials'),
tabMatIcon: 'people',
},
];

const content: Omit<IFarmerContent, 'id' | 'icon_path'> = {
slug: 'intro',
title: translateMarker('What is PICSA?'),
tools: [],
tags: [{ label: translateMarker('Tutorials') }],
tags: [{ label: translateMarker('Tutorials'), color: 'secondary' }],
steps,
};
export default content;
3 changes: 2 additions & 1 deletion libs/data/farmer_content/data/content/1_what_you_do.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ const content: Omit<IFarmerContent, 'id' | 'icon_path'> = {
slug: 'what-do-you-currently-do',
title: translateMarker('What do you currently do?'),
tools: [seasonal_calendar],
tags: [],
tags: [{ label: translateMarker('Resource Allocation Map') }],
steps,
showReviewSection: true,
};
export default content;
1 change: 1 addition & 0 deletions libs/data/farmer_content/data/content/2_climate_change.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,6 @@ const content: Omit<IFarmerContent, 'id' | 'icon_path'> = {
tools: [climate],
tags: [],
steps,
showReviewSection: true,
};
export default content;
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,6 @@ const content: Omit<IFarmerContent, 'id' | 'icon_path'> = {
tools: [probability_and_risk],
tags: [],
steps,
showReviewSection: true,
};
export default content;
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,6 @@ const content: Omit<IFarmerContent, 'id' | 'icon_path'> = {
tools: [options],
tags: [],
steps,
showReviewSection: true,
};
export default content;
1 change: 1 addition & 0 deletions libs/data/farmer_content/data/content/5_compare_options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,6 @@ const content: Omit<IFarmerContent, 'id' | 'icon_path'> = {
tools: [budget],
tags: [],
steps,
showReviewSection: true,
};
export default content;
1 change: 1 addition & 0 deletions libs/data/farmer_content/data/content/6_decide_and_plan.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,6 @@ const content: Omit<IFarmerContent, 'id' | 'icon_path'> = {
tags: [],
steps,
disabled: true,
showReviewSection: true,
};
export default content;
1 change: 0 additions & 1 deletion libs/data/farmer_content/data/tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ const TOOLS_BASE = {
href: 'crop-probability',
tabLabel: translateMarker('Tool'),
},
// resource_allocation_map: { label: translateMarker('Resource Allocation Map'), tabLabel: translateMarker('RAM Tool') },
seasonal_calendar: {
label: translateMarker('Seasonal Calendar'),
href: 'seasonal-calendar',
Expand Down
23 changes: 18 additions & 5 deletions libs/data/farmer_content/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,22 +11,35 @@ export interface IToolData {
tabLabel?: string;
}

interface IFarmerContentStepVideo {
type: 'video';
video: IPicsaVideoData;
interface IContentStepBase {
type: string;
/** Label to show when selecting content from tab */
tabLabel?: string;
/** Icon to show in tab */
tabMatIcon?: string;
}

interface IFarmerContentStepVideo extends IContentStepBase {
type: 'video';
video: IPicsaVideoData;
}

interface IFarmerContentStepVideoPlaylist extends IContentStepBase {
type: 'video_playlist';
videos: IPicsaVideoData[];
}

export type IFarmerContentStep = IFarmerContentStepVideo;
export type IFarmerContentStep = IFarmerContentStepVideo | IFarmerContentStepVideoPlaylist;

export interface IFarmerContent {
id: IFarmerContentId;
slug: string;
icon_path: string;
title: string;
tools: IToolData[];
tags: { label: string }[];
tags: { label: string; color?: 'primary' | 'secondary' }[];
steps: IFarmerContentStep[];
disabled?: boolean;
/** Include a photo-input section as part of review */
showReviewSection?: boolean;
}
2 changes: 1 addition & 1 deletion libs/data/resources/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
export * from './types';
export * from './farmerVideos';
export * from './videos';
6 changes: 5 additions & 1 deletion libs/data/resources/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@ import { ILocaleCode } from '../deployments';

export interface IPicsaVideo {
id: string;
locale_code: ILocaleCode;
/**
* Country and Language codes supported by video.
* The audio locale should be listed first and subtitle second if different
*/
locale_codes: ILocaleCode[];
size_kb: number;
resolution: '360p';
supabase_url: string;
Expand Down
Loading

0 comments on commit c8f7733

Please sign in to comment.