Smoothly loading messages Angular 8 - html

I'm writing sort of a chat application using Angular 8 and here's what I want to achieve:
My dialogue component that represents a chat between two users gets one page of last messages that consists of 10 messages after initiating. The div that contains these messages scrolls down to the very last message. When a user scrolls up and reaches a certain point the next page loads. The two arrays join and the user sees now 20 messages. Here's what I have so far:
HTML:
<div>
<div #scrollMe [scrollTop]="scrollMe.scrollHeight" (scroll)="onScroll($event)" style="overflow-y: scroll; height: 400px;">
<ul>
<li *ngFor="let message of messages?.reverse()">
</ul>
</div>
</div>
Typescipt:
loadMessages(page: number, itemsPerPage?: number) {
this.messageService.getMessageThread(page, itemsPerPage || 10)
.subscribe((res: PaginatedResult<MessageThread>) => {
if (this.messages == null) {
this.messages = res.result.messages;
} else {
this.messages = this.messages.concat(res.result.messages);
}
});
}
onScroll(event) {
if (event.target.scrollTop < 100) {
if (this.pagination.currentPage >= this.pagination.totalPages) {
return;
}
this.loadMessages(++this.pagination.currentPage);
}
}
It works, but the problem is that when I join these two arrays, my scrollbar jumps very ugly and since I hold the scrollbar it stays at the same position and keeps loading next pages. I am very new to Angular and front-end in general so I have a feeling that I'm missing something. I tried to find any ready-to-go solutions but could not. Any help would be appreciated.
Please note that I don't want to use JQuery.

Several things:
First, we need a loading flag:
loading = false;
Then we make loadMessages return an observable instead of handle the result:
loadMessages(page: number, itemsPerPage?: number) {
this.loading = true;
return this.messageService.getMessageThread(page, itemsPerPage || 10);
}
A separate method handleResponse handles the response by setting loading to false and concatenating the messages.
Then we can account for the request delay in the scroll handler and use the loading flag to prevent multiple requests:
onScroll(event) {
// get the scroll height before adding new messages
const startingScrollHeight = event.target.scrollHeight;
if (event.target.scrollTop < 100) {
if (this.pagination.currentPage >= this.pagination.totalPages) {
return;
}
else if (!this.loading) {
this.loadMessages(this.pagination.currentPage).subscribe((res) => {
this.handleResponse(res);
// using setTimeout lets the app "wait a beat" so it can measure
// new scroll height *after* messages are added
setTimeout(() => {
const newScrollHeight = this.scrollDiv.nativeElement.scrollHeight;
// set the scroll height from the difference of the new and starting scroll height
this.scrollDiv.nativeElement.scrollTo(0, newScrollHeight - startingScrollHeight);
});
});
}
}
}
Stackblitz (updated)

Related

Not able to style the suggested actions container with limited height and overflow hidden and scrollable

