For this challenge, you are going to build a mock comments section.
Design
We're going to focus on two aspects:
Users
Users come in 3 flavors, normal users, moderators, and admins. Normal users can only create new comments, and edit the their own
comments. Moderators have the added ability to delete comments (to
remove trolls), while admins have the ability to edit or delete any
comment.
Users can log in and out, and we track when they last logged in
Comments
Comments are simply a message, a timestamp, and the author.
Comments can also be a reply, so we'll store what the parent comment was.
Beneath is my code:
class Admin extends Moderator {
constructor(name) {
super(name);
}
canEdit(comment) {
return true;
}
}
class Comment {
constructor(author, message, repliedTo) {
this.createdAt = new Date();
this._author = author;
this._message = message;
this.repliedTo = repliedTo || null;
}
getMessage() {
return this._message;
}
setMessage(message) {
this._message = message;
}
getCreatedAt() {
return this.createdAt;
}
getAuthor() {
return this._author;
}
getRepliedTo() {
return this.repliedTo;
}
getString(comment) {
const authorName = comment.getAuthor().getName();
if (!comment.getRepliedTo()) return authorName;
return `${comment.getMessage()} by ${authorName} (replied to ${this.getString(comment.getRepliedTo())})`;
}
toString() {
const authorName = this.getAuthor().getName();
if (!this.getRepliedTo()) {
return `${this._message} by ${authorName}`;
}
return this.getString(this);
}
}
I get the error
The toString method should return the correct hierarchy (nested reply)
Although this supposed to be an assignment, the question was a bit technical and unclear; this's the proven solution
class User {
constructor(name) {
this._name = name;
this._loggedIn = false;
this._lastLoggedInAt = null;
}
isLoggedIn() {
return this._loggedIn;
}
getLastLoggedInAt() {
return this._lastLoggedInAt;
}
logIn() {
this._lastLoggedInAt = new Date();
this._loggedIn = true;
}
logOut() {
this._loggedIn = false
}
getName() {
return this._name;
}
setName(name) {
this._name = name;
}
canEdit(comment) {
if(comment._author._name === this._name) {
return true;
}
return false;
}
canDelete(comment) {
return false;
}
}
class Moderator extends User {
constructor(name) {
super(name);
}
canDelete(comment) {
return true;
}
}
class Admin extends Moderator {
constructor(name) {
super(name)
}
canEdit(comment) {
return true;
}
}
class Comment {
constructor(author = null, message, repliedTo = null) {
this._createdAt = new Date();
this._message = message;
this._repliedTo = repliedTo;
this._author = author;
}
getMessage() {
return this._message;
}
setMessage(message) {
this._message = message;
}
getCreatedAt() {
return this._createdAt;
}
getAuthor() {
return this._author;
}
getRepliedTo() {
return this._repliedTo;
}
toString() {
if(this._repliedTo === null) {
return this._message + " by " + this._author._name
}
return this._message + " by " + this._author._name + " (replied to " +
this._repliedTo._author._name + ")"
}
}
The error was because you were calling a getName() method on getAuthor method which wasn't available. You can get author name directly from Comment this._author._name.
I make use of JavaScript constructor coding style, to write this solution but it should not matter much as the solution does not need you to change your style. Observe that the fields (_author, _message, _repliedTo) are private, and private fields can only be accessed through public methods. And that is basically what I did here in the toString() method.
function Comment(author, message, repliedTo = null) {
var _author = author;
var _message = message;
var _repliedTo = repliedTo;
this.getAuthor = function() {
return _author;
};
this.getRepliedTo = function() {
return _repliedTo;
};
this.toString = function() {
return ((_repliedTo === null) ? message + " by " + _author.getName() : message + " by " + _author.getName() + " (replied to " + this.getRepliedTo().getAuthor().getName() + ")");
}
};
You can remove the getString() method...
toString()
{
return ((this._repliedTo === null) ? this._message + " by " +
this._author.getName() : this._message + " by " +
this._author.getName() + " (replied to " + this._repliedTo._author.getName() + ")");
}
class User {
function __construct($name) {
private $name;
private $loggedIn;
private $lastLoggedInAt;
$this->name = $name;
$this->loggedIn = false;
$this->lastLoggedInAt = null;
}
function isLoggedIn() {
return $this->loggedIn;
}
function getLastLoggedInAt() {
return $this->lastLoggedInAt;
}
function logIn() {
$this->lastLoggedInAt = new Date('Y-m-d H:i:s');
$this->loggedIn = true;
}
function logOut() {
$this->loggedIn = false;
}
function getName() {
return $this->name;
}
function setName($name) {
$this->name = $name;
}
function canEdit($comment) {
if($comment->author->name === $this->name) {
return true;
}
return false;
}
function canDelete($comment) {
return false;
}
}
class Moderator extends User {
function __construct($name) {
$this->name = $name;
}
function canDelete($comment) {
return true;
}
}
class Admin extends Moderator {
function constructor($name) {
$this->name = $name;
}
function canEdit($comment) {
return true;
}
}
class Comment {
function __construct($author = null, $message, $repliedTo = null) {
private $createdAt;
private $message;
private $repliedTo;
private $author;
$this->createdAt = new Date('Y-m-d H:i:s');
$this->message = $message;
$this->repliedTo = $repliedTo;
$this->author = $author;
}
function getMessage() {
return $this->message;
}
function setMessage($message) {
$this->message = $message;
}
function getCreatedAt() {
return $this->createdAt;
}
function getAuthor() {
return $this->author;
}
function getRepliedTo() {
return $this->repliedTo;
}
function __toString() {
if($this->repliedTo === null) {
return $this->message + " by " + $this->author->name;
}
return $this->message + " by " + $this->author->name + " (replied to " +
$this->repliedTo->author->name + ")";
}
}
Java version if anyone needs:
import java.util.Date;
public class Solution {
public static class User {
String name;
boolean loggedIn;
Date lastLoggedInAt;
public User(String name) {
this.name = name;
this.loggedIn = loggedIn;
this.lastLoggedInAt = lastLoggedInAt;
}
public boolean isLoggedIn() {
return this.loggedIn;
}
public Date getLastLoggedInAt() {
return this.lastLoggedInAt;
}
public void logIn() {
this.lastLoggedInAt = new Date();
this.loggedIn = true;
}
public void logOut() {
this.loggedIn = false;
}
public String getName() {
return this.name;
}
public void setName(String name) {
this.name = name;
}
public boolean canEdit(Comment comment) {
if(comment.getAuthor().name == this.name) {
return true;
}
return false;
}
public boolean canDelete(Comment comment) {
return false;
}
}
public static class Moderator extends User{
public Moderator(String name) {
super(name);
}
public boolean canDelete(Comment comment) {
return true;
}
}
public static class Admin extends Moderator{
public Admin(String name) {
super(name);
}
public boolean canEdit(Comment comment) {
return true;
}
}
public static class Comment {
User author;
//注意下面也要是author
String message;
Comment comment;
Date createdAt;
Comment repliedTo;
public Comment(User author, String message) {
this.author = author;
this.message = message;
}
public Comment(User author, String message, Comment repliedTo) {
this.author = author;
this.message = message;
this.repliedTo = repliedTo;
}
public String getMessage() {
return this.message;
}
public void setMessage(String message) {
this.message = message;
}
public Date getCreatedAt() {
return this.createdAt;
}
public User getAuthor() {
return this.author;
}
public Comment getRepliedTo() {
return this.repliedTo;
}
public String toString() {
if(this.repliedTo == null) {
return this.message + " by " + this.author.getName();
}
return this.message + " by " + this.author.getName() + " (replied to " +
this.repliedTo.getAuthor().name + ")";
}
}
}
Answers provided above won't pass basic unit tests for this assignment. Here's an option I successfully submitted to complete the challenge:
export class User {
constructor(name) {
this._name = name;
this._lastLoginDate = null;
this._loggedIn = false;
}
isLoggedIn() {
return this._loggedIn;
}
getLastLoggedInAt() {
return this._lastLoginDate;
}
logIn() {
this._lastLoginDate = new Date();
return Promise.resolve('Success').then(() => {
this._loggedIn = true;
});
}
logOut() {
this._loggedIn = false;
}
getName() {
return this._name;
}
setName(name) {
this._name = name;
}
canEdit(comment) {
if (comment.getAuthor().getName() === this.getName()) {
return true;
}
return false;
}
canDelete(comment) {
return false;
}
}
export class Moderator extends User {
constructor(name) {
super(name);
}
canDelete(comment) {
return true;
}
}
export class Admin extends Moderator {
constructor(name) {
super(name);
}
canEdit(comment) {
return true;
}
}
export class Comment {
constructor(author, message, repliedTo = null) {
this._author = author;
this._message = message;
this._repliedTo = repliedTo || null;
this._createdAt = new Date();
}
getMessage() {
return this._message;
}
setMessage(message) {
this._message = message;
}
getCreatedAt() {
return this._createdAt;
}
getAuthor() {
return this._author;
}
getRepliedTo() {
return this._repliedTo;
}
toString() {
return this.getRepliedTo() === null
? `${this.getMessage()} by ${this.getAuthor().getName()}`
: `${this.getMessage()} by ${this.getAuthor().getName()} (replied to ${this.getRepliedTo()
.getAuthor()
.getName()})`;
}
}
If you want to solve this thing using typescript:
export class User {
private _name: string;
private _loggedIn: boolean;
private _lastLoggedInAt: Date | null;
constructor(name: string) {
this._name = name;
this._loggedIn = false;
this._lastLoggedInAt = null;
}
isLoggedIn(): boolean {
return this._loggedIn;
}
getLastLoggedInAt(): Date | null {
return this._lastLoggedInAt;
}
async logIn(): Promise<void> {
this._lastLoggedInAt = new Date();
await Promise.resolve("suceess");
this._loggedIn = true;
}
logOut(): void {
this._loggedIn = false;
}
getName(): string {
return this._name;
}
setName(name: string): void {
this._name = name;
}
canEdit(comment: Comment): boolean {
if (comment.getAuthor().getName() === this._name) {
return true;
}
return false;
}
canDelete(comment: Comment): boolean {
return false;
}
}
export class Moderator extends User {
constructor(name: string) {
super(name);
}
canDelete(_comment: Comment): boolean {
return true;
}
}
export class Admin extends Moderator {
constructor(name: string) {
super(name);
}
canEdit(_comment: Comment): boolean {
return true;
}
}
export class Comment {
private _author: User;
private _message: string;
private _repliedTo?: Comment | null;
private _createdAt: Date;
constructor(author: User, message: string, repliedTo?: Comment) {
this._author = author;
this._message = message;
this._repliedTo = repliedTo;
this._createdAt = new Date();
}
getMessage(): string {
return this._message;
}
setMessage(message: string): void {
this._message = message;
}
getCreatedAt(): Date {
return this._createdAt;
}
getAuthor(): User {
return this._author;
}
getRepliedTo(): Comment | null {
if (this._repliedTo) {
return this._repliedTo;
}
return null;
}
toString(): string {
if (this.getRepliedTo()) {
return `${this.getMessage()} by ${this.getAuthor().getName()} (replied to ${this._repliedTo
?.getAuthor()
.getName()})`;
}
return `${this.getMessage()} by ${this.getAuthor().getName()}`;
}
}
and few unit tests using Jest
describe('Normal user tests', function() {
it('should return name of the User', () => {
const user = new User("User 1");
expect(user.getName()).toEqual('User 1');
});
it('shoud return isLoggedIn as true when logIn method is called' , () => {
const user = new User("User 1");
user.logIn().then(() => {
expect(user.isLoggedIn()).toBeTruthy();
}).catch((error) => {
expect(user.isLoggedIn()).toBe(false);
});;
});
it('shoud return _lastLoggedInAt as the date when logIn method is called' , () => {
const user = new User("User 1");
user.logIn().then(() => {
expect(user.getLastLoggedInAt()).toBe(new Date());
})
});
it('shoud return _loggedIn as false when logOut method is called', () => {
const user = new User("User 1");
user.logOut();
expect(user.isLoggedIn()).toBe(false);
});
it('shoud setName of the user' , () => {
const user = new User("User");
user.setName("User 2");
expect(user.getName()).toEqual('User 2');
});
});
describe('Moderator user tests', function() {
it('should return name of the User', () => {
const moderator = new Moderator("Moderator 1");
const message = "Hello there"
const comment = new Comment (moderator, message);
expect(moderator.canDelete(comment)).toBeTruthy();
});
});
describe('Admin user tests', function() {
it('should return name of the User', () => {
const admin = new Admin("Admin 1");
const message = "Hello there"
const comment = new Comment (admin, message);
expect(admin.canEdit(comment)).toBeTruthy();
});
});
describe('Comment tests', function() {
it('should return message of the author', () => {
const user = new User("User 1");
const message = "Hi! This is my message."
const comment = new Comment (user, message)
expect(comment.getMessage()).toEqual(message);
});
it('should set new message', () => {
const user = new User("User 1");
const message = "Hi! This is my message."
const newMessage = "Hi! This is new message."
const comment = new Comment (user, message);
comment.setMessage(newMessage)
expect(comment.getMessage()).toEqual(newMessage);
});
it('should return null when replied to does not exists', () => {
const user = new User("User 1");
const message = "Hi! This is my message."
const comment = new Comment (user, message);
expect(comment.getRepliedTo()).toBe(null);
});
it('should return repliedTo when replied to exists', () => {
const user = new User("User 1");
const message = "Hi! This is my message."
const repliedToUser = new User("User 2");
const repliedToMessage = "Hi! This is my replied message.";
const repliedToComment = new Comment (repliedToUser, repliedToMessage);
const comment = new Comment (user, message, repliedToComment);
expect(comment.getRepliedTo()).toBe(repliedToComment);
});
it('should return repliedTo string message when replied to exists', () => {
const user = new User("User1");
const message = "Hello there"
const repliedToUser = new User("User2");
const repliedToMessage = "Hi! This is my replied message.";
const repliedToComment = new Comment (repliedToUser, repliedToMessage);
const comment = new Comment (user, message, repliedToComment);
expect(comment.toString()).toBe("Hello there by User1 (replied to User2)");
});
it('should return comment string message when replied to does not exists', () => {
const user = new User("User1");
const message = "Hello there"
const comment = new Comment (user, message);
expect(comment.toString()).toBe("Hello there by User1");
});
});
Related
Could you explain me how I could use concatMap on getPrices() and getDetails()?
export class HistoricalPricesComponent implements OnInit, OnDestroy {
private unsubscribe$ = new Subject < void > ();
infoTitle: string = "";
lines: HistoryPoint[] = [];
model: Currency = new Currency();
svm: string;
constructor(
private location: Location,
private service: HistoricalPricesService,
private activatedRoute: ActivatedRoute
) {}
ngOnInit(): void {
let svm: string | null;
svm = this.activatedRoute.snapshot.paramMap.get('svm');
if (!svm) {
this.goBack();
return;
}
this.svm = svm;
this.getPrices();
this.getDetails(svm)
}
ngOnDestroy(): void {
this.unsubscribe$.next();
this.unsubscribe$.complete();
}
getPrices(): void {
this.service.getInstrumentHistoryEquities(this.svm, this.model).pipe(
takeUntil(this.unsubscribe$)
).subscribe(res => {
if (res.RETURNCODE === ApiResponseCodeEnum.Ok) {
if (res.HISTO.POINT.length > 0) {
this.lines = res.HISTO.POINT.reverse();
}
}
});
}
getDetails(svm: string): void {
this.service.getInstrumentInfo(svm).pipe(
takeUntil(this.unsubscribe$)
).subscribe(res => {
if (res.RETURNCODE === ApiResponseCodeEnum.Ok) {
this.infoTitle += " " + res.ADVTITRE.BASIQUETITRE.LABEL + " (" + res.ADVTITRE.BASIQUETITRE.PLACELABEL + ")";
}
});
}
goBack(): void {
this.location.back();
}
}
I tried to look on this page
https://www.tektutorialshub.com/angular/using-concatmap-in-angular/
But I don't know where to start?
The example does not allow me to understand how I could create this?
here it is
concatMap is a operator stream so you just need to use it through a creator stream to that I initialize two Subject for each actions getPrices & getDetails.
Then I perform the AJAX call following the concatMap strategy and get that into Observable in order to be combined or used directly into the template of the component.
export class HistoricalPricesComponent implements OnInit, OnDestroy {
private unsubscribe$ = new Subject < void > ();
infoTitle: string = "";
lines: HistoryPoint[] = [];
model: Currency = new Currency();
svm: string;
// actions
getPrices$ = new Subject<void>();
getDetails$ = new Subject<string>();
// states
prices$: Observable<any>();
details$: Observable<any>();
constructor(
private location: Location,
private service: HistoricalPricesService,
private activatedRoute: ActivatedRoute
) {}
ngOnInit(): void {
let svm: string | null;
svm = this.activatedRoute.snapshot.paramMap.get('svm');
if (!svm) {
this.goBack();
return;
}
this.svm = svm;
this.prices$ = this.getPrices$.pipe(concatMap(this.getPrices)
this.details$ = this.getDetails$.pipe(concatMap((svm => this.getDetails(svm))
}
ngOnDestroy(): void {
this.getPrices$.complete()
this.getDetails$.complete()
this.unsubscribe$.complete();
}
getPrices(): void {
this.service.getInstrumentHistoryEquities(this.svm, this.model).pipe(
takeUntil(this.unsubscribe$)
).subscribe(res => {
if (res.RETURNCODE === ApiResponseCodeEnum.Ok) {
if (res.HISTO.POINT.length > 0) {
this.lines = res.HISTO.POINT.reverse();
}
}
});
}
getDetails(svm: string): void {
this.service.getInstrumentInfo(svm).pipe(
takeUntil(this.unsubscribe$)
).subscribe(res => {
if (res.RETURNCODE === ApiResponseCodeEnum.Ok) {
this.infoTitle += " " + res.ADVTITRE.BASIQUETITRE.LABEL + " (" + res.ADVTITRE.BASIQUETITRE.PLACELABEL + ")";
}
});
}
goBack(): void {
this.location.back();
}
}
Some simple code that I feel should work:
function getUser(id) {
var user;
// get user from DB
return user;
}
var user = getUser(1);
var uname = user.getName();
console.log(uname);
// user is expected to be an instance of the following class:
class User {#
id = 1;#
first = 'Bob';#
last = 'Test'
/*
* # here denotes a private variable. If the syntax is not available,
* simply use an underscore `var _`, so that it becomes
* var _id = 1;
* var _first = 'Bob';
* var _last = 'Test';
* They will no longer be true private variables, but it should be close enough.
*/
constructor() {}
get id() {
return this.#id;
}
set id(value) {
this.#id = value;
}
get firstName() {
return this.#first;
}
set firstName(value) {
this.#first = value;
}
get lastName() {
return this.#last;
}
set lastName(value) {
this.#last = value;
}
getName() {
return `${this.firstName} ${this.lastName}`;
}
}
The error is:
Uncaught TypeError: Cannot read properties of undefined (reading 'getName')
What is going on?
A similar error occurs when I use a service (and fetch to get the user):
async function getUser(id) {
// get user from API, using fetch from a suitable library (not relevant to question)
let user;
try {
const response = await fetch(`/users/${id}`);
user = await response.json();
} catch {}
return user;
}
(async function () {
const user = await getUser(1);
const uname = user.getName();
console.log(uname);
})();
// user is expected to be an instance of the following class:
class User {
#id = 1;
#first = 'Bob';
#last = 'Test';
/*
* # here denotes a private variable. If the syntax is not available,
* simply use an underscore `var _`, so that it becomes
* var _id = 1;
* var _first = 'Bob';
* var _last = 'Test';
* They will no longer be true private variables, but it should be close enough.
*/
constructor() {}
get id() {
return this.#id;
}
set id(value) {
this.#id = value;
}
get firstName() {
return this.#first;
}
set firstName(value) {
this.#first = value;
}
get lastName() {
return this.#last;
}
set lastName(value) {
this.#last = value;
}
getName() {
return `${this.firstName} ${this.lastName}`;
}
}
You cannot see the error in the Stack Snippet console but must open your Developer Tools to see it there.
The error simply includes a parenthetical "(in promise)" in the error text:
Uncaught (in promise) TypeError: Cannot read properties of undefined (reading 'getName')
The function that is supposed to be setting user to an object with a getName() method is instead returning undefined. That value does not have any properties or methods, so it throws the error shown.
There are several ways around the error, but it comes down to deciding what you want to do when getUser returns undefined. You can throw a different, more informative error. Either at the time you know you didn't get a User back from getUser(1):
class User {
#id = 1;
#first = 'Bob';
#last = 'Test'
constructor() {}
get id() {
return this.#id;
}
set id(value) {
this.#id = value;
}
get firstName() {
return this.#first;
}
set firstName(value) {
this.#first = value;
}
get lastName() {
return this.#last;
}
set lastName(value) {
this.#last = value;
}
getName() {
return `${this.firstName} ${this.lastName}`;
}
}
function getUser(id) {
// get user from DB
var user;
return user;
}
var user = getUser(1);
if (!(user instanceof User)) {
throw new Error(`The value returned from getUser(1): ${JSON.stringify(user)} was not a User.`);
}
var uid = user.getName();
console.log(uid);
Or later, via the "optional chaining" operator:
class User {
#id = 1;
#first = 'Bob';
#last = 'Test'
constructor() {}
get id() {
return this.#id;
}
set id(value) {
this.#id = value;
}
get firstName() {
return this.#first;
}
set firstName(value) {
this.#first = value;
}
get lastName() {
return this.#last;
}
set lastName(value) {
this.#last = value;
}
getName() {
return `${this.firstName} ${this.lastName}`;
}
}
function getUser(id) {
// get user from DB
var user;
return user;
}
var user = getUser(1);
var uname = user?.geName();
// If your environment does not allow for optional chaining, simply use &&:
// var uname = user && user.getName();
if (typeof uname !== 'string') {
throw new Error(`The value returned from getUser(1).getName(): ${JSON.stringify(uname)} was not a string.`);
}
console.log(uname);
You can also just choose to pass the problem on to the next consumer. So, assuming the code is part of a tautological getUserId(userId) function, you could pass the undefined on, again, via our friend the optional chaining operator ?..
class User {
#id = 1;
#first = 'Bob';
#last = 'Test'
constructor() {}
get id() {
return this.#id;
}
set id(value) {
this.#id = value;
}
get firstName() {
return this.#first;
}
set firstName(value) {
this.#first = value;
}
get lastName() {
return this.#last;
}
set lastName(value) {
this.#last = value;
}
getName() {
return `${this.firstName} ${this.lastName}`;
}
}
function getUser(id) {
// get user from DB
var user;
return user;
}
function getUserName(id) {
var user = getUser(id);
var uname = user?.getName();
return uname;
}
console.log(getUserName(1));
So I am building a android video call app using webrtc I have implemented every thing and tested the video call feature from my app it works like charm. But now I want to implement where the user can switch camera from rear to front and vice verse. I have seen a similar answer it was not for android it said to remove the stream and negotiate and new stream I didnt properly understand how to implement that in this code so I hope someone can help me with that thing
Thank you
JavaScript File
localVideo.style.opacity = 0
remoteVideo.style.opacity = 0
localVideo.onplaying = () => { localVideo.style.opacity = 1 }
remoteVideo.onplaying = () => { remoteVideo.style.opacity = 1 }
let peer
function init(userId) {
peer = new Peer(userId, {
host: 'messenger-by-margs-video-call.herokuapp.com',
secure: true
})
peer.on('open', () => {
Android.onPeerConnected()
})
listen()
}
let localStream
function listen() {
peer.on('call', (call) => {
navigator.getUserMedia({
audio: true,
video: true
}, (stream) => {
localVideo.srcObject = stream
localStream = stream
call.answer(stream)
call.on('stream', (remoteStream) => {
remoteVideo.srcObject = remoteStream
remoteVideo.className = "primary-video"
localVideo.className = "secondary-video"
})
})
})
}
function startCall(otherUserId) {
navigator.getUserMedia({
audio: true,
video: true
}, (stream) => {
localVideo.srcObject = stream
localStream = stream
const call = peer.call(otherUserId, stream)
call.on('stream', (remoteStream) => {
remoteVideo.srcObject = remoteStream
remoteVideo.className = "primary-video"
localVideo.className = "secondary-video"
})
})
}
function toggleVideo(b) {
if (b == "true") {
localStream.getVideoTracks()[0].enabled = true
} else {
localStream.getVideoTracks()[0].enabled = false
}
}
function toggleAudio(b) {
if (b == "true") {
localStream.getAudioTracks()[0].enabled = true
} else {
localStream.getAudioTracks()[0].enabled = false
}
}
Heres my kotlin activity file.
class CallActivity : AppCompatActivity() {
var username = ""
var friendsUsername = ""
var isPeerConnected = false
var firebaseRef = FirebaseDatabase.getInstance().getReference("Users")
var isAudio = true
var isVideo = true
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_video_call)
username = FirebaseAuth.getInstance().uid.toString();
friendsUsername = intent.getStringExtra("userid")!!
call.setOnClickListener {
sendCallRequest()
}
toggleAudioBtn.setOnClickListener {
isAudio = !isAudio
callJavascriptFunction("javascript:toggleAudio(\"${isAudio}\")")
toggleAudioBtn.setImageResource(if (isAudio) R.drawable.ic_baseline_mic_24 else R.drawable.ic_baseline_mic_off_24 )
}
toggleVideoBtn.setOnClickListener {
isVideo = !isVideo
callJavascriptFunction("javascript:toggleVideo(\"${isVideo}\")")
toggleVideoBtn.setImageResource(if (isVideo) R.drawable.ic_baseline_videocam_24 else R.drawable.ic_baseline_videocam_off_24 )
}
setupWebView()
}
private fun sendCallRequest() {
if (!isPeerConnected) {
Toast.makeText(this, "You're not connected. Check your internet", Toast.LENGTH_LONG).show()
return
}
firebaseRef.child(friendsUsername).child("videocall").child("incall").setValue(username)
firebaseRef.child(friendsUsername).child("videocall").child("isAvailable").addValueEventListener(object: ValueEventListener {
override fun onCancelled(error: DatabaseError) {}
override fun onDataChange(snapshot: DataSnapshot) {
if (snapshot.value.toString() == "true") {
listenForConnId()
}
}
})
}
private fun listenForConnId() {
firebaseRef.child(friendsUsername).child("videocall").child("connId").addValueEventListener(object: ValueEventListener {
override fun onCancelled(error: DatabaseError) {}
override fun onDataChange(snapshot: DataSnapshot) {
if (snapshot.value == null)
return
switchToControls()
callJavascriptFunction("javascript:startCall(\"${snapshot.value}\")")
}
})
}
#SuppressLint("SetJavaScriptEnabled")
private fun setupWebView() {
webView.webChromeClient = object: WebChromeClient() {
override fun onPermissionRequest(request: PermissionRequest?) {
request?.grant(request.resources)
}
}
webView.settings.javaScriptEnabled = true
webView.settings.mediaPlaybackRequiresUserGesture = false
webView.addJavascriptInterface(JavascriptInterface(this), "Android")
loadVideoCall()
}
private fun loadVideoCall() {
val filePath = "file:android_asset/call.html"
webView.loadUrl(filePath)
webView.webViewClient = object: WebViewClient() {
override fun onPageFinished(view: WebView?, url: String?) {
initializePeer()
}
}
}
var uniqueId = ""
private fun initializePeer() {
uniqueId = getUniqueID()
callJavascriptFunction("javascript:init(\"${uniqueId}\")")
firebaseRef.child(username).child("videocall").child("incall").addValueEventListener(object: ValueEventListener {
override fun onCancelled(error: DatabaseError) {}
override fun onDataChange(snapshot: DataSnapshot) {
onCallRequest(snapshot.value as? String)
}
})
}
private fun onCallRequest(caller: String?) {
if (caller == null) return
callLayout.visibility = View.VISIBLE
incomingCallTxt.text = "$caller is calling..."
acceptBtn.setOnClickListener {
firebaseRef.child(username).child("videocall").child("connId").setValue(uniqueId)
firebaseRef.child(username).child("videocall").child("isAvailable").setValue(true)
callLayout.visibility = View.GONE
switchToControls()
}
rejectBtn.setOnClickListener {
firebaseRef.child(username).child("videocall").child("incall").setValue(null)
callLayout.visibility = View.GONE
}
}
private fun switchToControls() {
//inputLayout.visibility = View.GONE
call.visibility = View.INVISIBLE
callControlLayout.visibility = View.VISIBLE
}
private fun getUniqueID(): String {
return UUID.randomUUID().toString()
}
private fun callJavascriptFunction(functionString: String) {
webView.post { webView.evaluateJavascript(functionString, null) }
}
fun onPeerConnected() {
isPeerConnected = true
}
}
I hope these are necessary information to my answer
I am trying to create a class that will fetch / cache users from my Firestore database. For some reason, I can't seem to save or expose the previous promise that was created. Here is my class:
export class UserCache {
private cacheTimeMilliseconds: number = 600000;
private userCache: any = {};
public getCacheUser(userid: string): Promise<User> {
return new Promise((resolve, reject) => {
let d = new Date();
d.setTime(d.getTime() - this.cacheTimeMilliseconds);
if (this.userCache[userid] && this.userCache[userid].complete && this.userCache[userid].lastAccess > d.getTime()) {
console.log("User cached");
resolve(this.userCache[userid].user);
}
console.log("Need to cache user");
this.userCache[userid] = {
complete: false
};
this.getSetUserFetchPromise(userid).then((data) => {
let user: User = <User>{ id: data.id, ...data.data() };
this.userCache[userid].user = user;
this.userCache[userid].complete = true;
this.userCache[userid].lastAccess = Date.now();
resolve(user);
});
});
}
private getSetUserFetchPromise(userid: string): Promise<any> {
console.log(this.userCache[userid]);
if (this.userCache[userid] && this.userCache[userid].promise) {
return this.userCache[userid].promise;
} else {
console.log("Creating new user fetch request.");
this.userCache[userid].promise = firestore().collection('users').doc(userid).get();
console.log(this.userCache[userid]);
return this.userCache[userid].promise;
}
}
}
Logs: (there are only 2 unique users, so should only be creating 2 new requests)
In the logs I can see that the promise is getting set in getSetUserFetchPromise, but the next time the function is called, the property is no longer set. I suspect it is either a scope or concurrency issue, but I can't seem to get around it.
I am calling getCacheUser in a consuming class with let oCache = new UserCache() and oCache.getCacheUser('USERID')
Edit following Tuan's answer below
UserCacheProvider.ts
import firestore from '#react-native-firebase/firestore';
import { User } from '../static/models';
class UserCache {
private cacheTimeMilliseconds: number = 600000;
private userCache: any = {};
public getCacheUser(userid: string): Promise<User> {
return new Promise((resolve, reject) => {
let d = new Date();
d.setTime(d.getTime() - this.cacheTimeMilliseconds);
if (this.userCache[userid] && this.userCache[userid].complete && this.userCache[userid].lastAccess > d.getTime()) {
console.log("User cached");
resolve(this.userCache[userid].user);
}
console.log("Need to cache user");
this.userCache[userid] = {
complete: false
};
this.getSetUserFetchPromise(userid).then((data) => {
let user: User = <User>{ id: data.id, ...data.data() };
this.userCache[userid].user = user;
this.userCache[userid].complete = true;
this.userCache[userid].lastAccess = Date.now();
resolve(user);
});
});
}
private getSetUserFetchPromise(userid: string): Promise<any> {
console.log(this.userCache[userid]);
if (this.userCache[userid] && this.userCache[userid].promise) {
return this.userCache[userid].promise;
} else {
console.log("Creating new user fetch request.");
this.userCache[userid].promise = firestore().collection('users').doc(userid).get();
console.log(this.userCache[userid]);
return this.userCache[userid].promise;
}
}
}
const userCache = new UserCache();
export default userCache;
ChatProvider.ts (usage)
let promises = [];
docs.forEach(doc => {
let message: Message = <Message>{ id: doc.id, ...doc.data() };
promises.push(UserCacheProvider.getCacheUser(message.senderid).then((oUser) => {
let conv: GCMessage = {
_id: message.id,
text: message.messagecontent,
createdAt: new Date(message.messagedate),
user: <GCUser>{ _id: oUser.id, avatar: oUser.thumbnail, name: oUser.displayname }
}
if (message.type && message.type == 'info') {
conv.system = true;
}
if (message.messageattachment && message.messageattachment != '') {
conv.image = message.messageattachment;
}
return conv;
}));
});
Promise.all(promises).then((values) => {
resolve(values);
});
Without seeing the calling code, it could be that getCacheUser is called twice before firestore resolves.
As an aside, I think refactoring the class may make debugging easier. I wonder why it caches the user, promise completion status, and the promise itself. Why not just cache the promise, something like:
interface UserCacheRecord {
promise: Promise<User>
lastAccess: number
}
export class UserCache {
private cacheTimeMilliseconds: number = 600000;
private userCache: { [userid: string]: UserCacheRecord } = {};
public async getCacheUser(userid: string): Promise<User> {
let d = new Date();
const cacheExpireTime = d.getTime() - this.cacheTimeMilliseconds
if (this.userCache[userid] && this.userCache[userid].lastAccess > cacheExpireTime) {
console.log("User cached");
return this.userCache[userid].promise
}
console.log("Need to cache user");
this.userCache[userid] = {
promise: this.getUser(userid),
lastAccess: Date.now()
}
return this.userCache[userid].promise
}
private async getUser(userid: string): Promise<User> {
const data = firestore().collection('users').doc(userid).get();
return <User>{ id: data.id, ...data.data() };
}
}
Currently, you create new UserCache everytime you access cache users. You have to export the instance of UserCache class, so just single instance is used for your app.
UserCache.ts
class UserCache {
}
const userCache = new UserCache();
export default userCache;
SomeFile.ts
import UserCache from './UserCache';
UserCache.getCacheUser('USERID')
Update
Added some tests
class UserCache {
userCache = {};
getUser(id) {
return new Promise((resolve, reject) => {
if (this.userCache[id]) {
resolve({
...this.userCache[id],
isCache: true,
});
}
this.requestUser(id).then(data => {
resolve(data);
this.userCache[id] = data;
});
});
}
requestUser(id) {
return Promise.resolve({
id,
});
}
}
const userCache = new UserCache();
export default userCache;
userCache.test.ts
import UserCache from '../test';
describe('Test user cache', () => {
test('User cached successfully', async () => {
const user1: any = await UserCache.getUser('test1');
expect(user1.isCache).toBeUndefined();
const user2: any = await UserCache.getUser('test1');
expect(user2.isCache).toBe(true);
});
});
when trying to cancel upload by unsubscribing what actually happen that i unsubscribe to upload progress but that actual upload is not cancelled and keep uploading to the server.
upload.components.ts
import { Component, OnInit, Input, Output, EventEmitter, OnDestroy } from '#angular/core';
import { Subject, Subscription, Observable } from 'rxjs';
import { HttpEventType } from '#angular/common/http';
import { UploadService } from '../../../services';
import { takeUntil } from 'rxjs/operators';
#Component({
selector: 'app-image-upload-item',
templateUrl: './image-upload-item.component.html',
styleUrls: ['./image-upload-item.component.scss']
})
export class ImageUploadItemComponent implements OnInit, OnDestroy {
#Input() index: any;
#Output() uploadSuccess: EventEmitter<any>;
#Output() uploadCanceled: EventEmitter<any>;
public localimageURL: string;
public uploadProgress: number;
public isUploadCompleted: boolean;
public uploadImageObservable: Subscription;
public isReadyForUpload: boolean;
public isUploading: boolean;
public progressMode: string;
public readonly unique: string = Math.floor((Math.random() *
100)).toString();
public readonly imagePreviewID = 'imagePreview' + this.unique;
_file: any;
#Input() public set file(value: any) {
const reader = new FileReader();
reader.onload = (e: any) => {
this.localimageURL = e.target.result;
};
this._file = value;
reader.readAsDataURL(this._file);
console.log(this._file);
}
constructor(private uploadService: UploadService) {
this.uploadProgress = 0;
this.isUploading = false;
this.localimageURL = '';
this.isUploadCompleted = false;
this.uploadSuccess = new EventEmitter<any>();
this.uploadCanceled = new EventEmitter<any>();
this.progressMode = 'indeterminate';
}
ngOnInit() {
this.uploadImageToServer(this._file);
// setTimeout(() => {
// console.log('im in set time out unsubscripting',
this.uploadImageObservable);
// this.uploadImageObservable.forEach(subscription => {
// subscription.unsubscribe();
// });
// }, 100);
}
ngOnDestroy() {
console.log('component destroyed');
this.uploadImageObservable.unsubscribe();
}
public clearUploadButtonClicked() {
// if (this.uploadImageObservable !== undefined) {
// console.log('image observable is defined');
// this.uploadImageObservable.unsubscribe();
// console.log(this.uploadImageObservable.closed);
// }
// this.uploadImageObservable.unsubscribe();
this._file = '';
this.uploadCanceled.emit({ index: this.index, uploaded: false });
}
public get showUploadProgress(): boolean {
return this.uploadProgress !== 0;
}
public uploadImageToServer(file) {
this.isUploading = true;
const progress = new Subject<number>();
progress.subscribe(value => {
this.uploadProgress = value;
});
this.uploadImageObservable = this.uploadService.uploadImage(file)
.subscribe(result => {
const type = result.type;
const data = result.data;
console.log(result);
if (type === HttpEventType.UploadProgress) {
const percentDone = Math.round(100 * data.loaded / data.total);
progress.next(percentDone);
if (percentDone === 100) {
this.progressMode = 'indeterminate';
}
} else if (type === HttpEventType.Response) {
if (data) {
progress.complete();
this.progressMode = 'determinate';
this.isReadyForUpload = false;
this.isUploadCompleted = true;
this.isUploading = false;
this.uploadSuccess.emit({ index: this.index, mediaItem: data });
}
}
}, errorEvent => {
});
}
}
upload.service.ts
public uploadImage(imageFile: File): Observable<any> {
const formData: FormData = new FormData();
if (imageFile !== undefined) {
formData.append('image', imageFile, imageFile.name);
const req = new HttpRequest('POST', environment.uploadImageEndPoint,
formData, {
reportProgress: true,
});
return new Observable<any>(observer => {
this.httpClient.request<any>(req).subscribe(event => {
if (event.type === HttpEventType.Response) {
const responseBody = event.body;
if (responseBody) {
this.alertService.success(responseBody.message);
observer.next({ type: event.type, data: new
MediaItem(responseBody.mediaItem) });
}
} else if (event.type === HttpEventType.UploadProgress) {
observer.next({ type: event.type, data: { loaded: event.loaded, total:
event.total } });
} else {
observer.next(event);
}
}, errorEvent => {
if (errorEvent.status === 400) {
this.alertService.error(errorEvent.error['image']);
} else {
this.alertService.error('Server Error, Please try again later!');
}
observer.next(null);
});
});
}
}
how can i cancel upload request properly with observable unsubscribe
note i already tried pipe takeuntil() and nothing changed
What you'll want to do is return the result from the pipe function on the http request return observable. Right now you have multiple streams and the component's unsubscribe is only unsubscribing to the observable wrapping the http request observable (not connected).
You'll want to do something like:
return this.httpClient.request<any>(req).pipe(
// use rxjs operators here
);
You'll then use rxjs operators (I've been doing this for a while, but I still highly reference this site) to perform any logic needed and reflect things like your errors and upload progress to the component calling the service. On the component side, you'll keep your subscribe/unsubscribe logic.
For instance, you can use the switchMap operator to transform what is returning to the component from the http request observable and specify the value to return to the component, and catchError to react to any errors accordingly.
return this.httpClient.request<any>(req).pipe(
switchMap(event => {
if (event.type === HttpEventType.Response) {
const responseBody = event.body;
if (responseBody) {
this.alertService.success(responseBody.message);
return { type: event.type, data: new MediaItem(responseBody.mediaItem) };
}
} else if (event.type === HttpEventType.UploadProgress) {
return { type: event.type, data: { loaded: event.loaded, total: event.total } };
}
return event;
}),
catchError(errorEvent => {
if (errorEvent.status === 400) {
this.alertService.error(errorEvent.error['image']);
} else {
this.alertService.error('Server Error, Please try again later!');
}
return of(<falsy or error value>);
}),
);
Alternatively you could model it a little more after this example by just returning the http function call from the service to the component and handling things in the subscribe there.
actually i found a way as follows
public uploadImage(imageFile: File): Observable<any> {
const formData: FormData = new FormData();
if (imageFile !== undefined) {
formData.append('image', imageFile, imageFile.name);
const req = new HttpRequest('POST', environment.uploadImageEndPoint, formData, {
reportProgress: true,
});
return this.httpClient.request<any>(req).pipe(
map((res: any) => {
return res;
}),
catchError(errorEvent => {
if (errorEvent.status === 400) {
this.alertService.error(errorEvent.error['image']);
} else {
this.alertService.error('Server Error, Please try again later!');
return Observable.throw(errorEvent);
}
return Observable.throw(errorEvent);
}));
}
}