diff --git a/webroot/js/chat/chat.js b/webroot/js/chat/chat.js
index bc000bade..23cfa3b4d 100644
--- a/webroot/js/chat/chat.js
+++ b/webroot/js/chat/chat.js
@@ -74,6 +74,7 @@ export default class Chat extends Component {
placeholder="Message"
class="appearance-none block w-full bg-gray-200 text-gray-700 border border-black-500 rounded py-2 px-2 my-2 focus:bg-white"
>
+
diff --git a/webroot/js/utils/chat.js b/webroot/js/utils/chat.js
index 939b072c5..dc26d23ca 100644
--- a/webroot/js/utils/chat.js
+++ b/webroot/js/utils/chat.js
@@ -8,7 +8,7 @@ export const CHAT_PLACEHOLDER_OFFLINE = 'Chat is offline.';
export function formatMessageText(message) {
showdown.setFlavor('github');
- var markdownToHTML = new showdown.Converter({
+ let formattedText = new showdown.Converter({
emoji: true,
openLinksInNewWindow: true,
tables: false,
@@ -16,20 +16,158 @@ export function formatMessageText(message) {
literalMidWordUnderscores: true,
strikethrough: true,
ghMentions: false,
- }).makeHtml(this.body);
- const linked = autoLink(markdownToHTML, {
- embed: true,
- removeHTTP: true,
- linkAttr: {
- target: '_blank'
- }
- });
- const highlighted = highlightUsername(linked);
- return addNewlines(highlighted);
+ }).makeHtml(message);
+
+ formattedText = linkify(formattedText, message);
+ formattedText = highlightUsername(formattedText);
+
+ return addNewlines(formattedText);
}
function highlightUsername(message) {
const username = document.getElementById('self-message-author').value;
- const pattern = new RegExp('@?' + username.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&'), 'gi');
+ const pattern = new RegExp('@?' + username.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&'), 'gi');
return message.replace(pattern, '
$&');
}
+
+function linkify(text, rawText) {
+ const urls = getURLs(stripTags(rawText));
+ if (urls) {
+ urls.forEach(function (url) {
+ let linkURL = url;
+
+ // Add http prefix if none exist in the URL so it actually
+ // will work in an anchor tag.
+ if (linkURL.indexOf('http') === -1) {
+ linkURL = 'http://' + linkURL;
+ }
+
+ // Remove the protocol prefix in the display URLs just to make
+ // things look a little nicer.
+ const displayURL = url.replace(/(^\w+:|^)\/\//, '');
+ const link = `
${displayURL}`;
+ text = text.replace(url, link);
+
+ if (getYoutubeIdFromURL(url)) {
+ if (isTextJustURLs(text, [url, displayURL])) {
+ text = '';
+ } else {
+ text += '
';
+ }
+
+ const youtubeID = getYoutubeIdFromURL(url);
+ text += getYoutubeEmbedFromID(youtubeID);
+ } else if (url.indexOf('instagram.com/p/') > -1) {
+ if (isTextJustURLs(text, [url, displayURL])) {
+ text = '';
+ } else {
+ text += `
`;
+ }
+ text += getInstagramEmbedFromURL(url);
+ } else if (isImage(url)) {
+ if (isTextJustURLs(text, [url, displayURL])) {
+ text = '';
+ } else {
+ text += `
`;
+ }
+ text += getImageForURL(url);
+ }
+ }.bind(this));
+ }
+ return text;
+}
+
+function isTextJustURLs(text, urls) {
+ for (var i = 0; i < urls.length; i++) {
+ const url = urls[i];
+ if (stripTags(text) === url) {
+ return true;
+ }
+ }
+ return false;
+}
+
+
+function stripTags(str) {
+ return str.replace(/<\/?[^>]+(>|$)/g, "");
+}
+
+function getURLs(str) {
+ var exp = /((\w+:\/\/\S+)|(\w+[\.:]\w+\S+))[^\s,\.]/ig;
+ return str.match(exp);
+}
+
+function getYoutubeIdFromURL(url) {
+ try {
+ var regExp = /^.*(youtu.be\/|v\/|u\/\w\/|embed\/|watch\?v=|\&v=)([^#\&\?]*).*/;
+ var match = url.match(regExp);
+
+ if (match && match[2].length == 11) {
+ return match[2];
+ } else {
+ return null;
+ }
+ } catch (e) {
+ console.log(e);
+ return null;
+ }
+}
+
+function getYoutubeEmbedFromID(id) {
+ return `
`;
+}
+
+function getInstagramEmbedFromURL(url) {
+ const urlObject = new URL(url.replace(/\/$/, ""));
+ urlObject.pathname += "/embed";
+ return `
`;
+}
+
+function isImage(url) {
+ const re = /\.(jpe?g|png|gif)$/;
+ const isImage = re.test(url);
+ return isImage;
+}
+
+function getImageForURL(url) {
+ return `

`;
+}
+
+
+// Taken from https://stackoverflow.com/questions/3972014/get-contenteditable-caret-index-position
+export function getCaretPosition(editableDiv) {
+ var caretPos = 0,
+ sel, range;
+ if (window.getSelection) {
+ sel = window.getSelection();
+ if (sel.rangeCount) {
+ range = sel.getRangeAt(0);
+ if (range.commonAncestorContainer.parentNode == editableDiv) {
+ caretPos = range.endOffset;
+ }
+ }
+ } else if (document.selection && document.selection.createRange) {
+ range = document.selection.createRange();
+ if (range.parentElement() == editableDiv) {
+ var tempEl = document.createElement("span");
+ editableDiv.insertBefore(tempEl, editableDiv.firstChild);
+ var tempRange = range.duplicate();
+ tempRange.moveToElementText(tempEl);
+ tempRange.setEndPoint("EndToEnd", range);
+ caretPos = tempRange.text.length;
+ }
+ }
+ return caretPos;
+}
+
+// Pieced together from parts of https://stackoverflow.com/questions/6249095/how-to-set-caretcursor-position-in-contenteditable-element-div
+export function setCaretPosition(editableDiv, position) {
+ var range = document.createRange();
+ var sel = window.getSelection();
+ range.selectNode(editableDiv);
+ range.setStart(editableDiv.childNodes[0], position);
+ range.collapse(true);
+
+ sel.removeAllRanges();
+ sel.addRange(range);
+}
diff --git a/webroot/styles/layout.css b/webroot/styles/layout.css
index b179dfef1..611408072 100644
--- a/webroot/styles/layout.css
+++ b/webroot/styles/layout.css
@@ -23,9 +23,9 @@ a:hover {
}
-.visually-hidden {
+.visually-hidden {
position: absolute !important;
- height: 1px;
+ height: 1px;
width: 1px;
overflow: hidden;
clip: rect(1px 1px 1px 1px); /* IE6, IE7 */
@@ -111,7 +111,7 @@ footer span {
flex-direction: row;
justify-content: space-between;
-
+
}
#stream-info span {
font-size: .7em;
@@ -128,23 +128,7 @@ footer span {
/* ************************************************8 */
-.user-content {
- padding: 3em;
- display: flex;
- flex-direction: row;
-}
-.user-content .user-image {
- padding: 1em;
- margin-right: 2em;
- min-width: var(--user-image-width);
- width: var(--user-image-width);
- height: var(--user-image-width);
- max-height: var(--user-image-width);
- background-repeat: no-repeat;
- background-position: center center;
- background-size: calc(var(--user-image-width) - 1em);
-}
/* .user-image img {
display: inline-block;
@@ -156,58 +140,7 @@ footer span {
}
h2 {
- font-size: 3em;
-}
-.user-content-header {
- margin-bottom: 2em;
-}
-
-.tag-list {
- flex-direction: row;
- margin: 1em 0;
-}
-.tag-list li {
- font-size: .75em;
- text-transform: uppercase;
- margin-right: .75em;
- padding: .5em;
-}
-
-
-.social-list {
- flex-direction: row;
- align-items: center;
- justify-content: flex-start;
- flex-wrap: wrap;
-}
-.social-list .follow-label {
- font-weight: bold;
- font-size: .75em;
- margin-right: .5em;
- text-transform: uppercase;
-}
-
-.user-social-item {
- display: flex;
- justify-content: flex-start;
- align-items: center;
- margin-right: -.25em;
-}
-.user-social-item .platform-icon {
- --icon-width: 40px;
- height: var(--icon-width);
- width: var(--icon-width);
- background-image: url(../img/social-icons.gif);
- background-repeat: no-repeat;
- background-position: calc(var(--imgCol) * var(--icon-width)) calc(var(--imgRow) * var(--icon-width));
- transform: scale(.65);
-}
-
-.user-social-item.use-default .platform-label {
- font-size: .7em;
- text-transform: uppercase;
- display: inline-block;
- max-width: 10em;
+ font-size: 3em;
}
@@ -281,8 +214,8 @@ h2 {
margin-top: var(--header-height);
background-position: center center;
background-repeat: no-repeat;
-
- background-size: 30%;
+
+ background-size: 30%;
}
.owncast-video-container {
@@ -360,81 +293,6 @@ h2 {
}
-#messages-container {
- overflow: auto;
- padding: 1em 0;
-}
-#message-input-container {
- width: 100%;
- padding: 1em;
-}
-
-#message-form {
- flex-direction: column;
- align-items: flex-end;
- margin-bottom: 0;
-}
-#message-body-form {
- font-size: 1em;
- height: 60px;
-}
-#message-body-form:disabled{
- opacity: .5;
-}
-#message-body-form img {
- display: inline;
- padding-left: 5px;
- padding-right: 5px;
-}
-
-#message-body-form .emoji {
- width: 40px;
-}
-
-#message-form-actions {
- flex-direction: row;
- justify-content: space-between;
- align-items: center;
- width: 100%;
-}
-
-.message-text img {
- display: inline;
- padding-left: 5px;
- padding-right: 5px;
-}
-
-.message-text .emoji {
- width: 60px;
-}
-
-/* ************************************************8 */
-
-.message {
- padding: .85em;
- align-items: flex-start;
-}
-.message-avatar {
- margin-right: .75em;
-}
-.message-avatar img {
- max-width: unset;
- height: 3.0em;
- width: 3.0em;
- padding: 5px;
-}
-
-.message-content {
- font-size: .85em;
- max-width: 85%;
- word-wrap: break-word;
-}
-.message-content a {
- color: #7F9CF5; /* indigo-400 */
-}
-.message-content a:hover {
- text-decoration: underline;
-}
/* ************************************************8 */
@@ -452,7 +310,7 @@ h2 {
--right-col-width: 20em;
--user-image-width: 6em;
}
-
+
#chat-container {
width: var(--right-col-width);
}
@@ -504,21 +362,6 @@ h2 {
}
}
-/* try not making the video fixed position for now */
-@media (min-height: 861px) {
- /* main {
- position: fixed;
- z-index: 9;
- width: 100%;
- }
- #user-content {
- margin-top: calc(var(--video-container-height) + var(--header-height) + 2em)
- } */
-}
-
-
-
-
@@ -530,194 +373,3 @@ h2 {
flex-direction: column;
}
}
-
-.extra-user-content {
- padding: 1em 3em 3em 3em;
-}
-
-.extra-user-content ol {
- list-style: decimal;
-}
-
-.extra-user-content ul {
- list-style: unset;
-}
-
-.extra-user-content h1, .extra-user-content h2, .extra-user-content h3, .extra-user-content h4 {
- color: #111111;
- font-weight: 400; }
-
-.extra-user-content h1, .extra-user-content h2, .extra-user-content h3, .extra-user-content h4, .extra-user-content h5, .extra-user-content p {
- margin-bottom: 24px;
- padding: 0; }
-
-.extra-user-content h1 {
- font-size: 48px; }
-
-.extra-user-content h2 {
- font-size: 36px;
- margin: 24px 0 6px; }
-
-.extra-user-content h3 {
- font-size: 24px; }
-
-.extra-user-content h4 {
- font-size: 21px; }
-
-.extra-user-content h5 {
- font-size: 18px; }
-
-.extra-user-content a {
- color: #0099ff;
- margin: 0;
- padding: 0;
- vertical-align: baseline; }
-
-.extra-user-content ul, .extra-user-content ol {
- padding: 0;
- margin: 0; }
-
-.extra-user-content li {
- line-height: 24px; }
-
-.extra-user-content li ul, .extra-user-content li ul {
- margin-left: 24px; }
-
-.extra-user-content p, .extra-user-content ul, .extra-user-content ol {
- font-size: 16px;
- line-height: 24px;
- }
-
-.extra-user-content pre {
- padding: 0px 24px;
- max-width: 800px;
- white-space: pre-wrap; }
-
-.extra-user-content code {
- font-family: Consolas, Monaco, Andale Mono, monospace;
- line-height: 1.5;
- font-size: 13px; }
-
-.extra-user-content aside {
- display: block;
- float: right;
- width: 390px; }
-
-.extra-user-content blockquote {
- margin: 1em 2em;
- max-width: 476px; }
-
-.extra-user-content blockquote p {
- color: #666;
- max-width: 460px; }
-
-.extra-user-content hr {
- width: 540px;
- text-align: left;
- margin: 0 auto 0 0;
- color: #999; }
-
-.extra-user-content table {
- border-collapse: collapse;
- margin: 1em 1em;
- border: 1px solid #CCC; }
-
-.extra-user-content table thead {
- background-color: #EEE; }
-
-.extra-user-content table thead td {
- color: #666; }
-
-.extra-user-content table td {
- padding: 0.5em 1em;
- border: 1px solid #CCC; }
-
-.message-text iframe {
- width: 100%;
- height: 170px;
- border-radius: 15px;
-}
-
-.message-text .instagram-embed {
- height: 314px;
-}
-
-.message-text code {
- background-color:darkslategrey;
- padding: 3px;
-}
-/* Emoji picker */
-#emoji-button {
- position: relative;
- top: -65px;
- right: 10px;
- cursor: pointer;
-}
-
-.message-text .embedded-image {
- width: 100%;
- height: 170px;
- border-radius: 15px;
-}
-
-.message-text code {
- background-color:darkslategrey;
- padding: 3px;
-}
-
-/* Emoji picker */
-#emoji-button {
- position: relative;
- top: -65px;
- right: 10px;
- cursor: pointer;
-}
-.message-text .embedded-image {
- width: 100%;
- height: 170px;
- border-radius: 15px;
-}
-
-.message-text code {
- background-color:darkslategrey;
- padding: 3px;
-}
-.message-text .highlighted {
- color: orange;
- font-weight: 400;
- font-size: 14px;
-
-}
-
-.message-text code {
- background-color:darkslategrey;
- padding: 3px;
-}
-
-/*
-The chat input has a fake placeholder that is styled below.
-It pulls the placeholder text from the div's placeholder attribute.
-But really it's just the innerHTML content.
-*/
-
-/* If the div is empty then show the placeholder */
-#message-body-form:empty:before{
- content: attr(placeholder);
- pointer-events: none;
- display: block; /* For Firefox */
-
- /* Style the div's placeholder text color */
- color: rgba(0, 0, 0, 0.5);
-}
-
-/* When chat is enabled (contenteditable=true) */
-#message-body-form[contenteditable=true]:before {
- opacity: 1.0;
-}
-
-
-/* When chat is disabled (contenteditable=false) chat input div should appear disabled. */
-#message-body-form[contenteditable=false] {
- opacity: 0.6;
-}
-
diff --git a/webroot/styles/message.css b/webroot/styles/message.css
index 67b7b77d6..1dd2021ef 100644
--- a/webroot/styles/message.css
+++ b/webroot/styles/message.css
@@ -48,6 +48,7 @@
}
+
.message {
padding: .85em;
align-items: flex-start;
@@ -74,14 +75,93 @@
text-decoration: underline;
}
+
.message-text iframe {
width: 100%;
height: 170px;
border-radius: 15px;
}
+.message-text .instagram-embed {
+ height: 314px;
+}
+
+.message-text code {
+ background-color:darkslategrey;
+ padding: 3px;
+}
/* Emoji picker */
#emoji-button {
- margin: 0 .5em;
- font-size: 1.5em
-}
\ No newline at end of file
+ position: relative;
+ top: -65px;
+ right: 10px;
+ cursor: pointer;
+}
+
+.message-text .embedded-image {
+ width: 100%;
+ height: 170px;
+ border-radius: 15px;
+}
+
+.message-text code {
+ background-color:darkslategrey;
+ padding: 3px;
+}
+
+/* Emoji picker */
+#emoji-button {
+ position: relative;
+ top: -65px;
+ right: 10px;
+ cursor: pointer;
+}
+.message-text .embedded-image {
+ width: 100%;
+ height: 170px;
+ border-radius: 15px;
+}
+
+.message-text code {
+ background-color:darkslategrey;
+ padding: 3px;
+}
+.message-text .highlighted {
+ color: orange;
+ font-weight: 400;
+ font-size: 14px;
+
+}
+
+.message-text code {
+ background-color:darkslategrey;
+ padding: 3px;
+}
+
+/*
+The chat input has a fake placeholder that is styled below.
+It pulls the placeholder text from the div's placeholder attribute.
+But really it's just the innerHTML content.
+*/
+
+/* If the div is empty then show the placeholder */
+#message-body-form:empty:before{
+ content: attr(placeholder);
+ pointer-events: none;
+ display: block; /* For Firefox */
+
+ /* Style the div's placeholder text color */
+ color: rgba(0, 0, 0, 0.5);
+}
+
+/* When chat is enabled (contenteditable=true) */
+#message-body-form[contenteditable=true]:before {
+ opacity: 1.0;
+}
+
+
+/* When chat is disabled (contenteditable=false) chat input div should appear disabled. */
+#message-body-form[contenteditable=false] {
+ opacity: 0.6;
+}
+
diff --git a/webroot/styles/user-content.css b/webroot/styles/user-content.css
index 7a04ee539..dea858f08 100644
--- a/webroot/styles/user-content.css
+++ b/webroot/styles/user-content.css
@@ -1,8 +1,79 @@
-.extra-user-content {
- padding: 1em 3em 3em 3em;
+.user-content {
+ padding: 3em;
+
+ display: flex;
+ flex-direction: row;
+}
+.user-content .user-image {
+ padding: 1em;
+ margin-right: 2em;
+ min-width: var(--user-image-width);
+ width: var(--user-image-width);
+ height: var(--user-image-width);
+ max-height: var(--user-image-width);
+ background-repeat: no-repeat;
+ background-position: center center;
+ background-size: calc(var(--user-image-width) - 1em);
+}
+
+.user-content-header {
+ margin-bottom: 2em;
+}
+
+.tag-list {
+ flex-direction: row;
+ margin: 1em 0;
+}
+.tag-list li {
+ font-size: .75em;
+ text-transform: uppercase;
+ margin-right: .75em;
+ padding: .5em;
}
+.social-list {
+ flex-direction: row;
+ align-items: center;
+ justify-content: flex-start;
+ flex-wrap: wrap;
+}
+.social-list .follow-label {
+ font-weight: bold;
+ font-size: .75em;
+ margin-right: .5em;
+ text-transform: uppercase;
+}
+
+.user-social-item {
+ display: flex;
+ justify-content: flex-start;
+ align-items: center;
+ margin-right: -.25em;
+}
+.user-social-item .platform-icon {
+ --icon-width: 40px;
+ height: var(--icon-width);
+ width: var(--icon-width);
+ background-image: url(../img/social-icons.gif);
+ background-repeat: no-repeat;
+ background-position: calc(var(--imgCol) * var(--icon-width)) calc(var(--imgRow) * var(--icon-width));
+ transform: scale(.65);
+}
+
+.user-social-item.use-default .platform-label {
+ font-size: .7em;
+ text-transform: uppercase;
+ display: inline-block;
+ max-width: 10em;
+}
+
+
+
+
+.extra-user-content {
+ padding: 1em 3em 3em 3em;
+}
.extra-user-content ol {
list-style: decimal;