I'm using these style for suggested actions. When the number of suggested actions are more, i want the suggested actions to be in a container with fixed height and overflow hidden with scrollable.
It looks like there are no style options to do the currently. Could you please check if this can done?
const styleOptions = {
suggestedActionBackground: 'White',
suggestedActionBorder: 0,
suggestedActionBorderRadius: 4,
suggestedActionBorderStyle: 'solid',
suggestedActionBorderWidth: 1,
suggestedActionHeight: 32,
suggestedActionLayout: 'stacked', // either "carousel" or "stacked"
};
enter image description here
This is achievable thru use of CSS, Web Chat's store, and an event listener. A couple things to note, however.
One, this requires manipulating the DOM directly. Generally, this is frowned upon in a React environment. Whether you are using the React version of Web Chat or not, Web Chat is built upon React. Because of this, components may change in the future which could break this setup. Please bare this in mind.
Two, the example below is a simple setup. You will need to adjust it to meet your needs. For instance, you may need to further isolate certain buttons or specific suggested actions as they arrive.
First, we setup our CSS. I have two classes. hideSuggestedActionsContainer is used to, initially, hide any suggested actions. If we don't set this immediately, then the CSS changes made to suggested actions will be momentarily visible to the user as they are rendered. suggestedActionContainer sets the container styling, including enabling (but hiding) scrolling in the container.
.hideSuggestedActionContainer {
display: none;
}
.suggestedActionContainer {
background: white;
width: 300px;
height: 200px;
display: flex;
flex-direction: column;
justify-content: flex-start;
overflow-y: scroll;
-ms-overflow-style: none;
}
.suggestedActionContainer::-webkit-scrollbar {
display: none;
}
In Web Chat, we utilize the store to manage what actions we take and when. The store is passed in as a parameter into Web Chat.
When Web Chat first connects (DIRECT_LINE/CONNECT_FULFILLED), we assign the hideSuggestedActionContainer class to the suggested actions DIV wrapper.
As the suggested action arrives (WEB_CHAT/SET_SUGGESTED_ACTIONS), we remove the hideSuggestedActionContainer class and assign the suggestedActionContainer class allowing the suggested action to be viewed.
At the same time, we monitor the incoming activities (DIRECT_LINE/INCOMING_ACTIVITY) looking for the associated HTML element that houses the suggested action. As Web Chat only allows for one suggested action to be displayed at a time, this should be the first object in the [role=status] array (again, things could change in the future). From that array, we collect the various suggested action buttons and, when one is clicked, we dispatch an event.
const store = window.WebChat.createStore( {}, ({dispatch}) => next => action => {
if ( action.type === 'DIRECT_LINE/CONNECT_FULFILLED') {
setTimeout( () => {
let actions = document.querySelectorAll( '[role=status]' );
let suggestedAction = actions[ 0 ];
suggestedAction.classList.add( 'hideSuggestedActionContainer' );
}, 20 )
}
if ( action.type === 'WEB_CHAT/SET_SUGGESTED_ACTIONS' ) {
const { suggestedActions } = action.payload;
if ( suggestedActions.length > 0 ) {
setTimeout( () => {
let actions = document.querySelectorAll( '[role=status]' );
let suggestedAction = actions[ 0 ];
suggestedAction.classList.remove('hideSuggestedActionContainer');
suggestedAction.classList.add( 'suggestedActionContainer' );
}, 20 )
}
}
if (action.type === 'DIRECT_LINE/INCOMING_ACTIVITY') {
const { activity } = action.payload;
if (activity.type === 'message') {
const actions = document.querySelectorAll( '[role=status]' );
const buttons = actions[0].querySelectorAll('button');
buttons.forEach( button => {
button.onclick = function() {
const buttonClick = new Event('buttonClick');
window.dispatchEvent( buttonClick )
};
} )
}
}
next(action);
} );
[ ... ]
window.WebChat.renderWebChat(
{
directLine: window.WebChat.createDirectLine( {
token: token
} ),
store: store,
styleOptions: styleOptions
},
document.getElementById( 'webchat' )
);
Below the Web Chat renderer, we place our event listener. As a suggested action button is clicked, we remove the suggestedActionContainer class and re-assign the hideSuggestedActionContainer class.
const buttonClickEvent = ( function () {
window.addEventListener( 'buttonClick', () => {
let actions = document.querySelectorAll( '[role=status]' );
let suggestedAction = actions[ 0 ];
suggestedAction.classList.remove( 'suggestedActionContainer' );
suggestedAction.classList.add( 'hideSuggestedActionContainer' );
} );
} )()
A note about the setTimeout() functions used in the store. The store processes every activity as it arrives before displaying it. Because of this, a slight delay needs to be applied to the changes we are pushing to the DOM. Without setTimeout(), the page tries to render our changes before the store is able to display the activity's associated HTML. I set the time outs to 20 milliseconds, but you may find you will need to adjust this. I have found that if I set the time out to too low a number then the changes occur before the activity is rendered. If it's too long (300+ ms or thereabouts), then the changes become visible to the user.
Hope of help!

How can I asynchronously render images using HTML canvas (and vue.js)?

