Refactor scrolling #2
239
chatapp.svg
239
chatapp.svg
|
@ -11,23 +11,36 @@
|
||||||
overflow="auto"
|
overflow="auto"
|
||||||
>
|
>
|
||||||
<defs>
|
<defs>
|
||||||
<symbol id="left_chat_message"><text style="text-align: left"></text></symbol>
|
<symbol id="kaereste_foto">
|
||||||
<symbol id="right_chat_message"><text style="text-align: right"></text></symbol>
|
<circle cx="20" cy="20" r="20" fill="blue" />
|
||||||
|
<line x1="20" y1="40" x2="20" y2="60" stroke="blue" />
|
||||||
|
<ellipse cx="20" cy="110" rx="10" ry="50" fill="green" />
|
||||||
|
<!-- left arm -->
|
||||||
|
<polyline fill="none"
|
||||||
|
stroke="blue"
|
||||||
|
points="15,70 5,50 0,30" />
|
||||||
|
<!-- right arm -->
|
||||||
|
<polyline fill="none"
|
||||||
|
stroke="blue"
|
||||||
|
points="25,70 35,50 40,30" />
|
||||||
|
</symbol>
|
||||||
</defs>
|
</defs>
|
||||||
<script type="application/ecmascript">
|
<script type="application/ecmascript">
|
||||||
<![CDATA[
|
<![CDATA[
|
||||||
|
'use strict';
|
||||||
|
|
||||||
const svgns = "http://www.w3.org/2000/svg";
|
const svgns = "http://www.w3.org/2000/svg";
|
||||||
|
|
||||||
// must be the same as the SVG dimensions
|
// must be the same as the SVG dimensions
|
||||||
const width = 300;
|
const width = 300;
|
||||||
const height = 500;
|
const height = 500;
|
||||||
|
|
||||||
const line_height = 6;
|
const line_height = 14;
|
||||||
const max_visible_lines = 10;
|
const bubble_padding = 6;
|
||||||
const messages_y_offset = 20;
|
const bubble_spacing = 5;
|
||||||
const typing_speed = 70;
|
const visible_chat_area = {'top': 60, 'bottom': 500};
|
||||||
|
const typing_speed = 7;
|
||||||
|
|
||||||
var chat_lines_count = 0;
|
|
||||||
var conversation_count = 0;
|
var conversation_count = 0;
|
||||||
|
|
||||||
/// SVG 1.1 doesn't do proper text splitting into several lines.
|
/// SVG 1.1 doesn't do proper text splitting into several lines.
|
||||||
|
@ -61,94 +74,84 @@ function split_text_into_lines(text, upper_line_length) {
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A class holding a chat message.
|
/// A class holding a text chat message.
|
||||||
///
|
///
|
||||||
/// ChatMessages's are owned by a Dialog.
|
/// *ChatMessages are owned by a Dialog.
|
||||||
function ChatMessage(message_text, is_myself) {
|
function TextChatMessage(message_text, is_myself) {
|
||||||
let lines = split_text_into_lines(message_text, 28);
|
let lines = split_text_into_lines(message_text, 28);
|
||||||
let tspans = []
|
let bubble_color = (is_myself)? 'white': 'pink';
|
||||||
let bubbles = [];
|
|
||||||
|
|
||||||
let container = document.getElementById('messages');
|
let container = document.getElementById('messages');
|
||||||
|
|
||||||
|
// attributes
|
||||||
|
this.height = 0; // height on screen when fully visible
|
||||||
|
this.group = null; // a <g> with a transform=translate(0,y_shift) attribute
|
||||||
|
|
||||||
/// Render the chat message on the screen
|
/// Render the chat message on the screen
|
||||||
this.draw = function() {
|
this.draw = function(y_offset, y_shift) {
|
||||||
let text = document.createElementNS(svgns, 'text');
|
let group = create_svg_node('g', {'transform': `translate(0, ${y_shift})`});
|
||||||
|
|
||||||
let x = (is_myself ? 190 : 110);
|
let x = (is_myself ? 190 : 110);
|
||||||
text.setAttribute('x', `${x}%`);
|
let text = create_svg_node('text', {
|
||||||
text.setAttribute('font-size', '14px');
|
'x': `${x}%`,
|
||||||
text.setAttribute('text-anchor', is_myself ? 'end' : 'start');
|
'font-size': `${line_height}px`,
|
||||||
|
'text-anchor': is_myself ? 'end' : 'start'
|
||||||
lines.forEach(function(line) {
|
|
||||||
let y = tspan_y_pos(chat_lines_count++);
|
|
||||||
let tspan = document.createElementNS(svgns, 'tspan');
|
|
||||||
tspan.setAttribute('x', `${x}%`);
|
|
||||||
tspan.setAttribute('y', `${y}%`); // important: y is lower text baseline
|
|
||||||
tspan.setAttribute('z', 10);
|
|
||||||
tspan.appendChild(document.createTextNode(line));
|
|
||||||
tspan.setAttribute('fill', 'green');
|
|
||||||
tspan.setAttribute('width', '5%');
|
|
||||||
|
|
||||||
text.appendChild(tspan);
|
|
||||||
tspans.push(tspan);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// done making text
|
let height_so_far = y_offset;
|
||||||
// it must be in the DOM for getBBox() to work
|
|
||||||
container.appendChild(text);
|
|
||||||
|
|
||||||
// make chat bubble
|
lines.forEach(function(line) {
|
||||||
const padding = 6;
|
let y = height_so_far;
|
||||||
const textbox = text.getBBox();
|
let tspan = create_svg_node('tspan', {
|
||||||
const y_percent = Math.round(((textbox.y - padding) / height) * 100);
|
'x': `${x}%`,
|
||||||
const bubble = document.createElementNS(svgns, 'rect');
|
'y': `${y + line_height}`, // important: y is lower text baseline
|
||||||
bubble.setAttribute('x', textbox.x-padding);
|
'fill': 'green',
|
||||||
bubble.setAttribute('y', `${y_percent}%`);
|
'width': '5%'
|
||||||
bubble.setAttribute('width', textbox.width + 2*padding);
|
});
|
||||||
bubble.setAttribute('height', textbox.height + 2*padding);
|
|
||||||
bubble.setAttribute('rx', 8);
|
|
||||||
bubble.setAttribute('fill', 'black');
|
|
||||||
bubbles.push(bubble);
|
|
||||||
|
|
||||||
// put stuff in dom in the right order
|
tspan.appendChild(document.createTextNode(line));
|
||||||
container.appendChild(bubble);
|
text.appendChild(tspan);
|
||||||
container.removeChild(text);
|
|
||||||
container.appendChild(text);
|
height_so_far += line_height;
|
||||||
|
});
|
||||||
|
|
||||||
|
container.appendChild(group);
|
||||||
|
group.appendChild(text); // needs to be part of the DOM *now*
|
||||||
|
let bubble = create_bubble(text, bubble_color);
|
||||||
|
group.appendChild(bubble);
|
||||||
|
container.appendChild(group);
|
||||||
|
redraw_on_top(text);
|
||||||
|
|
||||||
|
this.height = bubble.getBBox().height + bubble_spacing;
|
||||||
|
this.group = group;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Move a chat message on the screen (to simulate scrolling)
|
/// Move a chat message on the screen (to simulate scrolling)
|
||||||
this.shift_y_pos = function(by, lower_visibility_boundary) {
|
this.shift_y_pos = function(by) {
|
||||||
tspans.forEach(tspan => {
|
redraw_on_top(document.getElementById('contact_name_box'));
|
||||||
let is_already_hidden = tspan.getAttribute('visibility') == 'hidden';
|
this.group.setAttribute('transform', `translate(0, ${by})`);
|
||||||
let old = parseInt(tspan.getAttribute('y').split('%')[0]);
|
|
||||||
let new_value = old + by;
|
|
||||||
tspan.setAttribute('y', `${new_value}%`);
|
|
||||||
|
|
||||||
let visibility;
|
|
||||||
if(new_value < lower_visibility_boundary) {
|
|
||||||
visibility = 'hidden';
|
|
||||||
if(!is_already_hidden) {
|
|
||||||
chat_lines_count--;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
visibility = 'visible';
|
|
||||||
if(is_already_hidden) {
|
|
||||||
chat_lines_count++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
tspan.setAttribute('visibility', visibility);
|
|
||||||
});
|
|
||||||
bubbles.forEach(bubble => {
|
|
||||||
const old = parseInt(bubble.getAttribute('y').split('%')[0]);
|
|
||||||
const new_value = old + by;
|
|
||||||
bubble.setAttribute('y', `${new_value}%`);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Helper function to determine the position of a chat-tspan
|
/// A class holding a image-based chat message.
|
||||||
function tspan_y_pos(message_index) {
|
///
|
||||||
return messages_y_offset + message_index * line_height;
|
/// *ChatMessages are owned by a Dialog.
|
||||||
|
function ImageChatMessage(symbol_id, is_myself) {
|
||||||
|
// attributes
|
||||||
|
this.height = 0; // height on screen when fully visible
|
||||||
|
this.group = null; // a <g> with a transform=translate(0,y_shift) attribute
|
||||||
|
|
||||||
|
this.draw = function(y_offset, y_shift) {
|
||||||
|
console.error('draw() is not implemented yet');
|
||||||
|
}
|
||||||
|
|
||||||
|
this.shift_y_pos = function(by) {
|
||||||
|
console.error('shift_y_pos() is not implemented yet');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function create_chat_message(content, is_myself) {
|
||||||
|
let constr = (content.startsWith('#'))? ImageChatMessage: TextChatMessage;
|
||||||
|
return new constr(content, is_myself);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Promise-based version of setTimeout
|
/// Promise-based version of setTimeout
|
||||||
|
@ -183,6 +186,40 @@ function swipe_viewport() {
|
||||||
new Promise(animate);
|
new Promise(animate);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Create a chat bubble around an element.
|
||||||
|
//
|
||||||
|
// The element must already be inside the DOM for this to work.
|
||||||
|
function create_bubble(inner_element, color) {
|
||||||
|
const bbox = inner_element.getBBox();
|
||||||
|
const y_percent = Math.round(((bbox.y - bubble_padding) / height) * 100);
|
||||||
|
let bubble = create_svg_node('rect', {
|
||||||
|
'x': bbox.x - bubble_padding,
|
||||||
|
'y': `${y_percent}%`,
|
||||||
|
'width': bbox.width + 2 * bubble_padding,
|
||||||
|
'height': bbox.height + 2 * bubble_padding,
|
||||||
|
'rx': 8,
|
||||||
|
'fill': color,
|
||||||
|
});
|
||||||
|
return bubble;
|
||||||
|
}
|
||||||
|
|
||||||
|
function create_svg_node(tag_name, attrs) {
|
||||||
|
let node = document.createElementNS(svgns, tag_name);
|
||||||
|
for(let attr in attrs) {
|
||||||
|
node.setAttribute(attr, attrs[attr]);
|
||||||
|
}
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Ensure that an element is redrawn above all other elements
|
||||||
|
///
|
||||||
|
/// The element's children will also be redrawn on top
|
||||||
|
function redraw_on_top(element) {
|
||||||
|
let parent = element.parentNode;
|
||||||
|
parent.appendChild(parent.removeChild(element));
|
||||||
|
}
|
||||||
|
|
||||||
function removeAllChildren(parentNode) {
|
function removeAllChildren(parentNode) {
|
||||||
while(parentNode.firstChild) {
|
while(parentNode.firstChild) {
|
||||||
parentNode.removeChild(parentNode.lastChild);
|
parentNode.removeChild(parentNode.lastChild);
|
||||||
|
@ -193,7 +230,6 @@ function start_chat(who) {
|
||||||
let indicator = document.getElementById('contact_indicator');
|
let indicator = document.getElementById('contact_indicator');
|
||||||
indicator.childNodes[0].data = `Kontakt: ${who}`;
|
indicator.childNodes[0].data = `Kontakt: ${who}`;
|
||||||
removeAllChildren(document.getElementById('messages'));
|
removeAllChildren(document.getElementById('messages'));
|
||||||
chat_lines_count = 0;
|
|
||||||
swipe_viewport();
|
swipe_viewport();
|
||||||
|
|
||||||
switch(who) {
|
switch(who) {
|
||||||
|
@ -214,6 +250,8 @@ function start_chat(who) {
|
||||||
function Dialog(chat_partner) {
|
function Dialog(chat_partner) {
|
||||||
let conversation_id = ++conversation_count;
|
let conversation_id = ++conversation_count;
|
||||||
this.messages = [];
|
this.messages = [];
|
||||||
|
this.y_shift = visible_chat_area.top; // for scrolling
|
||||||
|
this.all_elements_height = 0; // also for scrolling
|
||||||
|
|
||||||
async function post_message(dialog, message, is_myself) {
|
async function post_message(dialog, message, is_myself) {
|
||||||
if(dialog.messages.length) await wait(message.length * typing_speed); // first message should be instant
|
if(dialog.messages.length) await wait(message.length * typing_speed); // first message should be instant
|
||||||
|
@ -222,12 +260,18 @@ function Dialog(chat_partner) {
|
||||||
return; // Do not add messages to old conversation
|
return; // Do not add messages to old conversation
|
||||||
}
|
}
|
||||||
|
|
||||||
let chat_message = new ChatMessage(message, is_myself);
|
let chat_message = create_chat_message(message, is_myself);
|
||||||
chat_message.draw();
|
chat_message.draw(dialog.all_elements_height, // offset
|
||||||
|
dialog.y_shift);
|
||||||
|
dialog.all_elements_height += chat_message.height;
|
||||||
dialog.messages.push(chat_message);
|
dialog.messages.push(chat_message);
|
||||||
|
|
||||||
while(chat_lines_count > max_visible_lines) {
|
let bottom = dialog.all_elements_height + dialog.y_shift;
|
||||||
dialog.messages.forEach(msg => msg.shift_y_pos(-line_height, messages_y_offset));
|
let scroll = Math.max(0, bottom - visible_chat_area.bottom);
|
||||||
|
while(scroll > line_height) {
|
||||||
|
dialog.y_shift -= line_height;
|
||||||
|
scroll -= line_height;
|
||||||
|
dialog.messages.forEach(msg => msg.shift_y_pos(dialog.y_shift));
|
||||||
await wait(50);
|
await wait(50);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -256,7 +300,7 @@ async function dialog_ven() {
|
||||||
await d.me('Alle vores beskeder skal scannes på grund af børn på nettet? Det lyder dumt!');
|
await d.me('Alle vores beskeder skal scannes på grund af børn på nettet? Det lyder dumt!');
|
||||||
await d.you('Her kan du læse mere om det:');
|
await d.you('Her kan du læse mere om det:');
|
||||||
await d.you('https://chatcontrol.eu');
|
await d.you('https://chatcontrol.eu');
|
||||||
}
|
}
|
||||||
|
|
||||||
async function dialog_mor() {
|
async function dialog_mor() {
|
||||||
let d = new Dialog('Mor');
|
let d = new Dialog('Mor');
|
||||||
|
@ -277,22 +321,23 @@ async function dialog_kaereste() {
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<!-- [right] contact name view -->
|
<!-- [right] contact name view -->
|
||||||
<rect x="100%" y="0" width="100%" height="10%" fill="yellow" />
|
<g id="contact_name_box">
|
||||||
<polyline points="300,20 320,0 320,40" fill="blue" onclick="swipe_viewport()" /> <!-- XXX: these polyline points depend on the svg width -->
|
<rect x="300" y="0" width="300" height="50" fill="yellow" />
|
||||||
<text x="125%" y="30" id="contact_indicator">loading...</text>
|
<polyline points="300,20 320,0 320,40" fill="blue" onclick="swipe_viewport()" /> <!-- XXX: these polyline points depend on the svg width -->
|
||||||
|
<text x="375" y="30" id="contact_indicator">loading...</text>
|
||||||
|
</g>
|
||||||
|
|
||||||
<!-- [right] messages view -->
|
<!-- [right] messages view -->
|
||||||
<rect x="100%" y="10%" width="100%" height="90%" style="stroke: green; stroke-width: 10px" fill="blue" />
|
<rect x="300" y="50" width="300" height="450" fill="aqua" />
|
||||||
<rect id="messages_background" x="105%" y="15%" z="2" width="90%" height="85%" fill="pink" />
|
|
||||||
<g id="messages"></g>
|
<g id="messages"></g>
|
||||||
|
|
||||||
<!-- [left] contact list -->
|
<!-- [left] contact list -->
|
||||||
<rect x="0" y="0" width="100%" height="10%" fill="yellow" />
|
<rect x="0" y="0" width="300" height="50" fill="yellow" />
|
||||||
<text x="20%" y="30">Dine kontakter</text>
|
<text x="60" y="30">Dine kontakter</text>
|
||||||
<rect x="0" y="10%" width="100%" height="100%" style="stroke: green; stroke-width: 10px" fill="aqua" />
|
<rect x="0" y="50" width="300" height="450" style="stroke: green; stroke-width: 10px" fill="aqua" />
|
||||||
|
|
||||||
<text x="5%" y="15%" onclick="start_chat('Ven')">Ven</text>
|
<text x="15" y="75" onclick="start_chat('Ven')">Ven</text>
|
||||||
<text x="5%" y="20%" onclick="start_chat('Mor')">Mor</text>
|
<text x="15" y="100" onclick="start_chat('Mor')">Mor</text>
|
||||||
<text x="5%" y="25%" onclick="start_chat('Kæreste')">Kæreste❤</text>
|
<text x="15" y="125" onclick="start_chat('Kæreste')">Kæreste❤</text>
|
||||||
|
|
||||||
</svg>
|
</svg>
|
||||||
|
|
Before Width: | Height: | Size: 9.2 KiB After Width: | Height: | Size: 10 KiB |
Loading…
Reference in a new issue