Merge branch 'master' into julius-dev

master
trivernis 4 years ago
commit 3d15e5a8d0

@ -6,30 +6,45 @@
<div [hidden]="!loggedIn"> <div [hidden]="!loggedIn">
<mat-card> <mat-card>
<mat-card-content> <mat-card-content>
<div id="inputPreviewWrapper"> <div id="inputPreviewWrapper" *ngIf="localFileUrl">
<h2 *ngIf="localFileUrl">Preview:</h2> <h2>Preview:</h2>
<img *ngIf="fileType == 'image'" id="inputPreview" [src]="localFileUrl"/> <div id="media-box">
<video *ngIf="fileType == 'video'" [src]="localFileUrl" controls="" class="html5-video-player"> <button class="discard-button" mat-button mat-icon-button (click)="discardFile()"
Your browser does not support playing HTML5 video. matTooltip="discard file"
</video> matTooltipShowDelay="200"
[disabled]="posting">
<mat-icon>close</mat-icon>
</button>
<img *ngIf="fileType == 'image'" id="inputPreview" [src]="localFileUrl"/>
<video *ngIf="fileType == 'video'" [src]="localFileUrl" id="inputPreview" controls="" class="html5-video-player">
Your browser does not support playing HTML5 video.
</video>
</div>
</div> </div>
<mat-form-field id="input"> <mat-form-field class="input">
<textarea matInput #content type="text" (input)="onTextInputChange()" [(ngModel)]="textInputValue" <textarea matInput #content type="text" (input)="onTextInputChange()" [(ngModel)]="textInputValue" [disabled]="posting"
mat-autosize="true" matAutosizeMaxRows="3" placeholder="post something"></textarea> mat-autosize="true" matAutosizeMaxRows="3" placeholder="post something"></textarea>
<button mat-button matSuffix mat-icon-button (click)="name.click()" </mat-form-field>
<input style="display: none" id="fileInput" type="file" accept="video/*,image/*" (change)="onFileInputChange($event)" #fileInput>
<div class="input">
<div class="left">
<p id="check">
<mat-checkbox color="primary" [(ngModel)]="checked" checked="checked" [disabled]="posting">I protected the environment.
</mat-checkbox>
</p>
</div>
<div class="right">
<button mat-button mat-icon-button (click)="fileInput.click()"
matTooltip="upload a picture or video (up to 10MB)" matTooltip="upload a picture or video (up to 10MB)"
matTooltipShowDelay="200"> matTooltipShowDelay="200"
[disabled]="posting">
<mat-icon>attach_file</mat-icon> <mat-icon>attach_file</mat-icon>
</button> </button>
<input style="display: none" id="input-file" type="file" accept="video/*,image/*" (change)="onFileInputChange($event)" #name> </div>
</mat-form-field> </div>
<p id="check">
<mat-checkbox color="primary" [(ngModel)]="checked" checked="checked">I protected the environment.
</mat-checkbox>
</p>
<mat-form-field id="action-chooser" *ngIf="checked"> <mat-form-field id="action-chooser" *ngIf="checked">
<mat-label>What did you do?</mat-label> <mat-label>What did you do?</mat-label>
<mat-select [(ngModel)]="activityId" name="action"> <mat-select [(ngModel)]="activityId" name="action" [disabled]="posting">
<mat-option>nothing ;)</mat-option> <mat-option>nothing ;)</mat-option>
<mat-option *ngFor="let action of actionlist.Actions" [value]="action.id" [matTooltip]="action.description" <mat-option *ngFor="let action of actionlist.Actions" [value]="action.id" [matTooltip]="action.description"
matTooltipShowDelay="200"> matTooltipShowDelay="200">
@ -38,7 +53,8 @@
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
<mat-error *ngIf="errorOccurred && textInputValue">{{getErrorMessage()}}</mat-error> <mat-error *ngIf="errorOccurred && textInputValue">{{getErrorMessage()}}</mat-error>
<button mat-raised-button *ngIf="textInputValue" color="primary" id="post-button" <mat-progress-bar id="progress-bar" *ngIf="posting" mode="indeterminate"></mat-progress-bar>
<button mat-raised-button *ngIf="textInputValue" color="primary" id="post-button" [disabled]="posting"
(click)=createPost(content,activityId)> (click)=createPost(content,activityId)>
POST POST
</button> </button>

