Skip to content

Commit

Permalink
Implement Texera User Avatars Featuer (#1373)
Browse files Browse the repository at this point in the history
This PR creates an Angular component to show the avatar of a Texera user.

The library/component **ngx-avatar** does not support retrieving Google users' avatars by Google ID (HaithemMosbahi/ngx-avatar#51), so I implemented the logic to retrieve Google users' avatars myself in the component. 

In order to send requests to Google People API, a public key is needed to provide. 
Creating a Google API Public Key is easy, please follow this instruction.
https://developers.google.com/maps/documentation/maps-static/get-api-key

Then fill the public key in the **environment.default.ts**
`google: {
    clientID: "",
    publicKey: "",
  },`

Demo: 
If the user is a normal Texera user, the avatar will be the default avatar with a random background color.
![normal user](https://user-images.githubusercontent.com/52941906/142727712-691254e9-d4ae-48f9-b70b-8a46bde91e54.gif)


If the user is a Google user, the avatar will be its Google profile picture. 
![google user](https://user-images.githubusercontent.com/52941906/142727715-3fc6e839-e33c-4405-9813-389cb573a152.gif)

Co-authored-by: Yicong Huang <[email protected]>
  • Loading branch information
Zhen Guan and Yicong-Huang authored Mar 2, 2022
1 parent 0edbbda commit 363f683
Show file tree
Hide file tree
Showing 13 changed files with 184 additions and 2 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ object JwtAuth {
val claims = new JwtClaims
claims.setSubject(user.getName)
claims.setClaim("userId", user.getUid)
claims.setClaim("googleId", user.getGoogleId)
claims.setExpirationTimeMinutesInTheFuture(dayToMin(expireInDays))
claims
}
Expand Down
2 changes: 2 additions & 0 deletions core/new-gui/src/app/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ import { NgbdModalFileAddComponent } from "./dashboard/component/feature-contain
import { UserFileSectionComponent } from "./dashboard/component/feature-container/user-file-section/user-file-section.component";
import { TopBarComponent } from "./dashboard/component/top-bar/top-bar.component";
import { UserIconComponent } from "./dashboard/component/top-bar/user-icon/user-icon.component";
import { UserAvatarComponent } from "./dashboard/component/user-avatar/user-avatar.component";
import { NgbdModalUserLoginComponent } from "./dashboard/component/top-bar/user-icon/user-login/ngbdmodal-user-login.component";
import { CodeEditorDialogComponent } from "./workspace/component/code-editor-dialog/code-editor-dialog.component";
import { CodeareaCustomTemplateComponent } from "./workspace/component/codearea-custom-template/codearea-custom-template.component";
Expand Down Expand Up @@ -133,6 +134,7 @@ registerLocaleData(en);
DashboardComponent,
TopBarComponent,
UserIconComponent,
UserAvatarComponent,
FeatureBarComponent,
FeatureContainerComponent,
SavedWorkflowSectionComponent,
Expand Down
5 changes: 4 additions & 1 deletion core/new-gui/src/app/common/service/user/auth.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,10 @@ export class AuthService {
if (token !== null && !this.jwtHelperService.isTokenExpired(token)) {
this.registerAutoLogout();
this.registerAutoRefreshToken();
return of(<User>{ name: this.jwtHelperService.decodeToken(token).sub });
return of(<User>{
name: this.jwtHelperService.decodeToken(token).sub,
googleId: this.jwtHelperService.decodeToken(token).googleId,
});
} else {
// access token is expired, logout instantly
return this.logout();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export const MOCK_USER_NAME = "testUser";
export const MOCK_USER = {
name: MOCK_USER_NAME,
uid: MOCK_USER_ID,
googleId: undefined,
};

/**
Expand Down
1 change: 1 addition & 0 deletions core/new-gui/src/app/common/type/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@ export interface User
extends Readonly<{
name: string;
uid: number;
googleId?: string;
}> {}
Original file line number Diff line number Diff line change
@@ -1,5 +1,16 @@
<div ngbDropdown class="d-inline-block user-icon">
<button class="btn btn-outline-primary" id="dropdownBasic1" ngbDropdownToggle>{{ user?.name || "Sign In" }}</button>
<button *ngIf="!user?.name;else user_avatar" class="btn btn-outline-primary" id="dropdownBasic1" ngbDropdownToggle>
Sign In
</button>
<ng-template #user_avatar>
<texera-user-avatar
class="user-avatar caret-off"
ngbDropdownToggle
[googleId]="user?.googleId || '' "
[userName]="user?.name || ''"
>
</texera-user-avatar>
</ng-template>
<div aria-labelledby="dropdownBasic1" class="user-dropdown" ngbDropdownMenu>
<button (click)="onClickLogin()" *ngIf="!user" class="dropdown-item">Sign In</button>
<button (click)="onClickRegister()" *ngIf="!user" class="dropdown-item">Sign up</button>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,15 @@
.user-dropdown {
width: 100%;
}

.user-avatar {
margin-right: 40px;
}

.caret-off::before {
display: none;
}

.caret-off::after {
display: none;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<div class="d-inline-block">
<nz-avatar
id="texera-user-avatar"
class="texera-user-avatar"
[nzSrc]="googleUserAvatarSrc || ''"
[nzAlt]=""
[nzText]="abbreviate(userName || '')"
>
</nz-avatar>
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
.texera-user-avatar {
object-fit: cover;
border-radius: 50%;
width: 38px;
height: 38px;
vertical-align: top;
font-size: smaller;
font-family: roboto, arial;
color: white;
line-height: 38px;
text-align: center;
}

.texera-user-avatar:hover {
box-shadow: #ccc 0px 0px 10px;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { ComponentFixture, TestBed } from "@angular/core/testing";
import { HttpClientModule } from "@angular/common/http";
import { UserAvatarComponent } from "./user-avatar.component";
import { HttpClientTestingModule } from "@angular/common/http/testing";

describe("UserIconComponent", () => {
let component: UserAvatarComponent;
let fixture: ComponentFixture<UserAvatarComponent>;

beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [UserAvatarComponent],
imports: [HttpClientModule, HttpClientTestingModule],
}).compileComponents();
});

beforeEach(() => {
fixture = TestBed.createComponent(UserAvatarComponent);
component = fixture.componentInstance;
component.googleId = undefined;
component.userName = "fake Texera user";
fixture.detectChanges();
});

it("should create", () => {
expect(component).toBeTruthy();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { GooglePeopleApiResponse } from "../../type/google-api-response";
import { Component, OnInit, Input } from "@angular/core";
import { UntilDestroy, untilDestroyed } from "@ngneat/until-destroy";
import { HttpClient } from "@angular/common/http";
import { environment } from "../../../../environments/environment";

@UntilDestroy()
@Component({
selector: "texera-user-avatar",
templateUrl: "./user-avatar.component.html",
styleUrls: ["./user-avatar.component.scss"],
})

/**
* UserAvatarComponent is used to show the avatar of a user
* The avatar of a Google user will be its Google profile picture
* The avatar of a normal user will be a default one with the initial
*
* Use Google People API to retrieve google user's profile picture
* Check https://developers.google.com/people/api/rest/v1/people/get for more details of the api usage
*
* @author Zhen Guan
*/
export class UserAvatarComponent implements OnInit {
public googleUserAvatarSrc: string = "";
private publicKey = environment.google.publicKey;

constructor(private http: HttpClient) {}

@Input() googleId?: string;
@Input() userName?: string;

ngOnInit(): void {
if (!this.googleId && !this.userName) {
throw new Error("google Id or user name should be provided");
} else if (this.googleId) {
this.userName = "";
// get the avatar of the google user
const googlePeopleAPIUrl = `https://people.googleapis.com/v1/people/${this.googleId}?personFields=names%2Cphotos&key=${this.publicKey}`;
this.http
.get<GooglePeopleApiResponse>(googlePeopleAPIUrl)
.pipe(untilDestroyed(this))
.subscribe(res => {
this.googleUserAvatarSrc = res.photos[0].url;
});
} else {
const r = Math.floor(Math.random() * 255);
const g = Math.floor(Math.random() * 255);
const b = Math.floor(Math.random() * 255);
const avatar = document.getElementById("texera-user-avatar");
if (avatar) {
avatar.style.backgroundColor = "rgba(" + r + "," + g + "," + b + ",0.8)";
}
}
}

/**
* abbreviates the name under 5 chars
* @param userName
*/
public abbreviate(userName: string): string {
if (userName.length <= 5) {
return userName;
} else {
return this.getUserInitial(userName).slice(0, 5);
}
}

public getUserInitial(userName: string): string {
return userName + "he";
}
}
24 changes: 24 additions & 0 deletions core/new-gui/src/app/dashboard/type/google-api-response.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
export interface Source
extends Readonly<{
type: string;
id: string;
}> {}

export interface Metadata
extends Readonly<{
primary: boolean;
source: Source;
}> {}

export interface Photo
extends Readonly<{
metadata: Metadata;
url: string;
}> {}

export interface GooglePeopleApiResponse
extends Readonly<{
resourceName: string;
etag: string;
photos: Photo[];
}> {}
1 change: 1 addition & 0 deletions core/new-gui/src/environments/environment.default.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ export const defaultEnvironment = {
*/
google: {
clientID: "",
publicKey: "",
},

/**
Expand Down

0 comments on commit 363f683

Please sign in to comment.