Skip to content

Commit

Permalink
Merge pull request #280 from e-picsa/feat-resource-share
Browse files Browse the repository at this point in the history
Feat: Resource share functionality
  • Loading branch information
chrismclarke authored Jun 19, 2024
2 parents a55845a + 22af0c7 commit 10cda69
Show file tree
Hide file tree
Showing 18 changed files with 191 additions and 18 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ dependencies {
implementation project(':capacitor-community-firebase-crashlytics')
implementation project(':capacitor-firebase-performance')
implementation project(':capacitor-screen-orientation')
implementation project(':capacitor-share')
implementation "androidx.webkit:webkit:1.4.0"
implementation "androidx.legacy:legacy-support-v4:1.0.0"
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,6 @@ project(':capacitor-firebase-performance').projectDir = new File('../../../../no

include ':capacitor-screen-orientation'
project(':capacitor-screen-orientation').projectDir = new File('../../../../node_modules/@capacitor/screen-orientation/android')

include ':capacitor-share'
project(':capacitor-share').projectDir = new File('../../../../node_modules/@capacitor/share/android')
1 change: 1 addition & 0 deletions apps/picsa-apps/extension-app-native/capacitor.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ const config: CapacitorConfig = {
'@capacitor-community/firebase-crashlytics',
'@capacitor-firebase/performance',
'@capacitor/screen-orientation',
"@capacitor/share",
],
// Enable app to use native http for requests (bypass cors)
// https://capacitorjs.com/docs/apis/http
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,11 @@ import {
ResourceItemLinkComponent,
ResourceItemVideoComponent,
} from './resource-item';
import { ResourceShareComponent } from './resource-share/resource-share.component';

const components = [
ResourceDownloadComponent,
ResourceShareComponent,
ResourceItemCollectionComponent,
ResourceItemFileComponent,
ResourceItemLinkComponent,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

<!-- TODO - should be able to refactor video item to be included here -->
<div style="position: relative">
@if(fileURI){
<resource-share class="share-link" [link]="dbDoc.url" [uri]="fileURI"></resource-share>
}

<!-- Download overlay -->
<div
class="download-overlay"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,9 @@
color: var(--color-primary);
}
}
.share-link {
position: absolute;
top: -6px;
right: -6px;
z-index: 2;
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,33 +3,36 @@
<div class="card-layout">
<!-- Side Image -->
<div
*ngIf="resource.cover?.image"
*ngIf="resource().cover?.image"
class="background-image"
[style.background-image]="'url(' + resource.cover?.image + ')'"
[attr.data-fit]="resource.cover?.imageFit"
[style.background-image]="'url(' + resource().cover?.image + ')'"
[attr.data-fit]="resource().cover?.imageFit"
></div>

<!-- Main content -->
<div class="central-content">
<mat-card-header>
<mat-card-title style="margin-top: 12px">{{ resource.title | translate }}</mat-card-title>
<mat-card-title style="margin-top: 12px">{{ resource().title | translate }}</mat-card-title>
</mat-card-header>
<mat-card-content>
<p *ngIf="resource.description">{{ resource.description | translate }}</p>
<p *ngIf="description()">{{ description() | translate }}</p>
<!-- Child Content -->
<ng-content></ng-content>
<div
class="action-button"
*ngIf="actionButtons[resource.subtype] as actionButton"
[attr.data-type]="resource.subtype"
*ngIf="actionButtons[resource().subtype] as actionButton"
[attr.data-type]="resource().subtype"
>
<mat-icon *ngIf="actionButton.svgIcon" [svgIcon]="actionButton.svgIcon"></mat-icon>
<mat-icon *ngIf="actionButton.matIcon">{{actionButton.matIcon}}</mat-icon>
</div>
<div *ngIf="resource.language" style="margin-top: 2em">
<span class="language-code">{{ resource.language.toUpperCase() }}</span>
<div *ngIf="language()" style="margin-top: 2em">
<span class="language-code">{{language().toUpperCase() }}</span>
</div>
</mat-card-content>
</div>
<!-- Assumption here is internal links are collections, to make sure we share the individual file or link, they are being skipped -->
@if(shareUrl()){
<resource-share [link]="shareUrl()" (click)="$event.stopPropagation()"></resource-share>
}
</div>
</mat-card>
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
}
.card-layout {
display: flex;
position: relative;
height: 100%;
}

Expand All @@ -16,6 +17,7 @@
.central-content {
display: flex;
flex-direction: column;
position: relative;
mat-card-content {
flex: 1;
display: flex;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Component, EventEmitter, Input, Output } from '@angular/core';
import { Component, computed, EventEmitter, Input, input, Output } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { Browser } from '@capacitor/browser';

Expand All @@ -10,7 +10,23 @@ import { IResourceLink } from '../../../schemas';
styleUrls: ['link.scss'],
})
export class ResourceItemLinkComponent {
@Input() resource: IResourceLink;
resource = input.required<IResourceLink>();

shareUrl = computed<string>(() => {
const { url, subtype } = this.resource();
// ignore internal links
if (subtype === 'internal') return '';
// add play store url prefix
if (subtype === 'play_store') return `https://play.google.com/store/apps/details?id=${url}`;
return url as string;
});

description = computed<string>(() => {
return this.resource().description || '';
});
language = computed<string>(() => {
return this.resource().language || '';
});

/** Check if any existing click handlers already bound to element to use as override */
// eslint-disable-next-line @angular-eslint/no-output-native
Expand All @@ -32,14 +48,14 @@ export class ResourceItemLinkComponent {
public handleClick() {
// If (click) binding present on element ignore own methods
if (this.click.observed) return;
switch (this.resource.subtype) {
switch (this.resource().subtype) {
case 'internal':
return this.handleInternalLink(this.resource.url);
return this.handleInternalLink(this.resource().url);
case 'play_store': {
return this.goToApp(this.resource.url);
return this.goToApp(this.resource().url);
}
default:
return this.handleExternalLink(this.resource.url);
return this.handleExternalLink(this.resource().url);
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<button class="share-button" mat-icon-button (click)="share()">
<mat-icon>share</mat-icon>
</button>
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
:host {
display: block;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';

import { ResourceShareComponent } from './resource-share.component';

describe('ResourceShareComponent', () => {
let component: ResourceShareComponent;
let fixture: ComponentFixture<ResourceShareComponent>;

beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ResourceShareComponent],
}).compileComponents();

fixture = TestBed.createComponent(ResourceShareComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});

it('should create', () => {
expect(component).toBeTruthy();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, input } from '@angular/core';
import { Capacitor } from '@capacitor/core';
import { RxAttachment } from 'rxdb';

import { IResourceFile } from '../../schemas';
import { ResourcesToolService } from '../../services/resources-tool.service';

@Component({
selector: 'resource-share',
templateUrl: './resource-share.component.html',
styleUrls: ['./resource-share.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ResourceShareComponent {
public attachment?: RxAttachment<IResourceFile>;

link = input.required<string>();
uri = input<string>();

constructor(private service: ResourcesToolService, private cdr: ChangeDetectorRef) {}

public async share() {
// on native prefer to share file directly
const uri = this.uri();
if (Capacitor.isNativePlatform() && uri) {
this.service.shareFileNative(uri);
// fallback to sharing link on web and non-file type
} else {
this.service.shareLink(this.link());
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
'Collection not found' | translate
}}</picsa-alert-box>

<mat-card *ngIf="collection">
<mat-card *ngIf="collection" style="flex: 1">
<!-- Header -->
<mat-card-header>
<mat-card-title>{{ collection.title | translate }}</mat-card-title>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
import { Clipboard } from '@angular/cdk/clipboard';
import { Injectable } from '@angular/core';
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 { 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';
import { FileService } from '@picsa/shared/services/core/file.service';
import { PicsaNotificationService } from '@picsa/shared/services/core/notification.service';
import { NativeStorageService } from '@picsa/shared/services/native';
import { _wait, arrayToHashmap } from '@picsa/utils';
import { RxCollection, RxDocument } from 'rxdb';
Expand All @@ -28,11 +31,15 @@ export class ResourcesToolService extends PicsaAsyncService {
private configurationService: ConfigurationService,
private nativeStorageService: NativeStorageService,
private fileService: FileService,
private analyticsService: AnalyticsService
private analyticsService: AnalyticsService,
private clipboard: Clipboard,
private notificationService: PicsaNotificationService
) {
super();
}

private canShare: boolean;

/**
* Initialisation method automatically called on instantiation
* Await completed state via the service `ready()` property
Expand All @@ -45,6 +52,8 @@ export class ResourcesToolService extends PicsaAsyncService {
}
await this.dbInit();
await this.populateHardcodedResources();
const { value: canShareValue } = await Share.canShare();
this.canShare = canShareValue;
}

private async dbInit() {
Expand Down Expand Up @@ -228,4 +237,41 @@ export class ResourcesToolService extends PicsaAsyncService {
private setAssetResourcesVersion() {
return localStorage.setItem(`picsa-resources-tool||assets-cache-version`, APP_VERSION.number);
}

public async shareLink(url: string) {
if (this.canShare) {
await Share.share({
title: 'Share Resource',
url: url,
dialogTitle: 'Share Resource Link',
});
} else {
// Simply copy the link to clipboard
this.clipboard.copy(url);
this.notificationService.showUserNotification({
matIcon: 'success',
message: 'Link to this resource has been copied for you to share.',
});
}
}
public async shareFileNative(uri: string) {
// HACK - files can only be shared from the cache folder (unless specific permissions granted)
// Copy to cache folder, share and delete from cache
// https://capacitorjs.com/docs/v5/apis/share#android
// https://capawesome.io/plugins/file-opener/#android
const cacheFileUri = await this.nativeStorageService.copyFileToCache(uri);
const filename = uri.split('/').pop() as string;
if (cacheFileUri) {
await Share.share({
files: [cacheFileUri],
title: filename,
dialogTitle: 'Share File',
text: 'Shared from Picsa App',
});
// NOTE - sharing callback will return after delegating task (e.g. open whatsapp to share),
// so do not delete cache file as no guarantee target task completed

// await Filesystem.deleteFile({ path: cacheFileUri, directory: Directory.Cache });
}
}
}
18 changes: 18 additions & 0 deletions libs/shared/src/services/native/storage-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,24 @@ export class NativeStorageService extends PicsaAsyncService {
return FileInfo;
}

/**
* Copy a file from data folder to cache folder.
* This is required if sharing files and explicit permission not granted for data folder
**/
public async copyFileToCache(uri: string) {
// determine the relative filepath (with subfolders) as written to data directory
const fileDirectory = await Filesystem.getUri({ directory: Directory.Data, path: '' });
const relativePath = uri.replace(fileDirectory.uri, '');
// copy file to cache, ignoring nested folder structures (just keep flat)
const { uri: cacheFileUri } = await Filesystem.copy({
from: relativePath,
directory: Directory.Data,
to: relativePath.split('/').pop() as string,
toDirectory: Directory.Cache,
});
return cacheFileUri;
}

public async deleteFile(relativePath: string) {
const directory = Directory.Data;
const path = `${this.cacheName}/${relativePath}`;
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@
"@capacitor/filesystem": "^5.1.4",
"@capacitor/network": "^5.0.6",
"@capacitor/screen-orientation": "^5.0.7",
"@capacitor/share": "^5.0.0",
"@ngx-translate/core": "~15.0.0",
"@ngx-translate/http-loader": "~8.0.0",
"@nx/angular": "18.2.1",
Expand Down
10 changes: 10 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -3397,6 +3397,15 @@ __metadata:
languageName: node
linkType: hard

"@capacitor/share@npm:^5.0.0":
version: 5.0.8
resolution: "@capacitor/share@npm:5.0.8"
peerDependencies:
"@capacitor/core": ^5.0.0
checksum: 64cd437c9a648a59cfeceadf6f9ab9ee642bae553eb3a37d24082fa201f37c36f94797454ead82d95142657b0fab73a616fcbbfa082d9117db9619276bee5a92
languageName: node
linkType: hard

"@colors/colors@npm:1.5.0":
version: 1.5.0
resolution: "@colors/colors@npm:1.5.0"
Expand Down Expand Up @@ -19325,6 +19334,7 @@ __metadata:
"@capacitor/filesystem": ^5.1.4
"@capacitor/network": ^5.0.6
"@capacitor/screen-orientation": ^5.0.7
"@capacitor/share": ^5.0.0
"@ngx-translate/core": ~15.0.0
"@ngx-translate/http-loader": ~8.0.0
"@nx/angular": 18.2.1
Expand Down

0 comments on commit 10cda69

Please sign in to comment.