I have a program that keeps track of a number. If you get above your goal number, you visit a page that shows how much extra you have stored up. On that page is a gumball machine, and depending on the number of excess you have stored up, a certain number of gumballs appear in side the machine. Everything works fine, but about 1 out of every 3 page loads, the gumballs appear first, and then the machine appears, covering all of the gumball images first.
This is being done through vue.js Here is the code I have:
created () {
this.$http.get('/gumballs/' + this.$auth.user().id).then(res => {)
this.createCanvas()
})
}
*******
createCanvas () {
// general canvas properties...
// gumball machine properties
this.createGumbllMachine(canvas.width, ctx, (err, succ) => {
if (err) {
console.log(err)
} else if (succ) {
this.createGumballs(canvas.width, ctx)
}
})
}
**********
createGumballMachine (width, ctx, cb) {
const imgObj = document.createElement('img')
imgObj.src = machineImg
imgObj.onload = () => {
ctx.drawImage(imgObj, (width / 2) - 350, 100)
}
return cb(null, 'ok')
}
I have tried to use a callback function as the last argument in createGumballMachine() so that the machine is drawn first and then the gumballs are drawn afterwards, but this doesn't work.
I also tried using async and await but I coudn't get the image to render at all that way. Something was happening where vue could never get past the created portion of it's lifecycle.
I have tried reading about this on other SO questions, but I can't figure it out. I know it has something to do the way ctx.dtawImage() works, but I'm stuck.
Any help is appreciated. If you need to see the code for createGumballs() let me know, but I don't see how that is causing the problem.
Linking a relevant answer which also explains the inconsistent behavior. Essentially, you can use naturalWidth to ensure that the gumball machine has loaded.
https://stackoverflow.com/a/51023755/4409088

Wixcode "static" html iframe

Apologies if this is not the correct place to post this. I'm completely new to HTML and such, but I wanted to put a button on my website which would remember how many times it been pressed and each time someone presses it it give you a number, say for example the next prime number. With enough googleing I managed to put together some (what I expect is really bad code) which I thought could do this. This is what I have (sorry if its not formatted correctly, I had trouble with copy pasting).
<head>
<title>Space Clicker</title>
</head>
<body>
<script type="text/javascript">
function isPrime(_n)
{
var _isPrime=true;
var _sqrt=Math.sqrt(_n);
for(var _i=2;_i<=_sqrt;_i++)
if((_n%_i)==0) _isPrime=false;
return _isPrime;
}
function nextPrime(_s,_n)
{
while(_n>0)if(isPrime(_s++))_n--;
return --_s;
}
var clicks = 0;
function hello() {
clicks += 1;
v = nextPrime(2,clicks);
document.getElementById("clicks1").innerHTML = clicks ;
document.getElementById("v").innerHTML = v ;
};
</script>
<button type="button" onclick="hello()">Get your prime</button>
<p>How many primes have been claimed: <a id="clicks1">0</a></p>
<p>Your prime: <a id="v">0</a></p>
</body>
The problem is that when I put this code in a iframe on my wixsite it seems to reload the code each time you look at the site, so it starts the counter again. What I would like it say the button has been pressed 5 times, it will stay at 5 until the next visitor comes along and presses it. Is such a thing possible?
You don't actually need an iframe for that, You can use wixCode to do that. WixCode let's you have a DB collection. and all you need to do is update the collection values on every click.
Let's say you add an Events collection can have the fields:
id, eventName, clicksCount
add to it a single row with eventName = 'someButtonClickEvent' and clicksCount = 0
Then add the following code to your page:
import wixData from 'wix-data';
$w.onReady(function () {});
export function button1_click(event) {
wixData.get("Events", "the_event_id")
.then( (results) => {
let item = results;
let toSave = {
"_id": "the_event_id",
"clicksCount": item.clicksCount++
};
wixData.update("Events", toSave)
})
}
now you need to add button1_click as the onClick handler of your button (in the wixCode properties panel).

Navigate to a specified position on a dynamic Angular 2 page

If I have scrolled down on a page where multiple elements have been generated on *ngFor, clicking on an element brings me to a different page via the router.
When I click on a button to get back to the previous page, I would like the screen to be scrolled down to the position I was in before I left the page.
How can I achieve this?
Let's suppose List page has ngFor loop like:
<div *ngFor="let note of (noteService.notes | async); let i = index" class="item" tabindex="0">
<a (click)="edit(note, i, $event)">
{{note.text}}
</a>
</div>
In edit function, it provides additional index into url like ...?i=11:
this.router.navigate(['group', this.noteService.groupName, 'edit', note.$key],
{ queryParams: { i: index } }); // pass in additional index in ngFor loop
Edit page remembers index as this.noteIndex in ngOnInit, then when going back:
this.router.navigate(['group', this.noteService.groupName],
{ queryParams: { i: this.noteIndex } }); // to let List page focus this note
Then List page's ngOnInit can do below:
this.noteService.announcedCountNotes.subscribe(
count => {
// noteService announces new count after it saves edit
// so by now, ngFor should be ready
if (idxToFocus) { // this.route.snapshot.queryParams['i'];
setTimeout(_ => {
var elements = document.querySelectorAll('div.item');
var len = elements.length;
if (len > 0 && idxToFocus >= 0 && idxToFocus < len) {
const el = elements[idxToFocus] as HTMLElement;
//el.scrollIntoView();
el.focus();
}
}, 0);
}
}
)
You might want to provide specific style for div.item:focus, this way worked at least on Chrome 59 (Windows 7 and iPad) for me.