@ -27,23 +27,26 @@
#feedlist #feedlist
width: 100% width: 100%
#input .input
width: 100% width: 100%
padding-left: 0.5em padding-left: 0.5em
padding-right: 0.5em padding-right: 0.5em
.mat-icon .left, .right
transform: scale(1.5) display: inline-block
width: 50%
.left
text-align: left
.right
text-align: right
#inputPreview #inputPreview
max-width: 75% max-width: 100%
max-height: 100% max-height: 45vh
width: auto width: auto
border-radius: 4px border-radius: 4px
mask-mode: luminance mask-mode: luminance
outline: none outline: none
user-select: none user-select: none
::ng-deep video ::ng-deep video
width: 100%
max-height: 40vh
outline: none outline: none
user-select: none user-select: none
#inputPreviewWrapper #inputPreviewWrapper
@ -51,6 +54,19 @@
text-align: center text-align: center
max-height: 512px max-height: 512px
margin-bottom: 1em margin-bottom: 1em
#media-box
position: relative
width: 100%
::ng-deep .light-theme .discard-button
margin: 0.5em
position: absolute
background: hsla(255,100%,100%,0.3)
::ng-deep .dark-theme .discard-button
margin: 0.5em
position: absolute
background: hsla(0,0%,0%,0.3)
#progress-bar
margin-top: 1em
#action-chooser #action-chooser
width: 100% width: 100%
@ -60,6 +76,10 @@
#check #check
margin: 0 margin: 0
padding-left: 0.5em padding-left: 0.5em
display: contents
#button-box
text-align: right
margin-left: auto
#post-button #post-button
width: 100% width: 100%

