sort components into widgets and views
This commit is contained in:
@@ -0,0 +1,415 @@
|
|||||||
|
// Helper functions for storing values in localStorage.
|
||||||
|
// By default localStorage can store only strings, not objects or other types.
|
||||||
|
|
||||||
|
// Replace old object with the new one.
|
||||||
|
Storage.prototype.setObject = function(key, value) {
|
||||||
|
this.setItem(key, JSON.stringify(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get stored object.
|
||||||
|
Storage.prototype.getObject = function(key) {
|
||||||
|
let value = this.getItem(key);
|
||||||
|
return value && JSON.parse(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Partially or wholly update stored object.
|
||||||
|
Storage.prototype.updateObject = function(key, value) {
|
||||||
|
let oldVal = this.getObject(key);
|
||||||
|
this.setObject(key, Object.assign(oldVal || {}, value));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Short representation of time in the past.
|
||||||
|
function shortDateFormat(then) {
|
||||||
|
var locale = window.navigator.userLanguage || window.navigator.language;
|
||||||
|
var now = new Date();
|
||||||
|
if (then.getFullYear() == now.getFullYear()) {
|
||||||
|
if (then.getMonth() == now.getMonth() && then.getDate() == now.getDate()) {
|
||||||
|
return then.toLocaleTimeString(locale, {hour12: false, hour: '2-digit', minute: '2-digit'});
|
||||||
|
} else {
|
||||||
|
return then.toLocaleDateString(locale,
|
||||||
|
{hour12: false, month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit'});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return then.toLocaleDateString(locale,
|
||||||
|
{hour12: false, year: 'numeric', month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit'});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert a number of bytes to human-readable format.
|
||||||
|
function bytesToHumanSize(bytes) {
|
||||||
|
if (!bytes || bytes == 0) {
|
||||||
|
return '0 Bytes';
|
||||||
|
}
|
||||||
|
|
||||||
|
var sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB'];
|
||||||
|
var bucket = Math.min(Math.floor(Math.log2(bytes) / 10) | 0, sizes.length-1);
|
||||||
|
var count = bytes / Math.pow(1024, bucket);
|
||||||
|
var round = bucket > 0 ? (count < 3 ? 2 : (count < 30 ? 1 : 0)) : 0;
|
||||||
|
return count.toFixed(round) + ' ' + sizes[bucket];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make a data URL from public.photo
|
||||||
|
function makeImageUrl(photo) {
|
||||||
|
return (photo && photo.type && photo.data) ?
|
||||||
|
'data:image/' + photo.type + ';base64,' + photo.data : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate linear dimensions for scaling image down to fit under a certain size.
|
||||||
|
// Returns an object which contains destination sizes, source sizes, and offsets
|
||||||
|
// into source (when making square images).
|
||||||
|
function fitImageSize(width, height, maxWidth, maxHeight, forceSquare) {
|
||||||
|
if (!width || !height || !maxWidth || !maxHeight) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (forceSquare) {
|
||||||
|
maxWidth = maxHeight = Math.min(maxWidth, maxHeight);
|
||||||
|
}
|
||||||
|
|
||||||
|
let scale = Math.min(
|
||||||
|
Math.min(width, maxWidth) / width,
|
||||||
|
Math.min(height, maxHeight) / height
|
||||||
|
);
|
||||||
|
|
||||||
|
let size = {
|
||||||
|
dstWidth: (width * scale) | 0,
|
||||||
|
dstHeight: (height * scale) | 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (forceSquare) {
|
||||||
|
// Also calculate parameters for making the image square.
|
||||||
|
size.dstWidth = size.dstHeight = Math.min(size.dstWidth, size.dstHeight);
|
||||||
|
size.srcWidth = size.srcHeight = Math.min(width, height);
|
||||||
|
size.xoffset = ((width - size.srcWidth) / 2) | 0;
|
||||||
|
size.yoffset = ((height - size.srcWidth) / 2) | 0;
|
||||||
|
} else {
|
||||||
|
size.xoffset = size.yoffset = 0;
|
||||||
|
size.srcWidth = width;
|
||||||
|
size.srcHeight = height;
|
||||||
|
}
|
||||||
|
return size;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure file's extension matches mime content type
|
||||||
|
function fileNameForMime(fname, mime) {
|
||||||
|
var idx = SUPPORTED_IMAGE_FORMATS.indexOf(mime);
|
||||||
|
var ext = MIME_EXTENSIONS[idx];
|
||||||
|
|
||||||
|
var at = fname.lastIndexOf('.');
|
||||||
|
if (at >= 0) {
|
||||||
|
fname = fname.substring(0, at);
|
||||||
|
}
|
||||||
|
return fname + '.' + ext;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get mime type from data URL header.
|
||||||
|
function getMimeType(header) {
|
||||||
|
var mime = /^data:(image\/[-+a-z0-9.]+);base64/.exec(header);
|
||||||
|
return (mime && mime.length > 1) ? mime[1] : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Given length of a binary object in bytes, calculate the length after
|
||||||
|
// base64 encoding.
|
||||||
|
function base64EncodedLen(n) {
|
||||||
|
return Math.floor((n + 2) / 3) * 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Given length of a base64-encoded object, calculate decoded size of the
|
||||||
|
// pbject in bytes.
|
||||||
|
function base64DecodedLen(n) {
|
||||||
|
return Math.floor(n / 4) * 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Re-encode string to standard base64 encoding with padding.
|
||||||
|
// The string may be base64-URL encoded without the padding.
|
||||||
|
function base64ReEncode(str) {
|
||||||
|
if (str) {
|
||||||
|
str = str.replace('-', '+').replace('_', '/');
|
||||||
|
try {
|
||||||
|
str = btoa(atob(str));
|
||||||
|
} catch(err) {
|
||||||
|
console.log("Failed to base64 re-decode string");
|
||||||
|
str = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return str;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert uploaded image into a base64-encoded string possibly scaling
|
||||||
|
// linear dimensions or constraining to a square.
|
||||||
|
function imageFileScaledToBase64(file, width, height, forceSquare, onSuccess, onError) {
|
||||||
|
var img = new Image();
|
||||||
|
img.crossOrigin = 'Anonymous';
|
||||||
|
img.onerror = function(err) {
|
||||||
|
onError("Image format unrecognized");
|
||||||
|
}
|
||||||
|
img.onload = function() {
|
||||||
|
var dim = fitImageSize(this.width, this.height, width, height, forceSquare);
|
||||||
|
if (!dim) {
|
||||||
|
onError("Invalid image");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var canvas = document.createElement('canvas');
|
||||||
|
canvas.width = dim.dstWidth;
|
||||||
|
canvas.height = dim.dstHeight;
|
||||||
|
var ctx = canvas.getContext('2d');
|
||||||
|
ctx.imageSmoothingEnabled = true;
|
||||||
|
ctx.drawImage(this, dim.xoffset, dim.yoffset, dim.srcWidth, dim.srcHeight,
|
||||||
|
0, 0, dim.dstWidth, dim.dstHeight);
|
||||||
|
|
||||||
|
var mime = (this.width != dim.dstWidth ||
|
||||||
|
this.height != dim.dstHeight ||
|
||||||
|
SUPPORTED_IMAGE_FORMATS.indexOf(file.type) < 0) ? 'image/jpeg' : file.type;
|
||||||
|
var imageBits = canvas.toDataURL(mime);
|
||||||
|
var parts = imageBits.split(',');
|
||||||
|
// Get actual image type: 'data:image/png;base64,'
|
||||||
|
mime = getMimeType(parts[0]);
|
||||||
|
if (!mime) {
|
||||||
|
onError("Unsupported image format");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Ensure the image is not too large
|
||||||
|
var quality = 0.78;
|
||||||
|
if (base64DecodedLen(imageBits.length) > MAX_INBAND_ATTACHMENT_SIZE) {
|
||||||
|
mime = 'image/jpeg';
|
||||||
|
}
|
||||||
|
if (mime == 'image/jpeg') {
|
||||||
|
// Reduce size of the jpeg by reducing image quality
|
||||||
|
while (base64DecodedLen(imageBits.length) > MAX_INBAND_ATTACHMENT_SIZE && quality > 0.45) {
|
||||||
|
imageBits = canvas.toDataURL(mime, quality);
|
||||||
|
quality *= 0.84;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (base64DecodedLen(imageBits.length) > MAX_INBAND_ATTACHMENT_SIZE) {
|
||||||
|
onError("The image size " + bytesToHumanSize(base64DecodedLen(imageBits.length)) +
|
||||||
|
" exceeds the " + bytesToHumanSize(MAX_INBAND_ATTACHMENT_SIZE) + " limit.", "err");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
canvas = null;
|
||||||
|
onSuccess(imageBits.split(',')[1], mime, dim.dstWidth, dim.dstHeight, fileNameForMime(file.name, mime));
|
||||||
|
};
|
||||||
|
img.src = URL.createObjectURL(file);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert uploaded image file to base64-encoded string without scaling/converting the image
|
||||||
|
function imageFileToBase64(file, onSuccess, onError) {
|
||||||
|
var reader = new FileReader();
|
||||||
|
reader.addEventListener('load', function() {
|
||||||
|
var parts = reader.result.split(',');
|
||||||
|
var mime = getMimeType(parts[0]);
|
||||||
|
if (!mime) {
|
||||||
|
onError("Failed to process image file");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get image size.
|
||||||
|
var img = new Image();
|
||||||
|
img.crossOrigin = 'Anonymous';
|
||||||
|
img.onload = function() {
|
||||||
|
onSuccess(parts[1], mime, this.width, this.height, fileNameForMime(file.name, mime));
|
||||||
|
}
|
||||||
|
img.onerror = function(err) {
|
||||||
|
onError("Image format not recognized");
|
||||||
|
}
|
||||||
|
img.src = URL.createObjectURL(file);
|
||||||
|
}, false);
|
||||||
|
reader.readAsDataURL(file);
|
||||||
|
}
|
||||||
|
|
||||||
|
function fileToBase64(file, onSuccess, onError) {
|
||||||
|
var reader = new FileReader();
|
||||||
|
reader.addEventListener('load', function() {
|
||||||
|
onSuccess(file.type, reader.result.split(',')[1], file.name);
|
||||||
|
});
|
||||||
|
reader.readAsDataURL(file);
|
||||||
|
}
|
||||||
|
|
||||||
|
// File pasted from the clipboard. It's either an inline image or a file attachment.
|
||||||
|
// FIXME: handle large files out of band.
|
||||||
|
function filePasted(event, onImageSuccess, onAttachmentSuccess, onError) {
|
||||||
|
var items = (event.clipboardData || event.originalEvent.clipboardData || {}).items;
|
||||||
|
for (var i in items) {
|
||||||
|
var item = items[i];
|
||||||
|
if (item.kind === 'file') {
|
||||||
|
var file = item.getAsFile();
|
||||||
|
if (!file) {
|
||||||
|
console.log("Failed to get file object from pasted file item", item.kind, item.type);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (file.type && file.type.split('/')[0] == 'image') {
|
||||||
|
// Handle inline image
|
||||||
|
if (file.size > MAX_INBAND_ATTACHMENT_SIZE || SUPPORTED_IMAGE_FORMATS.indexOf(file.type) < 0) {
|
||||||
|
imageFileScaledToBase64(file, MAX_IMAGE_DIM, MAX_IMAGE_DIM, false, onImageSuccess, onError);
|
||||||
|
} else {
|
||||||
|
imageFileToBase64(file, onImageSuccess, onError);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Handle file attachment
|
||||||
|
fileToBase64(file, onAttachmentSuccess, onError)
|
||||||
|
}
|
||||||
|
// Indicate that the pasted data contains a file.
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// No file found.
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make shortcut icon appear with a green dot + show unread count in title.
|
||||||
|
function updateFavicon(count) {
|
||||||
|
var oldIcon = document.getElementById('shortcut-icon');
|
||||||
|
if (oldIcon) {
|
||||||
|
var head = document.head || document.getElementsByTagName('head')[0];
|
||||||
|
var newIcon = document.createElement('link');
|
||||||
|
newIcon.type = 'image/png';
|
||||||
|
newIcon.id = 'shortcut-icon';
|
||||||
|
newIcon.rel = 'shortcut icon';
|
||||||
|
newIcon.href = 'img/logo32x32' + (count > 0 ? 'a' : '') + '.png';
|
||||||
|
head.removeChild(oldIcon);
|
||||||
|
head.appendChild(newIcon);
|
||||||
|
}
|
||||||
|
document.title = (count > 0 ? '('+count+') ' : '') + 'Tinode';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get 32 bit integer hash value for a string. Ideally it should produce the same value
|
||||||
|
// as Java's String#hash().
|
||||||
|
function stringHash(value) {
|
||||||
|
var hash = 0;
|
||||||
|
value = '' + value;
|
||||||
|
for (var i = 0; i < value.length; i++) {
|
||||||
|
hash = ((hash << 5) - hash) + value.charCodeAt(i);
|
||||||
|
hash = hash & hash; // Convert to 32bit integer
|
||||||
|
}
|
||||||
|
return hash;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper functions for hash navigation.
|
||||||
|
|
||||||
|
// Parse hash as in http://www.example.com/path#hash as if it were
|
||||||
|
// path and arguments.
|
||||||
|
function parseUrlHash(hash) {
|
||||||
|
// Split path from args, path -> parts[0], args->path[1]
|
||||||
|
let parts = hash.split('?', 2);
|
||||||
|
let params = {};
|
||||||
|
let path = [];
|
||||||
|
if (parts[0]) {
|
||||||
|
path = parts[0].substr(1).split('/');
|
||||||
|
}
|
||||||
|
if (parts[1]) {
|
||||||
|
parts[1].split('&').forEach(function(part) {
|
||||||
|
let item = part.split('=');
|
||||||
|
if (item[0]) {
|
||||||
|
params[decodeURIComponent(item[0])] = decodeURIComponent(item[1]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return {path: path, params: params};
|
||||||
|
}
|
||||||
|
|
||||||
|
function navigateTo(url) {
|
||||||
|
window.location.hash = url;
|
||||||
|
}
|
||||||
|
|
||||||
|
function composeUrlHash(path, params) {
|
||||||
|
var url = path.join('/');
|
||||||
|
var args = [];
|
||||||
|
for (var key in params) {
|
||||||
|
if (params.hasOwnProperty(key)) {
|
||||||
|
args.push(key + '=' + params[key]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (args.length > 0) {
|
||||||
|
url += '?' + args.join('&');
|
||||||
|
}
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
|
||||||
|
function addUrlParam(hash, key, value) {
|
||||||
|
var parsed = parseUrlHash(hash);
|
||||||
|
parsed.params[key] = value;
|
||||||
|
return composeUrlHash(parsed.path, parsed.params);
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeUrlParam(hash, key) {
|
||||||
|
var parsed = parseUrlHash(hash);
|
||||||
|
delete parsed.params[key];
|
||||||
|
return composeUrlHash(parsed.path, parsed.params);
|
||||||
|
}
|
||||||
|
function setUrlSidePanel(hash, sidepanel) {
|
||||||
|
var parsed = parseUrlHash(hash);
|
||||||
|
parsed.path[0] = sidepanel;
|
||||||
|
return composeUrlHash(parsed.path, parsed.params);
|
||||||
|
}
|
||||||
|
function setUrlTopic(hash, topic) {
|
||||||
|
var parsed = parseUrlHash(hash);
|
||||||
|
parsed.path[1] = topic;
|
||||||
|
// Close InfoView on topic change.
|
||||||
|
delete parsed.params.info;
|
||||||
|
return composeUrlHash(parsed.path, parsed.params);
|
||||||
|
}
|
||||||
|
// END of hash navigation helper functions
|
||||||
|
|
||||||
|
|
||||||
|
// Detect server address from the URL
|
||||||
|
function detectServerAddress() {
|
||||||
|
let host = DEFAULT_HOST;
|
||||||
|
if (typeof window.location == 'object') {
|
||||||
|
if (window.location.protocol == 'file:' || window.location.hostname == 'localhost') {
|
||||||
|
host = KNOWN_HOSTS.local;
|
||||||
|
} else if (window.location.hostname) {
|
||||||
|
host = window.location.hostname + (window.location.port ? ':' + window.location.port : '');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return host;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Detect if the page is served over HTTPS.
|
||||||
|
function isSecureConnection() {
|
||||||
|
if (typeof window.location == 'object') {
|
||||||
|
return window.location.protocol == 'https:';
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isLocalHost() {
|
||||||
|
if (typeof window.location == 'object') {
|
||||||
|
return window.location.hostname == 'localhost';
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create VCard which represents topic 'public' info
|
||||||
|
function vcard(fn, imageDataUrl) {
|
||||||
|
var card = null;
|
||||||
|
|
||||||
|
if ((fn && fn.trim()) || imageDataUrl) {
|
||||||
|
card = {};
|
||||||
|
if (fn) {
|
||||||
|
card.fn = fn.trim();
|
||||||
|
}
|
||||||
|
if (imageDataUrl) {
|
||||||
|
var dataStart = imageDataUrl.indexOf(',');
|
||||||
|
card.photo = {
|
||||||
|
data: imageDataUrl.substring(dataStart+1),
|
||||||
|
type: 'jpg'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return card;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deep-shallow compare two arrays: shallow compare each element.
|
||||||
|
function arrayEqual(a, b) {
|
||||||
|
// Compare lengths first.
|
||||||
|
if (a.length != b.length) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// Order of elements is ignored.
|
||||||
|
a.sort();
|
||||||
|
b.sort();
|
||||||
|
for (var i = 0, l = a.length; i < l; i++) {
|
||||||
|
if (a[i] !== b[i]) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import ContactList from './contact-list.jsx';
|
import ContactList from '../widgets/contact-list.jsx';
|
||||||
|
|
||||||
export default class ContactsView extends React.Component {
|
export default class ContactsView extends React.Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
@@ -2,9 +2,9 @@
|
|||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import AvatarUpload from './avatar-upload.jsx';
|
import AvatarUpload from '../widgets/avatar-upload.jsx';
|
||||||
import CheckBox from './checkbox.jsx';
|
import CheckBox from '../widgets/checkbox.jsx';
|
||||||
import VisiblePassword from './visible-password.jsx';
|
import VisiblePassword from '../widgets/visible-password.jsx';
|
||||||
|
|
||||||
export default class CreateAccountView extends React.PureComponent {
|
export default class CreateAccountView extends React.PureComponent {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import AvatarUpload from './avatar-upload.jsx';
|
import AvatarUpload from '../widgets/avatar-upload.jsx';
|
||||||
import CheckBox from './checkbox.jsx';
|
import CheckBox from '../widgets/checkbox.jsx';
|
||||||
import PermissionsEditor from './permissions-editor.jsx';
|
import PermissionsEditor from '../widgets/permissions-editor.jsx';
|
||||||
import TagManager from './tag-manager.jsx';
|
import TagManager from '../widgets/tag-manager.jsx';
|
||||||
|
|
||||||
/* Edit account parameters */
|
/* Edit account parameters */
|
||||||
|
|
||||||
@@ -2,15 +2,14 @@
|
|||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import AvatarUpload from './avatar-upload.jsx';
|
import AvatarUpload from '../widgets/avatar-upload.jsx';
|
||||||
import ContactList from './contact-list.jsx';
|
import ContactList from '../widgets/contact-list.jsx';
|
||||||
import GroupManager from './group-manager.jsx';
|
import GroupManager from '../widgets/group-manager.jsx';
|
||||||
import InPlaceEdit from './in-place-edit.jsx';
|
import InPlaceEdit from '../widgets/in-place-edit.jsx';
|
||||||
import MenuCancel from './menu-cancel.jsx';
|
import MenuCancel from '../widgets/menu-cancel.jsx';
|
||||||
import MoreButton from './more-button.jsx';
|
import MoreButton from '../widgets/more-button.jsx';
|
||||||
import PermissionsEditor from './permissions-editor.jsx';
|
import PermissionsEditor from '../widgets/permissions-editor.jsx';
|
||||||
import TagManager from './tag-manager.jsx';
|
import TagManager from '../widgets/tag-manager.jsx';
|
||||||
|
|
||||||
|
|
||||||
export default class InfoView extends React.Component {
|
export default class InfoView extends React.Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
@@ -3,8 +3,8 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import {FormattedMessage} from 'react-intl';
|
import {FormattedMessage} from 'react-intl';
|
||||||
|
|
||||||
import CheckBox from './checkbox.jsx';
|
import CheckBox from '../widgets/checkbox.jsx';
|
||||||
import VisiblePassword from './visible-password.jsx';
|
import VisiblePassword from '../widgets/visible-password.jsx';
|
||||||
|
|
||||||
export default class LoginView extends React.Component {
|
export default class LoginView extends React.Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
@@ -1,13 +1,13 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import ChatMessage from './chat-message.jsx';
|
import ChatMessage from '../widgets/chat-message.jsx';
|
||||||
import ErrorPanel from './error-panel.jsx';
|
import ErrorPanel from '../widgets/error-panel.jsx';
|
||||||
import GroupSubs from './group-subs.jsx';
|
import GroupSubs from '../widgets/group-subs.jsx';
|
||||||
import ImagePreview from './image-preview.jsx';
|
import ImagePreview from '../widgets/image-preview.jsx';
|
||||||
import LetterTile from './letter-tile.jsx';
|
import LetterTile from '../widgets/letter-tile.jsx';
|
||||||
import LoadSpinner from './load-spinner.jsx';
|
import LoadSpinner from '../widgets/load-spinner.jsx';
|
||||||
import LogoView from './logo-view.jsx';
|
import LogoView from './logo-view.jsx';
|
||||||
import SendMessage from './send-message.jsx';
|
import SendMessage from '../widgets/send-message.jsx';
|
||||||
|
|
||||||
const NOT_FOUND_TOPIC_TITLE = "Not found";
|
const NOT_FOUND_TOPIC_TITLE = "Not found";
|
||||||
|
|
||||||
@@ -2,10 +2,10 @@
|
|||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import ContactList from './contact-list.jsx';
|
import ContactList from '../widgets/contact-list.jsx';
|
||||||
import NewTopicById from './new-topic-by-id.jsx';
|
import NewTopicById from '../widgets/new-topic-by-id.jsx';
|
||||||
import NewTopicGroup from './new-topic-group.jsx';
|
import NewTopicGroup from '../widgets/new-topic-group.jsx';
|
||||||
import SearchContacts from './search-contacts.jsx';
|
import SearchContacts from '../widgets/search-contacts.jsx';
|
||||||
|
|
||||||
export default class NewTopicView extends React.Component {
|
export default class NewTopicView extends React.Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import VisiblePassword from './visible-password.jsx';
|
import VisiblePassword from '../widgets/visible-password.jsx';
|
||||||
|
|
||||||
export default class PasswordResetView extends React.PureComponent {
|
export default class PasswordResetView extends React.PureComponent {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
// Tinode config panel.
|
// Tinode config panel.
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import HostSelector from './host-selector.jsx';
|
import HostSelector from '../widgets/host-selector.jsx';
|
||||||
|
|
||||||
export default class SettingsView extends React.PureComponent {
|
export default class SettingsView extends React.PureComponent {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
@@ -1,14 +1,15 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
|
import SideNavbar from '../widgets/side-navbar.jsx';
|
||||||
|
import ErrorPanel from '../widgets/error-panel.jsx';
|
||||||
|
|
||||||
import ContactsView from './contacts-view.jsx';
|
import ContactsView from './contacts-view.jsx';
|
||||||
import CreateAccountView from './create-account-view.jsx';
|
import CreateAccountView from './create-account-view.jsx';
|
||||||
import EditAccountView from './edit-account-view.jsx';
|
import EditAccountView from './edit-account-view.jsx';
|
||||||
import ErrorPanel from './error-panel.jsx';
|
|
||||||
import LoginView from './login-view.jsx';
|
import LoginView from './login-view.jsx';
|
||||||
import NewTopicView from './new-topic-view.jsx';
|
import NewTopicView from './new-topic-view.jsx';
|
||||||
import PasswordResetView from './password-reset-view.jsx';
|
import PasswordResetView from './password-reset-view.jsx';
|
||||||
import SettingsView from './settings-view.jsx';
|
import SettingsView from './settings-view.jsx';
|
||||||
import SideNavbar from './side-navbar.jsx';
|
|
||||||
import ValidationView from './validation-view.jsx';
|
import ValidationView from './validation-view.jsx';
|
||||||
|
|
||||||
export default class SidepanelView extends React.Component {
|
export default class SidepanelView extends React.Component {
|
||||||
@@ -1,7 +1,11 @@
|
|||||||
// The top-level class to hold all functionality together.
|
// The top-level class to hold all functionality together.
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import ContextMenu from './context-menu.jsx';
|
import 'firebase/app';
|
||||||
|
import 'firebase/messaging';
|
||||||
|
|
||||||
|
import ContextMenu from '../widgets/context-menu.jsx';
|
||||||
|
|
||||||
import InfoView from './info-view.jsx';
|
import InfoView from './info-view.jsx';
|
||||||
import MessagesView from './messages-view.jsx';
|
import MessagesView from './messages-view.jsx';
|
||||||
import SidepanelView from './sidepanel-view.jsx';
|
import SidepanelView from './sidepanel-view.jsx';
|
||||||
-418
@@ -1,7 +1,3 @@
|
|||||||
// Babel JSX
|
|
||||||
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
if (typeof require == 'function') {
|
if (typeof require == 'function') {
|
||||||
if (typeof React == 'undefined') {
|
if (typeof React == 'undefined') {
|
||||||
var React = require('react');
|
var React = require('react');
|
||||||
@@ -77,417 +73,3 @@ const MAX_IMAGE_DIM = 768;
|
|||||||
// Supported image MIME types and corresponding file extensions.
|
// Supported image MIME types and corresponding file extensions.
|
||||||
const SUPPORTED_IMAGE_FORMATS = ['image/jpeg', 'image/gif', 'image/png', 'image/svg', 'image/svg+xml'];
|
const SUPPORTED_IMAGE_FORMATS = ['image/jpeg', 'image/gif', 'image/png', 'image/svg', 'image/svg+xml'];
|
||||||
const MIME_EXTENSIONS = ['jpg', 'gif', 'png', 'svg', 'svg'];
|
const MIME_EXTENSIONS = ['jpg', 'gif', 'png', 'svg', 'svg'];
|
||||||
|
|
||||||
// Helper functions for storing values in localStorage.
|
|
||||||
// By default localStorage can store only strings, not objects or other types.
|
|
||||||
|
|
||||||
// Replace old object with the new one.
|
|
||||||
Storage.prototype.setObject = function(key, value) {
|
|
||||||
this.setItem(key, JSON.stringify(value));
|
|
||||||
}
|
|
||||||
// Get stored object.
|
|
||||||
Storage.prototype.getObject = function(key) {
|
|
||||||
let value = this.getItem(key);
|
|
||||||
return value && JSON.parse(value);
|
|
||||||
}
|
|
||||||
// Partially or wholly update stored object.
|
|
||||||
Storage.prototype.updateObject = function(key, value) {
|
|
||||||
let oldVal = this.getObject(key);
|
|
||||||
this.setObject(key, Object.assign(oldVal || {}, value));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Short representation of time in the past.
|
|
||||||
function shortDateFormat(then) {
|
|
||||||
var locale = window.navigator.userLanguage || window.navigator.language;
|
|
||||||
var now = new Date();
|
|
||||||
if (then.getFullYear() == now.getFullYear()) {
|
|
||||||
if (then.getMonth() == now.getMonth() && then.getDate() == now.getDate()) {
|
|
||||||
return then.toLocaleTimeString(locale, {hour12: false, hour: '2-digit', minute: '2-digit'});
|
|
||||||
} else {
|
|
||||||
return then.toLocaleDateString(locale,
|
|
||||||
{hour12: false, month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit'});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return then.toLocaleDateString(locale,
|
|
||||||
{hour12: false, year: 'numeric', month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit'});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert a number of bytes to human-readable format.
|
|
||||||
function bytesToHumanSize(bytes) {
|
|
||||||
if (!bytes || bytes == 0) {
|
|
||||||
return '0 Bytes';
|
|
||||||
}
|
|
||||||
|
|
||||||
var sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB'];
|
|
||||||
var bucket = Math.min(Math.floor(Math.log2(bytes) / 10) | 0, sizes.length-1);
|
|
||||||
var count = bytes / Math.pow(1024, bucket);
|
|
||||||
var round = bucket > 0 ? (count < 3 ? 2 : (count < 30 ? 1 : 0)) : 0;
|
|
||||||
return count.toFixed(round) + ' ' + sizes[bucket];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Make a data URL from public.photo
|
|
||||||
function makeImageUrl(photo) {
|
|
||||||
return (photo && photo.type && photo.data) ?
|
|
||||||
'data:image/' + photo.type + ';base64,' + photo.data : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Calculate linear dimensions for scaling image down to fit under a certain size.
|
|
||||||
// Returns an object which contains destination sizes, source sizes, and offsets
|
|
||||||
// into source (when making square images).
|
|
||||||
function fitImageSize(width, height, maxWidth, maxHeight, forceSquare) {
|
|
||||||
if (!width || !height || !maxWidth || !maxHeight) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (forceSquare) {
|
|
||||||
maxWidth = maxHeight = Math.min(maxWidth, maxHeight);
|
|
||||||
}
|
|
||||||
|
|
||||||
let scale = Math.min(
|
|
||||||
Math.min(width, maxWidth) / width,
|
|
||||||
Math.min(height, maxHeight) / height
|
|
||||||
);
|
|
||||||
|
|
||||||
let size = {
|
|
||||||
dstWidth: (width * scale) | 0,
|
|
||||||
dstHeight: (height * scale) | 0,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (forceSquare) {
|
|
||||||
// Also calculate parameters for making the image square.
|
|
||||||
size.dstWidth = size.dstHeight = Math.min(size.dstWidth, size.dstHeight);
|
|
||||||
size.srcWidth = size.srcHeight = Math.min(width, height);
|
|
||||||
size.xoffset = ((width - size.srcWidth) / 2) | 0;
|
|
||||||
size.yoffset = ((height - size.srcWidth) / 2) | 0;
|
|
||||||
} else {
|
|
||||||
size.xoffset = size.yoffset = 0;
|
|
||||||
size.srcWidth = width;
|
|
||||||
size.srcHeight = height;
|
|
||||||
}
|
|
||||||
return size;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure file's extension matches mime content type
|
|
||||||
function fileNameForMime(fname, mime) {
|
|
||||||
var idx = SUPPORTED_IMAGE_FORMATS.indexOf(mime);
|
|
||||||
var ext = MIME_EXTENSIONS[idx];
|
|
||||||
|
|
||||||
var at = fname.lastIndexOf('.');
|
|
||||||
if (at >= 0) {
|
|
||||||
fname = fname.substring(0, at);
|
|
||||||
}
|
|
||||||
return fname + '.' + ext;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get mime type from data URL header.
|
|
||||||
function getMimeType(header) {
|
|
||||||
var mime = /^data:(image\/[-+a-z0-9.]+);base64/.exec(header);
|
|
||||||
return (mime && mime.length > 1) ? mime[1] : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Given length of a binary object in bytes, calculate the length after
|
|
||||||
// base64 encoding.
|
|
||||||
function base64EncodedLen(n) {
|
|
||||||
return Math.floor((n + 2) / 3) * 4;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Given length of a base64-encoded object, calculate decoded size of the
|
|
||||||
// pbject in bytes.
|
|
||||||
function base64DecodedLen(n) {
|
|
||||||
return Math.floor(n / 4) * 3;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Re-encode string to standard base64 encoding with padding.
|
|
||||||
// The string may be base64-URL encoded without the padding.
|
|
||||||
function base64ReEncode(str) {
|
|
||||||
if (str) {
|
|
||||||
str = str.replace('-', '+').replace('_', '/');
|
|
||||||
try {
|
|
||||||
str = btoa(atob(str));
|
|
||||||
} catch(err) {
|
|
||||||
console.log("Failed to base64 re-decode string");
|
|
||||||
str = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return str;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert uploaded image into a base64-encoded string possibly scaling
|
|
||||||
// linear dimensions or constraining to a square.
|
|
||||||
function imageFileScaledToBase64(file, width, height, forceSquare, onSuccess, onError) {
|
|
||||||
var img = new Image();
|
|
||||||
img.crossOrigin = 'Anonymous';
|
|
||||||
img.onerror = function(err) {
|
|
||||||
onError("Image format unrecognized");
|
|
||||||
}
|
|
||||||
img.onload = function() {
|
|
||||||
var dim = fitImageSize(this.width, this.height, width, height, forceSquare);
|
|
||||||
if (!dim) {
|
|
||||||
onError("Invalid image");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
var canvas = document.createElement('canvas');
|
|
||||||
canvas.width = dim.dstWidth;
|
|
||||||
canvas.height = dim.dstHeight;
|
|
||||||
var ctx = canvas.getContext('2d');
|
|
||||||
ctx.imageSmoothingEnabled = true;
|
|
||||||
ctx.drawImage(this, dim.xoffset, dim.yoffset, dim.srcWidth, dim.srcHeight,
|
|
||||||
0, 0, dim.dstWidth, dim.dstHeight);
|
|
||||||
|
|
||||||
var mime = (this.width != dim.dstWidth ||
|
|
||||||
this.height != dim.dstHeight ||
|
|
||||||
SUPPORTED_IMAGE_FORMATS.indexOf(file.type) < 0) ? 'image/jpeg' : file.type;
|
|
||||||
var imageBits = canvas.toDataURL(mime);
|
|
||||||
var parts = imageBits.split(',');
|
|
||||||
// Get actual image type: 'data:image/png;base64,'
|
|
||||||
mime = getMimeType(parts[0]);
|
|
||||||
if (!mime) {
|
|
||||||
onError("Unsupported image format");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// Ensure the image is not too large
|
|
||||||
var quality = 0.78;
|
|
||||||
if (base64DecodedLen(imageBits.length) > MAX_INBAND_ATTACHMENT_SIZE) {
|
|
||||||
mime = 'image/jpeg';
|
|
||||||
}
|
|
||||||
if (mime == 'image/jpeg') {
|
|
||||||
// Reduce size of the jpeg by reducing image quality
|
|
||||||
while (base64DecodedLen(imageBits.length) > MAX_INBAND_ATTACHMENT_SIZE && quality > 0.45) {
|
|
||||||
imageBits = canvas.toDataURL(mime, quality);
|
|
||||||
quality *= 0.84;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (base64DecodedLen(imageBits.length) > MAX_INBAND_ATTACHMENT_SIZE) {
|
|
||||||
onError("The image size " + bytesToHumanSize(base64DecodedLen(imageBits.length)) +
|
|
||||||
" exceeds the " + bytesToHumanSize(MAX_INBAND_ATTACHMENT_SIZE) + " limit.", "err");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
canvas = null;
|
|
||||||
onSuccess(imageBits.split(',')[1], mime, dim.dstWidth, dim.dstHeight, fileNameForMime(file.name, mime));
|
|
||||||
};
|
|
||||||
img.src = URL.createObjectURL(file);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert uploaded image file to base64-encoded string without scaling/converting the image
|
|
||||||
function imageFileToBase64(file, onSuccess, onError) {
|
|
||||||
var reader = new FileReader();
|
|
||||||
reader.addEventListener('load', function() {
|
|
||||||
var parts = reader.result.split(',');
|
|
||||||
var mime = getMimeType(parts[0]);
|
|
||||||
if (!mime) {
|
|
||||||
onError("Failed to process image file");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get image size.
|
|
||||||
var img = new Image();
|
|
||||||
img.crossOrigin = 'Anonymous';
|
|
||||||
img.onload = function() {
|
|
||||||
onSuccess(parts[1], mime, this.width, this.height, fileNameForMime(file.name, mime));
|
|
||||||
}
|
|
||||||
img.onerror = function(err) {
|
|
||||||
onError("Image format not recognized");
|
|
||||||
}
|
|
||||||
img.src = URL.createObjectURL(file);
|
|
||||||
}, false);
|
|
||||||
reader.readAsDataURL(file);
|
|
||||||
}
|
|
||||||
|
|
||||||
function fileToBase64(file, onSuccess, onError) {
|
|
||||||
var reader = new FileReader();
|
|
||||||
reader.addEventListener('load', function() {
|
|
||||||
onSuccess(file.type, reader.result.split(',')[1], file.name);
|
|
||||||
});
|
|
||||||
reader.readAsDataURL(file);
|
|
||||||
}
|
|
||||||
|
|
||||||
// File pasted from the clipboard. It's either an inline image or a file attachment.
|
|
||||||
// FIXME: handle large files out of band.
|
|
||||||
function filePasted(event, onImageSuccess, onAttachmentSuccess, onError) {
|
|
||||||
var items = (event.clipboardData || event.originalEvent.clipboardData || {}).items;
|
|
||||||
for (var i in items) {
|
|
||||||
var item = items[i];
|
|
||||||
if (item.kind === 'file') {
|
|
||||||
var file = item.getAsFile();
|
|
||||||
if (!file) {
|
|
||||||
console.log("Failed to get file object from pasted file item", item.kind, item.type);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (file.type && file.type.split('/')[0] == 'image') {
|
|
||||||
// Handle inline image
|
|
||||||
if (file.size > MAX_INBAND_ATTACHMENT_SIZE || SUPPORTED_IMAGE_FORMATS.indexOf(file.type) < 0) {
|
|
||||||
imageFileScaledToBase64(file, MAX_IMAGE_DIM, MAX_IMAGE_DIM, false, onImageSuccess, onError);
|
|
||||||
} else {
|
|
||||||
imageFileToBase64(file, onImageSuccess, onError);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Handle file attachment
|
|
||||||
fileToBase64(file, onAttachmentSuccess, onError)
|
|
||||||
}
|
|
||||||
// Indicate that the pasted data contains a file.
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// No file found.
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Make shortcut icon appear with a green dot + show unread count in title.
|
|
||||||
function updateFavicon(count) {
|
|
||||||
var oldIcon = document.getElementById('shortcut-icon');
|
|
||||||
if (oldIcon) {
|
|
||||||
var head = document.head || document.getElementsByTagName('head')[0];
|
|
||||||
var newIcon = document.createElement('link');
|
|
||||||
newIcon.type = 'image/png';
|
|
||||||
newIcon.id = 'shortcut-icon';
|
|
||||||
newIcon.rel = 'shortcut icon';
|
|
||||||
newIcon.href = 'img/logo32x32' + (count > 0 ? 'a' : '') + '.png';
|
|
||||||
head.removeChild(oldIcon);
|
|
||||||
head.appendChild(newIcon);
|
|
||||||
}
|
|
||||||
document.title = (count > 0 ? '('+count+') ' : '') + 'Tinode';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get 32 bit integer hash value for a string. Ideally it should produce the same value
|
|
||||||
// as Java's String#hash().
|
|
||||||
function stringHash(value) {
|
|
||||||
var hash = 0;
|
|
||||||
value = '' + value;
|
|
||||||
for (var i = 0; i < value.length; i++) {
|
|
||||||
hash = ((hash << 5) - hash) + value.charCodeAt(i);
|
|
||||||
hash = hash & hash; // Convert to 32bit integer
|
|
||||||
}
|
|
||||||
return hash;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper functions for hash navigation.
|
|
||||||
|
|
||||||
// Parse hash as in http://www.example.com/path#hash as if it were
|
|
||||||
// path and arguments.
|
|
||||||
function parseUrlHash(hash) {
|
|
||||||
// Split path from args, path -> parts[0], args->path[1]
|
|
||||||
let parts = hash.split('?', 2);
|
|
||||||
let params = {};
|
|
||||||
let path = [];
|
|
||||||
if (parts[0]) {
|
|
||||||
path = parts[0].substr(1).split('/');
|
|
||||||
}
|
|
||||||
if (parts[1]) {
|
|
||||||
parts[1].split('&').forEach(function(part) {
|
|
||||||
let item = part.split('=');
|
|
||||||
if (item[0]) {
|
|
||||||
params[decodeURIComponent(item[0])] = decodeURIComponent(item[1]);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return {path: path, params: params};
|
|
||||||
}
|
|
||||||
|
|
||||||
function navigateTo(url) {
|
|
||||||
window.location.hash = url;
|
|
||||||
}
|
|
||||||
|
|
||||||
function composeUrlHash(path, params) {
|
|
||||||
var url = path.join('/');
|
|
||||||
var args = [];
|
|
||||||
for (var key in params) {
|
|
||||||
if (params.hasOwnProperty(key)) {
|
|
||||||
args.push(key + '=' + params[key]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (args.length > 0) {
|
|
||||||
url += '?' + args.join('&');
|
|
||||||
}
|
|
||||||
return url;
|
|
||||||
}
|
|
||||||
|
|
||||||
function addUrlParam(hash, key, value) {
|
|
||||||
var parsed = parseUrlHash(hash);
|
|
||||||
parsed.params[key] = value;
|
|
||||||
return composeUrlHash(parsed.path, parsed.params);
|
|
||||||
}
|
|
||||||
|
|
||||||
function removeUrlParam(hash, key) {
|
|
||||||
var parsed = parseUrlHash(hash);
|
|
||||||
delete parsed.params[key];
|
|
||||||
return composeUrlHash(parsed.path, parsed.params);
|
|
||||||
}
|
|
||||||
function setUrlSidePanel(hash, sidepanel) {
|
|
||||||
var parsed = parseUrlHash(hash);
|
|
||||||
parsed.path[0] = sidepanel;
|
|
||||||
return composeUrlHash(parsed.path, parsed.params);
|
|
||||||
}
|
|
||||||
function setUrlTopic(hash, topic) {
|
|
||||||
var parsed = parseUrlHash(hash);
|
|
||||||
parsed.path[1] = topic;
|
|
||||||
// Close InfoView on topic change.
|
|
||||||
delete parsed.params.info;
|
|
||||||
return composeUrlHash(parsed.path, parsed.params);
|
|
||||||
}
|
|
||||||
// END of hash navigation helper functions
|
|
||||||
|
|
||||||
|
|
||||||
// Detect server address from the URL
|
|
||||||
function detectServerAddress() {
|
|
||||||
let host = DEFAULT_HOST;
|
|
||||||
if (typeof window.location == 'object') {
|
|
||||||
if (window.location.protocol == 'file:' || window.location.hostname == 'localhost') {
|
|
||||||
host = KNOWN_HOSTS.local;
|
|
||||||
} else if (window.location.hostname) {
|
|
||||||
host = window.location.hostname + (window.location.port ? ':' + window.location.port : '');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return host;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Detect if the page is served over HTTPS.
|
|
||||||
function isSecureConnection() {
|
|
||||||
if (typeof window.location == 'object') {
|
|
||||||
return window.location.protocol == 'https:';
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
function isLocalHost() {
|
|
||||||
if (typeof window.location == 'object') {
|
|
||||||
return window.location.hostname == 'localhost';
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create VCard which represents topic 'public' info
|
|
||||||
function vcard(fn, imageDataUrl) {
|
|
||||||
var card = null;
|
|
||||||
|
|
||||||
if ((fn && fn.trim()) || imageDataUrl) {
|
|
||||||
card = {};
|
|
||||||
if (fn) {
|
|
||||||
card.fn = fn.trim();
|
|
||||||
}
|
|
||||||
if (imageDataUrl) {
|
|
||||||
var dataStart = imageDataUrl.indexOf(',');
|
|
||||||
card.photo = {
|
|
||||||
data: imageDataUrl.substring(dataStart+1),
|
|
||||||
type: 'jpg'
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return card;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Deep-shallow compare two arrays: shallow compare each element.
|
|
||||||
function arrayEqual(a, b) {
|
|
||||||
// Compare lengths first.
|
|
||||||
if (a.length != b.length) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
// Order of elements is ignored.
|
|
||||||
a.sort();
|
|
||||||
b.sort();
|
|
||||||
for (var i = 0, l = a.length; i < l; i++) {
|
|
||||||
if (a[i] !== b[i]) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user