taking values separately using local storage in html5

I am making an app in html5.It is like a quiz based app. I am randomly fetching questions from the XML and displaying it one by one.I am using page navigation for that. After completing and submitting your answer u will switch to other page.if once i submit my answer i cannot attempt it back. but i can see the feedback and score on switching to that page that is my problem. I have display that feedback and score and to store it in local storage. i am able to do local storage but values that i am getting is overriding. so i am getting last submitted value.Now my concern is to divide that values navigation number wise.right now what is happening if i submit my answer and suppose i am at navigation number 3 n i am looking at navigation part 1 then there also i am getting last submitted value not the part 1 value.Please give ur suggestion and help me out for that.
Here is the code snippet:
//for navigation of pages
$(document).ready(function (){
/*$(document).bind("contextmenu",function(e){
return false;
});*/
var obj;
total=x.length;
for(var j=0;j<x.length;j++)
{
if(j==0)
{
$("#navigationlist").append('<li>'+(j+1)+'</li>');
display_nav(j,$("#selected_link"))
}
else
$("#navigationlist").append('<li>'+(j+1)+'</li>');
}
$("#next").bind("click",function (){
$(".navg").each(function(index){
if($(".navg").length==(i+1))
{
if(index==0)
obj=$(this);
}
else
{
if(index==(i+1))
obj=$(this);
}
});
for(var j=0;j<xmlDoc.getElementsByTagName("question").length;j++)
{
xmlDoc.getElementsByTagName("question")[j].removeAttribute("status");
}
$("#btnSubmit").attr("disabled","false");
$("#btnSubmit").attr("onclick","checekAnswer()");
display_nav(0,obj)
}
else
display_nav((i+1),obj)
});
});
and
correctAnswers++;
localStorage.setItem('feedback',JSON.stringify(feedback[0].childNodes[0].nodeValue));
$("#feedback").append(score[0].childNodes[0].nodeValue);
$("#feedback").append("<br/>");
$("#feedback").append(feedback[0].childNodes[0].nodeValue);
}
else
{
//var val = [];
//val.push(feedback[0].childNodes[0].nodeValue);
//localstorage.setItem('feedback', JSON.stringify(val));
//localStorage.setItem('feedback',JSON.stringify(feedback[0].childNodes[0].nodeValue));
//alert(localStorage.getItem("feedback"));
/*var v={"test":feedback[0].childNodes[0].nodeValue};
localStorage.setItem('feedback',v);
alert(localStorage.getItem('feedback'));*/
scores1.push(feedback[0].childNodes[0].nodeValue);
localStorage.setItem("highscores",JSON.stringify(scores1));
var scores = localStorage.getItem("highscores");
alert(scores);
scores = JSON.parse(scores);
alert(scores[0]);
$("#feedback").html(score[1].childNodes[0].nodeValue);
$("#feedback").append("<br/>");
$("#feedback").append(feedback[0].childNodes[0].nodeValue);
$("#feedback").append("hello");
}
//$("#counter").html("left="+xPos+",top="+yPos);
$("#trFeedBack").show("slow");
display_nav(j,obj)
}
} // end function
If I understand your question, your problem is to store items with same name but related to different pages.
LocalStorage being defined by domain, and not by page, you must change the keys you use. The usual solution is to prefix the names you want.
For example :
localStorage['pages.12.feedback'] = "the feedback I'm giving related to page 12";
localStorage['global.feedback'] = "the feedback I'm giving related to the global site";
(you'll notice I use the short notation, that I find more readable that using setItem)