@ -1,4 +1,4 @@
import {Component, OnInit} from '@angular/core'; import {Component, OnInit, ViewChild, ElementRef} from '@angular/core';
import {Post} from 'src/app/models/post'; import {Post} from 'src/app/models/post';
import {FeedService, Sort} from 'src/app/services/feed/feed.service'; import {FeedService, Sort} from 'src/app/services/feed/feed.service';
import {Activitylist} from 'src/app/models/activity'; import {Activitylist} from 'src/app/models/activity';
@ -17,11 +17,13 @@ export class FeedComponent implements OnInit {
loadingNew = true; loadingNew = true;
loadingMostLiked = true; loadingMostLiked = true;
// file upload variables // file upload variables
@ViewChild('fileInput', {static: false}) fileInput: ElementRef;
public uploading = false; public uploading = false;
public profilePictureUrl: BehaviorSubject<string | null>; public profilePictureUrl: BehaviorSubject<string | null>;
private file; private file;
fileType; fileType;
public localFileUrl; public localFileUrl;
posting = false;
checked = false; // if the "I protected the environment."-box is checked checked = false; // if the "I protected the environment."-box is checked
view = 'new'; view = 'new';
@ -61,6 +63,13 @@ export class FeedComponent implements OnInit {
this.feedService.postsAvailable.subscribe(available => { this.feedService.postsAvailable.subscribe(available => {
this.loadingMostLiked = this.loadingNew = available; this.loadingMostLiked = this.loadingNew = available;
}); });
this.feedService.posting.subscribe(posting => {
const temp = this.posting;
this.posting = posting;
if (temp !== this.posting && !this.posting) {
this.resetPostInput();
}
});
} }
/** /**
@ -70,38 +79,41 @@ export class FeedComponent implements OnInit {
*/ */
createPost(postElement, activityId: string) { createPost(postElement, activityId: string) {
if (postElement && activityId && this.checked) { if (postElement && activityId && this.checked) {
this.posting = true;
this.feedService.createPostActivity(postElement.value, activityId, this.file).subscribe(() => { this.feedService.createPostActivity(postElement.value, activityId, this.file).subscribe(() => {
postElement.value = '';
this.textInputValue = '';
this.checked = false;
this.file = null;
this.localFileUrl = null;
this.fileType = null;
if (this.view !== 'new') {
this.showNew();
}
}, (error: IErrorResponse) => { }, (error: IErrorResponse) => {
this.errorOccurred = true; this.errorOccurred = true;
this.posting = false;
this.errorMessage = error.error.errors[0].message; this.errorMessage = error.error.errors[0].message;
}); });
} else if (postElement) { } else if (postElement) {
this.feedService.createPost(postElement.value, this.file).subscribe(() => { this.posting = true;
postElement.value = ''; this.feedService.createPost(postElement.value, this.file).subscribe((result) => {
this.textInputValue = '';
this.checked = false;
this.file = null;
this.localFileUrl = null;
this.fileType = null;
if (this.view !== 'new') {
this.showNew();
}
}, (error: IErrorResponse) => { }, (error: IErrorResponse) => {
console.log(error);
this.posting = false;
this.errorOccurred = true; this.errorOccurred = true;
this.errorMessage = error.error.errors[0].message; this.errorMessage = error.error.errors[0].message;
}); });
} }
} }
discardFile() {
this.file = null;
this.localFileUrl = null;
this.fileType = null;
this.fileInput.nativeElement.value = '';
}
resetPostInput() {
this.textInputValue = '';
this.checked = false;
this.discardFile();
if (this.view !== 'new') {
this.showNew();
}
}
onFileInputChange(event) { onFileInputChange(event) {
this.errorOccurred = false; this.errorOccurred = false;
this.errorMessage = ''; this.errorMessage = '';
@ -119,7 +131,7 @@ export class FeedComponent implements OnInit {
} }
/** /**
* Fetches the next posts when scrolled * Fetches the next posts when scrolled down
*/ */
onScroll() { onScroll() {
this.feedService.getNextPosts(); this.feedService.getNextPosts();

@ -22,8 +22,11 @@
</mat-card-subtitle> </mat-card-subtitle>
</mat-card-header> </mat-card-header>
<mat-card-content> <mat-card-content>
<mat-spinner *ngIf="post.mediaLoading && post.mediaType === 'IMAGE'" style="margin:0 auto; margin-top: 2em;" diameter="50"></mat-spinner>
<div class="postMedia"> <div class="postMedia">
<img *ngIf="post.mediaType === 'IMAGE'" [src]="post.mediaUrl" alt="post image"/> <div [hidden]="post.mediaLoading">
<img *ngIf="post.mediaType === 'IMAGE'" [src]="post.mediaUrl" (load)="onLoad(this.post)" alt="post image"/>
</div>
<video *ngIf="post.mediaType === 'VIDEO'" controls> <video *ngIf="post.mediaType === 'VIDEO'" controls>
<source [src]="post.mediaUrl" type="video/webm"> <source [src]="post.mediaUrl" type="video/webm">
</video> </video>

@ -60,16 +60,19 @@ $mat-card-header-size: 40px !default
display: block display: block
margin-left: auto margin-left: auto
margin-right: auto margin-right: auto
margin-bottom: 0.5em
::ng-deep video ::ng-deep video
width: 100% width: 100%
max-height: 40vh max-height: 40vh
outline: none outline: none
user-select: none user-select: none
margin-bottom: 0.5em
::ng-deep audio ::ng-deep audio
width: 100% width: 100%
max-height: 40vh max-height: 40vh
outline: none outline: none
user-select: none user-select: none
margin-bottom: 0.5em
.mat-button .mat-button
min-width: 32px !important min-width: 32px !important

@ -49,6 +49,10 @@ export class PostlistComponent implements OnInit {
}); });
} }
onLoad(post: Post) {
post.mediaLoading = false;
}
public showUserProfile(post: any) { public showUserProfile(post: any) {
this.router.navigate(['profile/' + post.author.id]); this.router.navigate(['profile/' + post.author.id]);
} }

