chatcontrol-live-demo/chatapp.svg
om 496a6b7caf Refactor scrolling
So that it should become easier to implement scrolling with image posts.
Also a lot of other refactoring.
2022-08-21 12:26:25 +02:00

344 lines
10 KiB
XML

<?xml version="1.0"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
width="300"
height="500"
viewBox="0 0 300 500"
onload="start_chat('Ven')"
overflow="auto"
>
<defs>
<symbol id="kaereste_foto">
<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>
<script type="application/ecmascript">
<![CDATA[
'use strict';
const svgns = "http://www.w3.org/2000/svg";
// must be the same as the SVG dimensions
const width = 300;
const height = 500;
const line_height = 14;
const bubble_padding = 6;
const bubble_spacing = 5;
const visible_chat_area = {'top': 60, 'bottom': 500};
const typing_speed = 7;
var conversation_count = 0;
/// SVG 1.1 doesn't do proper text splitting into several lines.
/// we need to do it ourselves.
function split_text_into_lines(text, upper_line_length) {
let result = [];
while(text.length) {
if(text.length < upper_line_length) {
result.push(text);
break; // we are done
}
let found_split_point = false;
for(let i = upper_line_length; i; i--) {
if(text[i] == ' ') {
result.push(text.slice(0, i));
text = text.slice(i+1);
found_split_point = true;
break;
}
}
if(!found_split_point) {
// no <space> found. Split at character boundary instead
result.push(text.slice(0, upper_line_length));
text = text.slice(upper_line_length);
}
}
return result;
}
/// A class holding a text chat message.
///
/// *ChatMessages are owned by a Dialog.
function TextChatMessage(message_text, is_myself) {
let lines = split_text_into_lines(message_text, 28);
let bubble_color = (is_myself)? 'white': 'pink';
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
this.draw = function(y_offset, y_shift) {
let group = create_svg_node('g', {'transform': `translate(0, ${y_shift})`});
let x = (is_myself ? 190 : 110);
let text = create_svg_node('text', {
'x': `${x}%`,
'font-size': `${line_height}px`,
'text-anchor': is_myself ? 'end' : 'start'
});
let height_so_far = y_offset;
lines.forEach(function(line) {
let y = height_so_far;
let tspan = create_svg_node('tspan', {
'x': `${x}%`,
'y': `${y + line_height}`, // important: y is lower text baseline
'fill': 'green',
'width': '5%'
});
tspan.appendChild(document.createTextNode(line));
text.appendChild(tspan);
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)
this.shift_y_pos = function(by) {
redraw_on_top(document.getElementById('contact_name_box'));
this.group.setAttribute('transform', `translate(0, ${by})`);
}
}
/// A class holding a image-based chat message.
///
/// *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
const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
/// toggle viewport between message list and contact list
/// That means: show either the left or right side of the SVG
function swipe_viewport() {
let svg = document.getElementsByTagName('svg')[0];
let coords = svg.getAttribute('viewBox').split(' ');
let x = parseInt(coords.shift());
if(x == 0) {
// moving from messages to contacts
var step = 5;
var end = width;
} else {
// moving from contacts to messages
var step = -5;
var end = 0;
}
let viewBox_suffix = coords.join(' '); // only 3 elements
async function animate(resolve, reject) {
while(x!=end) {
await wait(1);
x += step;
svg.setAttribute('viewBox', `${x} ${viewBox_suffix}`);
}
resolve();
};
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) {
while(parentNode.firstChild) {
parentNode.removeChild(parentNode.lastChild);
}
}
function start_chat(who) {
let indicator = document.getElementById('contact_indicator');
indicator.childNodes[0].data = `Kontakt: ${who}`;
removeAllChildren(document.getElementById('messages'));
swipe_viewport();
switch(who) {
case 'Ven':
dialog_ven();
break;
case 'Mor':
dialog_mor();
break;
case 'Kæreste':
dialog_kaereste();
break;
default:
alert(`Unknown contact: ${who}`);
}
}
function Dialog(chat_partner) {
let conversation_id = ++conversation_count;
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) {
if(dialog.messages.length) await wait(message.length * typing_speed); // first message should be instant
if(conversation_id != conversation_count) {
return; // Do not add messages to old conversation
}
let chat_message = create_chat_message(message, is_myself);
chat_message.draw(dialog.all_elements_height, // offset
dialog.y_shift);
dialog.all_elements_height += chat_message.height;
dialog.messages.push(chat_message);
let bottom = dialog.all_elements_height + dialog.y_shift;
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);
}
}
this.me = async function(message) {
await post_message(this, message, true);
}
this.you = async function(message) {
await post_message(this, message, false);
}
}
async function dialog_ven() {
let d = new Dialog('Ven');
await d.me("hej");
await d.you("tak for sidst!");
await d.you('Har du hørt om den nye EU lov "ChatControl"?');
await d.me("nej, det har jeg ikke");
await d.me("hvad handler det om?");
await d.you('EU kommissionen planlægger at læse alle chatbeskeder i EU');
await d.me('Ja, men vi krypterer jo vores beskeder? Tough luck!');
await d.you('Det tager de højde for. Aflytningen vil ske før krypteringen på din telefon!');
await d.me('Det lyder overhovedet ikke rart. Hvorfor vil de gøre det?');
await d.you('De siger at det er for at beskytte børn på nettet.');
await d.you('Men det giver ikke meget mening');
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('https://chatcontrol.eu');
}
async function dialog_mor() {
let d = new Dialog('Mor');
await d.you("Jeg har fundet nogle gamle familiebilleder fra vores ferie 10 år siden");
await d.you("Her leger du på stranden");
await d.you("(naked stick figure image here)");
}
async function dialog_kaereste() {
let d = new Dialog('Kæreste');
await d.me('hej smukkeste');
await d.you('hej, jeg har lige tænkt på dig!');
await d.you('og derfor har jeg taget et sexet billede');
await d.you('(silly naked stick figure image here)')
}
]]>
</script>
<!-- [right] contact name view -->
<g id="contact_name_box">
<rect x="300" y="0" width="300" height="50" fill="yellow" />
<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 -->
<rect x="300" y="50" width="300" height="450" fill="aqua" />
<g id="messages"></g>
<!-- [left] contact list -->
<rect x="0" y="0" width="300" height="50" fill="yellow" />
<text x="60" y="30">Dine kontakter</text>
<rect x="0" y="50" width="300" height="450" style="stroke: green; stroke-width: 10px" fill="aqua" />
<text x="15" y="75" onclick="start_chat('Ven')">Ven</text>
<text x="15" y="100" onclick="start_chat('Mor')">Mor</text>
<text x="15" y="125" onclick="start_chat('Kæreste')">Kæreste&#10084;</text>
</svg>