Compare commits

...
40 Commits
Author SHA1 Message Date
or-else 94c8bab4ec getting rid of componentWillMount per React 16 specs 2018-08-19 19:44:14 +03:00
or-else 7ac3041493 bump version and rebuild 2018-08-19 19:30:28 +03:00
or-else 4be41e47f4 starting to get rid of componentWillMount per React spec 2018-08-19 11:22:43 +03:00
or-else f782bc11cc simplifies InfoView parameters 2018-08-19 10:58:22 +03:00
or-else be8d64e29e generated files updated 2018-08-18 09:16:39 +03:00
or-else e22a4de007 remove unnecessary state update 2018-08-18 09:03:42 +03:00
or-else fde40f4f51 build and bump version 2018-08-17 16:07:03 +03:00
or-else e2b19d72bc add css minification 2018-08-13 11:36:50 +03:00
or-else a0eac497c6 minor npm packege changes 2018-08-12 16:13:52 +03:00
or-else f2ef14c239 setting up offline babel transpiling and nmp-ifying 2018-08-12 11:03:26 +03:00
or-else 2b8514b495 fix for visual breakage (https://github.com/minimaxir/big-list-of-naughty-strings) 2018-08-10 16:42:41 +03:00
or-else e9e8d9881f make the webapp work with browserify-processed tinode.js (symlinked tinode.dev.js) 2018-08-10 15:16:40 +03:00
or-else 3975401205 fix canonical url and application-url 2018-08-09 16:56:53 +03:00
or-else 6da0952e5d adding package.json 2018-08-09 10:48:51 +03:00
or-else 45acee4617 fix for a broken "send" button 2018-08-08 21:07:18 +03:00
or-else ff5096cd47 multiple simultaneous uploads and downloads work now 2018-08-08 15:59:26 +03:00
or-else 4cd97e7ff8 refactoring upload progress 2018-08-07 20:55:55 +03:00
or-else df86411cb7 rearranging components, splitting js/babel from html 2018-08-07 12:15:10 +03:00
or-else 905501f22f use delayed message sending 2018-08-06 08:43:30 +03:00
or-else c48e5f1dbf don't print entire message 2018-07-30 21:23:43 +03:00
or-else 0a956f0234 don't crash on malformed attachments 2018-07-30 20:50:48 +03:00
or-else ae73764173 fix: exception while logging out 2018-07-29 19:14:41 +03:00
or-else 2f8e5c4b33 conversion to react 16 seems to be successful 2018-07-27 19:27:26 +03:00
or-else a542afd194 updating reactjs to version 16 2018-07-27 18:13:05 +03:00
or-else a8c9347591 remove debug output 2018-07-14 14:15:47 +03:00
or-else dc46f8f166 bug fix: incorrect handling of avatar pic in InfoView 2018-06-20 12:19:44 +03:00
or-else 6eb3551665 add attachment's URLs to message header 2018-06-15 14:34:06 +03:00
or-else 3829b26cc8 fix an issue with incorrect scaling of png images 2018-06-11 11:31:18 +03:00
or-else 973b71eb80 uploading and downloading works ok now 2018-06-10 18:55:18 +03:00
or-else 1b7fd6a00b file uploads mostly work now 2018-06-09 21:14:12 +03:00
or-else daa1ee3e0c send credentials only if download url is relative 2018-06-09 19:05:26 +03:00
or-else 35eb23e3af uploading works with some minor UI glitches 2018-06-09 11:13:04 +03:00
or-else 0c16a5c991 uploading works, downloading still does not, UI broken 2018-06-08 22:15:31 +03:00
or-else 926dd43ce6 fixed image preview, split max inband size from total max attachment size 2018-06-06 17:47:48 +03:00
or-else 499bf1ee14 separate download url from preview url 2018-06-06 16:41:16 +03:00
or-else 9c507ba631 update Drafty for external attachmanes, update UI 2018-06-06 16:11:50 +03:00
or-else 2f307e2803 making file upload widget more consistent visually 2018-06-06 10:15:39 +03:00
or-else 2c21a34d07 adding large file uploads 2018-05-31 21:41:06 -07:00
or-else 2e3ecb1c4e support copy-pasting images and file attachments 2018-05-23 13:29:31 -07:00
or-else e21bf82de1 inline documentation cleanup 2018-05-22 18:54:04 -07:00
11 changed files with 5220 additions and 5303 deletions
+3 -4
View File
@@ -1,12 +1,12 @@
# Single-page chat with Tinode
Example using [Tinode](https://github.com/tinode/chat/) to build a single-page chat application. The app uses
Single-page web chat application built with [Tinode](https://github.com/tinode/chat/). The app uses
[React](https://facebook.github.io/react/), Google's material design [fonts](https://www.google.com/fonts/)
and [icons](https://google.github.io/material-design-icons/#icon-font-for-the-web). The [Tinode javascript library](https://github.com/tinode/tinode-js/) has no external dependencies. Overall it's a lot like open source WhatsApp or Telegram web apps.
Although the app is generally usable, keep in mind that this is work in progress. Some bugs probably exist. The app was tested in the latest Chrome & Firefox only.
Although the app is generally usable, keep in mind that this is work in progress. Some bugs probably exist. The app was tested in the latest Chrome & Firefox only. NPM [is available](https://www.npmjs.com/package/tinode-webapp).
Try possibly newer or older version live at https://api.tinode.co/x/example-react-js/. Login as one of `alice`, `bob`, `carol`, `dave`, `frank`. Password is `<login>123`, e.g. password for `alice` is `alice123`. You can discover other users by email or phone by prefixing them with `email:` or `tel:` respectively. Emails are `<login>@example.com`, e.g. `alice@example.com`, phones are `17025550001` through `17025550009`. The demo is reset every night at 2:15am Pacific time.
Try possibly newer or older version live at https://api.tinode.co/. Login as one of `alice`, `bob`, `carol`, `dave`, `frank`. Password is `<login>123`, e.g. password for `alice` is `alice123`. You can discover other users by email or phone by prefixing them with `email:` or `tel:` respectively. Emails are `<login>@example.com`, e.g. `alice@example.com`, phones are `17025550001` through `17025550009`. The demo is reset every night at 2:15am Pacific time.
License: [Apache 2.0](http://www.apache.org/licenses/LICENSE-2.0)
@@ -17,7 +17,6 @@ Background pattern from http://subtlepatterns.com/, commercial and non-commercia
## Missing functionality
* Previews not generated for videos, audio, links or docs.
* No indication of some chat statuses (muted, blocked, etc.).
* Browser push notifications are not implemented.
* No end-to-end encryption.
* Editing chat permissions is incomplete.
+25 -1
View File
@@ -1060,6 +1060,7 @@ i.search {
margin: 0;
padding: 0;
align-items: flex-end;
overflow: hidden;
}
.chat-box .right {
@@ -1225,7 +1226,7 @@ i.search {
display: flex;
}
.attachment:last-of-type {
margin-bottom: -1rem;
margin-bottom: -0.45rem;
}
/* END of talk bubbles */
@@ -1701,3 +1702,26 @@ ul.tabbar li.active > a:after {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/* File upload styles */
.uploader {
display: flex;
}
/* Gray uploader bar of fixed length (background) */
.uploader > div {
position: relative;
height: 0.6rem;
padding: 0.15rem;
margin: 0.35rem;
width: 6rem;
line-height: 0;
border-radius: 0.1rem;
background-color: #cfd8dc;
}
/* Colored uploader progress bar of variable length */
.uploader > div > span {
display: inline-block;
height: 0.3rem;
background-color: #1e88e5;
border-radius: 0.08rem;
}
+1
View File
File diff suppressed because one or more lines are too long
-753
View File
@@ -1,753 +0,0 @@
// Basic parser and formatter for very simple text markup. Mostly targeted at
// mobile use cases similar to Telegram and WhatsApp.
//
// Supports:
// *abc* -> <b>abc</b>
// _abc_ -> <i>abc</i>
// ~abc~ -> <del>abc</del>
// `abc` -> <tt>abc</tt>
// Nested frmatting is supported, e.g. *abc _def_* -> <b>abc <i>def</i></b>
//
// URLs, @mentions, and #hashtags are extracted and converted into links.
//
// JSON data representation is inspired by Draft.js raw formatting.
/*
Text:
this is *bold*, `code` and _italic_, ~strike~
combined *bold and _italic_*
an url: https://www.example.com/abc#fragment and another _www.tinode.co_
this is a @mention and a #hashtag in a string
second #hashtag
Sample JSON representation of the text above:
{
"txt": "this is bold, code and italic, strike combined bold and italic an url: https://www.example.com/abc#fragment " +
"and another www.tinode.co this is a @mention and a #hashtag in a string second #hashtag",
"fmt": [
{ "at":8, "len":4,"tp":"ST" },{ "at":14, "len":4, "tp":"CO" },{ "at":23, "len":6, "tp":"EM"},
{ "at":31, "len":6, "tp":"DL" },{ "tp":"BR", "len":1, "at":37 },{ "at":56, "len":6, "tp":"EM" },
{ "at":47, "len":15, "tp":"ST" },{ "tp":"BR", "len":1, "at":62 },{ "at":120, "len":13, "tp":"EM" },
{ "at":71, "len":36, "key":0 },{ "at":120, "len":13, "key":1 },{ "tp":"BR", "len":1, "at":133 },
{ "at":144, "len":8, "key":2 },{ "at":159, "len":8, "key":3 },{ "tp":"BR", "len":1, "at":179 },
{ "at":187, "len":8, "key":3 },{ "tp":"BR", "len":1, "at":195 }
],
"ent": [
{ "tp":"LN", "data":{ "url":"https://www.example.com/abc#fragment" } },
{ "tp":"LN", "data":{ "url":"http://www.tinode.co" } },
{ "tp":"MN", "data":{ "val":"mention" } },
{ "tp":"HT", "data":{ "val":"hashtag" } }
]
}
*/
(function(environment) { // closure for web browsers
'use strict';
// Regular expressions for parsing inline formats. Javascript does not support lookbehind,
// so it's a bit messy.
var INLINE_STYLES = [
// Strong = bold, *bold text*
{name: "ST", start: /(?:^|\W)(\*)[^\s*]/, end: /[^\s*](\*)(?=$|\W)/},
// Emphesized = italic, _italic text_
{name: "EM", start: /(?:^|[\W_])(_)[^\s_]/, end: /[^\s_](_)(?=$|[\W_])/},
// Deleted, ~strike this though~
{name: "DL", start: /(?:^|\W)(~)[^\s~]/, end: /[^\s~](~)(?=$|\W)/},
// Code block `this is monospace`
{name: "CO", start: /(?:^|\W)(`)[^`]/, end: /[^`](`)(?=$|\W)/}
];
// RegExps for entity extraction (RF = reference)
var ENTITY_TYPES = [
// URLs
{name: "LN", dataName: "url",
pack: function(val) {
// Check if the protocol is specified, if not use http
if (!/^[a-z]+:\/\//i.test(val)) {
val = 'http://' + val;
}
return {url: val};
},
re: /(https?:\/\/)?(?:www\.)?[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,4}\b(?:[-a-zA-Z0-9@:%_\+.~#?&//=]*)/g},
// Mentions @user (must be 2 or more characters)
{name: "MN", dataName: "val",
pack: function(val) { return {val: val.slice(1)}; },
re: /\B@(\w\w+)/g},
// Hashtags #hashtag, like metion 2 or more characters.
{name: "HT", dataName: "val",
pack: function(val) { return {val: val.slice(1)}; },
re: /\B#(\w\w+)/g}
];
// HTML tag name suggestions
var HTML_TAGS = {
ST: { name: 'b', isVoid: false },
EM: { name: 'i', isVoid: false },
DL: { name: 'del', isVoid: false },
CO: { name: 'tt', isVoid: false },
BR: { name: 'br', isVoid: true },
LN: { name: 'a', isVoid: false },
MN: { name: 'a', isVoid: false },
HT: { name: 'a', isVoid: false },
IM: { name: 'img', isVoid: true }
};
// Convert base64-encoded string into Blob.
function base64toObjectUrl(b64, contentType) {
var bin = atob(b64);
var length = bin.length;
var buf = new ArrayBuffer(length);
var arr = new Uint8Array(buf);
for (var i = 0; i < length; i++) {
arr[i] = bin.charCodeAt(i);
}
return URL.createObjectURL(new Blob([buf], {type: contentType}));
}
// Helpers for converting Drafty to HTML.
var DECORATORS = {
ST: { open: function() { return '<b>'; }, close: function() { return '</b>'; }},
EM: { open: function() { return '<i>'; }, close: function() { return '</i>'}},
DL: { open: function() { return '<del>'; }, close: function() { return '</del>'}},
CO: { open: function() { return '<tt>'; }, close: function() { return '</tt>'}},
BR: { open: function() { return ''; }, close: function() { return '<br/>'}},
LN: {
open: function(data) { return '<a href="' + data.url + '">'; },
close: function(data) { return '</a>'; },
props: function(data) { return { href: data.url, target: "_blank" }; },
},
MN: {
open: function(data) { return '<a href="#' + data.val + '">'; },
close: function(data) { return '</a>'; },
props: function(data) { return { name: data.val }; },
},
HT: {
open: function(data) { return '<a href="#' + data.val + '">'; },
close: function(data) { return '</a>'; },
props: function(data) { return { name: data.val }; },
},
IM: {
open: function(data) {
var blobUrl = base64toObjectUrl(data.val, data.mime);
var res = (data.name ? '<a href="' + blobUrl + '" download="' + data.name + '">' : '') +
'<img src="' + blobUrl + '"' +
(data.width ? ' width="' + data.width + '"' : '') +
(data.height ? ' height="' + data.height + '"' : '') + ' border="0" />';
console.log("open: " + res);
return res;
},
close: function(data) {
return (data.name ? '</a>' : '');
},
props: function(data) {
var url = base64toObjectUrl(data.val, data.mime);
return {
src: url,
title: data.name,
'data-width': data.width,
'data-height': data.height,
'data-name': data.name,
'data-size': (data.val.length * 0.75) | 0,
'data-mime': data.mime
};
},
}
};
var Drafty = (function() {
// Take a string and defined earlier style spans, re-compose them into a tree where each leaf is
// a same-style (including unstyled) string. I.e. 'hello *bold _italic_* and ~more~ world' ->
// ('hello ', (b: 'bold ', (i: 'italic')), ' and ', (s: 'more'), ' world');
//
// This is needed in order to clear markup, i.e. 'hello *world*' -> 'hello world' and convert
// ranges from markup-ed offsets to plain text offsets.
function chunkify(line, start, end, spans) {
var chunks = [];
if (spans.length == 0) {
return [];
}
for (var i in spans) {
// Get the next chunk from the queue
var span = spans[i];
// Grab the initial unstyled chunk
if (span.start > start) {
chunks.push({text: line.slice(start, span.start)});
}
// Grab the styled chunk. It may include subchunks.
var chunk = {type: span.type};
var chld = chunkify(line, span.start + 1, span.end - 1, span.children);
if (chld.length > 0) {
chunk.children = chld;
} else {
chunk.text = span.text;
}
chunks.push(chunk);
start = span.end + 1; // '+1' is to skip the formatting character
}
// Grab the remaining unstyled chunk, after the last span
if (start < end) {
chunks.push({text: line.slice(start, end)});
}
return chunks;
}
// Same as chunkify but used for formatting.
function forEach(line, start, end, spans, formatter, context) {
// Add un-styled range before the styled span starts.
// Process ranges calling formatter for each range.
var result = [];
for (var i = 0; i < spans.length; i++) {
var span = spans[i];
// Add un-styled range before the styled span starts.
if (start < span.at) {
result.push(formatter.call(context, null, undefined, line.slice(start, span.at)));
start = span.at;
}
// Get all spans which are within current span.
var subspans = [];
for (var si = i + 1; si < spans.length && spans[si].at < span.at + span.len; si++) {
subspans.push(spans[si]);
i = si;
}
var tag = HTML_TAGS[span.tp] || {};
result.push(formatter.call(context, span.tp, span.data,
tag.isVoid ? null : forEach(line, start, span.at + span.len, subspans, formatter, context)));
start = span.at + span.len;
}
// Add the last unformatted range.
if (start < end) {
result.push(formatter.call(context, null, undefined, line.slice(start, end)));
}
return result;
}
// Detect starts and ends of formatting spans. Unformatted spans are
// ignored at this stage.
function spannify(original, re_start, re_end, type) {
var result = [];
var index = 0;
var line = original.slice(0); // make a copy;
while (line.length > 0) {
// match[0]; // match, like '*abc*'
// match[1]; // match captured in parenthesis, like 'abc'
// match['index']; // offset where the match started.
// Find the opening token.
var start = re_start.exec(line);
if (start == null) {
break;
}
// Because javascript RegExp does not support lookbehind, the actual offset may not point
// at the markup character. Find it in the matched string.
var start_offset = start['index'] + start[0].lastIndexOf(start[1]);
// Clip the processed part of the string.
line = line.slice(start_offset + 1);
// start_offset is an offset within the clipped string. Convert to original index.
start_offset += index;
// Index now point to the beginning of 'line' within the 'original' string.
index = start_offset + 1;
// Find the matching closing token.
var end = re_end ? re_end.exec(line) : null;
if (end == null) {
break;
}
var end_offset = end['index'] + end[0].indexOf(end[1]);
// Clip the processed part of the string.
line = line.slice(end_offset + 1);
// Update offsets
end_offset += index;
// Index now point to the beginning of 'line' within the 'original' string.
index = end_offset + 1;
result.push({
text: original.slice(start_offset+1, end_offset),
children: [],
start: start_offset,
end: end_offset,
type: type
});
}
return result;
}
// Convert linear array or spans into a tree representation.
// Keep standalone and nested spans, throw away partially overlapping spans.
function toTree(spans) {
if (spans.length == 0) {
return [];
}
var tree = [spans[0]];
var last = spans[0];
for (var i = 1; i < spans.length; i++) {
// Keep spans which start after the end of the previous span or those which
// are complete within the previous span.
if (spans[i].start > last.end) {
// Span is completely outside of the previous span.
tree.push(spans[i]);
last = spans[i];
} else if (spans[i].end < last.end) {
// Span is fully inside of the previous span. Push to subnode.
last.children.push(spans[i]);
}
// Span could partially overlap, ignoring it as invalid.
}
// Recursively rearrange the subnodes.
for (var i in tree) {
tree[i].children = toTree(tree[i].children);
}
return tree;
}
// Get a list of entities from a text.
function extractEntities(line) {
var match;
var extracted = [];
ENTITY_TYPES.map(function(entity) {
while ((match = entity.re.exec(line)) !== null) {
extracted.push({
offset: match['index'],
len: match[0].length,
unique: match[0],
data: entity.pack(match[0]),
type: entity.name});
}
});
if (extracted.length == 0) {
return extracted;
}
// Remove entities detected inside other entities, like #hashtag in a URL.
extracted.sort(function(a,b) {
return a.offset - b.offset;
});
var idx = -1;
extracted = extracted.filter(function(el) {
var result = (el.offset > idx);
idx = el.offset + el.len;
return result;
});
return extracted;
}
// Convert the chunks into format suitable for serialization.
function draftify(chunks, startAt) {
var plain = "";
var ranges = [];
for (var i in chunks) {
var chunk = chunks[i];
if (!chunk.text) {
var drafty = draftify(chunk.children, plain.length + startAt);
chunk.text = drafty.txt;
ranges = ranges.concat(drafty.fmt);
}
if (chunk.type) {
ranges.push({at: plain.length + startAt, len: chunk.text.length, tp: chunk.type});
}
plain += chunk.text;
}
return {txt: plain, fmt: ranges};
}
// Splice two strings: insert second string into the first one at the given index
function splice(src, at, insert) {
return src.slice(0, at) + insert + src.slice(at);
}
return {
/**
* Parse plain text into structured representation.
*/
parse: function(content) {
// Make sure we are parsing strings only.
if (typeof content != "string") {
return null;
}
// Split text into lines. It makes further processing easier.
var lines = content.split(/\r?\n/);
// Holds entities referenced from text
var entityMap = [];
var entityIndex = {};
// Processing lines one by one, hold intermediate result in blx.
var blx = [];
lines.map(function(line) {
var spans = [];
var entities = [];
// Find formatted spans in the string.
// Try to match each style.
INLINE_STYLES.map(function(style) {
// Each style could be matched multiple times.
spans = spans.concat(spannify(line, style.start, style.end, style.name));
});
var block;
if (spans.length == 0) {
block = {txt: line};
} else {
// Sort spans by style occurence early -> late
spans.sort(function(a,b) {
return a.start - b.start;
});
// Convert an array of possibly overlapping spans into a tree
spans = toTree(spans);
// Build a tree representation of the entire string, not
// just the formatted parts.
var chunks = chunkify(line, 0, line.length, spans);
var drafty = draftify(chunks, 0);
block = {txt: drafty.txt, fmt: drafty.fmt};
}
// Extract entities from the cleaned up string.
entities = extractEntities(block.txt);
if (entities.length > 0) {
var ranges = [];
for (var i in entities) {
// {offset: match['index'], unique: match[0], len: match[0].length, data: ent.packer(), type: ent.name}
var entity = entities[i];
var index = entityIndex[entity.unique];
if (!index) {
index = entityMap.length;
entityIndex[entity.unique] = index;
entityMap.push({tp: entity.type, data: entity.data});
}
ranges.push({at: entity.offset, len: entity.len, key: index});
}
block.ent = ranges;
}
blx.push(block);
});
var result = {txt: ""};
// Merge lines and save line breaks as BR inline formatting.
if (blx.length > 0) {
result.txt = blx[0].txt;
result.fmt = (blx[0].fmt || []).concat(blx[0].ent || []);
for (var i = 1; i<blx.length; i++) {
var block = blx[i];
var offset = result.txt.length + 1;
result.fmt.push({tp: "BR", len: 1, at: offset - 1});
result.txt += " " + block.txt;
if (block.fmt) {
result.fmt = result.fmt.concat(block.fmt.map(function(s) {
s.at += offset; return s;
}));
}
if (block.ent) {
result.fmt = result.fmt.concat(block.ent.map(function(s) {
s.at += offset; return s;
}));
}
}
if (result.fmt.length == 0) {
delete result.fmt;
}
if (entityMap.length > 0) {
result.ent = entityMap;
}
}
return result;
},
/**
* Add inline image to drafty content
*
* @param {Drafty} content object to add image to
* @param {integer} at index where the object is inserted
* @param {string} mime mime-type of the image, e.g. "image/png"
* @param {string} bits base64-encoded image content
* @param {integer} width width of the image
* @param {integer} height height of the image
* @param {string} fname file name suggestion for downloading the image.
*/
insertImage: function(content, at, mime, base64bits, width, height, fname) {
content = content || {txt: " "};
content.ent = content.ent || [];
content.fmt = content.fmt || [];
content.fmt.push({
at: at,
len: 1,
key: content.ent.length
});
content.ent.push({
tp: "IM",
data: {
mime: mime,
val: base64bits,
width: width,
height: height,
name: fname
}
});
return content;
},
attachFile: function(content, mime, base64bits, fname) {
content = content || {txt: ""};
content.ent = content.ent || [];
content.fmt = content.fmt || [];
content.fmt.push({
at: -1,
len: 0,
key: content.ent.length
});
content.ent.push({
tp: "EX",
data: {
mime: mime,
val: base64bits,
name: fname
}
});
return content;
},
/**
* Given the structured representation of rich text, convert it to HTML.
* No attempt is made to strip pre-existing html markup.
* This is potentially unsafe because block.txt may contain malicious
* markup.
*
* @param {drafy} content - structured representation of rich text.
*
* @return HTML-representation of content
*/
unsafeToHTML: function(content) {
var {txt, fmt, ent} = content;
var markup = [];
if (fmt) {
for (var i in fmt) {
var range = fmt[i];
var tp = range.tp, data;
if (!tp) {
var entity = ent[range.key];
if (entity) {
tp = entity.tp;
data = entity.data;
}
}
if (DECORATORS[tp]) {
// Because we later sort in descending order, closing markup must come first.
// Otherwise zero-length objects will not be represented correctly.
markup.push({idx: range.at + range.len, what: DECORATORS[tp].close(data)});
markup.push({idx: range.at, what: DECORATORS[tp].open(data)});
}
}
}
markup.sort(function(a, b) {
return b.idx - a.idx; // in descending order
});
for (var i in markup) {
if (markup[i].what) {
txt = splice(txt, markup[i].idx, markup[i].what);
}
}
return txt;
},
/**
* Transform using custom formatting.
*
* @param {Drafty} content - content to transform.
* @param {function} formatter - callback which transforms individual elements
* @param {Object} context - context provided to formatter as 'this'.
*
* @return {Object} context
*/
format: function(content, formatter, context) {
var {txt, fmt, ent} = content;
txt = txt || "";
if (!fmt) {
return [txt];
}
var spans = [].concat(fmt);
// Zero values may have been stripped. Restore them.
spans.map(function(s) {
s.at = s.at || 0;
s.len = s.len || 0;
});
// Soft spans first by start index (asc) then by length (desc).
spans.sort(function(a, b) {
if (a.at - b.at == 0) {
return b.len - a.len; // longer one comes first (<0)
}
return a.at - b.at;
});
// Denormalize entities into spans. Create a copy of the objects to leave
// original Drafty object unchanged.
spans = spans.map(function(s) {
var data;
var tp = s.tp;
if (!tp) {
s.key = s.key || 0;
data = ent[s.key].data;
tp = ent[s.key].tp;
}
return {tp: tp, data: data, at: s.at, len: s.len};
});
return forEach(txt, 0, txt.length, spans, formatter, context);
},
/**
* Given structured representation of rich text, convert it to plain text.
*
* @param {drafty} content - content to convert to plain text.
*/
toPlainText: function(content) {
return content.text;
},
/**
* Returns true if content has no markup and no entities.
*
* @param {Drafty} content - content to check for presence of markup.
* @returns true is content is plain text, false otherwise.
*/
isPlainText: function(content, noLineBreaks) {
return !(content.fmt || content.ent);
},
/**
* Check if the drafty content has attachments.
*
* @param {Drafty} content - content to check for attachments.
* @returns true if there are attachments.
*/
hasAttachment: function(content) {
if (content.ent && content.ent.length > 0) {
for (var i in content.ent) {
if (content.ent[i].tp == "EX") {
return true;
}
}
}
return false;
},
/**
* Enumerate attachments
*
* @param {Drafty} content - drafty object to process for attachments.
* @param {function} callback - callback to call for each attachment.
* @param {Object} content - value of "this" for callback.
*/
attachments: function(content, callback, context) {
if (content.ent && content.ent.length > 0) {
for (var i in content.ent) {
if (content.ent[i].tp == "EX") {
callback.call(context, content.ent[i].data, i);
}
}
}
},
// Get blob URL for a given entity
getBlobUrl: function(entity) {
return base64toObjectUrl(entity.val, entity.mime);
},
getAttachmentSize: function(entity) {
// Value is base64 encoded. The actual object size is smaller than the encoded length.
return entity.val ? (entity.val.length * 0.75) | 0 : 0;
},
getAttachmentType: function(entity) {
return entity.mime || "text/plain";
},
/**
* Get HTML tag for a given two-letter style name
* @param {string} style - two-letter style, like ST or LN
* @returns
*/
tagName: function(style) {
return HTML_TAGS[style] ? HTML_TAGS[style].name : undefined;
},
/**
* For a given data bundle generate an object with HTML attributes,
* for instance, given {url: "http://www.example.com/"} return
* {href: "http://www.example.com/"}
* @param {string} style - tw-letter style to generate attributes for.
* @param {Object} data - data bundle to convert to attributes
* @returns object with HTML attributes.
*/
attrValue: function(style, data) {
if (data && DECORATORS[style]) {
return DECORATORS[style].props(data);
}
return undefined;
},
/**
* Content MIME type.
* @returns string suitabe for HTTP Content-Type field.
*/
getContentType: function() {
return "text/x-drafty";
}
};
});
// Export for the window object or node; Check that is not already defined.
if (typeof(environment.Drafty) === 'undefined') {
environment.Drafty = Drafty();
}
})(this); // 'this' will be a window object for the browsers.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

+44
View File
@@ -0,0 +1,44 @@
<!--
DEVELOPMENT version of the web app.
It links to online babel transformer and non-babelified app script
-->
<!DOCTYPE html>
<html lang="en">
<head>
<title>Tinode</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="application-name" content="Tinode">
<meta name="description" content="Instant messenger">
<meta name="application-url" content="https://api.tinode.co/">
<!-- Browser styling for Chrome, FF, Opera -->
<meta name="theme-color" content="#3949AB">
<!-- Ask Google not to offer page translation -->
<meta name="google" value="notranslate">
<link rel="manifest" href="manifest.json">
<link id="shortcut-icon" rel="shortcut icon" href="img/logo32x32.png" type="image/png" />
<link rel="apple-touch-icon" sizes="192x192" href="img/logo192.png" type="image/png" />
<link rel="canonical" href="https://api.tinode.co/" />
<!-- Google's Roboto font -->
<link href="https://fonts.googleapis.com/css?family=Roboto:300,400,400italic,500,700&subset=latin,cyrillic"
rel="stylesheet" type="text/css" />
<!-- Google's material design icons -->
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet" />
<!-- Styles for local elements -->
<link rel="stylesheet" href="css/base.css" />
<!-- ReactJs scripts -->
<script crossorigin src="https://unpkg.com/react@16/umd/react.production.min.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.production.min.js"></script>
<script crossorigin src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<!-- core tinode library -->
<script src="tinode.js"></script>
<!-- the application -->
<script type="text/babel" src="index.js"></script>
</head>
<body>
<div id="mountPoint"></div>
</body>
</html>
+7 -4545
View File
File diff suppressed because it is too large Load Diff
+5073
View File
File diff suppressed because it is too large Load Diff
+1
View File
File diff suppressed because one or more lines are too long
+59
View File
@@ -0,0 +1,59 @@
{
"name": "tinode-webapp",
"description": "Tinode Web Client",
"version": "0.15.5-rc2",
"devDependencies": {
"babel-cli": "^6.26.0",
"babel-minify": "^0.4.3",
"babel-preset-env": "^1.7.0",
"babel-preset-minify": "^0.4.3",
"babel-preset-react": "^6.24.1",
"cssnano": "^4.0.5"
},
"repository": {
"type": "git",
"url": "https://github.com/tinode/example-react-js.git"
},
"scripts": {
"build": "npm run build:js && npm run build:css",
"build:js": "babel ./index.js --presets react,minify --out-file ./index.min.js",
"build:css": "postcss css/base.css > css/base.min.css",
"babel-version": "babel --version"
},
"files": [
"audio/msg.mp3",
"css/base.css",
"css/base.min.css",
"img/ignasi_pattern_s.png",
"img/logo.svg",
"img/logo32x32.png",
"img/logo32x32a.png",
"img/logo96.png",
"img/logo192.png",
"index-dev.html",
"index.html",
"index.js",
"index.min.js",
"manifest.json",
"web-desktop-2.png",
"web-mob-chat-1.png",
"web-mob-contacts-1.png",
"web-mob-info-1.png",
"web-mob-new-chat-1.png"
],
"keywords": [
"chat",
"instant-messenger",
"message",
"messenger",
"react",
"web"
],
"main": "index.js",
"bugs": {
"url": "https://github.com/tinode/example-react-js/issues"
},
"homepage": "https://github.com/tinode/chat",
"email": "gene@tinode.co",
"license": "Apache-2.0"
}
+7
View File
@@ -0,0 +1,7 @@
module.exports = {
plugins: [
require('cssnano')({
preset: 'default',
}),
],
};