@ -54,7 +54,7 @@ $mat-card-header-size: 100px !default
#icon #icon
display: none display: none
position: absolute position: absolute
z-index: 11 z-index: 100
color: white color: white
$mat-card-header-size: 100px !default $mat-card-header-size: 100px !default

@ -17,7 +17,7 @@
</button> </button>
</mat-menu> </mat-menu>
</div> </div>
<div mat-card-avatar (click)="showGroupProfile(group)"> <div mat-card-avatar (click)="showFriendProfile(friend)">
<img class="profile-picture" [src]="friend.profilePicture"/> <img class="profile-picture" [src]="friend.profilePicture"/>
</div> </div>
<mat-card-title (click)="showFriendProfile(friend)">{{friend.name}}</mat-card-title> <mat-card-title (click)="showFriendProfile(friend)">{{friend.name}}</mat-card-title>

@ -1,5 +1,6 @@
import {Author} from './author'; import {Author} from './author';
import {Activity} from './activity'; import {Activity} from './activity';
import { environment } from 'src/environments/environment';
export class Post { export class Post {
id: number; id: number;
@ -12,6 +13,7 @@ export class Post {
deletable: boolean; deletable: boolean;
author: Author; author: Author;
activity: Activity; activity: Activity;
mediaLoading: boolean;
mediaUrl: string; mediaUrl: string;
mediaType: 'VIDEO' | 'IMAGE'; mediaType: 'VIDEO' | 'IMAGE';
@ -39,8 +41,9 @@ export class Post {
this.author = author; this.author = author;
this.activity = activity; this.activity = activity;
if (media) { if (media) {
this.mediaUrl = media.url; this.mediaUrl = environment.greenvironmentUrl + media.url;
this.mediaType = media.type; this.mediaType = media.type;
this.mediaLoading = true;
} }
} }
} }

