Skip to content

Commit

Permalink
multiple images per post
Browse files Browse the repository at this point in the history
  • Loading branch information
RemcoSimonides committed Mar 31, 2024
1 parent 8d550bc commit 697b360
Show file tree
Hide file tree
Showing 32 changed files with 553 additions and 61 deletions.
1 change: 1 addition & 0 deletions apps/backend-functions/src/firestore/goals/goal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ async snapshot => {
deleteCollection(db, `Goals/${goal.id}/Story`, 500)
deleteCollection(db, `Goals/${goal.id}/Comments`, 500)
deleteCollection(db, `Goals/${goal.id}/ChatGPT`, 500)
deleteCollection(db, `Goals/${goal.id}/Media`, 500)

if (goal.publicity === 'public') {
await deleteFromAlgolia('goal', goal.id)
Expand Down
11 changes: 11 additions & 0 deletions apps/backend-functions/src/firestore/goals/media/media.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { gcsBucket, onDocumentDelete } from '@strive/api/firebase'
import { createMedia } from '@strive/model'
import { toDate } from '../../../shared/utils'

export const mediaDeletedHandler = onDocumentDelete(`Goals/{goalId}/Media/{mediaId}`, 'mediaDeletedHandler',
async (snapshot) => {
const media = createMedia(toDate({ ...snapshot.data(), id: snapshot.id }))

const fileRef = `${media.storagePath}/${media.id}`
gcsBucket.file(fileRef).delete({ ignoreNotFound: true })
})
11 changes: 8 additions & 3 deletions apps/backend-functions/src/firestore/goals/posts/post.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { db, gcsBucket, onDocumentCreate, onDocumentDelete, onDocumentUpdate } from '@strive/api/firebase'
import { db, onDocumentCreate, onDocumentDelete, onDocumentUpdate } from '@strive/api/firebase'
import { toDate } from '../../../shared/utils'
import { createGoalSource, createPost } from '@strive/model'
import { addGoalEvent } from '../../../shared/goal-event/goal.events'
Expand Down Expand Up @@ -38,9 +38,14 @@ async (snapshot, context) => {
const { goalId, postId } = context.params
const post = createPost(toDate({ ...snapshot.data(), id: snapshot.id }))

if (post.mediaURL) {
gcsBucket.file(post.mediaURL).delete({ ignoreNotFound: true })
const mediaRefs = post.mediaIds.map(mediaId => `Goals/${goalId}/Media/${mediaId}`)
const batch = db.batch()

for (const ref of mediaRefs) {
batch.delete(db.doc(ref))
}

await batch.commit()

db.doc(`Goals/${goalId}/Story/${postId}`).delete()
})
1 change: 1 addition & 0 deletions apps/backend-functions/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export { goalInviteTokenCreatedHandler } from './firestore/goals/invite-tokens/i
export { postCreatedHandler, postDeletedHandler, postChangeHandler } from './firestore/goals/posts/post'
export { commentCreatedHandler } from './firestore/goals/comments/comment'
export { chatGPTMessageCreatedHandler } from './firestore/goals/chatgpt/chatgpt'
export { mediaDeletedHandler } from './firestore/goals/media/media'

export { goalEventCreatedHandler } from './firestore/goal-events/goal-events'

Expand Down
4 changes: 1 addition & 3 deletions apps/journal/src/app/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,13 +41,11 @@ Sentry.init(

// Swiper
import { register } from 'swiper/element/bundle'
import { SwiperDirective } from '@strive/utils/directives/swiper.directive'
register();

@NgModule({
declarations: [
AppComponent,
SwiperDirective
AppComponent
],
imports: [
//enable offline persistance
Expand Down
10 changes: 9 additions & 1 deletion apps/journal/src/app/pages/goal/goal.page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ import { SupportService } from '@strive/support/support.service'
import { ScreensizeService } from '@strive/utils/services/screensize.service'
import { StoryService } from '@strive/story/story.service'
import { PostService } from '@strive/post/post.service'
import { MediaService } from '@strive/media/media.service'
// Strive Interfaces
import { Goal, GoalStakeholder, groupByObjective, SupportsGroupedByGoal, Milestone, StoryItem, sortGroupedSupports, createGoalStakeholder, createPost, Stakeholder } from '@strive/model'
import { CommentService } from '@strive/chat/comment.service'
Expand Down Expand Up @@ -119,6 +120,7 @@ export class GoalPageComponent implements OnDestroy {
private goalService: GoalService,
private inviteTokenService: InviteTokenService,
private location: Location,
private mediaService: MediaService,
private milestoneService: MilestoneService,
private modalCtrl: ModalController,
private popoverCtrl: PopoverController,
Expand Down Expand Up @@ -191,7 +193,13 @@ export class GoalPageComponent implements OnDestroy {
joinWith({
user: ({ userId }) => userId ? this.profileService.valueChanges(userId) : of(undefined),
milestone: ({ milestoneId, goalId }) => milestoneId ? this.milestoneService.valueChanges(milestoneId, { goalId }) : of(undefined),
post: ({ postId, goalId }) => postId ? this.postService.valueChanges(postId, { goalId }) : of(undefined)
post: ({ postId, goalId }) => postId
? this.postService.valueChanges(postId, { goalId }).pipe(
joinWith({
medias: post => post?.mediaIds ? this.mediaService.valueChanges(post.mediaIds, { goalId }) : of([])
})
)
: of(undefined)
}),
shareReplay({ bufferSize: 1, refCount: true })
)
Expand Down
3 changes: 1 addition & 2 deletions apps/journal/src/global.scss
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,7 @@
/* Variables */
body {
--page-max-width: 800px;
--swiper-navigation-size: 20px;
--swiper-navigation-color: var(--ion-color-secondary);
--swiper-pagination-color: var(--ion-color-primary);
scrollbar-color: light;
}

Expand Down
7 changes: 7 additions & 0 deletions firestore.rules
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,13 @@ service cloud.firestore {
allow delete: if userIsGoalAdmin(goalId) || userIsGoalAchiever(goalId)
}

match /Media/{mediaId} {
allow read;
allow create: if userIsGoalAdmin(goalId) || userIsGoalAchiever(goalId)
allow update: if userIsGoalAdmin(goalId) || userIsGoalAchiever(goalId)
allow delete: if userIsGoalAdmin(goalId) || userIsGoalAchiever(goalId)
}

match /Supports/{supportId} {
allow read;
allow create: if isSignedIn()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,6 @@ import { isValidHttpUrl } from '@strive/utils/helpers'
import { Camera, CameraResultType } from '@capacitor/camera'
import { captureException, captureMessage } from '@sentry/capacitor'



type CropStep = 'drop' | 'crop' | 'hovering' | 'show'

@Component({
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<swiper-container space-between="16" slides-per-view="auto" #swiper>
<ng-container *ngFor="let control of form.controls">
<swiper-slide>
<img [src]="control.value.preview" />
</swiper-slide>
</ng-container>
<swiper-slide>
<section [ngSwitch]="step | async">
<!-- Drop -->
<article *ngSwitchCase="'drop'" class="drop-zone" (click)="selectImages()">
<ion-icon name="images-outline" />
<span>Add Photo</span>
</article>

<!-- Hovering -->
<article *ngSwitchCase="'hovering'" class="drop-zone hover" (click)="selectImages()">
<ion-icon name="images-outline" />
<span>Drop your file</span>
</article>
</section>
</swiper-slide>
</swiper-container>

<input #fileUploader multiple hidden [accept]="accept" type="file" (change)="inputChange($event)" />
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
// @import 'swiper/scss';
// @import 'swiper/scss/free-mode';

:host {
display: flex;
gap: 12px;
overflow: auto;
height: 160px;

swiper-container {
width: 100%;

swiper-slide {
max-width: 100%;
width: fit-content;
}
}

img {
height: 100%;
border-radius: 12px;
}

section {
height: 100%;
}

article.drop-zone {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
gap: 16px;
height: 100%;
border: 2px dashed var(--ion-color-primary);
border-radius: 12px;
width: 180px;
box-sizing: border-box;

&:hover {
cursor: pointer;
}

p {
white-space: nowrap;
}

div {
display: flex;
flex-direction: column;
align-items: center;
margin-bottom: 1em;

img {
width: 100px;
}
}
}

article.hover {
border: 2px solid var(--ion-color-primary);
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
import { CUSTOM_ELEMENTS_SCHEMA, ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, HostListener, Input, OnInit, ViewChild, ViewEncapsulation } from '@angular/core'
import { CommonModule } from '@angular/common'
import { FormArray } from '@angular/forms'
import { SafeUrl } from '@angular/platform-browser'
import { IonicModule, ToastController } from '@ionic/angular'
import { SwiperContainer } from 'swiper/swiper-element'

import { BehaviorSubject } from 'rxjs'

import { Camera, GalleryPhoto } from '@capacitor/camera'
import { captureException, captureMessage } from '@sentry/capacitor'
import { EditMediaForm } from '@strive/media/forms/media.form'
import { delay } from '@strive/utils/helpers'

type CropStep = 'drop' | 'hovering'

@Component({
standalone: true,
imports: [
CommonModule,
IonicModule
],
schemas: [CUSTOM_ELEMENTS_SCHEMA],
selector: '[form] strive-images-selector',
templateUrl: 'images-selector.component.html',
styleUrls: ['./images-selector.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
encapsulation: ViewEncapsulation.ShadowDom
})
export class ImagesSelectorComponent {

step = new BehaviorSubject<CropStep>('drop')

accept = ['.jpg', '.jpeg', '.png', '.webp']
previewUrl$ = new BehaviorSubject<string | SafeUrl>('')

@Input() form!: FormArray<EditMediaForm>

@ViewChild('fileUploader') fileUploader?: ElementRef<HTMLInputElement>
@ViewChild('swiper') swiper?: ElementRef<SwiperContainer>;

constructor(
private cdr: ChangeDetectorRef,
private toast: ToastController
) {}

@HostListener('drop', ['$event'])
onDrop($event: DragEvent) {
$event?.preventDefault()
if ($event?.dataTransfer?.files.length) {
this.filesSelected($event.dataTransfer.files)
}
this.step.next('drop')
}

@HostListener('dragover', ['$event'])
onDragOver($event: DragEvent) {
$event.preventDefault()
this.step.next('hovering')
}

@HostListener('dragleave', ['$event'])
onDragLeave($event: DragEvent) {
$event.preventDefault()
this.step.next('drop')
}

async selectImages() {
try {
const images = await Camera.pickImages({ quality: 100 })
for (const image of images.photos) {
const file = await getFileFromGalleryPhoto(image)
if (file) {
this.filesSelected(file)
} else {
this.toast.create({ message: 'Something went wrong', duration: 3000 })
captureMessage('Unsupported file type chosen')
captureException(image)
}
}
} catch (err) {
this.fileUploader?.nativeElement.click()
}
}

inputChange(event: any) {
const files = event.target.files
this.filesSelected(files)
}

filesSelected(file: FileList | File) {
const files = isFileList(file) ? Array.from(file) : [file]

for (const file of files) {
if (file.type?.split('/')[0] !== 'image') {
this.toast.create({ message: 'Unsupported file type', duration: 3000 }).then(toast => toast.present())
return
}

const preview = URL.createObjectURL(file)
const mediaForm = new EditMediaForm({ id: '', preview, file })
this.form.push(mediaForm)
this.form.markAsDirty()
}

this.cdr.markForCheck()
this.step.next('drop')

if (this.fileUploader) {
this.fileUploader.nativeElement.value = ''
}
if (this.swiper) {
const swiper = this.swiper.nativeElement.swiper
delay(200).then(() => {
swiper.slideTo(this.form.length + 1)
})
}
}
}

function isFileList(file: FileList | File): file is FileList {
return (file as FileList).item !== undefined
}


async function getFileFromGalleryPhoto(photo: GalleryPhoto): Promise<File | undefined> {
try {
const response = await fetch(photo.webPath);
const blob = await response.blob();
const name = photo.webPath.split('/').pop();
const fileName = `${name}.${photo.format}`
const file = new File([blob], fileName, { type: blob.type });
return file;
} catch (error) {
console.error("Error fetching blob data:", error);
return;
}
}
Loading

0 comments on commit 697b360

Please sign in to comment.