From 3b71cdad64138d0c20a36d5e8765b66ab7c9a8c1 Mon Sep 17 00:00:00 2001 From: Yang Wang <yang.wang@savoirfairelinux.com> Date: Thu, 10 Oct 2019 15:34:43 -0400 Subject: [PATCH] chatview: initial commit for chatview folder created in lrc Change-Id: I0b960170ce4a89f162f7a804bdbca229757db0d7 --- messagewebview.cpp | 18 +- messagewebview.h | 2 +- ressources.qrc | 15 +- ring-client-windows.vcxproj | 15 +- ring-client-windows.vcxproj.filters | 19 +- web/chatview.css | 1182 ------------------ web/chatview.html | 64 - web/chatview.js | 1794 --------------------------- web/linkify-html.js | 827 ------------ web/linkify-string.js | 118 -- web/linkify.js | 1271 ------------------- web/qwebchannel.js | 427 ------- 12 files changed, 38 insertions(+), 5714 deletions(-) delete mode 100644 web/chatview.css delete mode 100644 web/chatview.html delete mode 100644 web/chatview.js delete mode 100644 web/linkify-html.js delete mode 100644 web/linkify-string.js delete mode 100644 web/linkify.js delete mode 100644 web/qwebchannel.js diff --git a/messagewebview.cpp b/messagewebview.cpp index 8b5ee1b..71a89e3 100644 --- a/messagewebview.cpp +++ b/messagewebview.cpp @@ -92,6 +92,7 @@ MessageWebView::MessageWebView(QWidget *parent) webChannel_ = new QWebChannel(page()); webChannel_->registerObject(QStringLiteral("jsbridge"), jsBridge_); page()->setWebChannel(webChannel_); + page()->profile()->setHttpUserAgent("jami-windows"); connect(this, &QWebEngineView::renderProcessTerminated, [this](QWebEnginePage::RenderProcessTerminationStatus termStatus, int statusCode) { @@ -239,20 +240,21 @@ void MessageWebView::runJsText() void MessageWebView::buildView() { - auto html = Utils::QByteArrayFromFile(":/web/chatview.html"); - page()->setHtml(html, QUrl(":/web/chatview.html")); + auto html = Utils::QByteArrayFromFile(":/chatview.html"); + page()->setHtml(html, QUrl(":/chatview.html")); connect(this, &QWebEngineView::loadFinished, this, &MessageWebView::slotLoadFinished); } void MessageWebView::slotLoadFinished() { - insertStyleSheet("chatcss", Utils::QByteArrayFromFile(":/web/chatview.css")); - page()->runJavaScript(Utils::QByteArrayFromFile(":/web/linkify.js"), QWebEngineScript::MainWorld); - page()->runJavaScript(Utils::QByteArrayFromFile(":/web/linkify-html.js"), QWebEngineScript::MainWorld); - page()->runJavaScript(Utils::QByteArrayFromFile(":/web/linkify-string.js"), QWebEngineScript::MainWorld); - page()->runJavaScript(Utils::QByteArrayFromFile(":/web/qwebchannel.js"), QWebEngineScript::MainWorld); - page()->runJavaScript(Utils::QByteArrayFromFile(":/web/chatview.js"), QWebEngineScript::MainWorld); + insertStyleSheet("chatcss", Utils::QByteArrayFromFile(":/chatview.css")); + insertStyleSheet("chatwin",Utils::QByteArrayFromFile(":/chatview-windows.css")); + page()->runJavaScript(Utils::QByteArrayFromFile(":/linkify.js"), QWebEngineScript::MainWorld); + page()->runJavaScript(Utils::QByteArrayFromFile(":/linkify-html.js"), QWebEngineScript::MainWorld); + page()->runJavaScript(Utils::QByteArrayFromFile(":/linkify-string.js"), QWebEngineScript::MainWorld); + page()->runJavaScript(Utils::QByteArrayFromFile(":/qwebchannel.js"), QWebEngineScript::MainWorld); + page()->runJavaScript(Utils::QByteArrayFromFile(":/chatview.js"), QWebEngineScript::MainWorld); } void diff --git a/messagewebview.h b/messagewebview.h index 1dc8095..84691d4 100644 --- a/messagewebview.h +++ b/messagewebview.h @@ -1,5 +1,5 @@ /*************************************************************************** - * Copyright (C) 2019-2019 by Savoir-faire Linux * + * Copyright (C) 2019-2019 by Savoir-faire Linux * * Author: Andreas Traczyk <andreas.traczyk@savoirfairelinux.com> * * * * This program is free software; you can redistribute it and/or modify * diff --git a/ressources.qrc b/ressources.qrc index bf74d10..31b5bf0 100644 --- a/ressources.qrc +++ b/ressources.qrc @@ -67,13 +67,14 @@ <file>images/icons/round-remove_circle-24px.svg</file> <file>images/icons/round-settings-24px.svg</file> <file>images/icons/round-undo-24px.svg</file> - <file>web/chatview.css</file> - <file>web/chatview.html</file> - <file>web/chatview.js</file> - <file>web/linkify.js</file> - <file>web/linkify-html.js</file> - <file>web/linkify-string.js</file> - <file>web/qwebchannel.js</file> + <file alias="chatview.css">../lrc/src/web-chatview/chatview.css</file> + <file alias="chatview.html">../lrc/src/web-chatview/chatview.html</file> + <file alias="chatview.js">../lrc/src/web-chatview/chatview.js</file> + <file alias="linkify.js">../lrc/src/web-chatview/linkify.js</file> + <file alias="linkify-html.js">../lrc/src/web-chatview/linkify-html.js</file> + <file alias="linkify-string.js">../lrc/src/web-chatview/linkify-string.js</file> + <file alias="qwebchannel.js">../lrc/src/web-chatview/qwebchannel.js</file> + <file alias="chatview-windows.css">../lrc/src/web-chatview/chatview-windows.css</file> <file>images/icons/round-check_circle-24px.svg</file> <file>images/icons/round-error-24px.svg</file> <file>images/icons/round-save_alt-24px.svg</file> diff --git a/ring-client-windows.vcxproj b/ring-client-windows.vcxproj index bd8a47e..3f1ae4c 100644 --- a/ring-client-windows.vcxproj +++ b/ring-client-windows.vcxproj @@ -786,6 +786,14 @@ del /s /q $(OutDir)\Jami.exp</Command> <ClInclude Include="ui_videoview.h" /> </ItemGroup> <ItemGroup> + <None Include="..\lrc\src\web-chatview\chatview-windows.css" /> + <None Include="..\lrc\src\web-chatview\chatview.css" /> + <None Include="..\lrc\src\web-chatview\chatview.html" /> + <None Include="..\lrc\src\web-chatview\chatview.js" /> + <None Include="..\lrc\src\web-chatview\linkify-html.js" /> + <None Include="..\lrc\src\web-chatview\linkify-string.js" /> + <None Include="..\lrc\src\web-chatview\linkify.js" /> + <None Include="..\lrc\src\web-chatview\qwebchannel.js" /> <None Include="translations\ring_client_windows.ts" /> <None Include="translations\ring_client_windows_ar.ts" /> <None Include="translations\ring_client_windows_bg.ts" /> @@ -838,13 +846,6 @@ del /s /q $(OutDir)\Jami.exp</Command> <None Include="translations\ring_client_windows_zh.ts" /> <None Include="translations\ring_client_windows_zh_CN.ts" /> <None Include="translations\ring_client_windows_zh_TW.ts" /> - <None Include="web\chatview.css" /> - <None Include="web\chatview.html" /> - <None Include="web\chatview.js" /> - <None Include="web\linkify-html.js" /> - <None Include="web\linkify-string.js" /> - <None Include="web\linkify.js" /> - <None Include="web\qwebchannel.js" /> </ItemGroup> <ItemGroup> <QtUic Include="aboutdialog.ui"> diff --git a/ring-client-windows.vcxproj.filters b/ring-client-windows.vcxproj.filters index 906dc2f..a729d4e 100644 --- a/ring-client-windows.vcxproj.filters +++ b/ring-client-windows.vcxproj.filters @@ -618,25 +618,28 @@ <None Include="translations\ring_client_windows_zh_TW.ts"> <Filter>Translation Files</Filter> </None> - <None Include="web\chatview.css"> + <None Include="..\lrc\src\web-chatview\chatview.css"> <Filter>Resource Files\web</Filter> </None> - <None Include="web\chatview.html"> + <None Include="..\lrc\src\web-chatview\chatview.html"> <Filter>Resource Files\web</Filter> </None> - <None Include="web\linkify.js"> + <None Include="..\lrc\src\web-chatview\chatview.js"> <Filter>Resource Files\web</Filter> </None> - <None Include="web\linkify-html.js"> + <None Include="..\lrc\src\web-chatview\chatview-windows.css"> <Filter>Resource Files\web</Filter> </None> - <None Include="web\linkify-string.js"> + <None Include="..\lrc\src\web-chatview\linkify.js"> <Filter>Resource Files\web</Filter> </None> - <None Include="web\chatview.js"> + <None Include="..\lrc\src\web-chatview\linkify-html.js"> <Filter>Resource Files\web</Filter> </None> - <None Include="web\qwebchannel.js"> + <None Include="..\lrc\src\web-chatview\linkify-string.js"> + <Filter>Resource Files\web</Filter> + </None> + <None Include="..\lrc\src\web-chatview\qwebchannel.js"> <Filter>Resource Files\web</Filter> </None> </ItemGroup> @@ -876,4 +879,4 @@ <Filter>Resource Files</Filter> </Image> </ItemGroup> -</Project> \ No newline at end of file +</Project> diff --git a/web/chatview.css b/web/chatview.css deleted file mode 100644 index 6866114..0000000 --- a/web/chatview.css +++ /dev/null @@ -1,1182 +0,0 @@ -/** Variable and font definitions */ - -:root { - /* color definitions */ - --jami-light-blue: rgba(59, 193, 211, 0.3); - --jami-dark-blue: #004e86; - --jami-green: #219d55; - --jami-green-hover: #1f8b4c; - --jami-red: #dc2719; - --jami-red-hover: #b02e2c; - /* main properties */ - --bg-color: #ffffff; - /* navbar properties */ - --navbar-height: 40px; - --navbar-padding-top: 8px; - --navbar-padding-bottom: var(--navbar-padding-top); - /* message bar properties */ - --textarea-max-height: 150px; - --placeholder-text-color: #d3d3d3; - /* button properties */ - --action-icon-color: #00; - --deactivated-icon-color: #bebebe; - --action-icon-hover-color: #ededed; - --action-critical-icon-hover-color: rgba(211, 77, 59, 0.3); /* complementary color of jami light blue */ - --action-critical-icon-press-color: rgba(211, 77, 59, 0.5); - --action-critical-icon-color: #4E1300; - --non-action-icon-color: #212121; - --action-icon-press-color: rgba(212, 212, 212, 1.0); - --invite-hover-color: white; - /* hairline properties */ - --hairline-color: #f0f0f0; - --hairline-thickness: 2px; -} - -@font-face { - font-family: emoji; - /* Fonts for text outside emoji blocks */ - src: local('Open sans'), local('Helvetica'), local('Segoe UI'), local('sans-serif'); -} - -@font-face { - font-family: emoji; - src: local('Noto Color Emoji'), local('Android Emoji'), local('Twitter Color Emoji'); - /* Emoji unicode blocks */ - unicode-range: U+1F300-1F5FF, U+1F600-1F64F, U+1F680-1F6FF, U+2600-26FF; -} - -/** Body */ - -body { - --messagebar-size: 57px; - margin: 0; - overflow: hidden; - background-color: var(--bg-color); - padding-bottom: var(--messagebar-size); - /* disable selection highlight because it looks very bad */ - -webkit-user-select: text; - -} - -::-webkit-scrollbar-track { - background-color: var(--bg-color); -} - -::-webkit-scrollbar { - width: 8px; - background-color: var(--bg-color); -} - -::-webkit-scrollbar-thumb { - background-color: #f0f0f0; -} - -/** Navbar */ - -.navbar-wrapper { - /* on top, over everything and full width */ - position: fixed; - left: 0; - right: 0; - z-index: 500; - top: 0; -} - -#navbar { - background-color: var(--bg-color); - padding-right: 8px; - padding-left: 8px; - padding-top: var(--navbar-padding-top); - padding-bottom: var(--navbar-padding-bottom); - height: var(--navbar-height); - overflow: hidden; - align-items: center; - /* takes whole width */ - left: 0; - right: 0; - /* hairline */ - border-bottom: var(--hairline-thickness) solid var(--hairline-color); - display: flex; -} - -.hiddenState { - /* Used to hide navbar and message bar */ - display: none !important; -} - -.svgicon { - display: block; - margin: auto; - height: 70%; -} - -.nav-button { - width: 30px; - height: 30px; - margin: 8px; - padding: 2px; - display: flex; - cursor: pointer; - align-self: center; - border-radius: 16px; -} - - .nav-button.deactivated { - width: 30px; - height: 30px; - margin: 8px; - padding: 2px; - align-self: center; - display: flex; - border-radius: 16px; - cursor: auto; - } - -.action-button svg { - fill: var(--action-icon-color); -} - -.action-button.deactivated svg { - fill: var(--deactivated-icon-color); -} - -.non-action-button svg { - fill: var(--non-action-icon-color); -} - -.non-action-button:hover, .action-button:hover { - background: var(--action-icon-hover-color); -} - -.non-action-button:active, .action-button:active { - background: var(--action-icon-press-color); -} - -.action-button.deactivated:hover, .action-button.deactivated:active { - background: none; -} - -.action-critical-button svg { - fill: var(--action-critical-icon-color); -} - -.action-critical-button:hover { - background: var(--action-critical-icon-hover-color); -} - -.action-critical-button:active { - background: var(--action-critical-icon-press-color); -} - -#callButtons { - display: flex; -} - -#navbar #addBannedContactButton, #navbar #addToConversationsButton { - display: none; -} - -#navbar.onBannedState #addToConvButton, #navbar.onBannedState #callButtons, #navbar.onBannedState #addToConversationsButton { - display: none; -} - -#navbar.onBannedState #addBannedContactButton { - display: flex; -} - -/** Invitation bar */ - -#invitation { - visibility: hidden; - background: var(--bg-color); - position: absolute; - width: 100%; - padding-bottom: 10px; - - /* hairline */ - border-bottom: var(--hairline-thickness) solid var(--hairline-color); -} - - #invitation #invite_header { - margin: 10px; - list-style: none; - display: flex; - align-items: center; - justify-content: center; - /* enable selection (it is globally disabled in the body) */ - -webkit-user-select: auto; - } - -#invitation .sender_image { - width: 50px; - height: 50px; -} - -#invitation #actions { - margin-right: auto; - margin-left: auto; - list-style: none; - display: flex; - align-items: center; - justify-content: center; - - /* enable selection (it is globally disabled in the body) */ - -webkit-user-select: auto; -} - -#invitation #actions div { - display: flex; - align-items: center; - justify-content: center; - margin-right: 5px; - margin-left: 5px; -} - - #invitation #text { - text-align: center; - font: 11pt sans-serif; - - /* enable selection (it is globally disabled in the body) */ - -webkit-user-select: auto; - } - -#invite_contact_name { - font-weight: 500; -} - -/** Messaging bar */ -#sendMessage { - background-color: var(--bg-color); - display: flex; - overflow: hidden; - padding: 4px; - align-items: center; - position: fixed; - left: 0; - right: 0; - z-index: 500; - bottom: 0; - /* hairline */ - border-top: var(--hairline-thickness) solid var(--hairline-color); -} - -#message { - font-family: emoji; - flex: 1; - background-color: var(--bg-color); - border: 0; - overflow-y: scroll; - color: black; - max-height: var(--textarea-max-height); - margin-right: 10px; - resize: none; - /* enable selection (it is globally disabled in the body) */ - -webkit-user-select: auto; -} - - #message:focus, - #message.focus { - outline: none; - } - -#container[disabled] { - background-color: #ccc; -} - -input[placeholder], [placeholder], *[placeholder] { - color: var(--placeholder-text-color); -} - -/** Main chat view */ - -#lazyloading-icon { - margin: auto; - margin-bottom: 10px; - margin-top: 5px; - vertical-align: center; - width: 30px; - display: flex; -} - -#container { - position: relative; - height: 100%; - width: 100%; - margin: 0; - padding: 0; - display: flex; - flex-direction: column; - /* When there are not enough messages to occupy full height of the - container, make sure they are displayed at the bottom, not at the - top. */ - justify-content: flex-end; -} - -a:link { - text-decoration: none; - color: #0e649b; - transition: all 0.2s ease-in-out; - border-bottom: dotted 1px; -} - -a:hover { - border: 0; -} - -#back_to_bottom_button_container { - position: absolute; - z-index: 1; - display: flex; - justify-content: center; - width: 100%; - height: 4em; - pointer-events: none; -} - -#file_image_send_container { - visibility: hidden; - position: fixed; - bottom: var(--messagebar-size); - z-index: 1; - display: flex; - justify-content: flex-start; - left: 0; - right: 0; - height: 8em; - /*border-top-left-radius: 25px; - border-top-right-radius: 25px;*/ - border: 2px solid lightgray; - padding: 20px; - border-bottom: none; - background-color: #cfdbdd; - overflow-x: overlay; -} - - #file_image_send_container::-webkit-scrollbar{ - height: 10px; - } - - #file_image_send_container::after { - /*Create the margins with pseudo-elements*/ - /*to solve overflow:scroll and The Right Padding Problem*/ - content: ' '; - min-width: 20px; - } - -.img_wrapper { - position: relative; - max-width: 65px; - min-width: 65px; - max-height: 80px; - border: 3px solid rgba(255,255,255,0); - padding: 30px; - border-radius: 20px; - background-color: #cfdbdd; - display: flex; - justify-content: center; - align-items: center; - margin: 5px; -} - -/* Make the image responsive */ - .img_wrapper img { - height: 118px; - min-width: 131px; - object-fit: cover; - border-radius: 20px; - } - -/* Style the button and place it at the top right corner of the image */ - .img_wrapper .btn { - position: absolute; - color: #fff; - border: 1px solid #AEAEAE; - border-radius: 40%; - background: rgba(96,95,97,0.5); - font-size: 10px; - font-weight: bold; - top: -5%; - right: -5%; - } - - .img_wrapper .btn:hover { - background-color: lightgray; - } - - .img_wrapper .btn:focus { - outline: none; - color: black; - } - -.file_wrapper { - position: relative; - max-width: 65px; - min-width: 65px; - max-height: 80px; - border: 3px solid rgba(255,255,255,0); - padding: 30px; - border-radius: 20px; - background-color: white; - display: flex; - justify-content: flex-start; - align-items: center; - margin: 5px; - font-family: sans-serif; -} - /* Style the button and place it at the top right corner of the image */ - .file_wrapper .btn { - position: absolute; - color: #fff; - border: 1px solid #AEAEAE; - border-radius: 40%; - background: rgba(96,95,97,0.5); - font-size: 10px; - font-weight: bold; - top: -5%; - right: -5%; - } - - .file_wrapper .btn:hover { - background-color: lightgray; - } - - .file_wrapper .btn:focus { - outline: none; - color: black; - } - - .file_wrapper .svg-icon { - position: absolute; - max-width: 2em; - max-height: 25px; - margin-right: 2em; - top: 8%; - left: 1px; - } - - .file_wrapper .svg-icon path, - .file_wrapper .svg-icon polygon, - .file_wrapper .svg-icon rect { - fill: #000000; - } - - .file_wrapper .svg-icon circle { - stroke: #4691f6; - stroke-width: 1; - } - .file_wrapper .fileinfo { - position: absolute; - top: 30%; - left: 7%; - } - -#back_to_bottom_button { - visibility: hidden; - margin: auto; - font: 0.875em emoji; - text-align: center; - width: 10em; - border-radius: 2em; - background-color: var(--jami-dark-blue); - color: #fff; - padding: 0.5em; - box-shadow: 2px 2px 4px black; - opacity: 1; - overflow: hidden; - white-space: nowrap; - transition: opacity .5s ease, width .2s ease, color .1s ease .2s; - pointer-events: all; -} - - #back_to_bottom_button:hover { - cursor: pointer; - } - - #back_to_bottom_button.fade { - opacity: 0; - width: 1em; - color: transparent; - transition: .2s opacity ease .1s, .2s width ease .1s, color .1s ease; - } - #back_to_bottom_button.fade:hover { - cursor: context-menu; - } - -#messages { - position: relative; - z-index: 0; - width: 100%; - overflow: hidden; - height: auto; - padding-top: 0.5em; - opacity: 1; - transition: 0.5s opacity; -} - - #messages.fade { - opacity: 0; - transition: none; - } - - #messages:hover { - overflow-y: overlay; - } - -.last_message { - /* The last message gets a bigger bottom padding so that it is not - "glued" to the message bar. */ - padding-bottom: 1.5em !important; - margin-top: 4px; -} - -/* General messages */ - -.internal_mes_wrapper { - max-width: 70%; - margin: 0px 0 0 0; - display: flex; - flex-direction: column; - /* If a message is smaller (in width) than the timestamp, do not fill - full width and pack message at the left. */ - align-items: flex-start; - align-content: flex-start; -} - -.message_out > .internal_mes_wrapper { - /* If message is in the outgoing direction, pack at the right. */ - align-items: flex-end; - align-content: flex-end; -} - -.message_wrapper { - max-width: calc(100% - 2em); - border-radius: 10px; - padding: 0.5em 1em 0.5em 1em; - position: relative; - display: flex; - flex-direction: row; -} - -.message_type_data_transfer .message_wrapper { - display: flex; - flex-direction: column; - padding: 0; - width: 450px; - max-width: none; -} - -.transfer_info_wrapper { - display: flex; - flex-direction: row; -} - -.message { - font: 0.875em emoji; - margin: 0; - display: flex; - justify-content: flex-start; - align-items: top; - overflow: hidden; - /* enable selection (it is globally disabled in the body) */ - -webkit-user-select: auto; -} - -.message_in { - padding-left: 25%; -} - -.message_out { - padding-right: 25%; - /* Message sent by the user should be displayed at the right side of - the screen. */ - flex-direction: row-reverse; -} - -.message_delivery_status { - margin: 10px 10px; - color: #A0A0A0; -} - -.message_sender { - display: none; -} - -.sender_image, .invite_sender_image { - border-radius: 50%; - margin: 0px 10px 0px 10px; -} - -div.last_message > span { - margin: 0px 10px 0px 10px; -} - -.message_out .message_wrapper { - border-top-right-radius: 0; - transform-origin: top right; -} - -.message_in .message_wrapper { - border-top-left-radius: 0; - transform-origin: top left; -} - -.message_out .message_wrapper { - background-color: #cfd8dc; -} - -.message_in .message_wrapper { - background-color: #cfebf5; -} - -.message_in .sender_image, -.message_out .sender_image { - visibility: hidden; -} - -.message_in.last_of_sequence .sender_image, -.message_in.single_message .sender_image { - visibility: visible; -} - -.message_in.last_of_sequence .sender_image { - margin-top: 2px; -} - -.message_in.middle_of_sequence .sender_image { - margin-top: 0px; -} - -.generated_message.message_in .message_wrapper, -.generated_message.message_out .message_wrapper { - background-color: transparent !important; - border-radius: 0px !important; -} - -.single_message.message_in .message_wrapper, -.single_message.message_out .message_wrapper { - border-radius: 20px 20px 20px 20px !important; -} - -.last_of_sequence.message_in .message_wrapper { - border-radius: 4px 20px 20px 20px; -} - -.first_of_sequence.message_in .message_wrapper { - border-radius: 20px 20px 20px 4px; -} - -.middle_of_sequence.message_in .message_wrapper { - border-radius: 4px 20px 20px 5px; -} - -.last_of_sequence.message_out .message_wrapper { - border-radius: 20px 4px 20px 20px; -} - -.first_of_sequence.message_out .message_wrapper { - border-radius: 20px 20px 4px 20px; -} - -.middle_of_sequence.message_out .message_wrapper { - border-radius: 20px 5px 4px 20px; -} - -.middle_of_sequence .internal_mes_wrapper, -.last_of_sequence .internal_mes_wrapper, -.last_message .internal_mes_wrapper { - margin: 0px 0 0 0 !important; -} - -.message_out .sender_image { - margin: 8px; -} - -.first_of_sequence.message_out .internal_mes_wrapper, -.single_message.message_out .internal_mes_wrapper { - margin: 0px 0 0 0 !important; -} - -@-webkit-keyframes fade-in { - from { - opacity: 0; - } - - to { - opacity: 1; - } -} - -.sender_image_cell { - vertical-align: bottom; - min-width: 16px; -} - -.message_in .sender_image_cell { - min-width: 56px; -} - -.dummy_cell { - padding: 0; -} - -.timestamp_cell { - padding: 0; - max-width: 0px; - overflow: visible; - white-space: nowrap; -} - -.timestamp_cell_out { - padding: 0; - text-align: right; - direction: rtl; - max-width: 0px; - overflow: visible; - white-space: nowrap; -} - -table { - border-collapse: collapse; - border-spacing: 0 0px; - margin: 0; - padding: 0; -} - -.timestamp { - display: inline-flex; - justify-content: flex-start; - align-self: stretch; - color: #333; - font-size: 10px; - padding: 5px; -} - -.timestamp_out { - flex-direction: row-reverse; -} - -.timestamp_action { - margin: auto; - padding: 0; - vertical-align: center; - opacity: 0; - transition: visibility 0.3s linear, opacity 0.3s linear; -} - -.message_type_contact .message_wrapper:hover .timestamp_action, -.message_type_call .message_wrapper:hover .timestamp_action { - opacity: 1; -} - -/* Ellipsis - dropdown menu */ - -input[type=checkbox] { - display: none; -} - -.menu_interaction { - margin: 5px; - padding: 10px; - padding-top: 0; - opacity: 0; - height: 20px; - transition: visibility 0.3s linear, opacity 0.3s linear; -} - -.message_type_contact .menu_interaction { - display: none; - visibility: hidden; -} - -.message_type_call .menu_interaction { - margin: auto; - padding: 0; - vertical-align: center; -} - - .message_type_call .menu_interaction .dropdown { - margin-top: -17px; - } - -.message:hover:not(.message_type_contact) .menu_interaction { - display: block; - opacity: 1; -} - -.dropdown { - display: none; - z-index: 1; - position: absolute; - background-color: #fff; - padding-top: 3px; - padding-bottom: 3px; -} - - .dropdown div { - color: #111; - padding: 10px; - } - - .dropdown div:hover { - background-color: #ddd; - } - -.showmenu:checked ~ .dropdown { - display: block; -} - -.menuoption { - user-select: none; - cursor: pointer; -} - -/* Buttons */ - -.flat-button { - border: 0; - border-radius: 5px; - transition: all 0.3s ease; - color: #f9f9f9; - padding: 10px 20px 10px 20px; - vertical-align: middle; - cursor: pointer; - flex: 1; - padding: 0; -} - -.left_buttons { - align-self: center; - max-width: 90px; - padding-left: 1em; -} - -/* Status */ - -.status_circle { - fill: #A0A0A0; - -webkit-animation: circle-dance; - -webkit-animation-duration: 0.8s; - -webkit-animation-iteration-count: infinite; - -webkit-animation-direction: alternate; - -webkit-animation-timing-function: ease-in-out; -} - -.anim-first { - -webkit-animation-delay: 0.7s; -} - -.anim-second { - -webkit-animation-delay: 0.9s; -} - -.anim-third { - -webkit-animation-delay: 1.1s; -} - -@-webkit-keyframes circle-dance { - 0%,50% { - -webkit-transform: translateY(0); - fill: #A0A0A0; - } - - 100% { - -webkit-transform: translateY(-8px); - fill: #000; - } -} - -.status-x { - stroke-dasharray: 12; -} - -/* Contact + Call interactions */ -.message_type_contact .message_wrapper, -.message_type_call .message_wrapper { - width: auto; - margin: auto; - display: flex; - flex-wrap: wrap; - background-color: var(--bg-color); - padding: 0; -} - - .message_type_contact .message_wrapper:before, - .message_type_call .message_wrapper:before { - display: none; - } - -.message_type_contact .text, -.message_type_call .text { - align-self: center; - font-size: 1.2em; - padding: 1em; -} - -/* file interactions */ - -.message_type_data_transfer .internal_mes_wrapper { - padding: 0; - display: flex; - flex-wrap: wrap; -} - -.invite-btn-red { - transition: background-color 0.5s ease; -} - -.invite-btn-red:hover { - background: var(--jami-red); -} - -.invite-btn-red svg { - fill: var(--jami-red); - transition: fill 0.5s ease; -} - -.invite-btn-red:hover svg { - fill: var(--invite-hover-color); -} - -.invite-btn-green { - transition: background-color 0.5s ease; -} - -.invite-btn-green:hover { - background: var(--jami-green); -} - -.invite-btn-green svg { - fill: var(--jami-green); - transition: fill 0.5s ease; -} - -.invite-btn-green:hover svg { - fill: var(--invite-hover-color); -} - -.accept, .refuse { - border-radius: 50%; - cursor: pointer; -} - -.accept svg, -.refuse svg { - padding: 8px; - width: 24px; - height: 24px; -} - -.accept { - fill: var(--jami-green); -} - -.accept:hover { - fill: var(--invite-hover-color); - background: var(--jami-green-hover); -} - -.refuse { - fill: var(--jami-red); -} - -.refuse:hover { - fill: var(--invite-hover-color); - background: var(--jami-red-hover); -} - -.message_type_data_transfer .text { - text-align: left; - align-self: left; - padding: 1em; -} - -.truncate-ellipsis { - display: table; - table-layout: fixed; - width: 100%; - white-space: nowrap; -} - - .truncate-ellipsis > * { - display: table-cell; - overflow: hidden; - text-overflow: ellipsis; - } - -.message_type_data_transfer .filename { - cursor: pointer; - font-size: 1.1em; -} - -.message_type_data_transfer .informations { - color: #555; - font-size: 0.8em; -} - -.message_progress_bar { - width: 100%; - height: 1em; - position: relative; - overflow: hidden; - background-color: #eee; - border-radius: 0; - box-shadow: 0 2px 5px rgba(0, 0, 0, 0.25) inset; -} - - .message_progress_bar > span { - display: inline; - height: 100%; - background-color: #01a2b8; - position: absolute; - overflow: hidden; - } - -/* text interactions */ - -.message_type_text .internal_mes_wrapper { - padding: 0px; -} - -.message_text { - word-break: break-all; - word-wrap: hyphenate; - max-width: 100%; -} - - .message_text pre { - display: inline; - } - -pre { - font: inherit; - font-family: inherit; - font-size: inherit; - font-style: inherit; - font-variant: inherit; - font-weight: inherit; - margin: 0; - padding: 0; - white-space: pre-wrap; -} - -/* Media interactions */ -.media_wrapper img { - max-width: 800px; - max-height: 700px; - margin: 2px 0 2px 0; - border-radius: 10px; -} - -.playVideo { - background-color: rgba(0, 0, 0, 0.6); - height: 50px; - width: 50px; - border-radius: 5px; - float: right; - position: absolute; - top: calc(50% - 25px); - left: calc(50% - 25px); - z-index: 3; -} - -.containerVideo { - width: 100%; - position: relative; -} - -.playVideo svg { - height: 40px; - width: 40px; - margin: 5px; -} - -/* Text interaction */ -.failure, -.sending { - margin: 10px 10px; - color: #A0A0A0; -} - -/* classic screens */ -@media screen and (max-width: 1920px), screen and (max-height: 1080px) { - .message_in { - padding-left: 15%; - } - - .message_out { - padding-right: 15%; - } - - .internal_mes_wrapper { - max-width: 60%; - } - - .media_wrapper img { - /* It is perfectly fine to specify max-widths in px when the - wrapper's max-width is in %, as long as the max-width in px - doesn't exceed the one in %. */ - max-width: 450px; - max-height: 450px; - } - - .menu_interaction { - margin: 5px; - padding: 2px; - height: 10px; - font-size: 0.7em; - transition: visibility 0.3s linear,opacity 0.3s linear; - } -} - -/* lower resolutions */ -@media screen and (max-width: 1000px), screen and (max-height: 480px) { - .message_in { - padding-left: 0; - } - - .message_out { - padding-right: 0; - } - - .message_type_contact, - .message_type_call { - max-width: 100%; - } - - .internal_mes_wrapper { - max-width: 90%; - } - - /* Media interactions */ - .media_wrapper img { - max-width: 300px; - max-height: 300px; - } -} - -@media screen and (max-width: 550px) { - .message_type_data_transfer .message_wrapper { - width: 250px; - } -} - -/* Special case */ -@media screen and (max-width: 350px) { - .sender_image { - display: none; - } - - /* File interactions */ - .message_type_data_transfer .left_buttons { - max-width: 100%; - } - - .message_type_data_transfer .text { - max-width: 100%; - padding-left: 0; - } - - .message_type_data_transfer .message_wrapper { - width: 200px; - } -} diff --git a/web/chatview.html b/web/chatview.html deleted file mode 100644 index 5263398..0000000 --- a/web/chatview.html +++ /dev/null @@ -1,64 +0,0 @@ -<html> -<!-- Empty head might be needed for setSenderImage --> -<head> - <meta name="viewport" content="width=device-width, initial-scale=1" /> - <meta charset="utf-8"> - <!--<link rel="stylesheet" href="chatview.css"> - <script type="text/javascript" src="linkify.js"></script> - <script type="text/javascript" src="linkify-html.js"></script> - <script type="text/javascript" src="linkify-string.js"></script> - <script type="text/javascript" src="qwebchannel.js"></script>--> -</head> -<body> - <div class="navbar-wrapper"> - <div id="invitation"> - <div id="invite_header"> - <span id="invite_image"></span> - <div id="text"></div> - </div> - <div id="actions"> - <div id="accept-btn" class="nav-button action-button invite-btn-green" onclick="acceptInvitation()" title="Accept"> - <svg height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg"> - <path d="M0 0h24v24H0z" fill="none" /> - <path d="M15 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm-9-2V7H4v3H1v2h3v3h2v-3h3v-2H6zm9 4c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z" /> - </svg> - </div> - <div id="refuse-btn" class="nav-button action-button invite-btn-red" onclick="refuseInvitation()" title="Refuse"> - <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"> - <path fill="none" d="M0 0h24v24H0V0z" /> - <path d="M18.3 5.71c-.39-.39-1.02-.39-1.41 0L12 10.59 7.11 5.7c-.39-.39-1.02-.39-1.41 0-.39.39-.39 1.02 0 1.41L10.59 12 5.7 16.89c-.39.39-.39 1.02 0 1.41.39.39 1.02.39 1.41 0L12 13.41l4.89 4.89c.39.39 1.02.39 1.41 0 .39-.39.39-1.02 0-1.41L13.41 12l4.89-4.89c.38-.38.38-1.02 0-1.4z" /> - </svg> - </div> - <div id="block-btn" class="nav-button action-button invite-btn-red" onclick="blockConversation()" title="Block"> - <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"> - <path d="M0 0h24v24H0z" fill="none" /> - <path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zM4 12c0-4.42 3.58-8 8-8 1.85 0 3.55.63 4.9 1.69L5.69 16.9C4.63 15.55 4 13.85 4 12zm8 8c-1.85 0-3.55-.63-4.9-1.69L18.31 7.1C19.37 8.45 20 10.15 20 12c0 4.42-3.58 8-8 8z" /> - </svg> - </div> - </div> - </div> - </div> - <div id="container"> - <div id="messages" onscroll="onScrolled()"></div> - <div id="back_to_bottom_button_container"> - <div id="back_to_bottom_button" onclick="back_to_bottom()">Jump to latest ▼</div> - </div> - <div id="file_image_send_container"></div> - <div id="sendMessage"> - <div class="nav-button action-button" onclick="selectFile()" title="Send File"> - <svg class="svgicon" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"> - <path d="M16.5 6v11.5c0 2.21-1.79 4-4 4s-4-1.79-4-4V5c0-1.38 1.12-2.5 2.5-2.5s2.5 1.12 2.5 2.5v10.5c0 .55-.45 1-1 1s-1-.45-1-1V6H10v9.5c0 1.38 1.12 2.5 2.5 2.5s2.5-1.12 2.5-2.5V5c0-2.21-1.79-4-4-4S7 2.79 7 5v12.5c0 3.04 2.46 5.5 5.5 5.5s5.5-2.46 5.5-5.5V6h-1.5z" /> - <path d="M0 0h24v24H0z" fill="none" /> - </svg> - </div> - <textarea id="message" autofocus placeholder="Type a message" onkeyup="grow_text_area()" onkeydown="process_messagebar_keydown()" rows="1"></textarea> - <div class="nav-button action-button" onclick="sendMessage(); grow_text_area()" title="Send"> - <svg class="svgicon" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"> - <path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z" /> - <path d="M0 0h24v24H0z" fill="none" /> - </svg> - </div> - </div> - </div> -</body> -</html> diff --git a/web/chatview.js b/web/chatview.js deleted file mode 100644 index a1515f6..0000000 --- a/web/chatview.js +++ /dev/null @@ -1,1794 +0,0 @@ -"use strict" - -/* Constants used at several places*/ -const messageBarPlaceHolder = "Type a message" -/* Constants used at several places*/ -// scrollDetectionThresh represents the number of pixels a user can scroll -// without disabling the automatic go-back-to-bottom when a new message is -// received -const scrollDetectionThresh = 200 -// printHistoryPart loads blocks of messages. Each block contains -// scrollBuffer messages -const scrollBuffer = 20 -// The first time a conversation is loaded, the lazy loading system makes -// sure at least initialScrollBufferFactor screens of messages are loaded -const initialScrollBufferFactor = 3 -// Some signal like the onscrolled signals are debounced so that the their -// assigned function isn't fired too often -const debounceTime = 200 - -/* Buffers */ -// current index in the history buffer -var historyBufferIndex = 0 -// buffer containing the conversation's messages -var historyBuffer = [] - -/* We retrieve refs to the most used navbar and message bar elements for efficiency purposes */ -/* NOTE: always use getElementById when possible, way more efficient */ -const messageBar = document.getElementById("sendMessage") -const messageBarInput = document.getElementById("message") -const invitation = document.getElementById("invitation") -const inviteImage = document.getElementById("invite_image") -const invitationText = document.getElementById("text") -var messages = document.getElementById("messages") -var backToBottomBtn = document.getElementById("back_to_bottom_button") -var sendContainer = document.getElementById("file_image_send_container") - -messageBarInput.onpaste = pasteKeyDetected; - -/* States: allows us to avoid re-doing something if it isn't meaningful */ -var displayLinksEnabled = true -var hoverBackButtonAllowed = true -var hasInvitation = false -var isTemporary = false -var isBanned = false -var isAccountEnabled = true -var isInitialLoading = false -var imagesLoadingCounter = 0 -var canLazyLoad = false; - -/* Set the default target to _self and handle with QWebEnginePage::acceptNavigationRequest */ -var linkifyOptions = { - attributes: null, - className: 'linkified', - defaultProtocol: 'http', - events: null, - format: function (value, type) { - return value; - }, - formatHref: function (href, type) { - return href; - }, - ignoreTags: [], - nl2br: false, - tagName: 'a', - target: { - url: '_self' - }, - validate: true -}; - -new QWebChannel(qt.webChannelTransport, function(channel) { - window.jsbridge = channel.objects.jsbridge; -}); - -function onScrolled_() { - if (!canLazyLoad) - return; - // back to bottom button - if(messages.scrollTop >= messages.scrollHeight - messages.clientHeight - scrollDetectionThresh) { - // fade out - if (!backToBottomBtn.classList.contains('fade')) { - backToBottomBtn.classList.add('fade'); - backToBottomBtn.removeAttribute("onclick"); - } - } else { - // fade in - if (backToBottomBtn.classList.contains('fade')) { - backToBottomBtn.style.visibility = "visible"; - backToBottomBtn.classList.remove('fade'); - backToBottomBtn.onclick = back_to_bottom; - } - } - if (messages.scrollTop == 0 && historyBufferIndex != historyBuffer.length) { - /* At the top and there's something to print */ - printHistoryPart(messages, messages.scrollHeight) - } -} - -const debounce = (fn, time) => { - let timeout - - return function() { - const functionCall = () => fn.apply(this, arguments) - - clearTimeout(timeout) - timeout = setTimeout(functionCall, time) - } -} - -/* exported onScrolled */ -var onScrolled = debounce(onScrolled_, debounceTime) - -/** - * Generic wrapper. Execute passed function keeping scroll position identical. - * - * @param func function to execute - * @param args parameters as array - */ -function exec_keeping_scroll_position(func, args) { - var atEnd = messages.scrollTop >= messages.scrollHeight - messages.clientHeight - scrollDetectionThresh - func(...args) - if (atEnd) { - messages.scrollTop = messages.scrollHeight - } -} - -/** - * Reset scrollbar at a given position. - * @param scroll position at which the scrollbar should be set. - * Here position means the number of pixels scrolled, - * i.e. scroll = 0 resets the scrollbar at the bottom. - */ -function back_to_scroll(scroll) { - messages.scrollTop = messages.scrollHeight - scroll -} - -/** - * Reset scrollbar at bottom. - */ -function back_to_bottom() { - back_to_scroll(0) -} - -/** - * Hide or show invitation. - * - * Invitation is hidden if no contactAlias/invalid alias is passed. - * Otherwise, invitation div is updated. - * - * @param contactAlias - * @param contactId - */ -/* exported showInvitation */ -function showInvitation(contactAlias, contactId) { - if (!contactAlias) { - if (hasInvitation) { - hasInvitation = false - invitation.style.visibility = "" - } - } else { - if (!inviteImage.classList.contains('invite_sender_image')) { - inviteImage.classList.add('invite_sender_image'); - } - const className = `invite_sender_image_${contactId}`.replace(/@/g, "_").replace(/\./g, "_") - if (!inviteImage.classList.contains(className)) { - inviteImage.classList.add(className); - } - hasInvitation = true - invitationText.innerHTML = "<span id='invite_contact_name'>" + contactAlias + "</span> is not in your contacts<br/>" - + "Note: you can automatically accept this invitation by sending a message." - invitation.style.visibility = "visible" - } -} - -/* exported setDisplayLinks */ -function setDisplayLinks(display) { - displayLinksEnabled = display -} - -/** - * This event handler dynamically resizes the message bar depending on the amount of - * text entered, while adjusting the body paddings so that that the message bar doesn't - * overlap messages when it grows. - */ -/* exported grow_text_area */ -function grow_text_area() { - exec_keeping_scroll_position(function(){ - var old_height = window.getComputedStyle(messageBar).height - messageBarInput.style.height = "auto" /* <-- necessary, no clue why */ - messageBarInput.style.height = messageBarInput.scrollHeight + "px" - var new_height = window.getComputedStyle(messageBar).height - - var msgbar_size = window.getComputedStyle(document.body).getPropertyValue("--messagebar-size") - var total_size = parseInt(msgbar_size) + parseInt(new_height) - parseInt(old_height) - - document.body.style.setProperty("--messagebar-size", total_size.toString() + "px") - }, []) -} - -/** - * This event handler processes keydown events from the message bar. When pressed key is - * the enter key, send the message unless shift or control was pressed too. - * - * @param key the pressed key - */ -/* exported process_messagebar_keydown */ -function process_messagebar_keydown(key) { - key = key || event - var map = {} - map[key.keyCode] = key.type == "keydown" - if (key.ctrlKey && map[13]) { - messageBarInput.value += "\n" - } - if (key.ctrlKey || key.shiftKey) { - return true - } - if (map[13]) { - sendMessage() - key.preventDefault() - } - return true -} - -/** - * Disable or enable textarea. - * - * @param isDisabled whether message bar should be enabled or disabled - */ -/* exported disableSendMessage */ -function disableSendMessage(isDisabled) -{ - messageBarInput.disabled = isDisabled -} - -/* - * Update timestamps messages. - */ -function updateView() { - updateTimestamps(messages) -} - -setInterval(updateView, 60000) - -/** - * Transform a date to a string group like "1 hour ago". - * - * @param date - */ -function formatDate(date) { - const seconds = Math.floor((new Date() - date) / 1000) - var interval = Math.floor(seconds / (3600 * 24)) - if (interval > 5) { - return date.toLocaleDateString() - } - if (interval > 1) { - return interval + "\u200E days ago" - } - if (interval === 1) { - return interval + "\u200E day ago" - } - interval = Math.floor(seconds / 3600) - if (interval > 1) { - return interval + "\u200E hours ago" - } - if (interval === 1) { - return interval + "\u200E hour ago" - } - interval = Math.floor(seconds / 60) - if (interval > 1) { - return interval + "\u200E minutes ago" - } - return "just now" -} - -/** - * Send content of message bar - */ -function sendMessage() -{ - //send image in sendContainer - var data_to_send = sendContainer.innerHTML; - var imgSrcExtract = new RegExp('<img src="(.*?)">', 'g'); - var fileSrcExtract = new RegExp('<div class="file_wrapper" data-path="(.*?)">', 'g'); - - var img_src; - while ((img_src = imgSrcExtract.exec(data_to_send)) !== null ) { - window.jsbridge.sendImage(img_src[1]); - } - - var file_src; - while ((file_src = fileSrcExtract.exec(data_to_send)) !== null) { - window.jsbridge.sendFile(file_src[1]); - } - - sendContainer.innerHTML = ""; - sendContainer.style.visibility = "hidden"; - reduce_send_container(); - - var message = messageBarInput.value - if (message.length > 0) { - messageBarInput.value = "" - window.jsbridge.sendMessage(message) - } - -} - -/* exported acceptInvitation */ -function acceptInvitation() { - window.jsbridge.acceptInvitation() -} -/* exported refuseInvitation */ -function refuseInvitation() { - window.jsbridge.refuseInvitation() -} -/* exported blockConversation */ -function blockConversation() { - window.jsbridge.blockConversation() -} - -/** - * Convert text to HTML. - */ -function escapeHtml(html) -{ - var text = document.createTextNode(html) - var div = document.createElement("div") - div.appendChild(text) - return div.innerHTML -} - -/** - * Get the youtube video id from a URL. - * @param url - */ -function youtube_id(url) { - const regExp = /^.*(youtu\.be\/|v\/|\/u\/w|embed\/|watch\?v=|&v=)([^#&?]*).*/ - const match = url.match(regExp) - return (match && match[2].length == 11) ? match[2] : null -} - -/** - * Returns HTML message from the message text, cleaned and linkified. - * @param message_text - */ -function getMessageHtml(message_text) -{ - const escaped_message = escapeHtml(message_text) - - var linkified_message = linkifyHtml(escaped_message, linkifyOptions) // eslint-disable-line no-undef - - const textPart = document.createElement("pre") - textPart.innerHTML = linkified_message - - return textPart.outerHTML -} - -/** - * Returns the message status, formatted for display - * @param message_delivery_status - */ -/* exported getMessageDeliveryStatusText */ -function getMessageDeliveryStatusText(message_delivery_status) -{ - var formatted_delivery_status = message_delivery_status - - switch(message_delivery_status) - { - case "sending": - case "ongoing": - formatted_delivery_status = "Sending<svg overflow='visible' viewBox='0 -2 16 14' height='16px' width='16px'><circle class='status_circle anim-first' cx='4' cy='12' r='1'/><circle class='status_circle anim-second' cx='8' cy='12' r='1'/><circle class='status_circle anim-third' cx='12' cy='12' r='1'/></svg>" - break - case "failure": - formatted_delivery_status = "Failure <svg overflow='visible' viewBox='0 -2 16 14' height='16px' width='16px'><path class='status-x x-first' stroke='#AA0000' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' fill='none' d='M4,4 L12,12'/><path class='status-x x-second' stroke='#AA0000' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' fill='none' d='M12,4 L4,12'/></svg>" - break - case "sent": - case "finished": - case "unknown": - case "read": - formatted_delivery_status = "" - break - default: - break - } - - return formatted_delivery_status -} - -/** - * Returns the message date, formatted for display - */ -function getMessageTimestampText(message_timestamp, custom_format) -{ - const date = new Date(1000 * message_timestamp) - if(custom_format) { - return formatDate(date) - } else { - return date.toLocaleString() - } -} - -/** - * Update timestamps. - * @param message_div - */ -function updateTimestamps(messages_div) { - const timestamps = messages_div.getElementsByClassName("timestamp") - for (var c = timestamps.length - 1; c >= 0 ; --c) { - var timestamp = timestamps.item(c) - timestamp.innerHTML = getMessageTimestampText(timestamp.getAttribute("message_timestamp"), true) - } -} - -/** - * Convert a value in filesize - */ -function humanFileSize(bytes) { - var thresh = 1024 - if(Math.abs(bytes) < thresh) { - return bytes + " B" - } - var units = ["kB","MB","GB","TB","PB","EB","ZB","YB"] - var u = -1 - do { - bytes /= thresh - ++u - } while(Math.abs(bytes) >= thresh && u < units.length - 1) - return bytes.toFixed(1)+" "+units[u] -} - -/** - * Hide or show add to conversations/calls whether the account is enabled - * @param accountEnabled true if account is enabled - */ -function hideControls(accountEnabled) { - if (!accountEnabled) { - callButtons.display = "none" - } else { - callButtons.display = "" - } -} - -/** - * Change the value of the progress bar. - * - * @param progress_bar - * @param message_object - */ -function updateProgressBar(progress_bar, message_object) { - var delivery_status = message_object["delivery_status"] - if ("progress" in message_object && !isErrorStatus(delivery_status) && message_object["progress"] !== 100) { - var progress_percent = (100 * message_object["progress"] / message_object["totalSize"]) - if (progress_percent !== 100) - progress_bar.childNodes[0].setAttribute("style", "width: " + progress_percent + "%") - else - progress_bar.setAttribute("style", "display: none") - } else - progress_bar.setAttribute("style", "display: none") -} - -/** - * Check if a status is an error status - * @param - */ -function isErrorStatus(status) { - return (status === "failure" - || status === "awaiting peer timeout" - || status === "canceled" - || status === "unjoinable peer") -} - -/** - * Build a new file interaction - * @param message_id - */ -function fileInteraction(message_id, message_direction) { - var message_wrapper = document.createElement("div") - message_wrapper.setAttribute("class", "message_wrapper") - - var transfer_info_wrapper = document.createElement("div") - transfer_info_wrapper.setAttribute("class", "transfer_info_wrapper") - message_wrapper.appendChild(transfer_info_wrapper) - - /* Buttons at the left for status information or accept/refuse actions. - The text is bold and clickable. */ - var left_buttons = document.createElement("div") - left_buttons.setAttribute("class", "left_buttons") - transfer_info_wrapper.appendChild(left_buttons) - - var full_div = document.createElement("div") - full_div.setAttribute("class", "full") - full_div.style.visibility = "hidden" - full_div.style.display = "none" - - var filename_wrapper = document.createElement("div") - filename_wrapper.setAttribute("class", "truncate-ellipsis") - - var message_text = document.createElement("span") - message_text.setAttribute("class", "filename") - filename_wrapper.appendChild(message_text) - - // And information like size or error message. - var informations_div = document.createElement("div") - informations_div.setAttribute("class", "informations") - - var text_div = document.createElement("div") - text_div.setAttribute("class", "text") - text_div.addEventListener("click", function () { - // ask ring to open the file - const filename = document.querySelector("#message_" + message_id + " .full").innerText - window.jsbridge.openFile(filename); - }) - - text_div.appendChild(filename_wrapper) - text_div.appendChild(full_div) - text_div.appendChild(informations_div) - transfer_info_wrapper.appendChild(text_div) - - // And finally, a progress bar - var message_transfer_progress_bar = document.createElement("span") - message_transfer_progress_bar.setAttribute("class", "message_progress_bar") - - var message_transfer_progress_completion = document.createElement("span") - message_transfer_progress_bar.appendChild(message_transfer_progress_completion) - message_wrapper.appendChild(message_transfer_progress_bar) - - const internal_mes_wrapper = document.createElement("div") - internal_mes_wrapper.setAttribute("class", "internal_mes_wrapper") - - var tbl = buildMsgTable(message_direction); - var msg_cell = tbl.querySelector(".msg_cell"); - msg_cell.appendChild(message_wrapper); - - internal_mes_wrapper.appendChild(tbl); - - return internal_mes_wrapper -} - -function buildMsgTable(message_direction) { - var tbl = document.createElement("table"); - - var row0 = document.createElement("tr"); - var sender_image_cell = document.createElement("td"); - sender_image_cell.setAttribute("class", "sender_image_cell") - var msg_cell = document.createElement("td"); - msg_cell.setAttribute("class", "msg_cell") - - row0.appendChild((message_direction === "in") ? sender_image_cell : msg_cell); - row0.appendChild((message_direction === "in") ? msg_cell : sender_image_cell); - - tbl.appendChild(row0); - - var row1 = document.createElement("tr"); - var dummy_cell = document.createElement("td"); - dummy_cell.setAttribute("class", "dummy_cell") - var timestamp_cell = document.createElement("td"); - timestamp_cell.setAttribute("class", "timestamp_cell") - - row1.appendChild((message_direction === "in") ? dummy_cell : timestamp_cell); - row1.appendChild((message_direction === "in") ? timestamp_cell : dummy_cell); - - tbl.appendChild(row1); - - return tbl; -} - -/** - * Build information text for passed file interaction message object - * - * @param message_object message object containing file interaction info - */ -function buildFileInformationText(message_object) { - var informations_txt = getMessageTimestampText(message_object["timestamp"], true) - if (message_object["totalSize"] && message_object["progress"]) { - if (message_object["delivery_status"] === "finished") { - informations_txt += " - " + humanFileSize(message_object["totalSize"]) - } else { - informations_txt += " - " + humanFileSize(message_object["progress"]) - + " / " + humanFileSize(message_object["totalSize"]) - } - } - - return informations_txt + " - " + message_object["delivery_status"] -} - -/** - * Update a file interaction (icons + filename + status + progress bar) - * - * @param message_div the message to update - * @param message_object new informations - * @param forceTypeToFile - */ -function updateFileInteraction(message_div, message_object, forceTypeToFile = false) { - if (!message_div.querySelector(".informations")) return // media - - var acceptSvg = "<svg height=\"24\" viewBox=\"0 0 24 24\" width=\"24\" xmlns=\"http://www.w3.org/2000/svg\"><path d=\"M0 0h24v24H0z\" fill=\"none\"/><path d=\"M9 16.2L4.8 12l-1.4 1.4L9 19 21 7l-1.4-1.4L9 16.2z\"/></svg>", - refuseSvg = "<svg height=\"24\" viewBox=\"0 0 24 24\" width=\"24\" xmlns=\"http://www.w3.org/2000/svg\"><path d=\"M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z\"/><path d=\"M0 0h24v24H0z\" fill=\"none\"/></svg>", - fileSvg = "<svg fill=\"#000000\" height=\"24\" viewBox=\"0 0 24 24\" width=\"24\" xmlns=\"http://www.w3.org/2000/svg\"><path d=\"M16.5 6v11.5c0 2.21-1.79 4-4 4s-4-1.79-4-4V5c0-1.38 1.12-2.5 2.5-2.5s2.5 1.12 2.5 2.5v10.5c0 .55-.45 1-1 1s-1-.45-1-1V6H10v9.5c0 1.38 1.12 2.5 2.5 2.5s2.5-1.12 2.5-2.5V5c0-2.21-1.79-4-4-4S7 2.79 7 5v12.5c0 3.04 2.46 5.5 5.5 5.5s5.5-2.46 5.5-5.5V6h-1.5z\"/><path d=\"M0 0h24v24H0z\" fill=\"none\"/></svg>", - warningSvg = "<svg fill=\"#000000\" height=\"24\" viewBox=\"0 0 24 24\" width=\"24\" xmlns=\"http://www.w3.org/2000/svg\"><path d=\"M0 0h24v24H0z\" fill=\"none\"/><path d=\"M1 21h22L12 2 1 21zm12-3h-2v-2h2v2zm0-4h-2v-4h2v4z\"/></svg>" - var message_delivery_status = message_object["delivery_status"] - var message_direction = message_object["direction"] - var message_id = message_object["id"] - var message_text = message_object["text"] - - if (isImage(message_text) && message_delivery_status === "finished" && displayLinksEnabled && !forceTypeToFile) { - // Replace the old wrapper by the downloaded image - var old_wrapper = message_div.querySelector(".internal_mes_wrapper") - if (old_wrapper) { - old_wrapper.parentNode.removeChild(old_wrapper) - } - - var errorHandler = function() { - var wrapper = message_div.querySelector(".internal_mes_wrapper") - var message_wrapper = message_div.querySelector(".message_wrapper") - if (message_wrapper) { - message_wrapper.parentNode.removeChild(message_wrapper) - } - - var media_wrapper = message_div.querySelector(".media_wrapper") - if (media_wrapper) { - media_wrapper.parentNode.removeChild(media_wrapper) - } - - var new_interaction = fileInteraction(message_id, message_direction) - var new_message_wrapper = new_interaction.querySelector(".message_wrapper") - wrapper.prepend(new_message_wrapper) - updateFileInteraction(message_div, message_object, true) - } - - var new_wrapper = mediaInteraction(message_id, message_direction, message_text, null, errorHandler) - message_div.insertBefore(new_wrapper, message_div.querySelector(".menu_interaction")) - message_div.querySelector("img").id = message_id - message_div.querySelector("img").msg_obj = message_object - addSenderImage(message_div, message_object["type"], message_object["sender_contact_method"]); - return - } - - // Set informations text - var informations_div = message_div.querySelector(".informations") - informations_div.innerText = buildFileInformationText(message_object) - - // Update flat buttons - var left_buttons = message_div.querySelector(".left_buttons") - left_buttons.innerHTML = "" - if (message_delivery_status === "awaiting peer" || - message_delivery_status === "awaiting host" || - message_delivery_status.indexOf("ongoing") === 0) { - - if (message_direction === "in" && message_delivery_status.indexOf("ongoing") !== 0) { - // add buttons to accept or refuse a call. - var accept_button = document.createElement("div") - accept_button.innerHTML = acceptSvg - accept_button.setAttribute("title", "Accept") - accept_button.setAttribute("class", "flat-button accept") - accept_button.onclick = function() { - window.jsbridge.acceptFile(message_id); - } - left_buttons.appendChild(accept_button) - } - - var refuse_button = document.createElement("div") - refuse_button.innerHTML = refuseSvg - refuse_button.setAttribute("title", "Refuse") - refuse_button.setAttribute("class", "flat-button refuse") - refuse_button.onclick = function() { - window.jsbridge.refuseFile(message_id); - } - left_buttons.appendChild(refuse_button) - } else { - var status_button = document.createElement("div") - var statusFile = fileSvg - if (isErrorStatus(message_delivery_status)) - statusFile = warningSvg - status_button.innerHTML = statusFile - status_button.setAttribute("class", "flat-button") - left_buttons.appendChild(status_button) - } - - message_div.querySelector(".full").innerText = message_text - message_div.querySelector(".filename").innerText = message_text.split("/").pop() - updateProgressBar(message_div.querySelector(".message_progress_bar"), message_object) -} - -/** - * Return if a file is an image - * @param file - */ -function isImage(file) { - return file.toLowerCase().match(/\.(jpeg|jpg|gif|png)$/) !== null -} - -/** - * Return if a file is a youtube video - * @param file - */ -function isVideo(file) { - const availableProtocols = ["http:", "https:"] - const videoHostname = ["youtube.com", "www.youtube.com", "youtu.be"] - const urlParser = document.createElement("a") - urlParser.href = file - return (availableProtocols.includes(urlParser.protocol) && videoHostname.includes(urlParser.hostname)) -} - -/** - * Build a container for passed video thumbnail - * @param linkElt video thumbnail div - */ -function buildVideoContainer(linkElt) { - const containerElt = document.createElement("div") - containerElt.setAttribute("class", "containerVideo") - const playDiv = document.createElement("div") - playDiv.setAttribute("class", "playVideo") - playDiv.innerHTML = "<svg fill=\"#ffffff\" viewBox=\"0 0 24 24\" xmlns=\"http://www.w3.org/2000/svg\">\ - <path d=\"M8 5v14l11-7z\"/>\ - <path d=\"M0 0h24v24H0z\" fill=\"none\"/>\ - </svg>" - linkElt.appendChild(playDiv) - containerElt.appendChild(linkElt) - - return containerElt -} - -/** - * Try to show an image or a video link (youtube for now) - * @param message_id - * @param link to show - * @param ytid if it's a youtube video - * @param errorHandler the new media's onerror field will be set to this function - */ -function mediaInteraction(message_id, message_direction, link, ytid, errorHandler) { - /* TODO promise? - Try to display images. */ - const media_wrapper = document.createElement("div") - media_wrapper.setAttribute("class", "media_wrapper") - const linkElt = document.createElement("a") - linkElt.href = link - linkElt.style.textDecoration = "none" - linkElt.style.border = "none" - const imageElt = document.createElement("img") - - imageElt.src = ytid ? `http://img.youtube.com/vi/${ytid}/0.jpg` : link - - /* Note, here, we don't check the size of the image. - in the future, we can check the content-type and content-length with a request - and maybe disable svg */ - - if (isInitialLoading) { - /* During initial load, make sure the scrollbar stays at the bottom. - Also, the final scrollHeight is only known after the last image was - loaded. We want to display a specific number of messages screens so - we have to set up a callback (on_image_load_finished) which will - check on that and reschedule a new display batch if not enough - messages have been loaded in the DOM. */ - imagesLoadingCounter++ - imageElt.onload = function() { - back_to_bottom() - on_image_load_finished() - } - - if (errorHandler) { - imageElt.onerror = function() { - errorHandler() - back_to_bottom() - on_image_load_finished() - } - } - } else if (messages.scrollTop >= messages.scrollHeight - messages.clientHeight - scrollDetectionThresh) { - /* Keep the scrollbar at the bottom. Images are loaded asynchronously and - the scrollbar position is changed each time an image is loaded and displayed. - In order to make sure the scrollbar stays at the bottom, reset scrollbar - position each time an image was loaded. */ - imageElt.onload = back_to_bottom - - if (errorHandler) { - imageElt.onerror = function() { - errorHandler() - back_to_bottom() - } - } - } else if (errorHandler) { - imageElt.onerror = errorHandler - } - - linkElt.appendChild(imageElt) - - if (ytid) { - media_wrapper.appendChild(buildVideoContainer(linkElt)) - } else { - media_wrapper.appendChild(linkElt) - } - - const internal_mes_wrapper = document.createElement("div") - internal_mes_wrapper.setAttribute("class", "internal_mes_wrapper") - - var tbl = buildMsgTable(message_direction); - var msg_cell = tbl.querySelector(".msg_cell"); - msg_cell.appendChild(media_wrapper); - - internal_mes_wrapper.appendChild(tbl) - - return internal_mes_wrapper -} - -/** - * Build a new text interaction - * @param message_id - * @param htmlText the DOM to show - */ -function textInteraction(message_id, message_direction, htmlText) { - const message_wrapper = document.createElement("div") - message_wrapper.setAttribute("class", "message_wrapper") - var message_text = document.createElement("div") - message_text.setAttribute("class", "message_text") - message_text.innerHTML = htmlText - message_wrapper.appendChild(message_text) - // TODO STATUS - - const internal_mes_wrapper = document.createElement("div") - internal_mes_wrapper.setAttribute("class", "internal_mes_wrapper") - - var tbl = buildMsgTable(message_direction); - var msg_cell = tbl.querySelector(".msg_cell"); - msg_cell.appendChild(message_wrapper); - - internal_mes_wrapper.appendChild(tbl); - - return internal_mes_wrapper -} - -/** - * Update a text interaction (text) - * @param message_div the message to update - * @param delivery_status the status of the message - */ -function updateTextInteraction(message_div, delivery_status) { - if (!message_div.querySelector(".message_text")) return // media - var sending = message_div.querySelector(".sending") - switch(delivery_status) - { - case "ongoing": - case "sending": - if (!sending) { - sending = document.createElement("div") - sending.setAttribute("class", "sending") - sending.innerHTML = "<svg overflow=\"hidden\" viewBox=\"0 -2 16 14\" height=\"16px\" width=\"16px\"><circle class=\"status_circle anim-first\" cx=\"4\" cy=\"12\" r=\"1\"/><circle class=\"status_circle anim-second\" cx=\"8\" cy=\"12\" r=\"1\"/><circle class=\"status_circle anim-third\" cx=\"12\" cy=\"12\" r=\"1\"/></svg>" - // add sending animation to message; - message_div.insertBefore(sending, message_div.querySelector(".menu_interaction")) - } - message_div.querySelector(".message_text").style.color = "#888" - break - case "failure": - // change text color to red - message_div.querySelector(".message_text").color = "#000" - var failure_div = message_div.querySelector(".failure") - if (!failure_div) { - failure_div = document.createElement("div") - failure_div.setAttribute("class", "failure") - failure_div.innerHTML = "<svg overflow=\"visible\" viewBox=\"0 -2 16 14\" height=\"16px\" width=\"16px\"><path class=\"status-x x-first\" stroke=\"#AA0000\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"3\" fill=\"none\" d=\"M4,4 L12,12\"/><path class=\"status-x x-second\" stroke=\"#AA0000\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"3\" fill=\"none\" d=\"M12,4 L4,12\"/></svg>" - // add failure animation to message - message_div.insertBefore(failure_div, message_div.querySelector(".menu_interaction")) - } - message_div.querySelector(".message_text").style.color = "#000" - if (sending) sending.style.display = "none" - break - case "sent": - case "finished": - case "unknown": - case "read": - // change text color to black - message_div.querySelector(".message_text").style.color = "#000" - if (sending) sending.style.display = "none" - break - default: - break - } -} - -/** - * Build a new interaction (call or contact) - */ -function actionInteraction() { - var message_wrapper = document.createElement("div") - message_wrapper.setAttribute("class", "message_wrapper") - - // A file interaction contains buttons at the left of the interaction - // for the status or accept/refuse buttons - var left_buttons = document.createElement("div") - left_buttons.setAttribute("class", "left_buttons") - message_wrapper.appendChild(left_buttons) - // Also contains a bold clickable text - var text_div = document.createElement("div") - text_div.setAttribute("class", "text") - message_wrapper.appendChild(text_div) - return message_wrapper -} - -/** - * Update a call interaction (icon + text) - * @param message_div the message to update - * @param message_object new informations - */ -function updateCallInteraction(message_div, message_object) { - const outgoingCall = "<svg fill=\"#219d55\" height=\"24\" viewBox=\"0 0 24 24\" width=\"24\" xmlns=\"http://www.w3.org/2000/svg\"><path d=\"M0 0h24v24H0z\" fill=\"none\"/><path d=\"M9 5v2h6.59L4 18.59 5.41 20 17 8.41V15h2V5z\"/></svg>" - const callMissed = "<svg fill=\"#dc2719\" height=\"24\" viewBox=\"0 0 24 24\" width=\"24\" xmlns=\"http://www.w3.org/2000/svg\"><path d=\"M0 0h24v24H0z\" fill=\"none\"/><path d=\"M19.59 7L12 14.59 6.41 9H11V7H3v8h2v-4.59l7 7 9-9z\"/></svg>" - const outgoingMissed = "<svg fill=\"#dc2719\" height=\"24\" viewBox=\"0 0 24 24\" width=\"24\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\"><defs><path d=\"M24 24H0V0h24v24z\" id=\"a\"/></defs><clipPath id=\"b\"><use overflow=\"visible\" xlink:href=\"#a\"/></clipPath><path clip-path=\"url(#b)\" d=\"M3 8.41l9 9 7-7V15h2V7h-8v2h4.59L12 14.59 4.41 7 3 8.41z\"/></svg>" - const callReceived = "<svg fill=\"#219d55\" height=\"24\" viewBox=\"0 0 24 24\" width=\"24\" xmlns=\"http://www.w3.org/2000/svg\"><path d=\"M0 0h24v24H0z\" fill=\"none\"/><path d=\"M20 5.41L18.59 4 7 15.59V9H5v10h10v-2H8.41z\"/></svg>" - - const message_text = message_object["text"] - const message_direction = (message_text.toLowerCase().indexOf("incoming") !== -1) ? "in" : "out" - const missed = message_text.indexOf("Missed") !== -1 - - message_div.querySelector(".text").innerText = message_text.substring(2) - - var left_buttons = message_div.querySelector(".left_buttons") - left_buttons.innerHTML = "" - var status_button = document.createElement("div") - var statusFile = "" - if (missed) - statusFile = (message_direction === "in") ? callMissed : outgoingMissed - else - statusFile = (message_direction === "in") ? callReceived : outgoingCall - status_button.innerHTML = statusFile - status_button.setAttribute("class", "flat-button") - left_buttons.appendChild(status_button) -} - -/** - * Update a contact interaction (icon + text) - * @param message_div the message to update - * @param message_object new informations - */ -function updateContactInteraction(message_div, message_object) { - const message_text = message_object["text"] - - message_div.querySelector(".text").innerText = message_text - - var left_buttons = message_div.querySelector(".left_buttons") - left_buttons.innerHTML = "" - var status_button = document.createElement("div") - status_button.innerHTML = "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\">\ -<path d=\"M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z\"/>\ -<path d=\"M0 0h24v24H0z\" fill=\"none\"/></svg>" - status_button.setAttribute("class", "flat-button") - left_buttons.appendChild(status_button) -} - -/** - * Remove an interaction from the conversation - * @param interaction_id - */ -/* exported removeInteraction */ -function removeInteraction(interaction_id) { - var interaction = document.getElementById(`message_${interaction_id}`) - if (!interaction) { - return - } - - if (interaction.previousSibling) { - /* if element was the most recently received message, make sure the - last_message property is given away to the previous sibling */ - if (interaction.classList.contains("last_message")) { - interaction.previousSibling.classList.add("last_message") - } - - /* same for timestamp */ - var timestamp = interaction.querySelector(".timestamp") - var previousTimeStamp = interaction.previousSibling.querySelector(".timestamp") - if (timestamp && !previousTimeStamp) { - interaction.previousSibling.querySelector(".timestamp_cell").appendChild(timestamp) - } - } - - var firstMessage = getPreviousInteraction(interaction) - var secondMessage = getNextInteraction(interaction) - - updateSequencing(firstMessage, secondMessage) - - interaction.parentNode.removeChild(interaction) -} - -/** - * Build message dropdown - * @return a message dropdown for passed message id - */ -function buildMessageDropdown(message_id) { - const menu_element = document.createElement("div") - menu_element.setAttribute("class", "menu_interaction") - menu_element.innerHTML = - `<input type="checkbox" id="showmenu${message_id}" class="showmenu"> - <label for="showmenu${message_id}"> - <svg fill="#888888" height="12" viewBox="0 0 24 24" width="12" xmlns="http://www.w3.org/2000/svg"> - <path d="M0 0h24v24H0z" fill="none"/> - <path d="M6 10c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm12 0c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm-6 0c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2z"/> - </svg> - </label>` - menu_element.onclick = function() { - const button = this.querySelector(".showmenu") - button.checked = !button.checked - } - menu_element.onmouseleave = function() { - const button = this.querySelector(".showmenu") - button.checked = false - } - const dropdown = document.createElement("div") - const dropdown_classes = [ - "dropdown", - `dropdown_${message_id}` - ] - dropdown.setAttribute("class", dropdown_classes.join(" ")) - - const remove = document.createElement("div") - remove.setAttribute("class", "menuoption") - remove.innerHTML = "Delete" - remove.msg_id = message_id - remove.onclick = function() { - window.jsbridge.deleteInteraction(`${this.msg_id}`); - } - dropdown.appendChild(remove) - menu_element.appendChild(dropdown) - - return menu_element -} - -/** - * Build a message div for passed message object - * @param message_object to treat - */ -function buildNewMessage(message_object) { - const message_id = message_object["id"] - const message_type = message_object["type"] - const message_text = message_object["text"] - const message_direction = message_object["direction"] - const delivery_status = message_object["delivery_status"] - const message_sender_contact_method = message_object["sender_contact_method"] - - var classes = [ - "message", - `message_${message_direction}`, - `message_type_${message_type}` - ] - - var type = "" - var message_div = document.createElement("div") - message_div.setAttribute("id", `message_${message_id}`) - message_div.setAttribute("class", classes.join(" ")) - - // Build message for each types. - - // Build main content - if (message_type === "data_transfer") { - if (isImage(message_text) && delivery_status === "finished" && displayLinksEnabled) { - var errorHandler = function() { - var wrapper = message_div.querySelector(".internal_mes_wrapper") - var message_wrapper = message_div.querySelector(".message_wrapper") - if (message_wrapper) { - message_wrapper.parentNode.removeChild(message_wrapper) - } - - var media_wrapper = message_div.querySelector(".media_wrapper") - if (media_wrapper) { - media_wrapper.parentNode.removeChild(media_wrapper) - } - - var new_interaction = fileInteraction(message_id, message_direction) - var new_message_wrapper = new_interaction.querySelector(".message_wrapper") - wrapper.prepend(new_message_wrapper) - updateFileInteraction(message_div, message_object, true) - } - message_div.append(mediaInteraction(message_id, message_direction, message_text, null, errorHandler)) - message_div.querySelector("img").id = message_id - message_div.querySelector("img").msg_obj = message_object - } else { - message_div.append(fileInteraction(message_id, message_direction)) - } - } else if (message_type === "text") { - // TODO add the possibility to update messages (remove and rebuild) - const htmlText = getMessageHtml(message_text) - if (displayLinksEnabled) { - const parser = new DOMParser() - const DOMMsg = parser.parseFromString(htmlText, "text/xml") - const links = DOMMsg.querySelectorAll("a") - if (DOMMsg.childNodes.length && links.length) { - var isTextToShow = (DOMMsg.childNodes[0].childNodes.length > 1) - const ytid = (isVideo(message_text))? youtube_id(message_text) : "" - if (!isTextToShow && (ytid || isImage(message_text))) { - type = "media" - message_div.append(mediaInteraction(message_id, message_direction, message_text, ytid)) - } - } - } - if (type !== "media") { - type = "text" - message_div.append(textInteraction(message_id, message_direction, htmlText)) - } - } else if (message_type === "call" || message_type === "contact") { - message_div.append(actionInteraction()) - } else { - const temp = document.createElement("div") - temp.innerText = message_type - message_div.appendChild(temp) - } - - var message_dropdown = buildMessageDropdown(message_id) - if (message_type !== "call") { - message_div.appendChild(message_dropdown) - } else { - var wrapper = message_div.querySelector(".message_wrapper") - wrapper.insertBefore(message_dropdown, wrapper.firstChild) - } - - // Add sender images if necessary (like if the interaction doesn't take the whole width) - addSenderImage(message_div, message_type, message_sender_contact_method) - - return message_div -} - -function addSenderImage(message_div, message_type, message_sender_contact_method) { - const need_sender = (message_type === "data_transfer" || message_type === "text") - if (need_sender) { - var sender_image_cell = message_div.querySelector(".sender_image_cell") - if (sender_image_cell) { - var message_sender_image = document.createElement("span") - var cssSafeStr = message_sender_contact_method.replace(/@/g, "_").replace(/\./g, "_"); - message_sender_image.setAttribute("class", `sender_image sender_image_${cssSafeStr}`) - sender_image_cell.appendChild(message_sender_image) - } else { - console.warn("can't find sender_image_cell"); - } - } -} - -/** - * Build a timestamp for passed message object - * @param message_object to treat - */ -function buildNewTimestamp(message_object) { - const message_type = message_object["type"] - const message_direction = message_object["direction"] - const message_timestamp = message_object["timestamp"] - - const formattedTimestamp = getMessageTimestampText(message_timestamp, true) - const date_elt = document.createElement("div") - - date_elt.innerText = formattedTimestamp - var typeIsCallOrContact = (message_type === "call" || message_type === "contact") - var timestamp_div_classes = ["timestamp", typeIsCallOrContact ? "timestamp_action" : `timestamp_${message_direction}`] - date_elt.setAttribute("class", timestamp_div_classes.join(" ")) - date_elt.setAttribute("message_timestamp", message_timestamp) - - return date_elt -} - -/** - * Add a message to the conversation. - * @param message_object to treat - * @param new_message if it's a new message or if we need to update - * @param insert_after if we want the message at the end or the top of the conversation - * @param messages_div - */ -function addOrUpdateMessage(message_object, new_message, insert_after = true, messages_div) { - const message_id = message_object["id"] - const message_type = message_object["type"] - const message_direction = message_object["direction"] - const delivery_status = message_object["delivery_status"] - - var message_div = messages_div.querySelector("#message_" + message_id) - if (new_message) { - message_div = buildNewMessage(message_object) - - /* Show timestamp if either: - - message has type call or contact - - or most recently added timestamp in this set is different - - or message is the first message in this set */ - - var date_elt = buildNewTimestamp(message_object) - var timestamp = messages_div.querySelector(".timestamp") - - if (message_type === "call" || message_type === "contact") { - message_div.querySelector(".message_wrapper").appendChild(date_elt) - } else if (insert_after || !timestamp || timestamp.className !== date_elt.className - || timestamp.innerHTML !== date_elt.innerHTML) { - message_div.querySelector(".timestamp_cell").appendChild(date_elt) - if (message_direction === "out") - message_div.querySelector(".timestamp_cell").setAttribute("class", "timestamp_cell_out") - } - - var isGenerated = message_type === "call" || message_type === "contact" - if (isGenerated) { - message_div.classList.add("generated_message") - } - - if (insert_after) { - var previousMessage = messages_div.lastChild - messages_div.appendChild(message_div) - computeSequencing(previousMessage, message_div, null, insert_after) - if (previousMessage) { - previousMessage.classList.remove("last_message") - } - message_div.classList.add("last_message") - - /* When inserting at the bottom we should also check that the - previously sent message does not have the same timestamp. - If it's the case, remove it.*/ - var previous_timestamp = message_div.previousSibling.querySelector(".timestamp") - if (previous_timestamp && - previous_timestamp.className === date_elt.className && - previous_timestamp.innerHTML === date_elt.innerHTML && - !message_div.previousSibling.classList.contains("last_of_sequence")) { - previous_timestamp.parentNode.removeChild(previous_timestamp) - } - } else { - var nextMessage = messages_div.firstChild - messages_div.prepend(message_div) - computeSequencing(message_div, nextMessage, null, insert_after) - } - } - - if (isErrorStatus(delivery_status) && message_direction === "out") { - const dropdown = messages_div.querySelector(`.dropdown_${message_id}`) - if (!dropdown.querySelector(".retry")) { - const retry = document.createElement("div") - retry.setAttribute("class", "retry") - retry.innerHTML = "Retry" - retry.msg_id = message_id - retry.onclick = function() { - window.jsbridge.retryInteraction(`${this.msg_id}`); - } - dropdown.insertBefore(retry, message_div.querySelector(".delete")) - } - } - - // Update informations if needed - if (message_type === "data_transfer") - updateFileInteraction(message_div, message_object) - if (message_type === "text" && message_direction === "out") - // Modify sent status if necessary - updateTextInteraction(message_div, delivery_status) - if (message_type === "call") - updateCallInteraction(message_div, message_object) - if (message_type === "contact") - updateContactInteraction(message_div, message_object) - - // Clean timestamps - updateTimestamps(messages_div) -} - -function getNextInteraction(interaction, includeLazyLoadedBlock = true) -{ - var nextInteraction = interaction.nextSibling - if (!nextInteraction && includeLazyLoadedBlock) { - var nextBlock = interaction.parentNode.nextElementSibling - if (nextBlock) { - nextInteraction = nextBlock.firstElementChild - } - } - return nextInteraction -} - -function getPreviousInteraction(interaction, includeLazyLoadedBlock = true) -{ - var previousInteraction = interaction.previousSibling - if (!previousInteraction && includeLazyLoadedBlock) { - var previousBlock = interaction.parentNode.previousElementSibling - if (previousBlock) { - previousInteraction = previousBlock.lastElementChild - } - } - return previousInteraction -} - -function isSequenceBreak(firstMessage, secondMessage, insert_after = true) -{ - if (!firstMessage || !secondMessage) { - return false - } - var first_message_direction = firstMessage.classList.contains("message_out") ? "out" : "in" - var second_message_direction = secondMessage.classList.contains("message_out") ? "out" : "in" - if (second_message_direction != first_message_direction) { - return true - } - var firstMessageIsGenerated = firstMessage.classList.contains("generated_message") - var secondMessageIsGenerated = secondMessage.classList.contains("generated_message") - if (firstMessageIsGenerated != secondMessageIsGenerated) { - return true - } - if (insert_after) { - const internal_message_wrapper = firstMessage.querySelector(".internal_mes_wrapper") - if (internal_message_wrapper) { - const firstTimestamp = internal_message_wrapper.querySelector(".timestamp") - return !!(firstTimestamp) && firstTimestamp.innerHTML !== "just now" - } - return false - } else { - const internal_message_wrapper = firstMessage.querySelector(".internal_mes_wrapper") - if (internal_message_wrapper) { - return !!(internal_message_wrapper.querySelector(".timestamp")) - } - return false - } -} - -function updateSequencing(firstMessage, secondMessage) -{ - if (firstMessage) { - if (secondMessage) { - var sequence_break = isSequenceBreak(firstMessage, secondMessage, false) - if (sequence_break) { - if (firstMessage.classList.contains("middle_of_sequence")) { - firstMessage.classList.remove("middle_of_sequence") - firstMessage.classList.add("last_of_sequence") - } else if (firstMessage.classList.contains("first_of_sequence")) { - firstMessage.classList.remove("first_of_sequence") - firstMessage.classList.add("single_message") - } - if (secondMessage.classList.contains("middle_of_sequence")) { - secondMessage.classList.remove("middle_of_sequence") - secondMessage.classList.add("first_of_sequence") - } else if (secondMessage.classList.contains("last_of_sequence")) { - secondMessage.classList.remove("last_of_sequence") - secondMessage.classList.add("single_message") - } - } else { - if (firstMessage.classList.contains("last_of_sequence")) { - firstMessage.classList.remove("last_of_sequence") - firstMessage.classList.add("middle_of_sequence") - } else if (firstMessage.classList.contains("single_message")) { - firstMessage.classList.remove("single_message") - firstMessage.classList.add("first_of_sequence") - } - if (secondMessage.classList.contains("first_of_sequence")) { - secondMessage.classList.remove("first_of_sequence") - secondMessage.classList.add("middle_of_sequence") - } else if (secondMessage.classList.contains("single_message")) { - secondMessage.classList.remove("single_message") - secondMessage.classList.add("last_of_sequence") - } - } - } else { - // this is the last interaction of the conversation - if (firstMessage.classList.contains("first_of_sequence")) { - firstMessage.classList.remove("first_of_sequence") - firstMessage.classList.add("single_message") - } else if (firstMessage.classList.contains("middle_of_sequence")) { - firstMessage.classList.remove("middle_of_sequence") - firstMessage.classList.add("last_of_sequence") - } - } - } else if (secondMessage) { - // this is the first interaction of the conversation - if (secondMessage.classList.contains("middle_of_sequence")) { - secondMessage.classList.remove("middle_of_sequence") - secondMessage.classList.add("first_of_sequence") - } else if (secondMessage.classList.contains("last_of_sequence")) { - secondMessage.classList.remove("last_of_sequence") - secondMessage.classList.add("single_message") - } - } -} - -function computeSequencing(firstMessage, secondMessage, lazyLoadingBlock, insert_after = true) -{ - if (insert_after) { - if (secondMessage) { - var secondMessageIsGenerated = secondMessage.classList.contains("generated_message") - if (firstMessage && !secondMessageIsGenerated) { - var firstMessageIsGenerated = firstMessage.classList.contains("generated_message") - var sequence_break = isSequenceBreak(firstMessage, secondMessage) - if (sequence_break) { - secondMessage.classList.add("single_message") - } else { - if (firstMessage.classList.contains("single_message")) { - firstMessage.classList.remove("single_message") - firstMessage.classList.add("first_of_sequence") - } else if (firstMessage.classList.contains("last_of_sequence")) { - firstMessage.classList.remove("last_of_sequence") - firstMessage.classList.add("middle_of_sequence") - } - if (firstMessageIsGenerated) { - secondMessage.classList.add("single_message") - } else { - secondMessage.classList.add("last_of_sequence") - } - } - } else if (!secondMessageIsGenerated) { - secondMessage.classList.add("single_message") - } - } - } else if (firstMessage) { - var firstMessageIsGenerated = firstMessage.classList.contains("generated_message") - if (secondMessage && !firstMessageIsGenerated) { - var secondMessageIsGenerated = secondMessage.classList.contains("generated_message") - var sequence_break = isSequenceBreak(firstMessage, secondMessage, false) - if (sequence_break) { - firstMessage.classList.add("single_message") - } else { - if (secondMessage.classList.contains("single_message")) { - secondMessage.classList.remove("single_message") - secondMessage.classList.add("last_of_sequence") - } else if (secondMessage.classList.contains("first_of_sequence")) { - secondMessage.classList.remove("first_of_sequence") - secondMessage.classList.add("middle_of_sequence") - } - if (secondMessageIsGenerated) { - firstMessage.classList.add("single_message") - } else { - firstMessage.classList.add("first_of_sequence") - } - } - } else if (!firstMessageIsGenerated) { - firstMessage.classList.add("single_message") - } - } -} - -/** - * Wrapper for addOrUpdateMessage. - * - * Add or update a message and make sure the scrollbar position - * is refreshed correctly - * - * @param message_object message to be added - */ -/* exported addMessage */ -function addMessage(message_object) -{ - if (!messages.lastChild) { - var block_wrapper = document.createElement("div") - messages.append(block_wrapper) - } - - exec_keeping_scroll_position(addOrUpdateMessage, [message_object, true, undefined, messages.lastChild]) -} - -/** - * Update a message that was previously added with addMessage and - * make sure the scrollbar position is refreshed correctly - * - * @param message_object message to be updated - */ -/* exported updateMessage */ -function updateMessage(message_object) -{ - var message_div = messages.querySelector("#message_" + message_object["id"]) - exec_keeping_scroll_position(addOrUpdateMessage, [message_object, false, undefined, message_div.parentNode]) -} - -/** - * Called whenever an image has finished loading. Check lazy loading status - * once all images have finished loading. - */ -function on_image_load_finished() { - imagesLoadingCounter-- - - if (!imagesLoadingCounter) { - /* This code is executed once all images have been loaded. */ - check_lazy_loading() - } -} - -/** - * Make sure at least initialScrollBufferFactor screens of messages are - * available in the DOM. - */ -function check_lazy_loading() { - if (!canLazyLoad) - return; - if (messages.scrollHeight < initialScrollBufferFactor * messages.clientHeight - && historyBufferIndex !== historyBuffer.length) { - /* Not enough messages loaded, print a new batch. Enable isInitialLoading - as reloading a single batch might not be sufficient to fulfill our - criteria (we want to be called back again to check on that) */ - isInitialLoading = true - printHistoryPart(messages, 0) - isInitialLoading = false - } -} - -/** - * Display 'scrollBuffer' messages from history in passed div (reverse order). - * - * @param messages_div that should be modified - * @param setMessages if enabled, #messages will be set to the resulting messages - * div after being modified. If #messages already exists it will - * be removed and replaced by the new div. - * @param fixedAt if setMessages is enabled, maintain scrollbar at the specified - * position (otherwise modifying #messages would result in - * changing the position of the scrollbar) - */ -function printHistoryPart(messages_div, fixedAt) -{ - if (historyBufferIndex === historyBuffer.length) { - return - } - /* If first element is a spinner, remove it */ - if (messages_div.firstChild && messages_div.firstChild.id === "lazyloading-icon") { - messages_div.removeChild(messages_div.firstChild) - } - - /* Elements are appended to a wrapper div. This div has no style - properties, it allows us to add all messages at once to the main - messages div. */ - var block_wrapper = document.createElement("div") - - messages_div.prepend(block_wrapper) - - for (var i = 0; i < scrollBuffer && historyBufferIndex < historyBuffer.length; ++historyBufferIndex && ++i) { - // TODO on-screen messages should be removed from the buffer - addOrUpdateMessage(historyBuffer[historyBuffer.length - 1 - historyBufferIndex], true, false, block_wrapper) - } - - var lastMessage = block_wrapper.lastChild - - updateSequencing(lastMessage, getNextInteraction(lastMessage)) - - var absoluteLastMessage = messages_div.lastChild.lastChild - if (absoluteLastMessage) { - absoluteLastMessage.classList.add("last_message") - } - - /* Add ellipsis (...) at the top if there are still messages to load */ - if (historyBufferIndex < historyBuffer.length) { - var llicon = document.createElement("span") - llicon.id = "lazyloading-icon" - llicon.innerHTML = "<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"#888888\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\"><path d=\"M0 0h24v24H0z\" fill=\"none\"/><path d=\"M6 10c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm12 0c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm-6 0c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2z\"/></svg>" - messages_div.prepend(llicon) - } - - if (fixedAt !== undefined) { - /* update scrollbar position to take text-message -related - scrollHeight changes in account (not necessary to wait - for DOM redisplay in this case). Changes due to image - messages are handled in their onLoad callbacks. */ - back_to_scroll(fixedAt) - /* schedule a scrollbar position update for changes which - are neither handled by the previous call nor by onLoad - callbacks. This call is necessary but not sufficient, - dropping the previous call would result in visual - glitches during initial load. */ - setTimeout(function() {back_to_scroll(fixedAt)}, 0) - } - - if (!imagesLoadingCounter) { - setTimeout(check_lazy_loading, 0) - } -} - -function hideMessagesDiv() -{ - if (!messages.classList.contains('fade')) { - messages.classList.add('fade'); - } -} - -function showMessagesDiv() -{ - if (messages.classList.contains('fade')) { - messages.classList.remove('fade'); - } -} - -/** - * Clear all messages. - */ -/* exported clearMessages */ -function clearMessages() -{ - canLazyLoad = false - - backToBottomBtn.style.visibility="hidden"; - - while (messages.firstChild) { - messages.removeChild(messages.firstChild) - } - - window.jsbridge.emitMessagesCleared() -} - -/** - * Set history buffer, initialize messages div and display a first batch - * of messages. - * - * Make sure that enough messages are displayed to fill initialScrollBufferFactor - * screens of messages (if enough messages are present in the conversation) - * - * @param messages_array should contain history to be printed - */ -/* exported printHistory */ -function printHistory(messages_array) -{ - historyBuffer = messages_array - historyBufferIndex = 0 - - isInitialLoading = true - printHistoryPart(messages, 0) - isInitialLoading = false - - canLazyLoad = true - - window.jsbridge.emitMessagesLoaded() -} - -/** - * Set the image for a given sender - * set_sender_image object should contain the following keys: - * - sender: the name of the sender - * - sender_image: base64 png encoding of the sender image - * - * @param set_sender_image_object sender image object as previously described - */ -/* exported setSenderImage */ -function setSenderImage(set_sender_image_object) -{ - var sender_contact_method = set_sender_image_object["sender_contact_method"].replace(/@/g, "_").replace(/\./g, "_"), - sender_image = set_sender_image_object["sender_image"], - sender_image_id = "sender_image_" + sender_contact_method, - invite_sender_image_id = "invite_sender_image_" + sender_contact_method, - currentSenderImage = document.getElementById(sender_image_id), // Remove the currently set sender image - style, invite_style - - if (currentSenderImage) { - currentSenderImage.parentNode.removeChild(currentSenderImage) - } - - // Create a new style element - style = document.createElement("style") - - style.type = "text/css" - style.id = sender_image_id - style.innerHTML = "." + sender_image_id + " {content: url(data:image/png;base64," + sender_image + ");height: 2.25em;width: 2.25em;}" - document.head.appendChild(style) - - invite_style = document.createElement("style") - - invite_style.type = "text/css" - invite_style.id = invite_sender_image_id - invite_style.innerHTML = "." + invite_sender_image_id + " {content: url(data:image/png;base64," + sender_image + ");height: 48px;width: 48px;}" - document.head.appendChild(invite_style) -} - -/** - * Copy Mouse Selected Text and return it - */ -function copy_text_selected() { - var selObj = document.getSelection(); - var selectedText = selObj.toString(); - return selectedText; -} - -/** - * Check if text is selected by mouse - */ -function isTextSelected() { - - var selObj = document.getSelection(); - var selectedText = selObj.toString(); - - if (selectedText.length != 0) - return true; - return false; -} - -/* exported selectFile - sselect files from Qt */ -function selectFile() { - - window.jsbridge.selectFile(); - //js or qt -} - -/** - * add file (local file) to message area - */ -function addFile_path(path, name, size) { - var html = '<div class="file_wrapper" data-path="' + path + '">' + - '<svg class="svg-icon" viewBox="0 0 20 20">' + - '<path fill = "none" d = "M17.222,5.041l-4.443-4.414c-0.152-0.151-0.356-0.235-0.571-0.235h-8.86c-0.444,0-0.807,0.361-0.807,0.808v17.602c0,0.448,0.363,0.808,0.807,0.808h13.303c0.448,0,0.808-0.36,0.808-0.808V5.615C17.459,5.399,17.373,5.192,17.222,5.041zM15.843,17.993H4.157V2.007h7.72l3.966,3.942V17.993z" ></path>' + - '<path fill="none" d="M5.112,7.3c0,0.446,0.363,0.808,0.808,0.808h8.077c0.445,0,0.808-0.361,0.808-0.808c0-0.447-0.363-0.808-0.808-0.808H5.92C5.475,6.492,5.112,6.853,5.112,7.3z"></path>' + - '<path fill="none" d="M5.92,5.331h4.342c0.445,0,0.808-0.361,0.808-0.808c0-0.446-0.363-0.808-0.808-0.808H5.92c-0.444,0-0.808,0.361-0.808,0.808C5.112,4.97,5.475,5.331,5.92,5.331z"></path>' + - '<path fill="none" d="M13.997,9.218H5.92c-0.444,0-0.808,0.361-0.808,0.808c0,0.446,0.363,0.808,0.808,0.808h8.077c0.445,0,0.808-0.361,0.808-0.808C14.805,9.58,14.442,9.218,13.997,9.218z"></path>' + - '<path fill="none" d="M13.997,11.944H5.92c-0.444,0-0.808,0.361-0.808,0.808c0,0.446,0.363,0.808,0.808,0.808h8.077c0.445,0,0.808-0.361,0.808-0.808C14.805,12.306,14.442,11.944,13.997,11.944z"></path>' + - '<path fill="none" d="M13.997,14.67H5.92c-0.444,0-0.808,0.361-0.808,0.808c0,0.447,0.363,0.808,0.808,0.808h8.077c0.445,0,0.808-0.361,0.808-0.808C14.805,15.032,14.442,14.67,13.997,14.67z"></path>' + - '</svg >' + - '<div class="fileinfo">' + - '<p>' + name + '</p>' + - '<p>' + size + '</p>' + - '</div >' + - '<button class="btn" onclick="remove(this)">X</button>' + - '</div >'; - // At first, visiblity can empty - if (sendContainer.style.visibility.length == 0 || sendContainer.style.visibility == "hidden") { - grow_send_container(); - sendContainer.style.visibility = "visible"; - } - //add html here since display is set to flex, image will change accordingly - sendContainer.innerHTML += html; -} - -/** - * add image (base64 array) to message area - */ -function addImage_base64(base64) { - - var html = '<div class="img_wrapper">' + - '<img src="data:image/png;base64,' + base64 + '"/>' + - '<button class="btn" onclick="remove(this)">X</button>' + - '</div >'; - // At first, visiblity can empty - if (sendContainer.style.visibility.length == 0 || sendContainer.style.visibility == "hidden") { - grow_send_container(); - sendContainer.style.visibility = "visible"; - } - //add html here since display is set to flex, image will change accordingly - sendContainer.innerHTML += html; -} - -/** - * add image (image path) to message area - */ -function addImage_path(path) { - - var html = '<div class="img_wrapper">' + - '<img src="' + path + '"/>' + - '<button class="btn" onclick="remove(this)">X</button>' + - '</div >'; - // At first, visiblity can empty - if (sendContainer.style.visibility.length == 0 || sendContainer.style.visibility == "hidden") { - grow_send_container(); - sendContainer.style.visibility = "visible"; - } - //add html here since display is set to flex, image will change accordingly - sendContainer.innerHTML += html; -} - -/** - * This function adjusts the body paddings so that that the file_image_send_container doesn't - * overlap messages when it grows. - */ -/* exported grow_send_container */ -function grow_send_container() { - exec_keeping_scroll_position(function () { - var msgbar_size = window.getComputedStyle(document.body).getPropertyValue("--messagebar-size"); - document.body.style.paddingBottom = (parseInt(msgbar_size) + 158).toString() + "px"; - //6em - }, []) -} - -/** - * This function adjusts the body paddings so that that the file_image_send_container will hide - * and recover padding bottom - */ -/* exported grow_send_container */ -function reduce_send_container() { - exec_keeping_scroll_position(function () { - document.body.style.paddingBottom = (parseInt(document.body.style.paddingBottom) - 158).toString() + "px"; - //6em - }, []) -} - -// Remove current cancel button division and hide the sendContainer -function remove(e) { - e.parentNode.parentNode.removeChild(e.parentNode); - if (sendContainer.innerHTML.length == 0) { - reduce_send_container(); - sendContainer.style.visibility = "hidden"; - } -} - -function pasteKeyDetected(e) { - e.preventDefault(); - window.jsbridge.emitPasteKeyDetected(); -} - -// Set the curser to a target position -function setCaretPosition(elem, caretPos) { - var range; - - if (elem.createTextRange) { - range = elem.createTextRange(); - range.move('character', caretPos); - range.select(); - } else { - elem.focus(); - if (elem.selectionStart !== undefined) { - elem.setSelectionRange(caretPos, caretPos); - } - } -} -function replaceText(text) { - var input = messageBarInput; - var currentContent = input.value; - var start = input.selectionStart; - var end = input.selectionEnd; - var output = [currentContent.slice(0, start), text, currentContent.slice(end)].join(''); - input.value = output; - setCaretPosition(input, start + text.length); -} diff --git a/web/linkify-html.js b/web/linkify-html.js deleted file mode 100644 index 1e5090c..0000000 --- a/web/linkify-html.js +++ /dev/null @@ -1,827 +0,0 @@ -/* - * Copyright (c) 2016 SoapBox Innovations Inc. - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. -*/ - -'use strict'; - -;(function (window, linkify) { - var linkifyHtml = function (linkify) { - 'use strict'; - - var HTML5NamedCharRefs = {}; - - function EntityParser(named) { - this.named = named; - } - - var HEXCHARCODE = /^#[xX]([A-Fa-f0-9]+)$/; - var CHARCODE = /^#([0-9]+)$/; - var NAMED = /^([A-Za-z0-9]+)$/; - - EntityParser.prototype.parse = function (entity) { - if (!entity) { - return; - } - var matches = entity.match(HEXCHARCODE); - if (matches) { - return '&#x' + matches[1] + ';'; - } - matches = entity.match(CHARCODE); - if (matches) { - return '&#' + matches[1] + ';'; - } - matches = entity.match(NAMED); - if (matches) { - return '&' + matches[1] + ';'; - } - }; - - var WSP = /[\t\n\f ]/; - var ALPHA = /[A-Za-z]/; - var CRLF = /\r\n?/g; - - function isSpace(char) { - return WSP.test(char); - } - - function isAlpha(char) { - return ALPHA.test(char); - } - - function preprocessInput(input) { - return input.replace(CRLF, "\n"); - } - - function EventedTokenizer(delegate, entityParser) { - this.delegate = delegate; - this.entityParser = entityParser; - - this.state = null; - this.input = null; - - this.index = -1; - this.line = -1; - this.column = -1; - this.tagLine = -1; - this.tagColumn = -1; - - this.reset(); - } - - EventedTokenizer.prototype = { - reset: function reset() { - this.state = 'beforeData'; - this.input = ''; - - this.index = 0; - this.line = 1; - this.column = 0; - - this.tagLine = -1; - this.tagColumn = -1; - - this.delegate.reset(); - }, - - tokenize: function tokenize(input) { - this.reset(); - this.tokenizePart(input); - this.tokenizeEOF(); - }, - - tokenizePart: function tokenizePart(input) { - this.input += preprocessInput(input); - - while (this.index < this.input.length) { - this.states[this.state].call(this); - } - }, - - tokenizeEOF: function tokenizeEOF() { - this.flushData(); - }, - - flushData: function flushData() { - if (this.state === 'data') { - this.delegate.finishData(); - this.state = 'beforeData'; - } - }, - - peek: function peek() { - return this.input.charAt(this.index); - }, - - consume: function consume() { - var char = this.peek(); - - this.index++; - - if (char === "\n") { - this.line++; - this.column = 0; - } else { - this.column++; - } - - return char; - }, - - consumeCharRef: function consumeCharRef() { - var endIndex = this.input.indexOf(';', this.index); - if (endIndex === -1) { - return; - } - var entity = this.input.slice(this.index, endIndex); - var chars = this.entityParser.parse(entity); - if (chars) { - this.index = endIndex + 1; - return chars; - } - }, - - markTagStart: function markTagStart() { - this.tagLine = this.line; - this.tagColumn = this.column; - }, - - states: { - beforeData: function beforeData() { - var char = this.peek(); - - if (char === "<") { - this.state = 'tagOpen'; - this.markTagStart(); - this.consume(); - } else { - this.state = 'data'; - this.delegate.beginData(); - } - }, - - data: function data() { - var char = this.peek(); - - if (char === "<") { - this.delegate.finishData(); - this.state = 'tagOpen'; - this.markTagStart(); - this.consume(); - } else if (char === "&") { - this.consume(); - this.delegate.appendToData(this.consumeCharRef() || "&"); - } else { - this.consume(); - this.delegate.appendToData(char); - } - }, - - tagOpen: function tagOpen() { - var char = this.consume(); - - if (char === "!") { - this.state = 'markupDeclaration'; - } else if (char === "/") { - this.state = 'endTagOpen'; - } else if (isAlpha(char)) { - this.state = 'tagName'; - this.delegate.beginStartTag(); - this.delegate.appendToTagName(char.toLowerCase()); - } - }, - - markupDeclaration: function markupDeclaration() { - var char = this.consume(); - - if (char === "-" && this.input.charAt(this.index) === "-") { - this.index++; - this.state = 'commentStart'; - this.delegate.beginComment(); - } - }, - - commentStart: function commentStart() { - var char = this.consume(); - - if (char === "-") { - this.state = 'commentStartDash'; - } else if (char === ">") { - this.delegate.finishComment(); - this.state = 'beforeData'; - } else { - this.delegate.appendToCommentData(char); - this.state = 'comment'; - } - }, - - commentStartDash: function commentStartDash() { - var char = this.consume(); - - if (char === "-") { - this.state = 'commentEnd'; - } else if (char === ">") { - this.delegate.finishComment(); - this.state = 'beforeData'; - } else { - this.delegate.appendToCommentData("-"); - this.state = 'comment'; - } - }, - - comment: function comment() { - var char = this.consume(); - - if (char === "-") { - this.state = 'commentEndDash'; - } else { - this.delegate.appendToCommentData(char); - } - }, - - commentEndDash: function commentEndDash() { - var char = this.consume(); - - if (char === "-") { - this.state = 'commentEnd'; - } else { - this.delegate.appendToCommentData("-" + char); - this.state = 'comment'; - } - }, - - commentEnd: function commentEnd() { - var char = this.consume(); - - if (char === ">") { - this.delegate.finishComment(); - this.state = 'beforeData'; - } else { - this.delegate.appendToCommentData("--" + char); - this.state = 'comment'; - } - }, - - tagName: function tagName() { - var char = this.consume(); - - if (isSpace(char)) { - this.state = 'beforeAttributeName'; - } else if (char === "/") { - this.state = 'selfClosingStartTag'; - } else if (char === ">") { - this.delegate.finishTag(); - this.state = 'beforeData'; - } else { - this.delegate.appendToTagName(char); - } - }, - - beforeAttributeName: function beforeAttributeName() { - var char = this.consume(); - - if (isSpace(char)) { - return; - } else if (char === "/") { - this.state = 'selfClosingStartTag'; - } else if (char === ">") { - this.delegate.finishTag(); - this.state = 'beforeData'; - } else { - this.state = 'attributeName'; - this.delegate.beginAttribute(); - this.delegate.appendToAttributeName(char); - } - }, - - attributeName: function attributeName() { - var char = this.consume(); - - if (isSpace(char)) { - this.state = 'afterAttributeName'; - } else if (char === "/") { - this.delegate.beginAttributeValue(false); - this.delegate.finishAttributeValue(); - this.state = 'selfClosingStartTag'; - } else if (char === "=") { - this.state = 'beforeAttributeValue'; - } else if (char === ">") { - this.delegate.beginAttributeValue(false); - this.delegate.finishAttributeValue(); - this.delegate.finishTag(); - this.state = 'beforeData'; - } else { - this.delegate.appendToAttributeName(char); - } - }, - - afterAttributeName: function afterAttributeName() { - var char = this.consume(); - - if (isSpace(char)) { - return; - } else if (char === "/") { - this.delegate.beginAttributeValue(false); - this.delegate.finishAttributeValue(); - this.state = 'selfClosingStartTag'; - } else if (char === "=") { - this.state = 'beforeAttributeValue'; - } else if (char === ">") { - this.delegate.beginAttributeValue(false); - this.delegate.finishAttributeValue(); - this.delegate.finishTag(); - this.state = 'beforeData'; - } else { - this.delegate.beginAttributeValue(false); - this.delegate.finishAttributeValue(); - this.state = 'attributeName'; - this.delegate.beginAttribute(); - this.delegate.appendToAttributeName(char); - } - }, - - beforeAttributeValue: function beforeAttributeValue() { - var char = this.consume(); - - if (isSpace(char)) {} else if (char === '"') { - this.state = 'attributeValueDoubleQuoted'; - this.delegate.beginAttributeValue(true); - } else if (char === "'") { - this.state = 'attributeValueSingleQuoted'; - this.delegate.beginAttributeValue(true); - } else if (char === ">") { - this.delegate.beginAttributeValue(false); - this.delegate.finishAttributeValue(); - this.delegate.finishTag(); - this.state = 'beforeData'; - } else { - this.state = 'attributeValueUnquoted'; - this.delegate.beginAttributeValue(false); - this.delegate.appendToAttributeValue(char); - } - }, - - attributeValueDoubleQuoted: function attributeValueDoubleQuoted() { - var char = this.consume(); - - if (char === '"') { - this.delegate.finishAttributeValue(); - this.state = 'afterAttributeValueQuoted'; - } else if (char === "&") { - this.delegate.appendToAttributeValue(this.consumeCharRef('"') || "&"); - } else { - this.delegate.appendToAttributeValue(char); - } - }, - - attributeValueSingleQuoted: function attributeValueSingleQuoted() { - var char = this.consume(); - - if (char === "'") { - this.delegate.finishAttributeValue(); - this.state = 'afterAttributeValueQuoted'; - } else if (char === "&") { - this.delegate.appendToAttributeValue(this.consumeCharRef("'") || "&"); - } else { - this.delegate.appendToAttributeValue(char); - } - }, - - attributeValueUnquoted: function attributeValueUnquoted() { - var char = this.consume(); - - if (isSpace(char)) { - this.delegate.finishAttributeValue(); - this.state = 'beforeAttributeName'; - } else if (char === "&") { - this.delegate.appendToAttributeValue(this.consumeCharRef(">") || "&"); - } else if (char === ">") { - this.delegate.finishAttributeValue(); - this.delegate.finishTag(); - this.state = 'beforeData'; - } else { - this.delegate.appendToAttributeValue(char); - } - }, - - afterAttributeValueQuoted: function afterAttributeValueQuoted() { - var char = this.peek(); - - if (isSpace(char)) { - this.consume(); - this.state = 'beforeAttributeName'; - } else if (char === "/") { - this.consume(); - this.state = 'selfClosingStartTag'; - } else if (char === ">") { - this.consume(); - this.delegate.finishTag(); - this.state = 'beforeData'; - } else { - this.state = 'beforeAttributeName'; - } - }, - - selfClosingStartTag: function selfClosingStartTag() { - var char = this.peek(); - - if (char === ">") { - this.consume(); - this.delegate.markTagAsSelfClosing(); - this.delegate.finishTag(); - this.state = 'beforeData'; - } else { - this.state = 'beforeAttributeName'; - } - }, - - endTagOpen: function endTagOpen() { - var char = this.consume(); - - if (isAlpha(char)) { - this.state = 'tagName'; - this.delegate.beginEndTag(); - this.delegate.appendToTagName(char.toLowerCase()); - } - } - } - }; - - function Tokenizer(entityParser, options) { - this.token = null; - this.startLine = 1; - this.startColumn = 0; - this.options = options || {}; - this.tokenizer = new EventedTokenizer(this, entityParser); - } - - Tokenizer.prototype = { - tokenize: function tokenize(input) { - this.tokens = []; - this.tokenizer.tokenize(input); - return this.tokens; - }, - - tokenizePart: function tokenizePart(input) { - this.tokens = []; - this.tokenizer.tokenizePart(input); - return this.tokens; - }, - - tokenizeEOF: function tokenizeEOF() { - this.tokens = []; - this.tokenizer.tokenizeEOF(); - return this.tokens[0]; - }, - - reset: function reset() { - this.token = null; - this.startLine = 1; - this.startColumn = 0; - }, - - addLocInfo: function addLocInfo() { - if (this.options.loc) { - this.token.loc = { - start: { - line: this.startLine, - column: this.startColumn - }, - end: { - line: this.tokenizer.line, - column: this.tokenizer.column - } - }; - } - this.startLine = this.tokenizer.line; - this.startColumn = this.tokenizer.column; - }, - - // Data - - beginData: function beginData() { - this.token = { - type: 'Chars', - chars: '' - }; - this.tokens.push(this.token); - }, - - appendToData: function appendToData(char) { - this.token.chars += char; - }, - - finishData: function finishData() { - this.addLocInfo(); - }, - - // Comment - - beginComment: function beginComment() { - this.token = { - type: 'Comment', - chars: '' - }; - this.tokens.push(this.token); - }, - - appendToCommentData: function appendToCommentData(char) { - this.token.chars += char; - }, - - finishComment: function finishComment() { - this.addLocInfo(); - }, - - // Tags - basic - - beginStartTag: function beginStartTag() { - this.token = { - type: 'StartTag', - tagName: '', - attributes: [], - selfClosing: false - }; - this.tokens.push(this.token); - }, - - beginEndTag: function beginEndTag() { - this.token = { - type: 'EndTag', - tagName: '' - }; - this.tokens.push(this.token); - }, - - finishTag: function finishTag() { - this.addLocInfo(); - }, - - markTagAsSelfClosing: function markTagAsSelfClosing() { - this.token.selfClosing = true; - }, - - // Tags - name - - appendToTagName: function appendToTagName(char) { - this.token.tagName += char; - }, - - // Tags - attributes - - beginAttribute: function beginAttribute() { - this._currentAttribute = ["", "", null]; - this.token.attributes.push(this._currentAttribute); - }, - - appendToAttributeName: function appendToAttributeName(char) { - this._currentAttribute[0] += char; - }, - - beginAttributeValue: function beginAttributeValue(isQuoted) { - this._currentAttribute[2] = isQuoted; - }, - - appendToAttributeValue: function appendToAttributeValue(char) { - this._currentAttribute[1] = this._currentAttribute[1] || ""; - this._currentAttribute[1] += char; - }, - - finishAttributeValue: function finishAttributeValue() {} - }; - - function tokenize(input, options) { - var tokenizer = new Tokenizer(new EntityParser(HTML5NamedCharRefs), options); - return tokenizer.tokenize(input); - } - - var HTML5Tokenizer = { - HTML5NamedCharRefs: HTML5NamedCharRefs, - EntityParser: EntityParser, - EventedTokenizer: EventedTokenizer, - Tokenizer: Tokenizer, - tokenize: tokenize - }; - - var options = linkify.options; - var Options = options.Options; - - - var StartTag = 'StartTag'; - var EndTag = 'EndTag'; - var Chars = 'Chars'; - var Comment = 'Comment'; - - /** - `tokens` and `token` in this section refer to tokens generated by the HTML - parser. - */ - function linkifyHtml(str) { - var opts = arguments.length <= 1 || arguments[1] === undefined ? {} : arguments[1]; - - var tokens = HTML5Tokenizer.tokenize(str); - var linkifiedTokens = []; - var linkified = []; - var i; - - opts = new Options(opts); - - // Linkify the tokens given by the parser - for (i = 0; i < tokens.length; i++) { - var token = tokens[i]; - - if (token.type === StartTag) { - linkifiedTokens.push(token); - - // Ignore all the contents of ignored tags - var tagName = token.tagName.toUpperCase(); - var isIgnored = tagName === 'A' || options.contains(opts.ignoreTags, tagName); - if (!isIgnored) { - continue; - } - - var preskipLen = linkifiedTokens.length; - skipTagTokens(tagName, tokens, ++i, linkifiedTokens); - i += linkifiedTokens.length - preskipLen - 1; - continue; - } else if (token.type !== Chars) { - // Skip this token, it's not important - linkifiedTokens.push(token); - continue; - } - - // Valid text token, linkify it! - var linkifedChars = linkifyChars(token.chars, opts); - linkifiedTokens.push.apply(linkifiedTokens, linkifedChars); - } - - // Convert the tokens back into a string - for (i = 0; i < linkifiedTokens.length; i++) { - var _token = linkifiedTokens[i]; - switch (_token.type) { - case StartTag: - var link = '<' + _token.tagName; - if (_token.attributes.length > 0) { - var attrs = attrsToStrings(_token.attributes); - link += ' ' + attrs.join(' '); - } - link += '>'; - linkified.push(link); - break; - case EndTag: - linkified.push('</' + _token.tagName + '>'); - break; - case Chars: - linkified.push(escapeText(_token.chars)); - break; - case Comment: - linkified.push('<!--' + escapeText(_token.chars) + '-->'); - break; - } - } - - return linkified.join(''); - } - - /** - `tokens` and `token` in this section referes to tokens returned by - `linkify.tokenize`. `linkified` will contain HTML Parser-style tokens - */ - function linkifyChars(str, opts) { - var tokens = linkify.tokenize(str); - var result = []; - - for (var i = 0; i < tokens.length; i++) { - var token = tokens[i]; - - if (token.type === 'nl' && opts.nl2br) { - result.push({ - type: StartTag, - tagName: 'br', - attributes: [], - selfClosing: true - }); - continue; - } else if (!token.isLink || !opts.check(token)) { - result.push({ type: Chars, chars: token.toString() }); - continue; - } - - var _opts$resolve = opts.resolve(token); - - var href = _opts$resolve.href; - var formatted = _opts$resolve.formatted; - var formattedHref = _opts$resolve.formattedHref; - var tagName = _opts$resolve.tagName; - var className = _opts$resolve.className; - var target = _opts$resolve.target; - var attributes = _opts$resolve.attributes; - - // Build up attributes - - var attributeArray = [['href', formattedHref]]; - - if (className) { - attributeArray.push(['class', className]); - } - - if (target) { - attributeArray.push(['target', target]); - } - - for (var attr in attributes) { - attributeArray.push([attr, attributes[attr]]); - } - - // Add the required tokens - result.push({ - type: StartTag, - tagName: tagName, - attributes: attributeArray, - selfClosing: false - }); - result.push({ type: Chars, chars: formatted }); - result.push({ type: EndTag, tagName: tagName }); - } - - return result; - } - - /** - Returns a list of tokens skipped until the closing tag of tagName. - - * `tagName` is the closing tag which will prompt us to stop skipping - * `tokens` is the array of tokens generated by HTML5Tokenizer which - * `i` is the index immediately after the opening tag to skip - * `skippedTokens` is an array which skipped tokens are being pushed into - - Caveats - - * Assumes that i is the first token after the given opening tagName - * The closing tag will be skipped, but nothing after it - * Will track whether there is a nested tag of the same type - */ - function skipTagTokens(tagName, tokens, i, skippedTokens) { - - // number of tokens of this type on the [fictional] stack - var stackCount = 1; - - while (i < tokens.length && stackCount > 0) { - var token = tokens[i]; - if (token.type === StartTag && token.tagName.toUpperCase() === tagName) { - // Nested tag of the same type, "add to stack" - stackCount++; - } else if (token.type === EndTag && token.tagName.toUpperCase() === tagName) { - // Closing tag - stackCount--; - } - skippedTokens.push(token); - i++; - } - - // Note that if stackCount > 0 here, the HTML is probably invalid - return skippedTokens; - } - - function escapeText(text) { - // Not required, HTML tokenizer ensures this occurs properly - return text; - } - - function escapeAttr(attr) { - return attr.replace(/"/g, '"'); - } - - function attrsToStrings(attrs) { - var attrStrs = []; - for (var i = 0; i < attrs.length; i++) { - var _attrs$i = attrs[i]; - var name = _attrs$i[0]; - var value = _attrs$i[1]; - - attrStrs.push(name + '="' + escapeAttr(value) + '"'); - } - return attrStrs; - } - - return linkifyHtml; - }(linkify); - window.linkifyHtml = linkifyHtml; -})(window, linkify); diff --git a/web/linkify-string.js b/web/linkify-string.js deleted file mode 100644 index 699a94d..0000000 --- a/web/linkify-string.js +++ /dev/null @@ -1,118 +0,0 @@ -/* - * Copyright (c) 2016 SoapBox Innovations Inc. - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. -*/ - -'use strict'; - -;(function (window, linkify) { - var linkifyString = function (linkify) { - 'use strict'; - - /** - Convert strings of text into linkable HTML text - */ - - var tokenize = linkify.tokenize; - var options = linkify.options; - var Options = options.Options; - - - function escapeText(text) { - return text.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>'); - } - - function escapeAttr(href) { - return href.replace(/"/g, '"'); - } - - function attributesToString(attributes) { - if (!attributes) { - return ''; - } - var result = []; - - for (var attr in attributes) { - var val = attributes[attr] + ''; - result.push(attr + '="' + escapeAttr(val) + '"'); - } - return result.join(' '); - } - - function linkifyStr(str) { - var opts = arguments.length <= 1 || arguments[1] === undefined ? {} : arguments[1]; - - opts = new Options(opts); - - var tokens = tokenize(str); - var result = []; - - for (var i = 0; i < tokens.length; i++) { - var token = tokens[i]; - - if (token.type === 'nl' && opts.nl2br) { - result.push('<br>\n'); - continue; - } else if (!token.isLink || !opts.check(token)) { - result.push(escapeText(token.toString())); - continue; - } - - var _opts$resolve = opts.resolve(token); - - var formatted = _opts$resolve.formatted; - var formattedHref = _opts$resolve.formattedHref; - var tagName = _opts$resolve.tagName; - var className = _opts$resolve.className; - var target = _opts$resolve.target; - var attributes = _opts$resolve.attributes; - - - var link = '<' + tagName + ' href="' + escapeAttr(formattedHref) + '"'; - - if (className) { - link += ' class="' + escapeAttr(className) + '"'; - } - - if (target) { - link += ' target="' + escapeAttr(target) + '"'; - } - - if (attributes) { - link += ' ' + attributesToString(attributes); - } - - link += '>' + escapeText(formatted) + '</' + tagName + '>'; - result.push(link); - } - - return result.join(''); - } - - if (!String.prototype.linkify) { - String.prototype.linkify = function (opts) { - return linkifyStr(this, opts); - }; - } - - return linkifyStr; - }(linkify); - window.linkifyStr = linkifyString; -})(window, linkify); diff --git a/web/linkify.js b/web/linkify.js deleted file mode 100644 index 15dc03b..0000000 --- a/web/linkify.js +++ /dev/null @@ -1,1271 +0,0 @@ -/* - * Copyright (c) 2016 SoapBox Innovations Inc. - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. -*/ - -;(function () { -'use strict'; - -var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol ? "symbol" : typeof obj; }; - -(function (exports) { - 'use strict'; - - function inherits(parent, child) { - var props = arguments.length <= 2 || arguments[2] === undefined ? {} : arguments[2]; - - var extended = Object.create(parent.prototype); - for (var p in props) { - extended[p] = props[p]; - } - extended.constructor = child; - child.prototype = extended; - return child; - } - - var defaults = { - defaultProtocol: 'http', - events: null, - format: noop, - formatHref: noop, - nl2br: false, - tagName: 'a', - target: typeToTarget, - validate: true, - ignoreTags: [], - attributes: null, - className: 'linkified' }; - - function Options(opts) { - opts = opts || {}; - - this.defaultProtocol = opts.defaultProtocol || defaults.defaultProtocol; - this.events = opts.events || defaults.events; - this.format = opts.format || defaults.format; - this.formatHref = opts.formatHref || defaults.formatHref; - this.nl2br = opts.nl2br || defaults.nl2br; - this.tagName = opts.tagName || defaults.tagName; - this.target = opts.target || defaults.target; - this.validate = opts.validate || defaults.validate; - this.ignoreTags = []; - - // linkAttributes and linkClass is deprecated - this.attributes = opts.attributes || opts.linkAttributes || defaults.attributes; - this.className = opts.className || opts.linkClass || defaults.className; - - // Make all tags names upper case - - var ignoredTags = opts.ignoreTags || defaults.ignoreTags; - for (var i = 0; i < ignoredTags.length; i++) { - this.ignoreTags.push(ignoredTags[i].toUpperCase()); - } - } - - Options.prototype = { - /** - * Given the token, return all options for how it should be displayed - */ - resolve: function resolve(token) { - var href = token.toHref(this.defaultProtocol); - return { - formatted: this.get('format', token.toString(), token), - formattedHref: this.get('formatHref', href, token), - tagName: this.get('tagName', href, token), - className: this.get('className', href, token), - target: this.get('target', href, token), - events: this.getObject('events', href, token), - attributes: this.getObject('attributes', href, token) - }; - }, - - - /** - * Returns true or false based on whether a token should be displayed as a - * link based on the user options. By default, - */ - check: function check(token) { - return this.get('validate', token.toString(), token); - }, - - - // Private methods - - /** - * Resolve an option's value based on the value of the option and the given - * params. - * @param [String] key Name of option to use - * @param operator will be passed to the target option if it's method - * @param [MultiToken] token The token from linkify.tokenize - */ - get: function get(key, operator, token) { - var option = this[key]; - - if (!option) { - return option; - } - - switch (typeof option === 'undefined' ? 'undefined' : _typeof(option)) { - case 'function': - return option(operator, token.type); - case 'object': - var optionValue = option[token.type] || defaults[key]; - return typeof optionValue === 'function' ? optionValue(operator, token.type) : optionValue; - } - - return option; - }, - getObject: function getObject(key, operator, token) { - var option = this[key]; - return typeof option === 'function' ? option(operator, token.type) : option; - } - }; - - /** - * Quick indexOf replacement for checking the ignoreTags option - */ - function contains(arr, value) { - for (var i = 0; i < arr.length; i++) { - if (arr[i] === value) { - return true; - } - } - return false; - } - - function noop(val) { - return val; - } - - function typeToTarget(href, type) { - return type === 'url' ? '_blank' : null; - } - - var options = Object.freeze({ - defaults: defaults, - Options: Options, - contains: contains - }); - - function createStateClass() { - return function (tClass) { - this.j = []; - this.T = tClass || null; - }; - } - - /** - A simple state machine that can emit token classes - - The `j` property in this class refers to state jumps. It's a - multidimensional array where for each element: - - * index [0] is a symbol or class of symbols to transition to. - * index [1] is a State instance which matches - - The type of symbol will depend on the target implementation for this class. - In Linkify, we have a two-stage scanner. Each stage uses this state machine - but with a slighly different (polymorphic) implementation. - - The `T` property refers to the token class. - - TODO: Can the `on` and `next` methods be combined? - - @class BaseState - */ - var BaseState = createStateClass(); - BaseState.prototype = { - defaultTransition: false, - - /** - @method constructor - @param {Class} tClass Pass in the kind of token to emit if there are - no jumps after this state and the state is accepting. - */ - - /** - On the given symbol(s), this machine should go to the given state - @method on - @param {Array|Mixed} symbol - @param {BaseState} state Note that the type of this state should be the - same as the current instance (i.e., don't pass in a different - subclass) - */ - on: function on(symbol, state) { - if (symbol instanceof Array) { - for (var i = 0; i < symbol.length; i++) { - this.j.push([symbol[i], state]); - } - return this; - } - this.j.push([symbol, state]); - return this; - }, - - - /** - Given the next item, returns next state for that item - @method next - @param {Mixed} item Should be an instance of the symbols handled by - this particular machine. - @return {State} state Returns false if no jumps are available - */ - next: function next(item) { - for (var i = 0; i < this.j.length; i++) { - var jump = this.j[i]; - var symbol = jump[0]; // Next item to check for - var state = jump[1]; // State to jump to if items match - - // compare item with symbol - if (this.test(item, symbol)) { - return state; - } - } - - // Nowhere left to jump! - return this.defaultTransition; - }, - - - /** - Does this state accept? - `true` only of `this.T` exists - @method accepts - @return {Boolean} - */ - accepts: function accepts() { - return !!this.T; - }, - - - /** - Determine whether a given item "symbolizes" the symbol, where symbol is - a class of items handled by this state machine. - This method should be overriden in extended classes. - @method test - @param {Mixed} item Does this item match the given symbol? - @param {Mixed} symbol - @return {Boolean} - */ - test: function test(item, symbol) { - return item === symbol; - }, - - - /** - Emit the token for this State (just return it in this case) - If this emits a token, this instance is an accepting state - @method emit - @return {Class} T - */ - emit: function emit() { - return this.T; - } - }; - - /** - State machine for string-based input - - @class CharacterState - @extends BaseState - */ - var CharacterState = inherits(BaseState, createStateClass(), { - /** - Does the given character match the given character or regular - expression? - @method test - @param {String} char - @param {String|RegExp} charOrRegExp - @return {Boolean} - */ - test: function test(character, charOrRegExp) { - return character === charOrRegExp || charOrRegExp instanceof RegExp && charOrRegExp.test(character); - } - }); - - /** - State machine for input in the form of TextTokens - - @class TokenState - @extends BaseState - */ - var State = inherits(BaseState, createStateClass(), { - - /** - * Similar to `on`, but returns the state the results in the transition from - * the given item - * @method jump - * @param {Mixed} item - * @param {Token} [token] - * @return state - */ - jump: function jump(token) { - var tClass = arguments.length <= 1 || arguments[1] === undefined ? null : arguments[1]; - - var state = this.next(new token('')); // dummy temp token - if (state === this.defaultTransition) { - // Make a new state! - state = new this.constructor(tClass); - this.on(token, state); - } else if (tClass) { - state.T = tClass; - } - return state; - }, - - - /** - Is the given token an instance of the given token class? - @method test - @param {TextToken} token - @param {Class} tokenClass - @return {Boolean} - */ - test: function test(token, tokenClass) { - return token instanceof tokenClass; - } - }); - - /** - Given a non-empty target string, generates states (if required) for each - consecutive substring of characters in str starting from the beginning of - the string. The final state will have a special value, as specified in - options. All other "in between" substrings will have a default end state. - - This turns the state machine into a Trie-like data structure (rather than a - intelligently-designed DFA). - - Note that I haven't really tried these with any strings other than - DOMAIN. - - @param {String} str - @param {CharacterState} start State to jump from the first character - @param {Class} endToken Token class to emit when the given string has been - matched and no more jumps exist. - @param {Class} defaultToken "Filler token", or which token type to emit when - we don't have a full match - @return {Array} list of newly-created states - */ - function stateify(str, start, endToken, defaultToken) { - var i = 0, - len = str.length, - state = start, - newStates = [], - nextState = void 0; - - // Find the next state without a jump to the next character - while (i < len && (nextState = state.next(str[i]))) { - state = nextState; - i++; - } - - if (i >= len) { - return []; - } // no new tokens were added - - while (i < len - 1) { - nextState = new CharacterState(defaultToken); - newStates.push(nextState); - state.on(str[i], nextState); - state = nextState; - i++; - } - - nextState = new CharacterState(endToken); - newStates.push(nextState); - state.on(str[len - 1], nextState); - - return newStates; - } - - function createTokenClass() { - return function (value) { - if (value) { - this.v = value; - } - }; - } - - /****************************************************************************** - Text Tokens - Tokens composed of strings - ******************************************************************************/ - - /** - Abstract class used for manufacturing text tokens. - Pass in the value this token represents - - @class TextToken - @abstract - */ - var TextToken = createTokenClass(); - TextToken.prototype = { - toString: function toString() { - return this.v + ''; - } - }; - - function inheritsToken(value) { - var props = value ? { v: value } : {}; - return inherits(TextToken, createTokenClass(), props); - } - - /** - A valid domain token - @class DOMAIN - @extends TextToken - */ - var DOMAIN = inheritsToken(); - - /** - @class AT - @extends TextToken - */ - var AT = inheritsToken('@'); - - /** - Represents a single colon `:` character - - @class COLON - @extends TextToken - */ - var COLON = inheritsToken(':'); - - /** - @class DOT - @extends TextToken - */ - var DOT = inheritsToken('.'); - - /** - A character class that can surround the URL, but which the URL cannot begin - or end with. Does not include certain English punctuation like parentheses. - - @class PUNCTUATION - @extends TextToken - */ - var PUNCTUATION = inheritsToken(); - - /** - The word localhost (by itself) - @class LOCALHOST - @extends TextToken - */ - var LOCALHOST = inheritsToken(); - - /** - Newline token - @class NL - @extends TextToken - */ - var TNL = inheritsToken('\n'); - - /** - @class NUM - @extends TextToken - */ - var NUM = inheritsToken(); - - /** - @class PLUS - @extends TextToken - */ - var PLUS = inheritsToken('+'); - - /** - @class POUND - @extends TextToken - */ - var POUND = inheritsToken('#'); - - /** - Represents a web URL protocol. Supported types include - - * `http:` - * `https:` - * `ftp:` - * `ftps:` - * There's Another super weird one - - @class PROTOCOL - @extends TextToken - */ - var PROTOCOL = inheritsToken(); - - /** - @class QUERY - @extends TextToken - */ - var QUERY = inheritsToken('?'); - - /** - @class SLASH - @extends TextToken - */ - var SLASH = inheritsToken('/'); - - /** - @class UNDERSCORE - @extends TextToken - */ - var UNDERSCORE = inheritsToken('_'); - - /** - One ore more non-whitespace symbol. - @class SYM - @extends TextToken - */ - var SYM = inheritsToken(); - - /** - @class TLD - @extends TextToken - */ - var TLD = inheritsToken(); - - /** - Represents a string of consecutive whitespace characters - - @class WS - @extends TextToken - */ - var WS = inheritsToken(); - - /** - Opening/closing bracket classes - */ - - var OPENBRACE = inheritsToken('{'); - var OPENBRACKET = inheritsToken('['); - var OPENANGLEBRACKET = inheritsToken('<'); - var OPENPAREN = inheritsToken('('); - var CLOSEBRACE = inheritsToken('}'); - var CLOSEBRACKET = inheritsToken(']'); - var CLOSEANGLEBRACKET = inheritsToken('>'); - var CLOSEPAREN = inheritsToken(')'); - - var TOKENS = Object.freeze({ - Base: TextToken, - DOMAIN: DOMAIN, - AT: AT, - COLON: COLON, - DOT: DOT, - PUNCTUATION: PUNCTUATION, - LOCALHOST: LOCALHOST, - NL: TNL, - NUM: NUM, - PLUS: PLUS, - POUND: POUND, - QUERY: QUERY, - PROTOCOL: PROTOCOL, - SLASH: SLASH, - UNDERSCORE: UNDERSCORE, - SYM: SYM, - TLD: TLD, - WS: WS, - OPENBRACE: OPENBRACE, - OPENBRACKET: OPENBRACKET, - OPENANGLEBRACKET: OPENANGLEBRACKET, - OPENPAREN: OPENPAREN, - CLOSEBRACE: CLOSEBRACE, - CLOSEBRACKET: CLOSEBRACKET, - CLOSEANGLEBRACKET: CLOSEANGLEBRACKET, - CLOSEPAREN: CLOSEPAREN - }); - - /** - The scanner provides an interface that takes a string of text as input, and - outputs an array of tokens instances that can be used for easy URL parsing. - - @module linkify - @submodule scanner - @main scanner - */ - - var tlds = 'aaa|aarp|abb|abbott|abogado|ac|academy|accenture|accountant|accountants|aco|active|actor|ad|adac|ads|adult|ae|aeg|aero|af|afl|ag|agency|ai|aig|airforce|airtel|al|alibaba|alipay|allfinanz|alsace|am|amica|amsterdam|an|analytics|android|ao|apartments|app|apple|aq|aquarelle|ar|aramco|archi|army|arpa|arte|as|asia|associates|at|attorney|au|auction|audi|audio|author|auto|autos|avianca|aw|ax|axa|az|azure|ba|baidu|band|bank|bar|barcelona|barclaycard|barclays|bargains|bauhaus|bayern|bb|bbc|bbva|bcg|bcn|bd|be|beats|beer|bentley|berlin|best|bet|bf|bg|bh|bharti|bi|bible|bid|bike|bing|bingo|bio|biz|bj|black|blackfriday|bloomberg|blue|bm|bms|bmw|bn|bnl|bnpparibas|bo|boats|boehringer|bom|bond|boo|book|boots|bosch|bostik|bot|boutique|br|bradesco|bridgestone|broadway|broker|brother|brussels|bs|bt|budapest|bugatti|build|builders|business|buy|buzz|bv|bw|by|bz|bzh|ca|cab|cafe|cal|call|camera|camp|cancerresearch|canon|capetown|capital|car|caravan|cards|care|career|careers|cars|cartier|casa|cash|casino|cat|catering|cba|cbn|cc|cd|ceb|center|ceo|cern|cf|cfa|cfd|cg|ch|chanel|channel|chase|chat|cheap|chloe|christmas|chrome|church|ci|cipriani|circle|cisco|citic|city|cityeats|ck|cl|claims|cleaning|click|clinic|clinique|clothing|cloud|club|clubmed|cm|cn|co|coach|codes|coffee|college|cologne|com|commbank|community|company|compare|computer|comsec|condos|construction|consulting|contact|contractors|cooking|cool|coop|corsica|country|coupon|coupons|courses|cr|credit|creditcard|creditunion|cricket|crown|crs|cruises|csc|cu|cuisinella|cv|cw|cx|cy|cymru|cyou|cz|dabur|dad|dance|date|dating|datsun|day|dclk|de|dealer|deals|degree|delivery|dell|deloitte|delta|democrat|dental|dentist|desi|design|dev|diamonds|diet|digital|direct|directory|discount|dj|dk|dm|dnp|do|docs|dog|doha|domains|download|drive|dubai|durban|dvag|dz|earth|eat|ec|edeka|edu|education|ee|eg|email|emerck|energy|engineer|engineering|enterprises|epson|equipment|er|erni|es|esq|estate|et|eu|eurovision|eus|events|everbank|exchange|expert|exposed|express|fage|fail|fairwinds|faith|family|fan|fans|farm|fashion|fast|feedback|ferrero|fi|film|final|finance|financial|firestone|firmdale|fish|fishing|fit|fitness|fj|fk|flickr|flights|florist|flowers|flsmidth|fly|fm|fo|foo|football|ford|forex|forsale|forum|foundation|fox|fr|fresenius|frl|frogans|frontier|fund|furniture|futbol|fyi|ga|gal|gallery|gallup|game|garden|gb|gbiz|gd|gdn|ge|gea|gent|genting|gf|gg|ggee|gh|gi|gift|gifts|gives|giving|gl|glass|gle|global|globo|gm|gmail|gmbh|gmo|gmx|gn|gold|goldpoint|golf|goo|goog|google|gop|got|gov|gp|gq|gr|grainger|graphics|gratis|green|gripe|group|gs|gt|gu|gucci|guge|guide|guitars|guru|gw|gy|hamburg|hangout|haus|hdfcbank|health|healthcare|help|helsinki|here|hermes|hiphop|hitachi|hiv|hk|hm|hn|hockey|holdings|holiday|homedepot|homes|honda|horse|host|hosting|hoteles|hotmail|house|how|hr|hsbc|ht|hu|hyundai|ibm|icbc|ice|icu|id|ie|ifm|iinet|il|im|immo|immobilien|in|industries|infiniti|info|ing|ink|institute|insurance|insure|int|international|investments|io|ipiranga|iq|ir|irish|is|iselect|ist|istanbul|it|itau|iwc|jaguar|java|jcb|je|jetzt|jewelry|jlc|jll|jm|jmp|jo|jobs|joburg|jot|joy|jp|jpmorgan|jprs|juegos|kaufen|kddi|ke|kerryhotels|kerrylogistics|kerryproperties|kfh|kg|kh|ki|kia|kim|kinder|kitchen|kiwi|km|kn|koeln|komatsu|kp|kpn|kr|krd|kred|kuokgroup|kw|ky|kyoto|kz|la|lacaixa|lamborghini|lamer|lancaster|land|landrover|lanxess|lasalle|lat|latrobe|law|lawyer|lb|lc|lds|lease|leclerc|legal|lexus|lgbt|li|liaison|lidl|life|lifeinsurance|lifestyle|lighting|like|limited|limo|lincoln|linde|link|live|living|lixil|lk|loan|loans|local|locus|lol|london|lotte|lotto|love|lr|ls|lt|ltd|ltda|lu|lupin|luxe|luxury|lv|ly|ma|madrid|maif|maison|makeup|man|management|mango|market|marketing|markets|marriott|mba|mc|md|me|med|media|meet|melbourne|meme|memorial|men|menu|meo|mg|mh|miami|microsoft|mil|mini|mk|ml|mm|mma|mn|mo|mobi|mobily|moda|moe|moi|mom|monash|money|montblanc|mormon|mortgage|moscow|motorcycles|mov|movie|movistar|mp|mq|mr|ms|mt|mtn|mtpc|mtr|mu|museum|mutuelle|mv|mw|mx|my|mz|na|nadex|nagoya|name|natura|navy|nc|ne|nec|net|netbank|network|neustar|new|news|nexus|nf|ng|ngo|nhk|ni|nico|nikon|ninja|nissan|nl|no|nokia|norton|nowruz|np|nr|nra|nrw|ntt|nu|nyc|nz|obi|office|okinawa|om|omega|one|ong|onl|online|ooo|oracle|orange|org|organic|origins|osaka|otsuka|ovh|pa|page|pamperedchef|panerai|paris|pars|partners|parts|party|passagens|pe|pet|pf|pg|ph|pharmacy|philips|photo|photography|photos|physio|piaget|pics|pictet|pictures|pid|pin|ping|pink|pizza|pk|pl|place|play|playstation|plumbing|plus|pm|pn|pohl|poker|porn|post|pr|praxi|press|pro|prod|productions|prof|promo|properties|property|protection|ps|pt|pub|pw|pwc|py|qa|qpon|quebec|quest|racing|re|read|realtor|realty|recipes|red|redstone|redumbrella|rehab|reise|reisen|reit|ren|rent|rentals|repair|report|republican|rest|restaurant|review|reviews|rexroth|rich|ricoh|rio|rip|ro|rocher|rocks|rodeo|room|rs|rsvp|ru|ruhr|run|rw|rwe|ryukyu|sa|saarland|safe|safety|sakura|sale|salon|samsung|sandvik|sandvikcoromant|sanofi|sap|sapo|sarl|sas|saxo|sb|sbs|sc|sca|scb|schaeffler|schmidt|scholarships|school|schule|schwarz|science|scor|scot|sd|se|seat|security|seek|select|sener|services|seven|sew|sex|sexy|sfr|sg|sh|sharp|shell|shia|shiksha|shoes|show|shriram|si|singles|site|sj|sk|ski|skin|sky|skype|sl|sm|smile|sn|sncf|so|soccer|social|softbank|software|sohu|solar|solutions|song|sony|soy|space|spiegel|spot|spreadbetting|sr|srl|st|stada|star|starhub|statefarm|statoil|stc|stcgroup|stockholm|storage|store|studio|study|style|su|sucks|supplies|supply|support|surf|surgery|suzuki|sv|swatch|swiss|sx|sy|sydney|symantec|systems|sz|tab|taipei|taobao|tatamotors|tatar|tattoo|tax|taxi|tc|tci|td|team|tech|technology|tel|telecity|telefonica|temasek|tennis|tf|tg|th|thd|theater|theatre|tickets|tienda|tiffany|tips|tires|tirol|tj|tk|tl|tm|tmall|tn|to|today|tokyo|tools|top|toray|toshiba|total|tours|town|toyota|toys|tp|tr|trade|trading|training|travel|travelers|travelersinsurance|trust|trv|tt|tube|tui|tunes|tushu|tv|tvs|tw|tz|ua|ubs|ug|uk|unicom|university|uno|uol|us|uy|uz|va|vacations|vana|vc|ve|vegas|ventures|verisign|versicherung|vet|vg|vi|viajes|video|viking|villas|vin|vip|virgin|vision|vista|vistaprint|viva|vlaanderen|vn|vodka|volkswagen|vote|voting|voto|voyage|vu|vuelos|wales|walter|wang|wanggou|watch|watches|weather|weatherchannel|webcam|weber|website|wed|wedding|weir|wf|whoswho|wien|wiki|williamhill|win|windows|wine|wme|wolterskluwer|work|works|world|ws|wtc|wtf|xbox|xerox|xin|xperia|xxx|xyz|yachts|yahoo|yamaxun|yandex|ye|yodobashi|yoga|yokohama|youtube|yt|za|zara|zero|zip|zm|zone|zuerich|zw'.split('|'); // macro, see gulpfile.js - - var NUMBERS = '0123456789'.split(''); - var ALPHANUM = '0123456789abcdefghijklmnopqrstuvwxyz'.split(''); - var WHITESPACE = [' ', '\f', '\r', '\t', '\v', ' ', ' ', '']; // excluding line breaks - - var domainStates = []; // states that jump to DOMAIN on /[a-z0-9]/ - var makeState = function makeState(tokenClass) { - return new CharacterState(tokenClass); - }; - - // Frequently used states - var S_START = makeState(); - var S_NUM = makeState(NUM); - var S_DOMAIN = makeState(DOMAIN); - var S_DOMAIN_HYPHEN = makeState(); // domain followed by 1 or more hyphen characters - var S_WS = makeState(WS); - - // States for special URL symbols - S_START.on('@', makeState(AT)).on('.', makeState(DOT)).on('+', makeState(PLUS)).on('#', makeState(POUND)).on('?', makeState(QUERY)).on('/', makeState(SLASH)).on('_', makeState(UNDERSCORE)).on(':', makeState(COLON)).on('{', makeState(OPENBRACE)).on('[', makeState(OPENBRACKET)).on('<', makeState(OPENANGLEBRACKET)).on('(', makeState(OPENPAREN)).on('}', makeState(CLOSEBRACE)).on(']', makeState(CLOSEBRACKET)).on('>', makeState(CLOSEANGLEBRACKET)).on(')', makeState(CLOSEPAREN)).on([',', ';', '!', '"', '\''], makeState(PUNCTUATION)); - - // Whitespace jumps - // Tokens of only non-newline whitespace are arbitrarily long - S_START.on('\n', makeState(TNL)).on(WHITESPACE, S_WS); - - // If any whitespace except newline, more whitespace! - S_WS.on(WHITESPACE, S_WS); - - // Generates states for top-level domains - // Note that this is most accurate when tlds are in alphabetical order - for (var i = 0; i < tlds.length; i++) { - var newStates = stateify(tlds[i], S_START, TLD, DOMAIN); - domainStates.push.apply(domainStates, newStates); - } - - // Collect the states generated by different protocls - var partialProtocolFileStates = stateify('file', S_START, DOMAIN, DOMAIN); - var partialProtocolFtpStates = stateify('ftp', S_START, DOMAIN, DOMAIN); - var partialProtocolHttpStates = stateify('http', S_START, DOMAIN, DOMAIN); - - // Add the states to the array of DOMAINeric states - domainStates.push.apply(domainStates, partialProtocolFileStates); - domainStates.push.apply(domainStates, partialProtocolFtpStates); - domainStates.push.apply(domainStates, partialProtocolHttpStates); - - // Protocol states - var S_PROTOCOL_FILE = partialProtocolFileStates.pop(); - var S_PROTOCOL_FTP = partialProtocolFtpStates.pop(); - var S_PROTOCOL_HTTP = partialProtocolHttpStates.pop(); - var S_PROTOCOL_SECURE = makeState(DOMAIN); - var S_FULL_PROTOCOL = makeState(PROTOCOL); // Full protocol ends with COLON - - // Secure protocols (end with 's') - S_PROTOCOL_FTP.on('s', S_PROTOCOL_SECURE).on(':', S_FULL_PROTOCOL); - - S_PROTOCOL_HTTP.on('s', S_PROTOCOL_SECURE).on(':', S_FULL_PROTOCOL); - - domainStates.push(S_PROTOCOL_SECURE); - - // Become protocol tokens after a COLON - S_PROTOCOL_FILE.on(':', S_FULL_PROTOCOL); - S_PROTOCOL_SECURE.on(':', S_FULL_PROTOCOL); - - // Localhost - var partialLocalhostStates = stateify('localhost', S_START, LOCALHOST, DOMAIN); - domainStates.push.apply(domainStates, partialLocalhostStates); - - // Everything else - // DOMAINs make more DOMAINs - // Number and character transitions - S_START.on(NUMBERS, S_NUM); - S_NUM.on('-', S_DOMAIN_HYPHEN).on(NUMBERS, S_NUM).on(ALPHANUM, S_DOMAIN); // number becomes DOMAIN - - S_DOMAIN.on('-', S_DOMAIN_HYPHEN).on(ALPHANUM, S_DOMAIN); - - // All the generated states should have a jump to DOMAIN - for (var _i = 0; _i < domainStates.length; _i++) { - domainStates[_i].on('-', S_DOMAIN_HYPHEN).on(ALPHANUM, S_DOMAIN); - } - - S_DOMAIN_HYPHEN.on('-', S_DOMAIN_HYPHEN).on(NUMBERS, S_DOMAIN).on(ALPHANUM, S_DOMAIN); - - // Set default transition - S_START.defaultTransition = makeState(SYM); - - /** - Given a string, returns an array of TOKEN instances representing the - composition of that string. - - @method run - @param {String} str Input string to scan - @return {Array} Array of TOKEN instances - */ - var run = function run(str) { - - // The state machine only looks at lowercase strings. - // This selective `toLowerCase` is used because lowercasing the entire - // string causes the length and character position to vary in some in some - // non-English strings. This happens only on V8-based runtimes. - var lowerStr = str.replace(/[A-Z]/g, function (c) { - return c.toLowerCase(); - }); - var len = str.length; - var tokens = []; // return value - - var cursor = 0; - - // Tokenize the string - while (cursor < len) { - var state = S_START; - var secondState = null; - var nextState = null; - var tokenLength = 0; - var latestAccepting = null; - var sinceAccepts = -1; - - while (cursor < len && (nextState = state.next(lowerStr[cursor]))) { - secondState = null; - state = nextState; - - // Keep track of the latest accepting state - if (state.accepts()) { - sinceAccepts = 0; - latestAccepting = state; - } else if (sinceAccepts >= 0) { - sinceAccepts++; - } - - tokenLength++; - cursor++; - } - - if (sinceAccepts < 0) { - continue; - } // Should never happen - - // Roll back to the latest accepting state - cursor -= sinceAccepts; - tokenLength -= sinceAccepts; - - // Get the class for the new token - var TOKEN = latestAccepting.emit(); // Current token class - - // No more jumps, just make a new token - tokens.push(new TOKEN(str.substr(cursor - tokenLength, tokenLength))); - } - - return tokens; - }; - - var start = S_START; - - var scanner = Object.freeze({ - State: CharacterState, - TOKENS: TOKENS, - run: run, - start: start - }); - - /****************************************************************************** - Multi-Tokens - Tokens composed of arrays of TextTokens - ******************************************************************************/ - - // Is the given token a valid domain token? - // Should nums be included here? - function isDomainToken(token) { - return token instanceof DOMAIN || token instanceof TLD; - } - - /** - Abstract class used for manufacturing tokens of text tokens. That is rather - than the value for a token being a small string of text, it's value an array - of text tokens. - - Used for grouping together URLs, emails, hashtags, and other potential - creations. - - @class MultiToken - @abstract - */ - var MultiToken = createTokenClass(); - - MultiToken.prototype = { - /** - String representing the type for this token - @property type - @default 'TOKEN' - */ - type: 'token', - - /** - Is this multitoken a link? - @property isLink - @default false - */ - isLink: false, - - /** - Return the string this token represents. - @method toString - @return {String} - */ - toString: function toString() { - var result = []; - for (var _i2 = 0; _i2 < this.v.length; _i2++) { - result.push(this.v[_i2].toString()); - } - return result.join(''); - }, - - - /** - What should the value for this token be in the `href` HTML attribute? - Returns the `.toString` value by default. - @method toHref - @return {String} - */ - toHref: function toHref() { - return this.toString(); - }, - - - /** - Returns a hash of relevant values for this token, which includes keys - * type - Kind of token ('url', 'email', etc.) - * value - Original text - * href - The value that should be added to the anchor tag's href - attribute - @method toObject - @param {String} [protocol] `'http'` by default - @return {Object} - */ - toObject: function toObject() { - var protocol = arguments.length <= 0 || arguments[0] === undefined ? 'http' : arguments[0]; - - return { - type: this.type, - value: this.toString(), - href: this.toHref(protocol) - }; - } - }; - - /** - Represents a list of tokens making up a valid email address - @class EMAIL - @extends MultiToken - */ - var EMAIL = inherits(MultiToken, createTokenClass(), { - type: 'email', - isLink: true, - toHref: function toHref() { - return 'mailto:' + this.toString(); - } - }); - - /** - Represents some plain text - @class TEXT - @extends MultiToken - */ - var TEXT = inherits(MultiToken, createTokenClass(), { type: 'text' }); - - /** - Multi-linebreak token - represents a line break - @class NL - @extends MultiToken - */ - var NL = inherits(MultiToken, createTokenClass(), { type: 'nl' }); - - /** - Represents a list of tokens making up a valid URL - @class URL - @extends MultiToken - */ - var URL = inherits(MultiToken, createTokenClass(), { - type: 'url', - isLink: true, - - /** - Lowercases relevant parts of the domain and adds the protocol if - required. Note that this will not escape unsafe HTML characters in the - URL. - @method href - @param {String} protocol - @return {String} - */ - toHref: function toHref() { - var protocol = arguments.length <= 0 || arguments[0] === undefined ? 'http' : arguments[0]; - - var hasProtocol = false; - var hasSlashSlash = false; - var tokens = this.v; - var result = []; - var i = 0; - - // Make the first part of the domain lowercase - // Lowercase protocol - while (tokens[i] instanceof PROTOCOL) { - hasProtocol = true; - result.push(tokens[i].toString().toLowerCase()); - i++; - } - - // Skip slash-slash - while (tokens[i] instanceof SLASH) { - hasSlashSlash = true; - result.push(tokens[i].toString()); - i++; - } - - // Lowercase all other characters in the domain - while (isDomainToken(tokens[i])) { - result.push(tokens[i].toString().toLowerCase()); - i++; - } - - // Leave all other characters as they were written - for (; i < tokens.length; i++) { - result.push(tokens[i].toString()); - } - - result = result.join(''); - - if (!(hasProtocol || hasSlashSlash)) { - result = protocol + '://' + result; - } - - return result; - }, - hasProtocol: function hasProtocol() { - return this.v[0] instanceof PROTOCOL; - } - }); - - var TOKENS$1 = Object.freeze({ - Base: MultiToken, - EMAIL: EMAIL, - NL: NL, - TEXT: TEXT, - URL: URL - }); - - /** - Not exactly parser, more like the second-stage scanner (although we can - theoretically hotswap the code here with a real parser in the future... but - for a little URL-finding utility abstract syntax trees may be a little - overkill). - - URL format: http://en.wikipedia.org/wiki/URI_scheme - Email format: http://en.wikipedia.org/wiki/Email_address (links to RFC in - reference) - - @module linkify - @submodule parser - @main parser - */ - - var makeState$1 = function makeState$1(tokenClass) { - return new State(tokenClass); - }; - - // The universal starting state. - var S_START$1 = makeState$1(); - - // Intermediate states for URLs. Note that domains that begin with a protocol - // are treated slighly differently from those that don't. - var S_PROTOCOL = makeState$1(); // e.g., 'http:' - var S_PROTOCOL_SLASH = makeState$1(); // e.g., '/', 'http:/'' - var S_PROTOCOL_SLASH_SLASH = makeState$1(); // e.g., '//', 'http://' - var S_DOMAIN$1 = makeState$1(); // parsed string ends with a potential domain name (A) - var S_DOMAIN_DOT = makeState$1(); // (A) domain followed by DOT - var S_TLD = makeState$1(URL); // (A) Simplest possible URL with no query string - var S_TLD_COLON = makeState$1(); // (A) URL followed by colon (potential port number here) - var S_TLD_PORT = makeState$1(URL); // TLD followed by a port number - var S_URL = makeState$1(URL); // Long URL with optional port and maybe query string - var S_URL_NON_ACCEPTING = makeState$1(); // URL followed by some symbols (will not be part of the final URL) - var S_URL_OPENBRACE = makeState$1(); // URL followed by { - var S_URL_OPENBRACKET = makeState$1(); // URL followed by [ - var S_URL_OPENANGLEBRACKET = makeState$1(); // URL followed by < - var S_URL_OPENPAREN = makeState$1(); // URL followed by ( - var S_URL_OPENBRACE_Q = makeState$1(URL); // URL followed by { and some symbols that the URL can end it - var S_URL_OPENBRACKET_Q = makeState$1(URL); // URL followed by [ and some symbols that the URL can end it - var S_URL_OPENANGLEBRACKET_Q = makeState$1(URL); // URL followed by < and some symbols that the URL can end it - var S_URL_OPENPAREN_Q = makeState$1(URL); // URL followed by ( and some symbols that the URL can end it - var S_URL_OPENBRACE_SYMS = makeState$1(); // S_URL_OPENBRACE_Q followed by some symbols it cannot end it - var S_URL_OPENBRACKET_SYMS = makeState$1(); // S_URL_OPENBRACKET_Q followed by some symbols it cannot end it - var S_URL_OPENANGLEBRACKET_SYMS = makeState$1(); // S_URL_OPENANGLEBRACKET_Q followed by some symbols it cannot end it - var S_URL_OPENPAREN_SYMS = makeState$1(); // S_URL_OPENPAREN_Q followed by some symbols it cannot end it - var S_EMAIL_DOMAIN = makeState$1(); // parsed string starts with local email info + @ with a potential domain name (C) - var S_EMAIL_DOMAIN_DOT = makeState$1(); // (C) domain followed by DOT - var S_EMAIL = makeState$1(EMAIL); // (C) Possible email address (could have more tlds) - var S_EMAIL_COLON = makeState$1(); // (C) URL followed by colon (potential port number here) - var S_EMAIL_PORT = makeState$1(EMAIL); // (C) Email address with a port - var S_LOCALPART = makeState$1(); // Local part of the email address - var S_LOCALPART_AT = makeState$1(); // Local part of the email address plus @ - var S_LOCALPART_DOT = makeState$1(); // Local part of the email address plus '.' (localpart cannot end in .) - var S_NL = makeState$1(NL); // single new line - - // Make path from start to protocol (with '//') - S_START$1.on(TNL, S_NL).on(PROTOCOL, S_PROTOCOL).on(SLASH, S_PROTOCOL_SLASH); - - S_PROTOCOL.on(SLASH, S_PROTOCOL_SLASH); - S_PROTOCOL_SLASH.on(SLASH, S_PROTOCOL_SLASH_SLASH); - - // The very first potential domain name - S_START$1.on(TLD, S_DOMAIN$1).on(DOMAIN, S_DOMAIN$1).on(LOCALHOST, S_TLD).on(NUM, S_DOMAIN$1); - - // Force URL for anything sane followed by protocol - S_PROTOCOL_SLASH_SLASH.on(TLD, S_URL).on(DOMAIN, S_URL).on(NUM, S_URL).on(LOCALHOST, S_URL); - - // Account for dots and hyphens - // hyphens are usually parts of domain names - S_DOMAIN$1.on(DOT, S_DOMAIN_DOT); - S_EMAIL_DOMAIN.on(DOT, S_EMAIL_DOMAIN_DOT); - - // Hyphen can jump back to a domain name - - // After the first domain and a dot, we can find either a URL or another domain - S_DOMAIN_DOT.on(TLD, S_TLD).on(DOMAIN, S_DOMAIN$1).on(NUM, S_DOMAIN$1).on(LOCALHOST, S_DOMAIN$1); - - S_EMAIL_DOMAIN_DOT.on(TLD, S_EMAIL).on(DOMAIN, S_EMAIL_DOMAIN).on(NUM, S_EMAIL_DOMAIN).on(LOCALHOST, S_EMAIL_DOMAIN); - - // S_TLD accepts! But the URL could be longer, try to find a match greedily - // The `run` function should be able to "rollback" to the accepting state - S_TLD.on(DOT, S_DOMAIN_DOT); - S_EMAIL.on(DOT, S_EMAIL_DOMAIN_DOT); - - // Become real URLs after `SLASH` or `COLON NUM SLASH` - // Here PSS and non-PSS converge - S_TLD.on(COLON, S_TLD_COLON).on(SLASH, S_URL); - S_TLD_COLON.on(NUM, S_TLD_PORT); - S_TLD_PORT.on(SLASH, S_URL); - S_EMAIL.on(COLON, S_EMAIL_COLON); - S_EMAIL_COLON.on(NUM, S_EMAIL_PORT); - - // Types of characters the URL can definitely end in - var qsAccepting = [DOMAIN, AT, LOCALHOST, NUM, PLUS, POUND, PROTOCOL, SLASH, TLD, UNDERSCORE, SYM]; - - // Types of tokens that can follow a URL and be part of the query string - // but cannot be the very last characters - // Characters that cannot appear in the URL at all should be excluded - var qsNonAccepting = [COLON, DOT, QUERY, PUNCTUATION, CLOSEBRACE, CLOSEBRACKET, CLOSEANGLEBRACKET, CLOSEPAREN, OPENBRACE, OPENBRACKET, OPENANGLEBRACKET, OPENPAREN]; - - // These states are responsible primarily for determining whether or not to - // include the final round bracket. - - // URL, followed by an opening bracket - S_URL.on(OPENBRACE, S_URL_OPENBRACE).on(OPENBRACKET, S_URL_OPENBRACKET).on(OPENANGLEBRACKET, S_URL_OPENANGLEBRACKET).on(OPENPAREN, S_URL_OPENPAREN); - - // URL with extra symbols at the end, followed by an opening bracket - S_URL_NON_ACCEPTING.on(OPENBRACE, S_URL_OPENBRACE).on(OPENBRACKET, S_URL_OPENBRACKET).on(OPENANGLEBRACKET, S_URL_OPENANGLEBRACKET).on(OPENPAREN, S_URL_OPENPAREN); - - // Closing bracket component. This character WILL be included in the URL - S_URL_OPENBRACE.on(CLOSEBRACE, S_URL); - S_URL_OPENBRACKET.on(CLOSEBRACKET, S_URL); - S_URL_OPENANGLEBRACKET.on(CLOSEANGLEBRACKET, S_URL); - S_URL_OPENPAREN.on(CLOSEPAREN, S_URL); - S_URL_OPENBRACE_Q.on(CLOSEBRACE, S_URL); - S_URL_OPENBRACKET_Q.on(CLOSEBRACKET, S_URL); - S_URL_OPENANGLEBRACKET_Q.on(CLOSEANGLEBRACKET, S_URL); - S_URL_OPENPAREN_Q.on(CLOSEPAREN, S_URL); - S_URL_OPENBRACE_SYMS.on(CLOSEBRACE, S_URL); - S_URL_OPENBRACKET_SYMS.on(CLOSEBRACKET, S_URL); - S_URL_OPENANGLEBRACKET_SYMS.on(CLOSEANGLEBRACKET, S_URL); - S_URL_OPENPAREN_SYMS.on(CLOSEPAREN, S_URL); - - // URL that beings with an opening bracket, followed by a symbols. - // Note that the final state can still be `S_URL_OPENBRACE_Q` (if the URL only - // has a single opening bracket for some reason). - S_URL_OPENBRACE.on(qsAccepting, S_URL_OPENBRACE_Q); - S_URL_OPENBRACKET.on(qsAccepting, S_URL_OPENBRACKET_Q); - S_URL_OPENANGLEBRACKET.on(qsAccepting, S_URL_OPENANGLEBRACKET_Q); - S_URL_OPENPAREN.on(qsAccepting, S_URL_OPENPAREN_Q); - S_URL_OPENBRACE.on(qsNonAccepting, S_URL_OPENBRACE_SYMS); - S_URL_OPENBRACKET.on(qsNonAccepting, S_URL_OPENBRACKET_SYMS); - S_URL_OPENANGLEBRACKET.on(qsNonAccepting, S_URL_OPENANGLEBRACKET_SYMS); - S_URL_OPENPAREN.on(qsNonAccepting, S_URL_OPENPAREN_SYMS); - - // URL that begins with an opening bracket, followed by some symbols - S_URL_OPENBRACE_Q.on(qsAccepting, S_URL_OPENBRACE_Q); - S_URL_OPENBRACKET_Q.on(qsAccepting, S_URL_OPENBRACKET_Q); - S_URL_OPENANGLEBRACKET_Q.on(qsAccepting, S_URL_OPENANGLEBRACKET_Q); - S_URL_OPENPAREN_Q.on(qsAccepting, S_URL_OPENPAREN_Q); - S_URL_OPENBRACE_Q.on(qsNonAccepting, S_URL_OPENBRACE_Q); - S_URL_OPENBRACKET_Q.on(qsNonAccepting, S_URL_OPENBRACKET_Q); - S_URL_OPENANGLEBRACKET_Q.on(qsNonAccepting, S_URL_OPENANGLEBRACKET_Q); - S_URL_OPENPAREN_Q.on(qsNonAccepting, S_URL_OPENPAREN_Q); - - S_URL_OPENBRACE_SYMS.on(qsAccepting, S_URL_OPENBRACE_Q); - S_URL_OPENBRACKET_SYMS.on(qsAccepting, S_URL_OPENBRACKET_Q); - S_URL_OPENANGLEBRACKET_SYMS.on(qsAccepting, S_URL_OPENANGLEBRACKET_Q); - S_URL_OPENPAREN_SYMS.on(qsAccepting, S_URL_OPENPAREN_Q); - S_URL_OPENBRACE_SYMS.on(qsNonAccepting, S_URL_OPENBRACE_SYMS); - S_URL_OPENBRACKET_SYMS.on(qsNonAccepting, S_URL_OPENBRACKET_SYMS); - S_URL_OPENANGLEBRACKET_SYMS.on(qsNonAccepting, S_URL_OPENANGLEBRACKET_SYMS); - S_URL_OPENPAREN_SYMS.on(qsNonAccepting, S_URL_OPENPAREN_SYMS); - - // Account for the query string - S_URL.on(qsAccepting, S_URL); - S_URL_NON_ACCEPTING.on(qsAccepting, S_URL); - - S_URL.on(qsNonAccepting, S_URL_NON_ACCEPTING); - S_URL_NON_ACCEPTING.on(qsNonAccepting, S_URL_NON_ACCEPTING); - - // Email address-specific state definitions - // Note: We are not allowing '/' in email addresses since this would interfere - // with real URLs - - // Tokens allowed in the localpart of the email - var localpartAccepting = [DOMAIN, NUM, PLUS, POUND, QUERY, UNDERSCORE, SYM, TLD]; - - // Some of the tokens in `localpartAccepting` are already accounted for here and - // will not be overwritten (don't worry) - S_DOMAIN$1.on(localpartAccepting, S_LOCALPART).on(AT, S_LOCALPART_AT); - S_TLD.on(localpartAccepting, S_LOCALPART).on(AT, S_LOCALPART_AT); - S_DOMAIN_DOT.on(localpartAccepting, S_LOCALPART); - - // Okay we're on a localpart. Now what? - // TODO: IP addresses and what if the email starts with numbers? - S_LOCALPART.on(localpartAccepting, S_LOCALPART).on(AT, S_LOCALPART_AT) // close to an email address now - .on(DOT, S_LOCALPART_DOT); - S_LOCALPART_DOT.on(localpartAccepting, S_LOCALPART); - S_LOCALPART_AT.on(TLD, S_EMAIL_DOMAIN).on(DOMAIN, S_EMAIL_DOMAIN).on(LOCALHOST, S_EMAIL); - // States following `@` defined above - - var run$1 = function run$1(tokens) { - var len = tokens.length; - var cursor = 0; - var multis = []; - var textTokens = []; - - while (cursor < len) { - var state = S_START$1; - var secondState = null; - var nextState = null; - var multiLength = 0; - var latestAccepting = null; - var sinceAccepts = -1; - - while (cursor < len && !(secondState = state.next(tokens[cursor]))) { - // Starting tokens with nowhere to jump to. - // Consider these to be just plain text - textTokens.push(tokens[cursor++]); - } - - while (cursor < len && (nextState = secondState || state.next(tokens[cursor]))) { - - // Get the next state - secondState = null; - state = nextState; - - // Keep track of the latest accepting state - if (state.accepts()) { - sinceAccepts = 0; - latestAccepting = state; - } else if (sinceAccepts >= 0) { - sinceAccepts++; - } - - cursor++; - multiLength++; - } - - if (sinceAccepts < 0) { - - // No accepting state was found, part of a regular text token - // Add all the tokens we looked at to the text tokens array - for (var _i3 = cursor - multiLength; _i3 < cursor; _i3++) { - textTokens.push(tokens[_i3]); - } - } else { - - // Accepting state! - - // First close off the textTokens (if available) - if (textTokens.length > 0) { - multis.push(new TEXT(textTokens)); - textTokens = []; - } - - // Roll back to the latest accepting state - cursor -= sinceAccepts; - multiLength -= sinceAccepts; - - // Create a new multitoken - var MULTI = latestAccepting.emit(); - multis.push(new MULTI(tokens.slice(cursor - multiLength, cursor))); - } - } - - // Finally close off the textTokens (if available) - if (textTokens.length > 0) { - multis.push(new TEXT(textTokens)); - } - - return multis; - }; - - var parser = Object.freeze({ - State: State, - TOKENS: TOKENS$1, - run: run$1, - start: S_START$1 - }); - - if (!Array.isArray) { - Array.isArray = function (arg) { - return Object.prototype.toString.call(arg) === '[object Array]'; - }; - } - - /** - Converts a string into tokens that represent linkable and non-linkable bits - @method tokenize - @param {String} str - @return {Array} tokens - */ - var tokenize = function tokenize(str) { - return run$1(run(str)); - }; - - /** - Returns a list of linkable items in the given string. - */ - var find = function find(str) { - var type = arguments.length <= 1 || arguments[1] === undefined ? null : arguments[1]; - - var tokens = tokenize(str); - var filtered = []; - - for (var i = 0; i < tokens.length; i++) { - var token = tokens[i]; - if (token.isLink && (!type || token.type === type)) { - filtered.push(token.toObject()); - } - } - - return filtered; - }; - - /** - Is the given string valid linkable text of some sort - Note that this does not trim the text for you. - - Optionally pass in a second `type` param, which is the type of link to test - for. - - For example, - - test(str, 'email'); - - Will return `true` if str is a valid email. - */ - var test = function test(str) { - var type = arguments.length <= 1 || arguments[1] === undefined ? null : arguments[1]; - - var tokens = tokenize(str); - return tokens.length === 1 && tokens[0].isLink && (!type || tokens[0].type === type); - }; - - exports.find = find; - exports.inherits = inherits; - exports.options = options; - exports.parser = parser; - exports.scanner = scanner; - exports.test = test; - exports.tokenize = tokenize; -})(window.linkify = window.linkify || {}); -})(); diff --git a/web/qwebchannel.js b/web/qwebchannel.js deleted file mode 100644 index abf8461..0000000 --- a/web/qwebchannel.js +++ /dev/null @@ -1,427 +0,0 @@ -/**************************************************************************** -** -** Copyright (C) 2016 The Qt Company Ltd. -** Copyright (C) 2016 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com, author Milian Wolff <milian.wolff@kdab.com> -** Contact: https://www.qt.io/licensing/ -** -** This file is part of the QtWebChannel module of the Qt Toolkit. -** -** $QT_BEGIN_LICENSE:LGPL$ -** Commercial License Usage -** Licensees holding valid commercial Qt licenses may use this file in -** accordance with the commercial license agreement provided with the -** Software or, alternatively, in accordance with the terms contained in -** a written agreement between you and The Qt Company. For licensing terms -** and conditions see https://www.qt.io/terms-conditions. For further -** information use the contact form at https://www.qt.io/contact-us. -** -** GNU Lesser General Public License Usage -** Alternatively, this file may be used under the terms of the GNU Lesser -** General Public License version 3 as published by the Free Software -** Foundation and appearing in the file LICENSE.LGPL3 included in the -** packaging of this file. Please review the following information to -** ensure the GNU Lesser General Public License version 3 requirements -** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. -** -** GNU General Public License Usage -** Alternatively, this file may be used under the terms of the GNU -** General Public License version 2.0 or (at your option) the GNU General -** Public license version 3 or any later version approved by the KDE Free -** Qt Foundation. The licenses are as published by the Free Software -** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 -** included in the packaging of this file. Please review the following -** information to ensure the GNU General Public License requirements will -** be met: https://www.gnu.org/licenses/gpl-2.0.html and -** https://www.gnu.org/licenses/gpl-3.0.html. -** -** $QT_END_LICENSE$ -** -****************************************************************************/ - -"use strict"; - -var QWebChannelMessageTypes = { - signal: 1, - propertyUpdate: 2, - init: 3, - idle: 4, - debug: 5, - invokeMethod: 6, - connectToSignal: 7, - disconnectFromSignal: 8, - setProperty: 9, - response: 10, -}; - -var QWebChannel = function(transport, initCallback) -{ - if (typeof transport !== "object" || typeof transport.send !== "function") { - console.error("The QWebChannel expects a transport object with a send function and onmessage callback property." + - " Given is: transport: " + typeof(transport) + ", transport.send: " + typeof(transport.send)); - return; - } - - var channel = this; - this.transport = transport; - - this.send = function(data) - { - if (typeof(data) !== "string") { - data = JSON.stringify(data); - } - channel.transport.send(data); - } - - this.transport.onmessage = function(message) - { - var data = message.data; - if (typeof data === "string") { - data = JSON.parse(data); - } - switch (data.type) { - case QWebChannelMessageTypes.signal: - channel.handleSignal(data); - break; - case QWebChannelMessageTypes.response: - channel.handleResponse(data); - break; - case QWebChannelMessageTypes.propertyUpdate: - channel.handlePropertyUpdate(data); - break; - default: - console.error("invalid message received:", message.data); - break; - } - } - - this.execCallbacks = {}; - this.execId = 0; - this.exec = function(data, callback) - { - if (!callback) { - // if no callback is given, send directly - channel.send(data); - return; - } - if (channel.execId === Number.MAX_VALUE) { - // wrap - channel.execId = Number.MIN_VALUE; - } - if (data.hasOwnProperty("id")) { - console.error("Cannot exec message with property id: " + JSON.stringify(data)); - return; - } - data.id = channel.execId++; - channel.execCallbacks[data.id] = callback; - channel.send(data); - }; - - this.objects = {}; - - this.handleSignal = function(message) - { - var object = channel.objects[message.object]; - if (object) { - object.signalEmitted(message.signal, message.args); - } else { - console.warn("Unhandled signal: " + message.object + "::" + message.signal); - } - } - - this.handleResponse = function(message) - { - if (!message.hasOwnProperty("id")) { - console.error("Invalid response message received: ", JSON.stringify(message)); - return; - } - channel.execCallbacks[message.id](message.data); - delete channel.execCallbacks[message.id]; - } - - this.handlePropertyUpdate = function(message) - { - for (var i in message.data) { - var data = message.data[i]; - var object = channel.objects[data.object]; - if (object) { - object.propertyUpdate(data.signals, data.properties); - } else { - console.warn("Unhandled property update: " + data.object + "::" + data.signal); - } - } - channel.exec({type: QWebChannelMessageTypes.idle}); - } - - this.debug = function(message) - { - channel.send({type: QWebChannelMessageTypes.debug, data: message}); - }; - - channel.exec({type: QWebChannelMessageTypes.init}, function(data) { - for (var objectName in data) { - var object = new QObject(objectName, data[objectName], channel); - } - // now unwrap properties, which might reference other registered objects - for (var objectName in channel.objects) { - channel.objects[objectName].unwrapProperties(); - } - if (initCallback) { - initCallback(channel); - } - channel.exec({type: QWebChannelMessageTypes.idle}); - }); -}; - -function QObject(name, data, webChannel) -{ - this.__id__ = name; - webChannel.objects[name] = this; - - // List of callbacks that get invoked upon signal emission - this.__objectSignals__ = {}; - - // Cache of all properties, updated when a notify signal is emitted - this.__propertyCache__ = {}; - - var object = this; - - // ---------------------------------------------------------------------- - - this.unwrapQObject = function(response) - { - if (response instanceof Array) { - // support list of objects - var ret = new Array(response.length); - for (var i = 0; i < response.length; ++i) { - ret[i] = object.unwrapQObject(response[i]); - } - return ret; - } - if (!response - || !response["__QObject*__"] - || response.id === undefined) { - return response; - } - - var objectId = response.id; - if (webChannel.objects[objectId]) - return webChannel.objects[objectId]; - - if (!response.data) { - console.error("Cannot unwrap unknown QObject " + objectId + " without data."); - return; - } - - var qObject = new QObject( objectId, response.data, webChannel ); - qObject.destroyed.connect(function() { - if (webChannel.objects[objectId] === qObject) { - delete webChannel.objects[objectId]; - // reset the now deleted QObject to an empty {} object - // just assigning {} though would not have the desired effect, but the - // below also ensures all external references will see the empty map - // NOTE: this detour is necessary to workaround QTBUG-40021 - var propertyNames = []; - for (var propertyName in qObject) { - propertyNames.push(propertyName); - } - for (var idx in propertyNames) { - delete qObject[propertyNames[idx]]; - } - } - }); - // here we are already initialized, and thus must directly unwrap the properties - qObject.unwrapProperties(); - return qObject; - } - - this.unwrapProperties = function() - { - for (var propertyIdx in object.__propertyCache__) { - object.__propertyCache__[propertyIdx] = object.unwrapQObject(object.__propertyCache__[propertyIdx]); - } - } - - function addSignal(signalData, isPropertyNotifySignal) - { - var signalName = signalData[0]; - var signalIndex = signalData[1]; - object[signalName] = { - connect: function(callback) { - if (typeof(callback) !== "function") { - console.error("Bad callback given to connect to signal " + signalName); - return; - } - - object.__objectSignals__[signalIndex] = object.__objectSignals__[signalIndex] || []; - object.__objectSignals__[signalIndex].push(callback); - - if (!isPropertyNotifySignal && signalName !== "destroyed") { - // only required for "pure" signals, handled separately for properties in propertyUpdate - // also note that we always get notified about the destroyed signal - webChannel.exec({ - type: QWebChannelMessageTypes.connectToSignal, - object: object.__id__, - signal: signalIndex - }); - } - }, - disconnect: function(callback) { - if (typeof(callback) !== "function") { - console.error("Bad callback given to disconnect from signal " + signalName); - return; - } - object.__objectSignals__[signalIndex] = object.__objectSignals__[signalIndex] || []; - var idx = object.__objectSignals__[signalIndex].indexOf(callback); - if (idx === -1) { - console.error("Cannot find connection of signal " + signalName + " to " + callback.name); - return; - } - object.__objectSignals__[signalIndex].splice(idx, 1); - if (!isPropertyNotifySignal && object.__objectSignals__[signalIndex].length === 0) { - // only required for "pure" signals, handled separately for properties in propertyUpdate - webChannel.exec({ - type: QWebChannelMessageTypes.disconnectFromSignal, - object: object.__id__, - signal: signalIndex - }); - } - } - }; - } - - /** - * Invokes all callbacks for the given signalname. Also works for property notify callbacks. - */ - function invokeSignalCallbacks(signalName, signalArgs) - { - var connections = object.__objectSignals__[signalName]; - if (connections) { - connections.forEach(function(callback) { - callback.apply(callback, signalArgs); - }); - } - } - - this.propertyUpdate = function(signals, propertyMap) - { - // update property cache - for (var propertyIndex in propertyMap) { - var propertyValue = propertyMap[propertyIndex]; - object.__propertyCache__[propertyIndex] = propertyValue; - } - - for (var signalName in signals) { - // Invoke all callbacks, as signalEmitted() does not. This ensures the - // property cache is updated before the callbacks are invoked. - invokeSignalCallbacks(signalName, signals[signalName]); - } - } - - this.signalEmitted = function(signalName, signalArgs) - { - invokeSignalCallbacks(signalName, this.unwrapQObject(signalArgs)); - } - - function addMethod(methodData) - { - var methodName = methodData[0]; - var methodIdx = methodData[1]; - object[methodName] = function() { - var args = []; - var callback; - for (var i = 0; i < arguments.length; ++i) { - var argument = arguments[i]; - if (typeof argument === "function") - callback = argument; - else if (argument instanceof QObject && webChannel.objects[argument.__id__] !== undefined) - args.push({ - "id": argument.__id__ - }); - else - args.push(argument); - } - - webChannel.exec({ - "type": QWebChannelMessageTypes.invokeMethod, - "object": object.__id__, - "method": methodIdx, - "args": args - }, function(response) { - if (response !== undefined) { - var result = object.unwrapQObject(response); - if (callback) { - (callback)(result); - } - } - }); - }; - } - - function bindGetterSetter(propertyInfo) - { - var propertyIndex = propertyInfo[0]; - var propertyName = propertyInfo[1]; - var notifySignalData = propertyInfo[2]; - // initialize property cache with current value - // NOTE: if this is an object, it is not directly unwrapped as it might - // reference other QObject that we do not know yet - object.__propertyCache__[propertyIndex] = propertyInfo[3]; - - if (notifySignalData) { - if (notifySignalData[0] === 1) { - // signal name is optimized away, reconstruct the actual name - notifySignalData[0] = propertyName + "Changed"; - } - addSignal(notifySignalData, true); - } - - Object.defineProperty(object, propertyName, { - configurable: true, - get: function () { - var propertyValue = object.__propertyCache__[propertyIndex]; - if (propertyValue === undefined) { - // This shouldn't happen - console.warn("Undefined value in property cache for property \"" + propertyName + "\" in object " + object.__id__); - } - - return propertyValue; - }, - set: function(value) { - if (value === undefined) { - console.warn("Property setter for " + propertyName + " called with undefined value!"); - return; - } - object.__propertyCache__[propertyIndex] = value; - var valueToSend = value; - if (valueToSend instanceof QObject && webChannel.objects[valueToSend.__id__] !== undefined) - valueToSend = { "id": valueToSend.__id__ }; - webChannel.exec({ - "type": QWebChannelMessageTypes.setProperty, - "object": object.__id__, - "property": propertyIndex, - "value": valueToSend - }); - } - }); - - } - - // ---------------------------------------------------------------------- - - data.methods.forEach(addMethod); - - data.properties.forEach(bindGetterSetter); - - data.signals.forEach(function(signal) { addSignal(signal, false); }); - - for (var name in data.enums) { - object[name] = data.enums[name]; - } -} - -//required for use with nodejs -if (typeof module === 'object') { - module.exports = { - QWebChannel: QWebChannel - }; -} -- GitLab