@ -10,8 +10,8 @@ import {BaseService} from '../base.service';
import {formatDate} from '@angular/common'; import {formatDate} from '@angular/common';
import {IFileUploadResult} from '../../models/interfaces/IFileUploadResult'; import {IFileUploadResult} from '../../models/interfaces/IFileUploadResult';
const createPostGqlQuery = `mutation($content: String!) { const createPostGqlQuery = `mutation($content: String!, $type: PostType) {
createPost(content: $content) { createPost(content: $content, type: $type) {
id, id,
content, content,
htmlContent, htmlContent,
@ -34,8 +34,8 @@ const createPostGqlQuery = `mutation($content: String!) {
createdAt} createdAt}
}`; }`;
const createPostActivityGqlQuery = `mutation($content: String!, $id: ID) { const createPostActivityGqlQuery = `mutation($content: String!, $id: ID, $type: PostType) {
createPost(content: $content activityId: $id) { createPost(content: $content activityId: $id, type: $type) {
id, id,
content, content,
htmlContent, htmlContent,
@ -70,7 +70,7 @@ const downvotePostGqlQuery = `mutation($postId: ID!) {
} }
}`; }`;
const getPostGqlQuery = `query($first: Int, $offset: Int, $sort: SortType){ const getPostsGqlQuery = `query($first: Int, $offset: Int, $sort: SortType){
getPosts (first: $first, offset: $offset, sort: $sort) { getPosts (first: $first, offset: $offset, sort: $sort) {
id, id,
content, content,
@ -109,6 +109,7 @@ export class FeedService extends BaseService {
} }
public postsAvailable = new BehaviorSubject<boolean>(true); public postsAvailable = new BehaviorSubject<boolean>(true);
public posting = new BehaviorSubject<boolean>(false);
public posts: BehaviorSubject<Post[]> = new BehaviorSubject([]); public posts: BehaviorSubject<Post[]> = new BehaviorSubject([]);
private activePostList: Sort = Sort.NEW; private activePostList: Sort = Sort.NEW;
private offset = 0; private offset = 0;
@ -122,7 +123,7 @@ export class FeedService extends BaseService {
*/ */
private static buildGetPostBody(sort: string, offset: number, first: number = 10) { private static buildGetPostBody(sort: string, offset: number, first: number = 10) {
return { return {
query: getPostGqlQuery, variables: { query: getPostsGqlQuery, variables: {
first, first,
offset, offset,
sort sort
@ -145,10 +146,15 @@ export class FeedService extends BaseService {
* @param file * @param file
*/ */
public createPost(pContent: String, file?: File) { public createPost(pContent: String, file?: File) {
let type: string;
if (file) { type = 'MEDIA'; } else {
type = 'TEXT';
}
const body = { const body = {
query: createPostGqlQuery, query: createPostGqlQuery,
variables: { variables: {
content: pContent content: pContent,
type
} }
}; };
return this.createPostRequest(body, file); return this.createPostRequest(body, file);
@ -161,10 +167,15 @@ export class FeedService extends BaseService {
* @param file * @param file
*/ */
public createPostActivity(pContent: String, activityId: String, file?: File) { public createPostActivity(pContent: String, activityId: String, file?: File) {
let type: string;
if (file) { type = 'MEDIA'; } else {
type = 'TEXT';
}
const body = { const body = {
query: createPostActivityGqlQuery, variables: { query: createPostActivityGqlQuery, variables: {
content: pContent, content: pContent,
id: activityId id: activityId,
type
} }
}; };
return this.createPostRequest(body, file); return this.createPostRequest(body, file);
@ -176,26 +187,39 @@ export class FeedService extends BaseService {
* @param file - a file that is being uploaded with the post * @param file - a file that is being uploaded with the post
*/ */
private createPostRequest(body: { variables: any; query: string }, file?: File) { private createPostRequest(body: { variables: any; query: string }, file?: File) {
return this.postGraphql(body, null, 0) this.posting.next(true);
if (file) {
return this.postGraphql(body, null, 0)
.pipe(tap(response => {
const updatedPosts = this.posts.getValue();
const post = this.constructPost(response);
this.uploadPostImage(post.id, file).subscribe((result) => {
if (this.activePostList === Sort.NEW) {
post.mediaUrl = result.fileName;
post.mediaType = result.fileName.endsWith('.png') ? 'IMAGE' : 'VIDEO';
updatedPosts.unshift(post);
this.posts.next(updatedPosts);
this.posting.next(false);
}
}, error => {
console.error(error);
this.posting.next(false);
this.deletePost(post.id);
});
}
));
} else if (!file) {
return this.postGraphql(body, null, 0)
.pipe(tap(response => { .pipe(tap(response => {
this.posting.next(false);
const updatedPosts = this.posts.getValue();
if (this.activePostList === Sort.NEW) { if (this.activePostList === Sort.NEW) {
const updatedPosts = this.posts.getValue();
const post = this.constructPost(response); const post = this.constructPost(response);
updatedPosts.unshift(post); updatedPosts.unshift(post);
if (file) { this.posts.next(updatedPosts);
this.uploadPostImage(post.id, file).subscribe((result) => {
post.mediaUrl = result.fileName;
post.mediaType = result.fileName.endsWith('.png') ? 'IMAGE' : 'VIDEO';
this.posts.next(updatedPosts);
}, error => {
console.error(error);
this.deletePost(post.id);
});
} else {
this.posts.next(updatedPosts);
}
} }
})); }));
}
} }
/** /**
@ -266,7 +290,7 @@ export class FeedService extends BaseService {
{headers: this.headers}) {headers: this.headers})
.pipe(this.retryRated()) .pipe(this.retryRated())
.subscribe(response => { .subscribe(response => {
this.posts.next(this.constructAllPosts(response)); this.posts.next(this.constructAllPosts(response.data.getPosts));
this.activePostList = sort; this.activePostList = sort;
}); });
} }
@ -280,7 +304,7 @@ export class FeedService extends BaseService {
this.http.post(environment.graphQLUrl, body, {headers: this.headers}) this.http.post(environment.graphQLUrl, body, {headers: this.headers})
.pipe(this.retryRated()) .pipe(this.retryRated())
.subscribe(response => { .subscribe(response => {
const posts = this.constructAllPosts(response); const posts = this.constructAllPosts(response.data.getPosts);
const previousPosts = this.posts.getValue(); const previousPosts = this.posts.getValue();
for (const post of previousPosts.reverse()) { for (const post of previousPosts.reverse()) {
if (!posts.find(p => p.id === post.id)) { if (!posts.find(p => p.id === post.id)) {
@ -337,7 +361,7 @@ export class FeedService extends BaseService {
public constructAllPosts(response: any): Post[] { public constructAllPosts(response: any): Post[] {
const posts = new Array<Post>(); const posts = new Array<Post>();
for (const post of response.data.getPosts) { for (const post of response) {
let profilePicture: string; let profilePicture: string;
if (post.author.profilePicture) { if (post.author.profilePicture) {
profilePicture = environment.greenvironmentUrl + post.author.profilePicture; profilePicture = environment.greenvironmentUrl + post.author.profilePicture;

@ -7,6 +7,7 @@ import {User} from 'src/app/models/user';
import {Subject} from 'rxjs'; import {Subject} from 'rxjs';
import {Activity} from 'src/app/models/activity'; import {Activity} from 'src/app/models/activity';
import {BaseService} from '../base.service'; import {BaseService} from '../base.service';
import {FeedService} from 'src/app/services/feed/feed.service';
const graphqlGetProfileQuery = `query($userId: ID) { const graphqlGetProfileQuery = `query($userId: ID) {
getUser(userId:$userId){ getUser(userId:$userId){
@ -30,6 +31,7 @@ const graphqlGetProfileQuery = `query($userId: ID) {
downvotes, downvotes,
userVote, userVote,
deletable, deletable,
media {url, type},
activity{ activity{
id id
name name
@ -39,7 +41,7 @@ const graphqlGetProfileQuery = `query($userId: ID) {
author{ author{
name, name,
handle, handle,
profilePicture profilePicture,
id}, id},
createdAt createdAt
} }
@ -51,7 +53,7 @@ const graphqlGetProfileQuery = `query($userId: ID) {
}) })
export class ProfileService extends BaseService { export class ProfileService extends BaseService {
constructor(http: HttpClient) { constructor(http: HttpClient, private feedService: FeedService) {
super(http); super(http);
} }
@ -96,38 +98,7 @@ export class ProfileService extends BaseService {
const temp = new Date(Number(response.data.getUser.joinedAt)); const temp = new Date(Number(response.data.getUser.joinedAt));
const date = temp.toLocaleString('en-GB'); const date = temp.toLocaleString('en-GB');
profile.joinedAt = date; profile.joinedAt = date;
for (const post of response.data.getUser.posts) { profile.posts = this.feedService.constructAllPosts(response.data.getUser.posts);
const id: number = post.id;
const content: string = post.content;
const htmlContent: string = post.htmlContent;
const upvotes: number = post.upvotes;
const downvotes: number = post.downvotes;
const userVote: string = post.userVote;
const deletable: boolean = post.deletable;
let profilePicture: string;
if (post.author.profilePicture) {
profilePicture = environment.greenvironmentUrl + post.author.profilePicture;
} else {
profilePicture = 'assets/images/default-profilepic.svg';
}
const author = new Author(post.author.id, post.author.name, post.author.handle, profilePicture);
const ptemp = new Date(Number(post.createdAt));
const pdate = ptemp.toLocaleString('en-GB');
let activity: Activity;
if (post.activity) {
activity = new Activity(
post.activity.id,
post.activity.name,
post.activity.description,
post.activity.points);
} else {
activity = null;
}
// tslint:disable-next-line: max-line-length
posts.push(new Post(id, content, htmlContent, upvotes, downvotes, userVote, deletable, pdate, author, activity));
}
profile.posts = posts;
return profile; return profile;
} }
return null; return null;

Loading…
Cancel
Save